fix: bundle node runtime for mac app

This commit is contained in:
Peter Steinberger
2026-01-10 15:28:37 +01:00
parent c782404bee
commit 449bee9645
13 changed files with 258 additions and 99 deletions

View File

@@ -71,6 +71,12 @@ enum GatewayEnvironment {
return FileManager.default.isExecutableFile(atPath: path) ? path : nil return FileManager.default.isExecutableFile(atPath: path) ? path : nil
} }
static func bundledNodeExecutable() -> String? {
guard let res = Bundle.main.resourceURL else { return nil }
let path = res.appendingPathComponent("Relay/node").path
return FileManager.default.isExecutableFile(atPath: path) ? path : nil
}
static func gatewayPort() -> Int { static func gatewayPort() -> Int {
if let raw = ProcessInfo.processInfo.environment["CLAWDBOT_GATEWAY_PORT"] { if let raw = ProcessInfo.processInfo.environment["CLAWDBOT_GATEWAY_PORT"] {
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -107,13 +113,15 @@ enum GatewayEnvironment {
if let bundled = self.bundledGatewayExecutable() { if let bundled = self.bundledGatewayExecutable() {
let installed = self.readGatewayVersion(binary: bundled) let installed = self.readGatewayVersion(binary: bundled)
let bundledNode = self.bundledNodeExecutable()
let bundledNodeVersion = bundledNode.flatMap { self.readNodeVersion(binary: $0) }
if let expected, let installed, !installed.compatible(with: expected) { if let expected, let installed, !installed.compatible(with: expected) {
let message = let message =
"Bundled gateway \(installed.description) is incompatible with app " + "Bundled gateway \(installed.description) is incompatible with app " +
"\(expected.description); rebuild the app bundle." "\(expected.description); rebuild the app bundle."
return GatewayEnvironmentStatus( return GatewayEnvironmentStatus(
kind: .incompatible(found: installed.description, required: expected.description), kind: .incompatible(found: installed.description, required: expected.description),
nodeVersion: nil, nodeVersion: bundledNodeVersion,
gatewayVersion: installed.description, gatewayVersion: installed.description,
requiredGateway: expected.description, requiredGateway: expected.description,
message: message) message: message)
@@ -121,10 +129,10 @@ enum GatewayEnvironment {
let gatewayVersionText = installed?.description ?? "unknown" let gatewayVersionText = installed?.description ?? "unknown"
return GatewayEnvironmentStatus( return GatewayEnvironmentStatus(
kind: .ok, kind: .ok,
nodeVersion: nil, nodeVersion: bundledNodeVersion,
gatewayVersion: gatewayVersionText, gatewayVersion: gatewayVersionText,
requiredGateway: expected?.description, requiredGateway: expected?.description,
message: "Bundled gateway \(gatewayVersionText) (bun)") message: "Bundled gateway \(gatewayVersionText) (node \(bundledNodeVersion ?? "unknown"))")
} }
let projectRoot = CommandResolver.projectRoot() let projectRoot = CommandResolver.projectRoot()
@@ -351,4 +359,26 @@ enum GatewayEnvironment {
else { return nil } else { return nil }
return Semver.parse(version) return Semver.parse(version)
} }
private static func readNodeVersion(binary: String) -> String? {
let process = Process()
process.executableURL = URL(fileURLWithPath: binary)
process.arguments = ["--version"]
process.environment = ["PATH": CommandResolver.preferredPaths().joined(separator: ":")]
let pipe = Pipe()
process.standardOutput = pipe
process.standardError = pipe
do {
try process.run()
process.waitUntilExit()
let data = pipe.fileHandleForReading.readToEndSafely()
let raw = String(data: data, encoding: .utf8)?
.trimmingCharacters(in: .whitespacesAndNewlines)
.replacingOccurrences(of: "^v", with: "", options: .regularExpression)
return raw?.isEmpty == false ? raw : nil
} catch {
return nil
}
}
} }

View File

@@ -187,7 +187,7 @@ Notes:
- `daemon install` is a no-op when already installed; use `clawdbot daemon install --force` to reinstall (profile/env/path changes). - `daemon install` is a no-op when already installed; use `clawdbot daemon install --force` to reinstall (profile/env/path changes).
Bundled mac app: Bundled mac app:
- Clawdbot.app can bundle a bun-compiled gateway binary and install a per-user LaunchAgent labeled `com.clawdbot.gateway`. - Clawdbot.app can bundle a Node-based gateway relay and install a per-user LaunchAgent labeled `com.clawdbot.gateway`.
- To stop it cleanly, use `clawdbot daemon stop` (or `launchctl bootout gui/$UID/com.clawdbot.gateway`). - To stop it cleanly, use `clawdbot daemon stop` (or `launchctl bootout gui/$UID/com.clawdbot.gateway`).
- To restart, use `clawdbot daemon restart` (or `launchctl kickstart -k gui/$UID/com.clawdbot.gateway`). - To restart, use `clawdbot daemon restart` (or `launchctl kickstart -k gui/$UID/com.clawdbot.gateway`).
- `launchctl` only works if the LaunchAgent is installed; otherwise use `clawdbot daemon install` first. - `launchctl` only works if the LaunchAgent is installed; otherwise use `clawdbot daemon install` first.

View File

@@ -322,7 +322,7 @@ kill -9 <PID> # last resort
``` ```
**Fix 3: Check embedded gateway** **Fix 3: Check embedded gateway**
Ensure the gateway relay was properly bundled. Run [`./scripts/package-mac-app.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/package-mac-app.sh) and ensure `bun` is installed. Ensure the gateway relay was properly bundled. Run [`./scripts/package-mac-app.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/package-mac-app.sh) and ensure Node is available (the script downloads a bundled runtime by default).
## Debug Mode ## Debug Mode

View File

@@ -1,32 +1,43 @@
--- ---
summary: "Bundled bun gateway: packaging, launchd, signing, and bytecode" summary: "Bundled Node gateway: packaging, launchd, signing, and bundling"
read_when: read_when:
- Packaging Clawdbot.app - Packaging Clawdbot.app
- Debugging the bundled gateway binary - Debugging the bundled gateway binary
- Changing bun build flags or codesigning - Changing relay bundling flags or codesigning
--- ---
# Bundled bun Gateway (macOS) # Bundled Node Gateway (macOS)
Goal: ship **Clawdbot.app** with a self-contained relay binary that can run both the CLI and the Gateway daemon. No global `npm install -g clawdbot`, no system Node requirement. Goal: ship **Clawdbot.app** with a self-contained relay that can run the CLI and
Gateway daemon. No global `npm install -g clawdbot`, no system Node requirement.
## What gets bundled ## What gets bundled
App bundle layout: App bundle layout:
- `Clawdbot.app/Contents/Resources/Relay/node`
- Node runtime binary (downloaded during packaging, stripped for size)
- `Clawdbot.app/Contents/Resources/Relay/dist/`
- Compiled CLI/gateway payload from `pnpm exec tsc`
- `Clawdbot.app/Contents/Resources/Relay/node_modules/`
- Production dependencies staged via `pnpm deploy --prod --no-optional --legacy`
- `Clawdbot.app/Contents/Resources/Relay/clawdbot` - `Clawdbot.app/Contents/Resources/Relay/clawdbot`
- bun `--compile` relay executable built from `dist/macos/relay.js` - Wrapper script that execs the bundled Node + dist entrypoint
- Supports:
- `clawdbot …` (CLI)
- `clawdbot gateway …` (LaunchAgent daemon)
- `Clawdbot.app/Contents/Resources/Relay/package.json` - `Clawdbot.app/Contents/Resources/Relay/package.json`
- tiny “p runtime compatibility” file (see below) - tiny “Pi runtime compatibility” file (see below, includes `"type": "module"`)
- `Clawdbot.app/Contents/Resources/Relay/skills/`
- Bundled skills payload (required for Pi tools)
- `Clawdbot.app/Contents/Resources/Relay/theme/` - `Clawdbot.app/Contents/Resources/Relay/theme/`
- p TUI theme payload (optional, but strongly recommended) - Pi TUI theme payload (optional, but strongly recommended)
- `Clawdbot.app/Contents/Resources/Relay/a2ui/`
- A2UI host assets (served by the gateway)
- `Clawdbot.app/Contents/Resources/Relay/control-ui/`
- Control UI build output (served by the gateway)
Why the sidecar files matter: Why the sidecar files matter:
- The embedded p runtime detects “bun binary mode” and then looks for `package.json` + `theme/` **next to `process.execPath`** (i.e. next to `clawdbot`). - The embedded Pi runtime detects “bundled relay mode” and then looks for
- So even if bun can embed assets, the runtime expects filesystem paths. Keep the sidecar files. `package.json` + `theme/` **next to `process.execPath`** (i.e. next to
`node`). Keep the sidecar files.
## Build pipeline ## Build pipeline
@@ -36,18 +47,18 @@ Packaging script:
It builds: It builds:
- TS: `pnpm exec tsc` - TS: `pnpm exec tsc`
- Swift app + helper: `swift build …` - Swift app + helper: `swift build …`
- bun relay: `bun build dist/macos/relay.js --compile --bytecode …` - Relay payload: `pnpm deploy --prod --no-optional --legacy` + copy `dist/`
- Node runtime: downloads the latest Node release (override via `NODE_VERSION`)
Important bundler flags: Important knobs:
- `--compile`: produces a standalone executable - `NODE_VERSION=22.12.0` → pin a specific Node version
- `--bytecode`: reduces startup time / parsing overhead (works here) - `NODE_DIST_MIRROR=…` → mirror for downloads (default: nodejs.org)
- externals: - `STRIP_NODE=0` → keep symbols (default strips to reduce size)
- `-e electron` - `BUNDLED_RUNTIME=bun` → switch the relay build back to Bun (`bun --compile`)
- Reason: avoid bundling Electron stubs in the relay binary
Version injection: Version injection:
- `--define "__CLAWDBOT_VERSION__=\"<pkg version>\""` - The relay wrapper exports `CLAWDBOT_BUNDLED_VERSION` so `--version` works
- The relay honors `__CLAWDBOT_VERSION__` / `CLAWDBOT_BUNDLED_VERSION` so `--version` doesnt depend on reading `package.json` at runtime. without reading `package.json` at runtime.
## Launchd (Gateway as LaunchAgent) ## Launchd (Gateway as LaunchAgent)
@@ -63,40 +74,26 @@ Manager:
Behavior: Behavior:
- “Clawdbot Active” enables/disables the LaunchAgent. - “Clawdbot Active” enables/disables the LaunchAgent.
- App quit does **not** stop the gateway (launchd keeps it alive). - App quit does **not** stop the gateway (launchd keeps it alive).
- CLI install (`clawdbot daemon install`) writes the same LaunchAgent; `clawdbot daemon install --force` rewrites it. - CLI install (`clawdbot daemon install`) writes the same LaunchAgent; `--force` rewrites it.
- `clawdbot doctor` audits the LaunchAgent config and can update it to current defaults.
Logging: Logging:
- launchd stdout/err: `/tmp/clawdbot/clawdbot-gateway.log` - launchd stdout/err: `/tmp/clawdbot/clawdbot-gateway.log`
Default LaunchAgent env: Default LaunchAgent env:
- `CLAWDBOT_IMAGE_BACKEND=sips` (avoid sharp native addon under bun) - `CLAWDBOT_IMAGE_BACKEND=sips` (avoid sharp native addon inside the bundle)
## Codesigning (hardened runtime + bun) ## Codesigning (hardened runtime + Node)
Symptom (when mis-signed): Node uses JIT. The bundled runtime is signed with:
- `Ran out of executable memory …` on launch - `com.apple.security.cs.allow-jit`
- `com.apple.security.cs.allow-unsigned-executable-memory`
Fix: This is applied by `scripts/codesign-mac-app.sh`.
- The bun executable needs JIT-ish permissions under hardened runtime.
- `scripts/codesign-mac-app.sh` signs `Relay/clawdbot` with:
- `com.apple.security.cs.allow-jit`
- `com.apple.security.cs.allow-unsigned-executable-memory`
## Image processing under bun ## Image processing
Problem: To avoid shipping native `sharp` addons inside the bundle, the gateway defaults
- bun cant load some native Node addons like `sharp` (and we dont want to ship native addon trees for the gateway). to `/usr/bin/sips` for image ops when run from the app (via launchd env + wrapper).
Solution:
- Image operations prefer `/usr/bin/sips` on macOS (especially under bun).
- When running in Node/dev, `sharp` is used when available.
- This affects inbound/outbound media, screenshots, and tool image sanitization.
## Browser control server
The Gateway starts the browser control server (loopback only) from the relay daemon process,
so the relay binary includes Playwright deps.
## Tests / smoke checks ## Tests / smoke checks
@@ -115,17 +112,3 @@ Then, in another shell:
```bash ```bash
pnpm -s clawdbot gateway call health --url ws://127.0.0.1:18999 --timeout 3000 pnpm -s clawdbot gateway call health --url ws://127.0.0.1:18999 --timeout 3000
``` ```
## Repo hygiene
Bun may leave dotfiles like `*.bun-build` in the repo root or subfolders.
- These are ignored via `.gitignore` (`*.bun-build`).
## DMG styling (human installer)
`scripts/create-dmg.sh` styles the DMG via Finder AppleScript.
Rules of thumb:
- Use a **72dpi** background image that matches the Finder window size in points.
- Preferred asset: `assets/dmg-background-small.png` (**500×320**).
- Default icon positions: app `{125,160}`, Applications `{375,160}`.

View File

@@ -13,10 +13,7 @@ Before building the app, ensure you have the following installed:
1. **Xcode 26.2+**: Required for Swift development. 1. **Xcode 26.2+**: Required for Swift development.
2. **Node.js & pnpm**: Required for the gateway and CLI components. 2. **Node.js & pnpm**: Required for the gateway and CLI components.
3. **Bun**: Required to package the embedded gateway relay. 3. **Node**: Required to package the embedded gateway relay (the script can download a bundled runtime).
```bash
curl -fsSL https://bun.sh/install | bash
```
## 1. Initialize Submodules ## 1. Initialize Submodules
@@ -42,6 +39,8 @@ To build the macOS app and package it into `dist/Clawdbot.app`, run:
./scripts/package-mac-app.sh ./scripts/package-mac-app.sh
``` ```
Use `BUNDLED_RUNTIME=node|bun` to switch the embedded gateway runtime (default: node).
If you don't have an Apple Developer ID certificate, the script will automatically use **ad-hoc signing** (`-`). If you don't have an Apple Developer ID certificate, the script will automatically use **ad-hoc signing** (`-`).
> **Note**: Ad-hoc signed apps may trigger security prompts. If the app crashes immediately with "Abort trap 6", see the [Troubleshooting](#troubleshooting) section. > **Note**: Ad-hoc signed apps may trigger security prompts. If the app crashes immediately with "Abort trap 6", see the [Troubleshooting](#troubleshooting) section.

View File

@@ -12,7 +12,7 @@ This app is usually built from [`scripts/package-mac-app.sh`](https://github.com
- calls [`scripts/codesign-mac-app.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/codesign-mac-app.sh) to sign the main binary, bundled CLI, and app bundle so macOS treats each rebuild as the same signed bundle and keeps TCC permissions (notifications, accessibility, screen recording, mic, speech). For stable permissions, use a real signing identity; ad-hoc is opt-in and fragile (see [`docs/mac/permissions.md`](/platforms/mac/permissions)). - calls [`scripts/codesign-mac-app.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/codesign-mac-app.sh) to sign the main binary, bundled CLI, and app bundle so macOS treats each rebuild as the same signed bundle and keeps TCC permissions (notifications, accessibility, screen recording, mic, speech). For stable permissions, use a real signing identity; ad-hoc is opt-in and fragile (see [`docs/mac/permissions.md`](/platforms/mac/permissions)).
- uses `CODESIGN_TIMESTAMP=auto` by default; it enables trusted timestamps for Developer ID signatures. Set `CODESIGN_TIMESTAMP=off` to skip timestamping (offline debug builds). - uses `CODESIGN_TIMESTAMP=auto` by default; it enables trusted timestamps for Developer ID signatures. Set `CODESIGN_TIMESTAMP=off` to skip timestamping (offline debug builds).
- inject build metadata into Info.plist: `ClawdbotBuildTimestamp` (UTC) and `ClawdbotGitCommit` (short hash) so the About pane can show build, git, and debug/release channel. - inject build metadata into Info.plist: `ClawdbotBuildTimestamp` (UTC) and `ClawdbotGitCommit` (short hash) so the About pane can show build, git, and debug/release channel.
- **Packaging requires Bun**: The embedded gateway relay is compiled using `bun`. Ensure it is installed (`curl -fsSL https://bun.sh/install | bash`). - **Packaging requires Node**: The embedded gateway relay is bundled with Node. Ensure Node is available for the packaging script (or set `NODE_VERSION` to pin the download).
- reads `SIGN_IDENTITY` from the environment. Add `export SIGN_IDENTITY="Apple Development: Your Name (TEAMID)"` (or your Developer ID Application cert) to your shell rc to always sign with your cert. Ad-hoc signing requires explicit opt-in via `ALLOW_ADHOC_SIGNING=1` or `SIGN_IDENTITY="-"` (not recommended for permission testing). - reads `SIGN_IDENTITY` from the environment. Add `export SIGN_IDENTITY="Apple Development: Your Name (TEAMID)"` (or your Developer ID Application cert) to your shell rc to always sign with your cert. Ad-hoc signing requires explicit opt-in via `ALLOW_ADHOC_SIGNING=1` or `SIGN_IDENTITY="-"` (not recommended for permission testing).
## Usage ## Usage

View File

@@ -88,10 +88,11 @@ Safety:
- `cd apps/macos && swift build` - `cd apps/macos && swift build`
- `swift run Clawdbot` (or Xcode) - `swift run Clawdbot` (or Xcode)
- Package app + CLI: `scripts/package-mac-app.sh` - Package app + CLI: `scripts/package-mac-app.sh`
- Switch bundled gateway runtime with `BUNDLED_RUNTIME=node|bun` (default: node).
## Related docs ## Related docs
- [Gateway runbook](/gateway) - [Gateway runbook](/gateway)
- [Bundled bun Gateway](/platforms/mac/bun) - [Bundled Node Gateway](/platforms/mac/bun)
- [macOS permissions](/platforms/mac/permissions) - [macOS permissions](/platforms/mac/permissions)
- [Canvas](/platforms/mac/canvas) - [Canvas](/platforms/mac/canvas)

View File

@@ -141,7 +141,7 @@ Use these hubs to discover every page, including deep dives and reference docs t
- [macOS remote](https://docs.clawd.bot/platforms/mac/remote) - [macOS remote](https://docs.clawd.bot/platforms/mac/remote)
- [macOS signing](https://docs.clawd.bot/platforms/mac/signing) - [macOS signing](https://docs.clawd.bot/platforms/mac/signing)
- [macOS release](https://docs.clawd.bot/platforms/mac/release) - [macOS release](https://docs.clawd.bot/platforms/mac/release)
- [macOS bun gateway](https://docs.clawd.bot/platforms/mac/bun) - [macOS bundled gateway (Node)](https://docs.clawd.bot/platforms/mac/bun)
- [macOS XPC](https://docs.clawd.bot/platforms/mac/xpc) - [macOS XPC](https://docs.clawd.bot/platforms/mac/xpc)
- [macOS skills](https://docs.clawd.bot/platforms/mac/skills) - [macOS skills](https://docs.clawd.bot/platforms/mac/skills)
- [macOS Peekaboo](https://docs.clawd.bot/platforms/mac/peekaboo) - [macOS Peekaboo](https://docs.clawd.bot/platforms/mac/peekaboo)

View File

@@ -7,7 +7,7 @@ TIMESTAMP_MODE="${CODESIGN_TIMESTAMP:-auto}"
ENT_TMP_BASE=$(mktemp -t clawdbot-entitlements-base.XXXXXX) ENT_TMP_BASE=$(mktemp -t clawdbot-entitlements-base.XXXXXX)
ENT_TMP_APP=$(mktemp -t clawdbot-entitlements-app.XXXXXX) ENT_TMP_APP=$(mktemp -t clawdbot-entitlements-app.XXXXXX)
ENT_TMP_APP_BASE=$(mktemp -t clawdbot-entitlements-app-base.XXXXXX) ENT_TMP_APP_BASE=$(mktemp -t clawdbot-entitlements-app-base.XXXXXX)
ENT_TMP_BUN=$(mktemp -t clawdbot-entitlements-bun.XXXXXX) ENT_TMP_RUNTIME=$(mktemp -t clawdbot-entitlements-runtime.XXXXXX)
if [ ! -d "$APP_BUNDLE" ]; then if [ ! -d "$APP_BUNDLE" ]; then
echo "App bundle not found: $APP_BUNDLE" >&2 echo "App bundle not found: $APP_BUNDLE" >&2
@@ -150,7 +150,7 @@ cat > "$ENT_TMP_APP_BASE" <<'PLIST'
</plist> </plist>
PLIST PLIST
cat > "$ENT_TMP_BUN" <<'PLIST' cat > "$ENT_TMP_RUNTIME" <<'PLIST'
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
@@ -215,8 +215,11 @@ if [ -d "$APP_BUNDLE/Contents/Resources/Relay" ]; then
find "$APP_BUNDLE/Contents/Resources/Relay" -type f \( -name "*.node" -o -name "*.dylib" \) -print0 | while IFS= read -r -d '' f; do find "$APP_BUNDLE/Contents/Resources/Relay" -type f \( -name "*.node" -o -name "*.dylib" \) -print0 | while IFS= read -r -d '' f; do
echo "Signing gateway payload: $f"; sign_item "$f" "$ENT_TMP_BASE" echo "Signing gateway payload: $f"; sign_item "$f" "$ENT_TMP_BASE"
done done
if [ -f "$APP_BUNDLE/Contents/Resources/Relay/node" ]; then
echo "Signing embedded node"; sign_item "$APP_BUNDLE/Contents/Resources/Relay/node" "$ENT_TMP_RUNTIME"
fi
if [ -f "$APP_BUNDLE/Contents/Resources/Relay/clawdbot" ]; then if [ -f "$APP_BUNDLE/Contents/Resources/Relay/clawdbot" ]; then
echo "Signing embedded relay"; sign_item "$APP_BUNDLE/Contents/Resources/Relay/clawdbot" "$ENT_TMP_BUN" echo "Signing embedded relay wrapper"; sign_plain_item "$APP_BUNDLE/Contents/Resources/Relay/clawdbot"
fi fi
fi fi
@@ -246,5 +249,5 @@ fi
# Finally sign the bundle # Finally sign the bundle
sign_item "$APP_BUNDLE" "$APP_ENTITLEMENTS" sign_item "$APP_BUNDLE" "$APP_ENTITLEMENTS"
rm -f "$ENT_TMP_BASE" "$ENT_TMP_APP_BASE" "$ENT_TMP_APP" "$ENT_TMP_BUN" rm -f "$ENT_TMP_BASE" "$ENT_TMP_APP_BASE" "$ENT_TMP_APP" "$ENT_TMP_RUNTIME"
echo "Codesign complete for $APP_BUNDLE" echo "Codesign complete for $APP_BUNDLE"

View File

@@ -22,6 +22,7 @@ if [[ "${BUILD_ARCHS_VALUE}" == "all" ]]; then
fi fi
IFS=' ' read -r -a BUILD_ARCHS <<< "$BUILD_ARCHS_VALUE" IFS=' ' read -r -a BUILD_ARCHS <<< "$BUILD_ARCHS_VALUE"
PRIMARY_ARCH="${BUILD_ARCHS[0]}" PRIMARY_ARCH="${BUILD_ARCHS[0]}"
BUNDLED_RUNTIME="${BUNDLED_RUNTIME:-node}"
SPARKLE_PUBLIC_ED_KEY="${SPARKLE_PUBLIC_ED_KEY:-AGCY8w5vHirVfGGDGc8Szc5iuOqupZSh9pMj/Qs67XI=}" SPARKLE_PUBLIC_ED_KEY="${SPARKLE_PUBLIC_ED_KEY:-AGCY8w5vHirVfGGDGc8Szc5iuOqupZSh9pMj/Qs67XI=}"
SPARKLE_FEED_URL="${SPARKLE_FEED_URL:-https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml}" SPARKLE_FEED_URL="${SPARKLE_FEED_URL:-https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml}"
AUTO_CHECKS=true AUTO_CHECKS=true
@@ -136,6 +137,116 @@ build_relay_binary() {
fi fi
} }
resolve_node_version() {
if [[ -n "${NODE_VERSION:-}" ]]; then
echo "${NODE_VERSION#v}"
return
fi
local mirror="${NODE_DIST_MIRROR:-https://nodejs.org/dist}"
local latest
if latest="$(/usr/bin/curl -fsSL "$mirror/index.tab" 2>/dev/null | /usr/bin/awk 'NR==2 {print $1}')" && [[ -n "$latest" ]]; then
echo "${latest#v}"
return
fi
if command -v node >/dev/null 2>&1; then
node -p "process.versions.node"
return
fi
echo "22.12.0"
}
node_dist_filename() {
local version="$1"
local arch="$2"
local node_arch="$arch"
if [[ "$arch" == "x86_64" ]]; then
node_arch="x64"
fi
echo "node-v${version}-darwin-${node_arch}.tar.gz"
}
download_node_binary() {
local version="$1"
local arch="$2"
local out="$3"
local mirror="${NODE_DIST_MIRROR:-https://nodejs.org/dist}"
local tarball
tarball="$(node_dist_filename "$version" "$arch")"
local tmp_dir
tmp_dir="$(mktemp -d)"
local url="$mirror/v${version}/${tarball}"
echo "⬇️ Downloading Node ${version} (${arch})"
/usr/bin/curl -fsSL "$url" -o "$tmp_dir/node.tgz"
/usr/bin/tar -xzf "$tmp_dir/node.tgz" -C "$tmp_dir"
local node_arch="$arch"
if [[ "$arch" == "x86_64" ]]; then
node_arch="x64"
fi
local node_src="$tmp_dir/node-v${version}-darwin-${node_arch}/bin/node"
if [[ ! -f "$node_src" ]]; then
echo "ERROR: Node binary missing in $tarball" >&2
rm -rf "$tmp_dir"
exit 1
fi
cp "$node_src" "$out"
chmod +x "$out"
rm -rf "$tmp_dir"
}
stage_relay_payload() {
local relay_dir="$1"
if [[ "${SKIP_RELAY_DEPS:-0}" != "1" ]]; then
local stage_dir="$relay_dir/.relay-deploy"
rm -rf "$stage_dir"
mkdir -p "$stage_dir"
echo "📦 Staging relay dependencies (pnpm deploy --prod --no-optional --legacy)"
(cd "$ROOT_DIR" && pnpm --filter . deploy "$stage_dir" --prod --no-optional --legacy)
rm -rf "$relay_dir/node_modules"
cp -a "$stage_dir/node_modules" "$relay_dir/node_modules"
rm -rf "$stage_dir"
else
echo "📦 Skipping relay dependency staging (SKIP_RELAY_DEPS=1)"
fi
echo "📦 Copying relay dist payload"
rm -rf "$relay_dir/dist"
cp -R "$ROOT_DIR/dist" "$relay_dir/dist"
}
write_relay_wrapper() {
local relay_dir="$1"
local wrapper="$relay_dir/clawdbot"
cat > "$wrapper" <<SH
#!/bin/sh
set -e
DIR="\$(cd "\$(dirname "\$0")" && pwd)"
NODE="\$DIR/node"
REL="\$DIR/dist/macos/relay.js"
export CLAWDBOT_BUNDLED_VERSION="\${CLAWDBOT_BUNDLED_VERSION:-$PKG_VERSION}"
export CLAWDBOT_IMAGE_BACKEND="\${CLAWDBOT_IMAGE_BACKEND:-sips}"
NODE_PATH="\$DIR/node_modules\${NODE_PATH:+:\$NODE_PATH}"
export NODE_PATH
exec "\$NODE" "\$REL" "\$@"
SH
chmod +x "$wrapper"
}
validate_bundled_runtime() {
case "$BUNDLED_RUNTIME" in
node|bun) return 0 ;;
*)
echo "ERROR: Unsupported BUNDLED_RUNTIME=$BUNDLED_RUNTIME (use node|bun)" >&2
exit 1
;;
esac
}
echo "📦 Ensuring deps (pnpm install)" echo "📦 Ensuring deps (pnpm install)"
(cd "$ROOT_DIR" && pnpm install --no-frozen-lockfile --config.node-linker=hoisted) (cd "$ROOT_DIR" && pnpm install --no-frozen-lockfile --config.node-linker=hoisted)
if [[ "${SKIP_TSC:-0}" != "1" ]]; then if [[ "${SKIP_TSC:-0}" != "1" ]]; then
@@ -249,31 +360,58 @@ fi
RELAY_DIR="$APP_ROOT/Contents/Resources/Relay" RELAY_DIR="$APP_ROOT/Contents/Resources/Relay"
if [[ "${SKIP_GATEWAY_PACKAGE:-0}" != "1" ]]; then if [[ "${SKIP_GATEWAY_PACKAGE:-0}" != "1" ]]; then
if ! command -v bun >/dev/null 2>&1; then validate_bundled_runtime
echo "ERROR: bun missing. Install bun to package the embedded gateway." >&2
exit 1
fi
echo "🧰 Building bundled relay (bun --compile)"
mkdir -p "$RELAY_DIR" mkdir -p "$RELAY_DIR"
RELAY_OUT="$RELAY_DIR/clawdbot" RELAY_CMD="$RELAY_DIR/clawdbot"
RELAY_BUILD_DIR="$RELAY_DIR/.relay-build"
rm -rf "$RELAY_BUILD_DIR" if [[ "$BUNDLED_RUNTIME" == "bun" ]]; then
mkdir -p "$RELAY_BUILD_DIR" if ! command -v bun >/dev/null 2>&1; then
for arch in "${BUILD_ARCHS[@]}"; do echo "ERROR: bun missing. Install bun or set BUNDLED_RUNTIME=node." >&2
RELAY_ARCH_OUT="$RELAY_BUILD_DIR/clawdbot-$arch" exit 1
build_relay_binary "$arch" "$RELAY_ARCH_OUT" fi
chmod +x "$RELAY_ARCH_OUT"
done echo "🧰 Building bundled relay (bun --compile)"
if [[ "${#BUILD_ARCHS[@]}" -gt 1 ]]; then RELAY_BUILD_DIR="$RELAY_DIR/.relay-build"
/usr/bin/lipo -create "$RELAY_BUILD_DIR"/clawdbot-* -output "$RELAY_OUT" rm -rf "$RELAY_BUILD_DIR"
mkdir -p "$RELAY_BUILD_DIR"
for arch in "${BUILD_ARCHS[@]}"; do
RELAY_ARCH_OUT="$RELAY_BUILD_DIR/clawdbot-$arch"
build_relay_binary "$arch" "$RELAY_ARCH_OUT"
chmod +x "$RELAY_ARCH_OUT"
done
if [[ "${#BUILD_ARCHS[@]}" -gt 1 ]]; then
/usr/bin/lipo -create "$RELAY_BUILD_DIR"/clawdbot-* -output "$RELAY_CMD"
else
cp "$RELAY_BUILD_DIR/clawdbot-${BUILD_ARCHS[0]}" "$RELAY_CMD"
fi
rm -rf "$RELAY_BUILD_DIR"
else else
cp "$RELAY_BUILD_DIR/clawdbot-${BUILD_ARCHS[0]}" "$RELAY_OUT" NODE_VERSION="$(resolve_node_version)"
echo "🧰 Preparing bundled Node runtime (v${NODE_VERSION})"
RELAY_NODE="$RELAY_DIR/node"
RELAY_NODE_BUILD_DIR="$RELAY_DIR/.node-build"
rm -rf "$RELAY_NODE_BUILD_DIR"
mkdir -p "$RELAY_NODE_BUILD_DIR"
for arch in "${BUILD_ARCHS[@]}"; do
NODE_ARCH_OUT="$RELAY_NODE_BUILD_DIR/node-$arch"
download_node_binary "$NODE_VERSION" "$arch" "$NODE_ARCH_OUT"
done
if [[ "${#BUILD_ARCHS[@]}" -gt 1 ]]; then
/usr/bin/lipo -create "$RELAY_NODE_BUILD_DIR"/node-* -output "$RELAY_NODE"
else
cp "$RELAY_NODE_BUILD_DIR/node-${BUILD_ARCHS[0]}" "$RELAY_NODE"
fi
chmod +x "$RELAY_NODE"
if [[ "${STRIP_NODE:-1}" == "1" ]]; then
/usr/bin/strip -x "$RELAY_NODE" 2>/dev/null || true
fi
rm -rf "$RELAY_NODE_BUILD_DIR"
stage_relay_payload "$RELAY_DIR"
write_relay_wrapper "$RELAY_DIR"
fi fi
rm -rf "$RELAY_BUILD_DIR"
echo "🧪 Verifying bundled relay (version)" echo "🧪 Verifying bundled relay (version)"
"$RELAY_OUT" --version >/dev/null "$RELAY_CMD" --version >/dev/null
echo "🎨 Copying gateway A2UI host assets" echo "🎨 Copying gateway A2UI host assets"
rm -rf "$RELAY_DIR/a2ui" rm -rf "$RELAY_DIR/a2ui"
@@ -292,6 +430,7 @@ if [[ "${SKIP_GATEWAY_PACKAGE:-0}" != "1" ]]; then
{ {
"name": "clawdbot-embedded", "name": "clawdbot-embedded",
"version": "$PKG_VERSION", "version": "$PKG_VERSION",
"type": "module",
"piConfig": { "piConfig": {
"name": "pi", "name": "pi",
"configDir": ".pi" "configDir": ".pi"

View File

@@ -57,7 +57,7 @@ function candidateBinDirs(opts: EnsureClawdbotPathOpts): string[] {
const candidates: string[] = []; const candidates: string[] = [];
// Bun bundled (macOS app): `clawdbot` lives in the Relay dir (process.execPath). // Bundled macOS app: `clawdbot` lives in the Relay dir (process.execPath).
try { try {
const execDir = path.dirname(execPath); const execDir = path.dirname(execPath);
const siblingClawdbot = path.join(execDir, "clawdbot"); const siblingClawdbot = path.join(execDir, "clawdbot");
@@ -95,7 +95,7 @@ function candidateBinDirs(opts: EnsureClawdbotPathOpts): string[] {
/** /**
* Best-effort PATH bootstrap so skills that require the `clawdbot` CLI can run * Best-effort PATH bootstrap so skills that require the `clawdbot` CLI can run
* under launchd/minimal environments (and inside the macOS bun bundle). * under launchd/minimal environments (and inside the macOS app bundle).
*/ */
export function ensureClawdbotCliOnPath(opts: EnsureClawdbotPathOpts = {}) { export function ensureClawdbotCliOnPath(opts: EnsureClawdbotPathOpts = {}) {
if (process.env.CLAWDBOT_PATH_BOOTSTRAPPED === "1") return; if (process.env.CLAWDBOT_PATH_BOOTSTRAPPED === "1") return;

View File

@@ -4,7 +4,9 @@ import process from "node:process";
declare const __CLAWDBOT_VERSION__: string; declare const __CLAWDBOT_VERSION__: string;
const BUNDLED_VERSION = const BUNDLED_VERSION =
typeof __CLAWDBOT_VERSION__ === "string" ? __CLAWDBOT_VERSION__ : "0.0.0"; (typeof __CLAWDBOT_VERSION__ === "string" && __CLAWDBOT_VERSION__) ||
process.env.CLAWDBOT_BUNDLED_VERSION ||
"0.0.0";
function argValue(args: string[], flag: string): string | undefined { function argValue(args: string[], flag: string): string | undefined {
const idx = args.indexOf(flag); const idx = args.indexOf(flag);

View File

@@ -4,7 +4,9 @@ import process from "node:process";
declare const __CLAWDBOT_VERSION__: string | undefined; declare const __CLAWDBOT_VERSION__: string | undefined;
const BUNDLED_VERSION = const BUNDLED_VERSION =
typeof __CLAWDBOT_VERSION__ === "string" ? __CLAWDBOT_VERSION__ : "0.0.0"; (typeof __CLAWDBOT_VERSION__ === "string" && __CLAWDBOT_VERSION__) ||
process.env.CLAWDBOT_BUNDLED_VERSION ||
"0.0.0";
function hasFlag(args: string[], flag: string): boolean { function hasFlag(args: string[], flag: string): boolean {
return args.includes(flag); return args.includes(flag);