From 53958cbdad08c19360ba39337a3600e5ab718b17 Mon Sep 17 00:00:00 2001 From: Forged2Form <9682873+formed2forge@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:04:28 -0400 Subject: [PATCH 1/5] fix(desktop): stage libnode dylib alongside Homebrew node in prepare-agent-runtime Homebrew node (v22+) is a small stub binary that dlopen()s libnode.X.dylib via @loader_path at startup. stage_local_node only copied the stub, so the binary aborted immediately with "Library not loaded: @rpath/libnode.X.dylib" when run from the Resources directory. Fix: after copying the stub, detect a libnode dependency via otool, locate the dylib in the Homebrew prefix, and copy it alongside the node binary so @loader_path can resolve it at both the validation step and app runtime. Co-Authored-By: Claude Sonnet 4.6 --- desktop/scripts/prepare-agent-runtime.sh | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/desktop/scripts/prepare-agent-runtime.sh b/desktop/scripts/prepare-agent-runtime.sh index 9c934bf5434..52b89cee98a 100755 --- a/desktop/scripts/prepare-agent-runtime.sh +++ b/desktop/scripts/prepare-agent-runtime.sh @@ -125,6 +125,34 @@ stage_local_node() { cp -f "$node_bin" "$NODE_RESOURCE" chmod +x "$NODE_RESOURCE" xattr -cr "$NODE_RESOURCE" 2>/dev/null || true + + # Homebrew node is a small stub that dlopen()s libnode.X.dylib via @loader_path. + # Copy the dylib alongside the binary so it resolves at both validation and runtime. + local libnode_name + libnode_name=$(otool -L "$node_bin" 2>/dev/null \ + | awk '/@rpath\/libnode\.[0-9]+\.dylib/ {gsub(/@rpath\//, ""); gsub(/ \(.*/, ""); print; exit}') + if [ -n "$libnode_name" ]; then + local libnode_src="" + local node_bin_dir + node_bin_dir="$(dirname "$(realpath "$node_bin")")" + for candidate in \ + "$node_bin_dir/../lib/$libnode_name" \ + "$node_bin_dir/$libnode_name" \ + "$(brew --prefix 2>/dev/null)/lib/$libnode_name"; do + if [ -f "$candidate" ]; then + libnode_src="$(realpath "$candidate")" + break + fi + done + if [ -z "$libnode_src" ]; then + echo "ERROR: node requires $libnode_name but it was not found near $node_bin or in Homebrew lib." >&2 + exit 1 + fi + cp -f "$libnode_src" "$(dirname "$NODE_RESOURCE")/$libnode_name" + xattr -cr "$(dirname "$NODE_RESOURCE")/$libnode_name" 2>/dev/null || true + log "Staged $libnode_name alongside node (Homebrew dynamic build)" + fi + log "Staged local Node $("$NODE_RESOURCE" --version) from $node_bin" } From 5dbaa81d30cbce2a301e812833ced732d56716ab Mon Sep 17 00:00:00 2001 From: Forged2Form <9682873+formed2forge@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:07:10 -0400 Subject: [PATCH 2/5] fix(desktop): fix libnode name extraction stripping leading tab The awk gsub left the tab from otool output before the filename, causing the path search to find nothing. Use match/substr to extract just the libnode.X.dylib token directly. Co-Authored-By: Claude Sonnet 4.6 --- desktop/scripts/prepare-agent-runtime.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/scripts/prepare-agent-runtime.sh b/desktop/scripts/prepare-agent-runtime.sh index 52b89cee98a..ffaffe79094 100755 --- a/desktop/scripts/prepare-agent-runtime.sh +++ b/desktop/scripts/prepare-agent-runtime.sh @@ -130,7 +130,7 @@ stage_local_node() { # Copy the dylib alongside the binary so it resolves at both validation and runtime. local libnode_name libnode_name=$(otool -L "$node_bin" 2>/dev/null \ - | awk '/@rpath\/libnode\.[0-9]+\.dylib/ {gsub(/@rpath\//, ""); gsub(/ \(.*/, ""); print; exit}') + | awk 'match($0, /libnode\.[0-9]+\.dylib/) {print substr($0, RSTART, RLENGTH); exit}') if [ -n "$libnode_name" ]; then local libnode_src="" local node_bin_dir From 5d398816777f4b1a8b28d1e74a25898cad04a439 Mon Sep 17 00:00:00 2001 From: Forged2Form <9682873+formed2forge@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:14:27 -0400 Subject: [PATCH 3/5] fix(desktop): sign libnode dylib before signing app bundle When node is staged from a Homebrew dynamic build, libnode.X.dylib lands in the resource bundle. codesign rejects the app bundle if it contains an unsigned dylib, causing "internal error in Code Signing subsystem". Sign any libnode.*.dylib found alongside the node binary before the final app bundle signature step. Co-Authored-By: Claude Sonnet 4.6 --- desktop/run.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/desktop/run.sh b/desktop/run.sh index a7d3bf8e375..bed75f6cd89 100755 --- a/desktop/run.sh +++ b/desktop/run.sh @@ -587,8 +587,15 @@ if [ -n "$SIGN_IDENTITY" ]; then fi # Sign the bundled node binary with developer identity + Node.entitlements # (macOS requires executables inside app bundles to be properly signed) - NODE_BIN="$APP_BUNDLE/Contents/Resources/Omi Computer_Omi Computer.bundle/node" + NODE_BUNDLE_DIR="$APP_BUNDLE/Contents/Resources/Omi Computer_Omi Computer.bundle" + NODE_BIN="$NODE_BUNDLE_DIR/node" if [ -f "$NODE_BIN" ]; then + # Sign any libnode dylib staged alongside the binary (Homebrew dynamic builds) + for libnode_dylib in "$NODE_BUNDLE_DIR"/libnode.*.dylib; do + [ -f "$libnode_dylib" ] || continue + substep "Signing $(basename "$libnode_dylib")" + codesign --force --options runtime --sign "$SIGN_IDENTITY" "$libnode_dylib" + done substep "Signing bundled node binary" codesign --force --options runtime --entitlements Desktop/Node.entitlements --sign "$SIGN_IDENTITY" "$NODE_BIN" fi From 2063737bd4bf3a98fbf6d1bd1b655739273c5fc6 Mon Sep 17 00:00:00 2001 From: Forged2Form <9682873+formed2forge@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:21:46 -0400 Subject: [PATCH 4/5] fix(desktop): sign libnode dylib with Node.entitlements (JIT required) libnode contains V8's JIT runtime. Signing it with --options runtime but no entitlements triggers a Code Signing subsystem internal error when the bundle is signed. Use the same Node.entitlements as the node binary (allow-jit + allow-unsigned-executable-memory). Co-Authored-By: Claude Sonnet 4.6 --- desktop/run.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/desktop/run.sh b/desktop/run.sh index bed75f6cd89..d37b7f41f26 100755 --- a/desktop/run.sh +++ b/desktop/run.sh @@ -590,11 +590,12 @@ if [ -n "$SIGN_IDENTITY" ]; then NODE_BUNDLE_DIR="$APP_BUNDLE/Contents/Resources/Omi Computer_Omi Computer.bundle" NODE_BIN="$NODE_BUNDLE_DIR/node" if [ -f "$NODE_BIN" ]; then - # Sign any libnode dylib staged alongside the binary (Homebrew dynamic builds) + # Sign any libnode dylib staged alongside the binary (Homebrew dynamic builds). + # libnode contains V8 JIT code, so it needs the same entitlements as the node binary. for libnode_dylib in "$NODE_BUNDLE_DIR"/libnode.*.dylib; do [ -f "$libnode_dylib" ] || continue substep "Signing $(basename "$libnode_dylib")" - codesign --force --options runtime --sign "$SIGN_IDENTITY" "$libnode_dylib" + codesign --force --options runtime --entitlements Desktop/Node.entitlements --sign "$SIGN_IDENTITY" "$libnode_dylib" done substep "Signing bundled node binary" codesign --force --options runtime --entitlements Desktop/Node.entitlements --sign "$SIGN_IDENTITY" "$NODE_BIN" From 7eb2aa541a3fe5aff40045e23487ca8d70080619 Mon Sep 17 00:00:00 2001 From: Forged2Form <9682873+formed2forge@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:17:19 -0400 Subject: [PATCH 5/5] fix(desktop): address code review findings on Homebrew node dylib fix - chmod u+w the staged libnode dylib after cp -f: Homebrew installs it 0444, so codesign --force would fail with EACCES without this - gitignore libnode.*.dylib in Desktop/Sources/Resources: only the `node` binary was previously ignored; the 70MB dylib was an accidental-commit hazard - remove --entitlements from libnode dylib signing: macOS reads process entitlements only from the main executable, not from loaded dylibs; sign with --options runtime only and correct the comment Co-Authored-By: Claude Sonnet 4.6 --- desktop/.gitignore | 1 + desktop/run.sh | 5 +++-- desktop/scripts/prepare-agent-runtime.sh | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/desktop/.gitignore b/desktop/.gitignore index ec66c1d4739..127c85effdd 100644 --- a/desktop/.gitignore +++ b/desktop/.gitignore @@ -58,6 +58,7 @@ scripts/ **/python_reference/ Desktop/Sources/Resources/ffmpeg Desktop/Sources/Resources/node +Desktop/Sources/Resources/libnode.*.dylib # Local Rust helper build output codex-proxy/target/ diff --git a/desktop/run.sh b/desktop/run.sh index d37b7f41f26..e2a3c3bfb8f 100755 --- a/desktop/run.sh +++ b/desktop/run.sh @@ -591,11 +591,12 @@ if [ -n "$SIGN_IDENTITY" ]; then NODE_BIN="$NODE_BUNDLE_DIR/node" if [ -f "$NODE_BIN" ]; then # Sign any libnode dylib staged alongside the binary (Homebrew dynamic builds). - # libnode contains V8 JIT code, so it needs the same entitlements as the node binary. + # Dylibs don't carry entitlements at runtime (macOS reads them from the main executable only); + # sign with --options runtime alone, no --entitlements. for libnode_dylib in "$NODE_BUNDLE_DIR"/libnode.*.dylib; do [ -f "$libnode_dylib" ] || continue substep "Signing $(basename "$libnode_dylib")" - codesign --force --options runtime --entitlements Desktop/Node.entitlements --sign "$SIGN_IDENTITY" "$libnode_dylib" + codesign --force --options runtime --sign "$SIGN_IDENTITY" "$libnode_dylib" done substep "Signing bundled node binary" codesign --force --options runtime --entitlements Desktop/Node.entitlements --sign "$SIGN_IDENTITY" "$NODE_BIN" diff --git a/desktop/scripts/prepare-agent-runtime.sh b/desktop/scripts/prepare-agent-runtime.sh index ffaffe79094..f52b27eefe1 100755 --- a/desktop/scripts/prepare-agent-runtime.sh +++ b/desktop/scripts/prepare-agent-runtime.sh @@ -149,6 +149,7 @@ stage_local_node() { exit 1 fi cp -f "$libnode_src" "$(dirname "$NODE_RESOURCE")/$libnode_name" + chmod u+w "$(dirname "$NODE_RESOURCE")/$libnode_name" xattr -cr "$(dirname "$NODE_RESOURCE")/$libnode_name" 2>/dev/null || true log "Staged $libnode_name alongside node (Homebrew dynamic build)" fi