From 070b10351e3fcb0b742052c443a4ea89b5db40ad Mon Sep 17 00:00:00 2001 From: chaodu-agent <274062505+chaodu-agent@users.noreply.github.com> Date: Fri, 13 Nov 2026 22:08:49 -0500 Subject: [PATCH 01/13] feat!: rename oab-instance-mcp to oab-computer (breaking, 0.12.0) Renames every user-visible technical name; the product name stays OpenAB Computer. No compatibility layer. Base 1eaa6ef1d810b507c6747547dbc2cc180c1afbd8. - executable/product/target oab-computer (Sources/oab-computer), app oab-computer.app - bundle id and LaunchAgent dev.openab.computer; pw-mcp agent dev.openab.computer.pw-mcp - Linux units oab-computer.service, oab-computer-pw-mcp.service - dirs ~/.local|~/.config|~/Library/Logs|Application Support|XDG state /oab-computer - Keychain service dev.openab.instance-mcp.grant -> dev.openab.computer.grant - env OAB_INSTANCE_MCP_* -> OAB_COMPUTER_*; pkg id dev.openab.computer.installer; pkg payload dir Library/Application Support/OpenAB/computer; release assets oab-computer-* - kept: InstanceMCPCore module and tests, the copy-source token path, issue links, history - new: warn-only detection of the old agent in both installers (never stops it) - new: scripts/test-name-consistency.sh, scripts/test-legacy-agent.sh (wired into CI) - README "Upgrading from 0.11 or earlier"; banners on dated ADR/requirements docs Verification (macmini, macOS 27.2.1, Xcode 26.3; Linux parts in OrbStack containers): - swift build -c release: ok; swift test: 507 tests, 0 failures, 1 skipped (the default-port test skips because the installed agent holds 8795) - release binary has no OAB_COMPUTER_NO_TAILNET_EXPOSURE string (0 matches) - cargo build/test on macOS fails to link (Linux-only crate), identical on main; in rust:1-bookworm: cargo build ok, cargo test 86 passed 0 failed 1 ignored, fmt ok, clippy ok, smoke.sh 71 passed 0 failed - scripts/test-name-consistency.sh: all checks passed; negative mutation check fails as intended - scripts/test-legacy-agent.sh: all checks passed (macOS stub launchctl, Linux stub systemctl) - scripts/test-pw-mcp-check.sh: stub tests ok - scripts/test-install-linux.sh: ok in debian:bookworm as non-root (units diff, token); on macOS it fails only on /private realpath and stat -c - scripts/test-packaging.sh: runs through the unsigned-app gate, the token-copy cases and bad tailscale cases, then fails at pkgbuild (BundleIsRelocatable) on macOS 27, identical on main - shellcheck (docker) clean on the touched and new scripts; workflows parse as YAML --- .github/workflows/ci.yml | 12 +- .github/workflows/release.yml | 40 ++-- Package.swift | 10 +- README.md | 142 ++++++++---- Sources/InstanceMCPCore/AttachManager.swift | 2 +- Sources/InstanceMCPCore/FirstRunSetup.swift | 12 +- Sources/InstanceMCPCore/GrantStore.swift | 10 +- Sources/InstanceMCPCore/MCPHTTPEndpoint.swift | 2 +- Sources/InstanceMCPCore/MenuModel.swift | 2 +- Sources/InstanceMCPCore/ProfileAudit.swift | 4 +- .../InstanceMCPCore/SwitchboardConfig.swift | 4 +- Sources/InstanceMCPCore/ToolProfile.swift | 2 +- .../Tools/AsyncExecTools.swift | 4 +- .../InstanceMCPCore/Tools/ExecSupport.swift | 2 +- Sources/InstanceMCPCore/Tools/ExecTool.swift | 2 +- Sources/InstanceMCPCore/Tools/Input.swift | 2 +- .../InstanceMCPCore/Tools/JobRegistry.swift | 4 +- .../Tools/ScreenshotTool.swift | 2 +- .../InstanceMCPCore/Tools/SysInfoTool.swift | 8 +- .../InstanceMCPCore/Tools/UIActionTool.swift | 2 +- .../Tools/UISnapshotTool.swift | 2 +- Sources/InstanceMCPCore/UpstreamMCP.swift | 2 +- .../PermissionSetupWindowController.swift | 0 .../StatusItemController.swift | 0 .../SwitchboardMenu.swift | 0 .../main.swift | 20 +- .../FirstRunSetupTests.swift | 36 ++- .../InstanceMCPCoreTests/MenuModelTests.swift | 12 +- .../PermissionRelaunchTests.swift | 2 +- .../TailnetExposureTests.swift | 4 +- .../TailscaleDetectionTests.swift | 4 +- .../UISnapshotTests.swift | 6 +- docs/adr/linux-rust-port.md | 2 + docs/adr/reverse-attach.md | 2 + docs/adr/sandbox-adapter.md | 2 + docs/linux-setup.md | 82 +++---- docs/operators.md | 2 +- docs/oracle-cloud-refarch.md | 10 + docs/parity-checklist.md | 2 +- docs/releasing.md | 46 ++-- docs/requirements/authn.md | 2 + docs/requirements/connect-closed-loop.md | 2 + docs/requirements/reverse-attach.md | 2 + docs/tool-profiles.md | 10 +- images/toolchain/Dockerfile | 2 +- poc/pw-mcp/README.md | 34 +-- poc/pw-mcp/check-legacy-mapping.sh | 4 +- poc/pw-mcp/install.sh | 12 +- poc/pw-mcp/pw-mcp.sh | 4 +- poc/reverse-attach-linux/README.md | 14 +- poc/reverse-attach-linux/pw-mcp.sh | 2 +- poc/reverse-attach-linux/smoke.sh | 4 +- poc/reverse-attach-linux/src/attach/store.rs | 6 +- poc/reverse-attach-linux/src/audit.rs | 6 +- poc/reverse-attach-linux/src/jobs/mod.rs | 6 +- poc/reverse-attach-linux/src/jobs/tools.rs | 2 +- poc/reverse-attach-linux/src/mcp.rs | 4 +- poc/reverse-attach-linux/src/tools/sysinfo.rs | 4 +- scripts/assemble-app.sh | 20 +- scripts/deploy.sh | 6 +- scripts/install-linux.sh | 53 +++-- scripts/install-prebuilt.sh | 53 +++-- scripts/package-pkg.sh | 10 +- scripts/pkg/postinstall | 18 +- scripts/release-version.sh | 2 +- scripts/smoke.sh | 6 +- scripts/test-install-linux.sh | 52 ++--- scripts/test-legacy-agent.sh | 155 +++++++++++++ scripts/test-name-consistency.sh | 215 ++++++++++++++++++ scripts/test-packaging.sh | 34 +-- scripts/test-pw-mcp-check.sh | 2 +- scripts/verify-release.sh | 6 +- 72 files changed, 886 insertions(+), 370 deletions(-) rename Sources/{oab-instance-mcp => oab-computer}/PermissionSetupWindowController.swift (100%) rename Sources/{oab-instance-mcp => oab-computer}/StatusItemController.swift (100%) rename Sources/{oab-instance-mcp => oab-computer}/SwitchboardMenu.swift (100%) rename Sources/{oab-instance-mcp => oab-computer}/main.swift (97%) create mode 100755 scripts/test-legacy-agent.sh create mode 100755 scripts/test-name-consistency.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 92aa592..c0baea6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,11 @@ jobs: - name: Build run: swift build - name: Packaging smoke (unsigned; release signing is tag-only) - run: scripts/test-packaging.sh .build/debug/oab-instance-mcp + run: scripts/test-packaging.sh .build/debug/oab-computer + - name: Name consistency and warn-only old-agent check (stub launchctl/systemctl) + run: | + bash scripts/test-name-consistency.sh + bash scripts/test-legacy-agent.sh - name: Test run: swift test @@ -81,10 +85,14 @@ jobs: steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: ShellCheck - run: shellcheck scripts/install-linux.sh scripts/test-install-linux.sh + run: shellcheck scripts/install-linux.sh scripts/test-install-linux.sh scripts/test-name-consistency.sh scripts/test-legacy-agent.sh - name: Generated units (fake HOME, stubbed systemctl/tailscale/loginctl) # Diffs each written unit against its expected text and runs systemd-analyze verify (#90). run: bash scripts/test-install-linux.sh + - name: Names agree between installer, units and release assets; old agent only warned about + run: | + bash scripts/test-name-consistency.sh + bash scripts/test-legacy-agent.sh linux-hands-node: name: Linux hands node (${{ matrix.arch }}) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 30569da..e1a62ac 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ permissions: contents: write concurrency: - group: instance-mcp-release-${{ github.ref }} + group: oab-computer-release-${{ github.ref }} cancel-in-progress: false jobs: @@ -73,13 +73,13 @@ jobs: swift build -c release --arch arm64 --scratch-path "$RUNNER_TEMP/build-arm64" swift build -c release --arch x86_64 --scratch-path "$RUNNER_TEMP/build-x86_64" /usr/bin/lipo -create \ - "$RUNNER_TEMP/build-arm64/release/oab-instance-mcp" \ - "$RUNNER_TEMP/build-x86_64/release/oab-instance-mcp" \ - -output "$DIST/oab-instance-mcp" - chmod 755 "$DIST/oab-instance-mcp" - /usr/bin/lipo -info "$DIST/oab-instance-mcp" | grep -q 'x86_64 arm64\|arm64 x86_64' - [ "$($DIST/oab-instance-mcp --version)" = "$VERSION" ] - scripts/assemble-app.sh "$DIST/oab-instance-mcp" "$DIST/oab-instance-mcp.app" "$VERSION" + "$RUNNER_TEMP/build-arm64/release/oab-computer" \ + "$RUNNER_TEMP/build-x86_64/release/oab-computer" \ + -output "$DIST/oab-computer" + chmod 755 "$DIST/oab-computer" + /usr/bin/lipo -info "$DIST/oab-computer" | grep -q 'x86_64 arm64\|arm64 x86_64' + [ "$($DIST/oab-computer --version)" = "$VERSION" ] + scripts/assemble-app.sh "$DIST/oab-computer" "$DIST/oab-computer.app" "$VERSION" - name: Import Developer ID certificates shell: bash @@ -110,7 +110,7 @@ jobs: shell: bash run: | set -euo pipefail - APP="$DIST/oab-instance-mcp.app" + APP="$DIST/oab-computer.app" /usr/bin/codesign --force --deep --options runtime --timestamp \ --keychain "$KEYCHAIN" --sign "$APP_SIGN_IDENTITY" "$APP" /usr/bin/codesign --verify --deep --strict --verbose=2 "$APP" @@ -122,18 +122,18 @@ jobs: --key "$RUNNER_TEMP/AuthKey.p8" --key-id "$NOTARY_KEY_ID" --issuer "$NOTARY_ISSUER_ID" --wait xcrun stapler staple "$APP" xcrun stapler validate "$APP" - rm -f "$DIST/oab-instance-mcp-$VERSION-universal.app.zip" + rm -f "$DIST/oab-computer-$VERSION-universal.app.zip" /usr/bin/ditto -c -k --sequesterRsrc --keepParent "$APP" \ - "$DIST/oab-instance-mcp-$VERSION-universal.app.zip" + "$DIST/oab-computer-$VERSION-universal.app.zip" - name: Build, sign, and notarize installer pkg shell: bash run: | set -euo pipefail - PKG="$DIST/oab-instance-mcp-$VERSION-universal.pkg" + PKG="$DIST/oab-computer-$VERSION-universal.pkg" EXPECT_TEAM="$EXPECTED_TEAM_ID" PKG_SIGN_IDENTITY="$INSTALLER_SIGN_IDENTITY" \ PKG_KEYCHAIN="$KEYCHAIN" \ - scripts/package-pkg.sh "$DIST/oab-instance-mcp.app" "$PKG" "$VERSION" + scripts/package-pkg.sh "$DIST/oab-computer.app" "$PKG" "$VERSION" /usr/sbin/pkgutil --check-signature "$PKG" | grep -q 'Developer ID Installer' xcrun notarytool submit "$PKG" \ --key "$RUNNER_TEMP/AuthKey.p8" --key-id "$NOTARY_KEY_ID" --issuer "$NOTARY_ISSUER_ID" --wait @@ -141,7 +141,7 @@ jobs: xcrun stapler validate "$PKG" /usr/sbin/spctl -a -vv --type install "$PKG" scripts/verify-release.sh \ - "$DIST/oab-instance-mcp-$VERSION-universal.app.zip" "$PKG" "$EXPECTED_TEAM_ID" + "$DIST/oab-computer-$VERSION-universal.app.zip" "$PKG" "$EXPECTED_TEAM_ID" - name: Checksums and release shell: bash @@ -149,14 +149,14 @@ jobs: set -euo pipefail cd "$DIST" /usr/bin/shasum -a 256 \ - "oab-instance-mcp-$VERSION-universal.app.zip" \ - "oab-instance-mcp-$VERSION-universal.pkg" > SHA256SUMS + "oab-computer-$VERSION-universal.app.zip" \ + "oab-computer-$VERSION-universal.pkg" > SHA256SUMS gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1 || \ gh release create "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \ --verify-tag --generate-notes --title "oab-computer $VERSION" gh release upload "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --clobber \ - "oab-instance-mcp-$VERSION-universal.app.zip" \ - "oab-instance-mcp-$VERSION-universal.pkg" SHA256SUMS + "oab-computer-$VERSION-universal.app.zip" \ + "oab-computer-$VERSION-universal.pkg" SHA256SUMS - name: Clean signing material if: always() @@ -219,9 +219,9 @@ jobs: shell: bash run: | set -euo pipefail - NAME="oab-instance-mcp-$VERSION-linux-${{ matrix.arch }}" + NAME="oab-computer-$VERSION-linux-${{ matrix.arch }}" mkdir -p "$NAME" - cp poc/reverse-attach-linux/target/release/reverse-attach "$NAME/oab-instance-mcp" + cp poc/reverse-attach-linux/target/release/reverse-attach "$NAME/oab-computer" cp poc/reverse-attach-linux/pw-mcp.sh scripts/install-linux.sh "$NAME/" cp poc/reverse-attach-linux/README.md "$NAME/README.md" tar czf "$NAME.tar.gz" "$NAME" diff --git a/Package.swift b/Package.swift index fa76871..624025b 100644 --- a/Package.swift +++ b/Package.swift @@ -2,10 +2,10 @@ import PackageDescription let package = Package( - name: "oab-instance-mcp", + name: "oab-computer", platforms: [.macOS(.v14)], products: [ - .executable(name: "oab-instance-mcp", targets: ["oab-instance-mcp"]), + .executable(name: "oab-computer", targets: ["oab-computer"]), .library(name: "InstanceMCPCore", targets: ["InstanceMCPCore"]), ], targets: [ @@ -23,15 +23,15 @@ let package = Package( ), // Thin CLI: flag parsing + wiring. No logic worth testing lives here. .executableTarget( - name: "oab-instance-mcp", + name: "oab-computer", dependencies: ["InstanceMCPCore"], - path: "Sources/oab-instance-mcp" + path: "Sources/oab-computer" ), .testTarget( name: "InstanceMCPCoreTests", // The executable is a dependency only so it is built beside the tests: one test runs it with the hidden // permission-check argument. - dependencies: ["InstanceMCPCore", "oab-instance-mcp"], + dependencies: ["InstanceMCPCore", "oab-computer"], path: "Tests/InstanceMCPCoreTests" ), ] diff --git a/README.md b/README.md index 7f958db..ff1e936 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# OpenAB Computer (`oab-instance-mcp`) +# OpenAB Computer (`oab-computer`) -> **Name.** The product is now OpenAB Computer (repository `openabdev/openab-computer`, tracked in -> [#63](https://github.com/openabdev/openab-computer/issues/63)). The executable, bundle id, release -> asset names, config directory and Linux unit names keep the old `instance-mcp` names on purpose: -> macOS TCC grants are keyed to the bundle id `dev.openab.instance-mcp`, and changing it would drop -> every grant. Commands in this README use those names. +> **Name.** The product is OpenAB Computer (repository `openabdev/openab-computer`, tracked in +> [#63](https://github.com/openabdev/openab-computer/issues/63)). Since 0.12.0 the executable, bundle +> id (`dev.openab.computer`), LaunchAgent, systemd units, directories and release assets are all named +> `oab-computer`. It was `oab-instance-mcp` up to 0.11; see +> [Upgrading from 0.11 or earlier](#upgrading-from-011-or-earlier). Commands in this README use the new names. The "hands and feet" half of [openabdev/openab#1544](https://github.com/openabdev/openab/issues/1544): a thin daemon that lives in a Mac's logged-in desktop session and exposes the machine to a @@ -33,7 +33,7 @@ flowchart LR s8444[":8444 → 127.0.0.1:8795
injects Tailscale-User-Login"] end subgraph gui["Aqua session · LaunchAgents in gui/501"] - mcp["oab-instance-mcp (Swift)
auth = allow-login AND bearer token
sys_info · exec · exec_start/poll/list/cancel
screenshot · ui_snapshot · ui_action · mouse · key · osascript"] + mcp["oab-computer (Swift)
auth = allow-login AND bearer token
sys_info · exec · exec_start/poll/list/cancel
screenshot · ui_snapshot · ui_action · mouse · key · osascript"] pw["@playwright/mcp
headed Chromium, persistent profile"] end dial["--switchboard client
dials out to /vm/attach"] @@ -43,7 +43,7 @@ flowchart LR input["Input
CGEvent mouse / keyboard"] apps["Apps
osascript / JXA"] end - tcc{{"TCC grants, once, on the Mac's own screen
Screen Recording · Accessibility · Full Disk Access
bound to bundle id dev.openab.instance-mcp"}} + tcc{{"TCC grants, once, on the Mac's own screen
Screen Recording · Accessibility · Full Disk Access
bound to bundle id dev.openab.computer"}} end cli -- "HTTPS · MCP Streamable HTTP" --> s8444 @@ -74,11 +74,11 @@ this exists for what SSH cannot reach. - [Goal](#goal) · [Features](#features) · [Status and known limits](#status-and-known-limits) · [Quick start](#quick-start) · [Platforms](#platforms) - [Tools](#tools) · [Auth](#auth) · [Network interfaces and ports](#network-interfaces-and-ports) - [Reverse attach](#lending-this-mac-to-a-sandboxed-agent-reverse-attach) · [Switchboard](#serving-a-switchboard---switchboard) · [Browser tools](#browser-tools-for-lent-sessions---upstream) -- Deploy & operate: [Download and install](#download-and-install) · [Build & test](#build--test-on-the-build-mac-the-laptop-never-compiles-swift) · [Deploy](#deploy-run-on-the-target) · [Menu bar](#menu-bar) · [Operate](#operate) +- Deploy & operate: [Download and install](#download-and-install) · [Upgrading from 0.11](#upgrading-from-011-or-earlier) · [Build & test](#build--test-on-the-build-mac-the-laptop-never-compiles-swift) · [Deploy](#deploy-run-on-the-target) · [Menu bar](#menu-bar) · [Operate](#operate) - Operator notes: [TCC & signing](#tcc-grants-survive-re-deploys-only-if-the-signature-does) · [Gotchas](#gotchas) - Detail: [UI tools](docs/ui-tools.md) · [Operator notes and gotchas](docs/operators.md) -Components: `oab-instance-mcp` is this package (Swift on macOS, zero deps; Rust on Linux). The +Components: `oab-computer` is this package (Swift on macOS, zero deps; Rust on Linux). The browser server is `@playwright/mcp`, not ours ([`poc/pw-mcp/README.md`](poc/pw-mcp/README.md)). Design notes: [`docs/requirements/connect-closed-loop.md`](docs/requirements/connect-closed-loop.md). @@ -124,7 +124,7 @@ The accessibility-tree tools are macOS only for now; Linux is deferred ## Quick start -**macOS** — download `oab-instance-mcp-VERSION-universal.pkg` from +**macOS** — download `oab-computer-VERSION-universal.pkg` from [Releases](https://github.com/openabdev/openab-computer/releases), double-click, and follow the permissions wizard ([details](#download-and-install)). Then point a caller at it: @@ -152,13 +152,13 @@ curl -s https://..ts.net/mcp/ \ ```sh V=$(curl -fsSL https://api.github.com/repos/openabdev/openab-computer/releases/latest | python3 -c 'import sys,json;print(json.load(sys.stdin)["tag_name"][1:])') A=$(dpkg --print-architecture) # arm64 | amd64 -curl -fsSLO "https://github.com/openabdev/openab-computer/releases/download/v$V/oab-instance-mcp-$V-linux-$A.tar.gz" -tar xzf oab-instance-mcp-$V-linux-$A.tar.gz && cd oab-instance-mcp-$V-linux-$A +curl -fsSLO "https://github.com/openabdev/openab-computer/releases/download/v$V/oab-computer-$V-linux-$A.tar.gz" +tar xzf oab-computer-$V-linux-$A.tar.gz && cd oab-computer-$V-linux-$A ./install-linux.sh # a box with no desktop: add --headless-seat ``` The URL is `https://..ts.net:8444/mcp` and the bearer token is in -`~/.config/oab-instance-mcp/token` on the node. +`~/.config/oab-computer/token` on the node. ## Platforms @@ -176,7 +176,7 @@ The URL is `https://..ts.net:8444/mcp` and the bearer token is in The rest of this README is the macOS reference; the two platforms share the wire contract, auth and reverse-attach shape, so callers configure them identically. On Linux the job tools (`exec_start`, `exec_poll`, `exec_list`, `exec_cancel`) take the same arguments and return the -same `structuredContent`; their logs are under `$XDG_STATE_HOME/oab-instance-mcp/jobs/` +same `structuredContent`; their logs are under `$XDG_STATE_HOME/oab-computer/jobs/` (default `~/.local/state/…`), and they are `owner`-only there too ([`docs/linux-setup.md`](docs/linux-setup.md#background-jobs)). @@ -186,7 +186,7 @@ same `structuredContent`; their logs are under `$XDG_STATE_HOME/oab-instance-mcp |---|---|---| | `sys_info` | host, OS, chip, displays, tailnet IPs, console user, TCC status | call first; tells the model what will work | | `exec` | `zsh -f -c ` as the desktop user | `cwd` (a leading `~` is expanded and validated — a missing dir or unreadable external volume returns a clear error, not a hang), `env`, `timeout_secs` (≤600), `max_output_bytes` (≤1 MiB/stream). Spawned with `POSIX_SPAWN_SETSID`; timeout → `killpg` → exit 137, `timed_out=true`. `structuredContent` carries exit/stdout/stderr/duration. Use for commands that finish in seconds | -| `exec_start` | start a background job, return a `job_id` immediately | for work that outlives one request (release builds). Same shell/session/TCC as `exec`; `cwd`, `env`, `timeout_secs` (0 = no timeout, stop via `exec_cancel`). stdout/stderr are tee'd to `~/Library/Logs/oab-instance-mcp/jobs/.out`/`.err` (never truncated; survive after the job is forgotten; `tail -f`-able) | +| `exec_start` | start a background job, return a `job_id` immediately | for work that outlives one request (release builds). Same shell/session/TCC as `exec`; `cwd`, `env`, `timeout_secs` (0 = no timeout, stop via `exec_cancel`). stdout/stderr are tee'd to `~/Library/Logs/oab-computer/jobs/.out`/`.err` (never truncated; survive after the job is forgotten; `tail -f`-able) | | `exec_poll` | fetch a job's state + incremental output | `job_id`, optional `stdout_since`/`stderr_since` byte offsets (from the prior poll) for only-new output; reads from the log files by seek. Terminal `state` (`exited`/`killed`) carries `exit_code`; `out_path`/`err_path` point at the files | | `exec_list` | all running jobs + the 10 most recently finished | recover a forgotten `job_id` or see what is running; each entry has state, pid, exit_code, command, cwd, timestamps, stdout/stderr byte sizes | | `exec_cancel` | stop a running job (or drop a finished one) | `job_id`, `signal` `KILL` (default) / `TERM` — signals the whole process group; poll once more for final output. Finished jobs are dropped from the registry (log files stay on disk) | @@ -227,12 +227,12 @@ Configured checks are **AND**-combined: a request must pass every check that is - `--token ` / `--token-file

` — additionally require `Authorization: Bearer`, constant-time compared. The token after the `Bearer` scheme (case-insensitive) must match byte for byte; whitespace around the whole header value is stripped by the HTTP parser, nothing inside it is. - **`deploy.sh` now generates one at `~/.config/oab-computer/token` on macOS (mode 600; Linux keeps `~/.config/oab-instance-mcp/token`) and passes + **`deploy.sh` now generates one at `~/.config/oab-computer/token` (mode 600) and passes `--token-file`**, so the deployed agent requires *both* an allow-listed Tailscale login *and* the bearer token. This is defence-in-depth: a leaked tailnet auth key that enrols a node as your Tailscale login passes the login check but still gets `401` without the token (verified 2026-09-23: no token → 401, correct token → 200, wrong token → 401). The token is stable across - re-deploys; rotate by deleting `~/.config/oab-computer/token` (macOS) and re-deploying. The first 0.12 run copies a 0.11 token from `~/.config/oab-instance-mcp/token` once, when `~/.config/oab-computer` is created; after that the old file is never read again, so a deleted token is not restored from it. (The switchboard files and grants still live under `~/.config/oab-instance-mcp` for now.) The menu bar shows a masked form and + re-deploys; rotate by deleting `~/.config/oab-computer/token` (macOS) and re-deploying. The first 0.12 run copies a 0.11 token from `~/.config/oab-instance-mcp/token` once, when `~/.config/oab-computer` is created; after that the old file is never read again, so a deleted token is not restored from it. The menu bar shows a masked form and copies the full token (see below); it is never written to `agent.log`. - `--insecure-local` — allow bare loopback requests with no Tailscale headers. Debugging only. - `/healthz` is unauthenticated and says only `ok`. @@ -368,7 +368,7 @@ differences: - There is no switchboard and no menu bar. - `install-linux.sh` runs `sudo tailscale serve` for `:8444` only with passwordless sudo. Otherwise it prints the command. -- `install-linux.sh` also starts Playwright MCP on `127.0.0.1:8794` (user unit `oab-pw-mcp.service`) +- `install-linux.sh` also starts Playwright MCP on `127.0.0.1:8794` (user unit `oab-computer-pw-mcp.service`) when `node` and `npm` are present, and sets `MCP_UPSTREAM=browser=http://127.0.0.1:8794/mcp`. Skip it with `--no-browser`. It makes no tailnet mapping for 8794. `MCP_UPSTREAM` takes plain `http://` URLs only. @@ -445,9 +445,9 @@ runs. ```sh openab-sb gen-secret # on the switchboard: verifier → [vm].secret_sha256 -oab-instance-mcp --token-file ~/.config/oab-computer/token \ +oab-computer --token-file ~/.config/oab-computer/token \ --switchboard wss://..ts.net/vm/attach \ - --switchboard-secret-file ~/.config/oab-instance-mcp/switchboard.secret \ + --switchboard-secret-file ~/.config/oab-computer/switchboard.secret \ --switchboard-profile observe # observe (default) | owner ``` @@ -463,7 +463,7 @@ oab-instance-mcp --token-file ~/.config/oab-computer/token \ does not clean an existing log, so rotate audit files written by an older release. - **Secure fields:** `ui_action` `set_value` on a password field is refused through a switchboard by default, and nothing is typed into the app. To allow it, write `allow` to - `~/.config/oab-instance-mcp/switchboard.secure-set-value` and re-run the installer (flag + `~/.config/oab-computer/switchboard.secure-set-value` and re-run the installer (flag `--switchboard-allow-secure-set-value`). Do this only when the switchboard has `audit_args = false` or runs openab-sb 0.2.2 or later. Otherwise, fill secure fields over a direct `/mcp` connection. @@ -473,7 +473,7 @@ oab-instance-mcp --token-file ~/.config/oab-computer/token \ minutes on `401`/`403` instead of stopping. The contract is openab-sb `SOUTHBOUND-CONTRACT.md`. - **Rotation:** the secret file is re-read on every dial. Update the file, and the next retry picks it up. -- **Installed with the package:** put the URL in `~/.config/oab-instance-mcp/switchboard.url`, the +- **Installed with the package:** put the URL in `~/.config/oab-computer/switchboard.url`, the secret in `switchboard.secret` (mode 600) and, optionally, the profile in `switchboard.profile` and `allow` in `switchboard.secure-set-value`. Then re-run the installer: it writes the flags into the LaunchAgent plist whenever both files @@ -514,7 +514,7 @@ Playwright's tools under its own `tools/list`, filtered by the connection's prof Tagged releases publish a universal, Developer-ID-signed and Apple-notarized installer: -1. Download `oab-instance-mcp-VERSION-universal.pkg` from +1. Download `oab-computer-VERSION-universal.pkg` from [GitHub Releases](https://github.com/openabdev/openab-computer/releases). 2. Sign into Tailscale and keep a desktop user logged in. 3. Double-click the package. It auto-detects your Tailscale login/name, preserves or creates the @@ -534,20 +534,74 @@ Linux hands nodes: see [Quick start](#quick-start) and [`docs/linux-setup.md`](d The `.app.zip` beside the package is an advanced/manual artifact. After unzipping: ```sh -/path/to/oab-instance-mcp.app/Contents/Resources/install-prebuilt.sh \ - /path/to/oab-instance-mcp.app --allow-login auto +/path/to/oab-computer.app/Contents/Resources/install-prebuilt.sh \ + /path/to/oab-computer.app --allow-login auto ``` The installer never re-signs the app: doing so would change the identity TCC grants are bound to. See [`docs/releasing.md`](docs/releasing.md) for artifacts, signing/notarization, required secrets, local packaging smoke, and the current first-release signing blocker. +## Upgrading from 0.11 or earlier + +0.12.0 renames the product's technical names from `oab-instance-mcp` to `oab-computer`. This is a +breaking change with no compatibility layer: nothing is read from or written to the old names, except +the one token copy below. The product name stays OpenAB Computer. + +| | 0.11 and earlier | 0.12.0 | +|---|---|---| +| Executable, app file | `oab-instance-mcp`, `oab-instance-mcp.app` | `oab-computer`, `oab-computer.app` | +| Bundle id, LaunchAgent label | `dev.openab.instance-mcp` | `dev.openab.computer` | +| Playwright LaunchAgent, Linux unit | `dev.openab.instance-mcp.pw-mcp`, `oab-pw-mcp.service` | `dev.openab.computer.pw-mcp`, `oab-computer-pw-mcp.service` | +| Linux unit | `oab-instance-mcp.service` | `oab-computer.service` | +| Install, config, logs | `~/.local/oab-instance-mcp`, `~/.config/oab-instance-mcp`, `~/Library/Logs/oab-instance-mcp` | `~/.local/oab-computer`, `~/.config/oab-computer`, `~/Library/Logs/oab-computer` | +| Saved grants, audit log (macOS), state (Linux) | `~/Library/Application Support/oab-instance-mcp`, `$XDG_STATE_HOME/oab-instance-mcp` | `~/Library/Application Support/oab-computer`, `$XDG_STATE_HOME/oab-computer` | +| Keychain service (attach secrets) | `dev.openab.instance-mcp.grant` | `dev.openab.computer.grant` | +| Environment variables | `OAB_INSTANCE_MCP_*` | `OAB_COMPUTER_*` | +| Release assets | `oab-instance-mcp-VERSION-*` | `oab-computer-VERSION-*` | + +To upgrade: + +1. Install 0.12.0 with the new installer (the `.pkg`, `install-prebuilt.sh`, or `install-linux.sh` from the + new Linux tarball). The installer only warns when it finds the old agent. It never stops or removes it. +2. macOS: grant Screen Recording and Accessibility again (and Full Disk Access if you use it). TCC + grants are keyed to the bundle id, which changed. To drop the stale entries, run + `tccutil reset ScreenCapture dev.openab.instance-mcp` and + `tccutil reset Accessibility dev.openab.instance-mcp`. +3. macOS: the bearer token is copied automatically from `~/.config/oab-instance-mcp/token` to + `~/.config/oab-computer/token` when the new config directory is first created. The old file stays in place. + Linux: the installer creates a new token, so update the clients that use it. +4. Not migrated, so redo them by hand: switchboard settings (`switchboard.url`, `switchboard.secret`, + `switchboard.profile`, `switchboard.secure-set-value`), saved grants and their Keychain secrets (lend again), + logs and audit files. Copy any of them from the old directory if you want them. +5. Remove the old agent. It still holds port 8795 and fights the new one. + + macOS: + + ```sh + launchctl bootout gui/$(id -u)/dev.openab.instance-mcp + rm ~/Library/LaunchAgents/dev.openab.instance-mcp.plist + # only if you used the Playwright agent: + launchctl bootout gui/$(id -u)/dev.openab.instance-mcp.pw-mcp + rm ~/Library/LaunchAgents/dev.openab.instance-mcp.pw-mcp.plist + ``` + + Linux: + + ```sh + systemctl --user disable --now oab-instance-mcp.service oab-pw-mcp.service + rm ~/.config/systemd/user/oab-instance-mcp.service ~/.config/systemd/user/oab-pw-mcp.service + systemctl --user daemon-reload + ``` + + After the new agent works you can delete the old directories. + ## Build & test (on the build Mac; the laptop never compiles Swift) ```sh -rsync -a --delete --exclude .build --exclude .git ./ :~/src/oab-instance-mcp/ -ssh 'cd ~/src/oab-instance-mcp && swift build -c release && swift test' -ssh 'cd ~/src/oab-instance-mcp && bash scripts/smoke.sh' # loopback, every step time-bounded +rsync -a --delete --exclude .build --exclude .git ./ :~/src/oab-computer/ +ssh 'cd ~/src/oab-computer && swift build -c release && swift test' +ssh 'cd ~/src/oab-computer && bash scripts/smoke.sh' # loopback, every step time-bounded ``` `~/src` is on the internal disk on purpose: `~/build` is a RAID symlink and LaunchAgents cannot @@ -556,26 +610,26 @@ read the RAID (TCC on external volumes). ## Deploy (run on the target) ```sh -ssh 'cd ~/src/oab-instance-mcp && \ +ssh 'cd ~/src/oab-computer && \ KEYCHAIN=~/Library/Keychains/signing.keychain-db KEYCHAIN_PASSWORD_FILE=~/.config/signing/keychain-password \ bash scripts/deploy.sh you@example.com ' ``` -`deploy.sh` wraps the binary in `~/.local/oab-instance-mcp/oab-instance-mcp.app` (bundle id -`dev.openab.instance-mcp`) so TCC grants bind to a stable identity, signs it with the -given Apple Development identity (any cert of yours; TCC grants are keyed on identity + bundle id, so keep using the same one), installs LaunchAgent `dev.openab.instance-mcp` in `gui/501`, +`deploy.sh` wraps the binary in `~/.local/oab-computer/oab-computer.app` (bundle id +`dev.openab.computer`) so TCC grants bind to a stable identity, signs it with the +given Apple Development identity (any cert of yours; TCC grants are keyed on identity + bundle id, so keep using the same one), installs LaunchAgent `dev.openab.computer` in `gui/501`, and runs `tailscale serve --bg --https=8444 http://127.0.0.1:8795`. Then, once, on the Mac's own screen, System Settings → Privacy & Security: -- Screen & System Audio Recording → enable **OpenAB Computer** (older builds: `oab-instance-mcp`) -- Accessibility → enable **OpenAB Computer** (older builds: `oab-instance-mcp`) +- Screen & System Audio Recording → enable **OpenAB Computer** +- Accessibility → enable **OpenAB Computer** -From 0.9.0 the app shows as **OpenAB Computer** in System Settings, Finder and the menu bar. Older -docs and earlier builds call it **oab-instance-mcp**; it is the same app (bundle id -`dev.openab.instance-mcp`), so grants made under the old name carry over. +The app shows as **OpenAB Computer** in System Settings, Finder and the menu bar. Its bundle id is +`dev.openab.computer` since 0.12.0 (earlier builds used `dev.openab.instance-mcp`), so grants made to +a build before 0.12.0 do not carry over. -then `launchctl kickstart -k gui/501/dev.openab.instance-mcp`. `sys_info` reports both as `true` when +then `launchctl kickstart -k gui/501/dev.openab.computer`. `sys_info` reports both as `true` when done. Grants survive re-signing with the same identity + bundle id (verified across 0.1.0→0.2.0). Also on the Mac: `sudo pmset -a displaysleep 0`. With display sleep on, an idle Mac returns black @@ -622,13 +676,13 @@ and the profiles in [docs/tool-profiles.md](docs/tool-profiles.md). The menu off ## Operate ```sh -ssh 'launchctl print gui/501/dev.openab.instance-mcp | grep -E "state|pid"' -ssh 'tail -20 ~/Library/Logs/oab-instance-mcp/agent.log' # one line per session open / tools/call / deny -ssh 'launchctl kickstart -k gui/501/dev.openab.instance-mcp' # restart +ssh 'launchctl print gui/501/dev.openab.computer | grep -E "state|pid"' +ssh 'tail -20 ~/Library/Logs/oab-computer/agent.log' # one line per session open / tools/call / deny +ssh 'launchctl kickstart -k gui/501/dev.openab.computer' # restart ssh '/Applications/Tailscale.app/Contents/MacOS/Tailscale serve status' curl -s https://..ts.net:8444/healthz -ssh 'ls -lt ~/Library/Logs/oab-instance-mcp/jobs/' # exec_start job logs (.out/.err), GC'd after 7 days -ssh 'tail -f ~/Library/Logs/oab-instance-mcp/jobs/.out' # follow a background build live +ssh 'ls -lt ~/Library/Logs/oab-computer/jobs/' # exec_start job logs (.out/.err), GC'd after 7 days +ssh 'tail -f ~/Library/Logs/oab-computer/jobs/.out' # follow a background build live ``` Measured from the laptop: `sys_info` 0.75 s, `exec` 0.47 s, a 1 s exec timeout returns in 1.6 s. diff --git a/Sources/InstanceMCPCore/AttachManager.swift b/Sources/InstanceMCPCore/AttachManager.swift index c18d646..33558f6 100644 --- a/Sources/InstanceMCPCore/AttachManager.swift +++ b/Sources/InstanceMCPCore/AttachManager.swift @@ -5,7 +5,7 @@ import Foundation /// A grant is created by a human through `POST /attach` (from OpenAB Connect or /// Remote, or a curl) and lives **here**, on the Mac. The granting device does not /// need to stay online; the Mac dials, redials and gives up on its own. Design: -/// instance-mcp `docs/requirements/reverse-attach.md`, "How a grant works". +/// oab-computer `docs/requirements/reverse-attach.md`, "How a grant works". /// /// One grant per (runtime, session): a new one for the same target replaces the /// old, which is also how renewal works. The profile is fixed for a grant's life. diff --git a/Sources/InstanceMCPCore/FirstRunSetup.swift b/Sources/InstanceMCPCore/FirstRunSetup.swift index 2c4c435..c0ee898 100644 --- a/Sources/InstanceMCPCore/FirstRunSetup.swift +++ b/Sources/InstanceMCPCore/FirstRunSetup.swift @@ -38,21 +38,21 @@ public enum FirstRunSetup { [legacyTokenPath(home: home), oldestTokenPath(home: home)] } - /// `~/.config/oab-instance-mcp`: the directory the installer and the installed agent read. A no-flag launch + /// `~/.config/oab-computer`: the directory the installer and the installed agent read. A no-flag launch /// under a given home uses it for the switchboard config as well. public static func configDirectory(home: String) -> String { - home + "/.config/oab-instance-mcp" + home + "/.config/oab-computer" } /// The reverse-attach grant record under `home`: the file FileGrantStore.defaultURL names for the account. public static func grantsURL(home: String) -> URL { - URL(fileURLWithPath: home + "/Library/Application Support/oab-instance-mcp/grants.json") + URL(fileURLWithPath: home + "/Library/Application Support/oab-computer/grants.json") } /// The agent log under `home`. The menu's Open Log item opens it, and a LaunchAgent writes it through /// StandardErrorPath. public static func logPath(home: String) -> String { - home + "/Library/Logs/oab-instance-mcp/agent.log" + home + "/Library/Logs/oab-computer/agent.log" } /// Sends this process's stderr to `path` (append, mode 644), as a LaunchAgent's StandardErrorPath does. A Finder @@ -91,7 +91,7 @@ public enum FirstRunSetup { /// Shown in an alert when the no-flag launch finds its port taken, for example by the installed agent. public static func portInUseMessage(host: String, port: UInt16) -> String { - "Port \(port) on \(host) is already in use, probably by the installed oab-instance-mcp agent. " + + "Port \(port) on \(host) is already in use, probably by the installed oab-computer agent. " + "This app did not start. Quit the other process, then open the app again." } @@ -275,7 +275,7 @@ public enum FirstRunSetup { /// and exits 0, so the status could never be parsed. Measured on two Macs: only that variable decides it. static func tailscaleEnvironment(_ base: [String: String]) -> [String: String] { var env = base - if (env["TERM_PROGRAM"] ?? "").isEmpty { env["TERM_PROGRAM"] = "oab-instance-mcp" } + if (env["TERM_PROGRAM"] ?? "").isEmpty { env["TERM_PROGRAM"] = "oab-computer" } return env } diff --git a/Sources/InstanceMCPCore/GrantStore.swift b/Sources/InstanceMCPCore/GrantStore.swift index cc31d07..c9e89c0 100644 --- a/Sources/InstanceMCPCore/GrantStore.swift +++ b/Sources/InstanceMCPCore/GrantStore.swift @@ -9,10 +9,10 @@ import Security /// /// Split by sensitivity: /// - the **record** (id, runtime, session, profile, principal, dates) is JSON in -/// `~/Library/Application Support/oab-instance-mcp/grants.json`, mode 600 in a +/// `~/Library/Application Support/oab-computer/grants.json`, mode 600 in a /// 0700 directory — inspectable, no secret in it; /// - the **attach secret** is a generic password in the login Keychain, service -/// `dev.openab.instance-mcp.grant`, account = grant id, accessible only after +/// `dev.openab.computer.grant`, account = grant id, accessible only after /// first unlock on this device. The Keychain ACL binds it to this code-signing /// identity, like the TCC grants. /// @@ -90,7 +90,7 @@ public final class FileGrantStore: GrantStore, @unchecked Sendable { public static var defaultURL: URL { FileManager.default.homeDirectoryForCurrentUser - .appendingPathComponent("Library/Application Support/oab-instance-mcp/grants.json") + .appendingPathComponent("Library/Application Support/oab-computer/grants.json") } public init(url: URL = FileGrantStore.defaultURL, secrets: SecretStore = KeychainSecretStore(), @@ -166,7 +166,7 @@ public final class FileGrantStore: GrantStore, @unchecked Sendable { } public struct KeychainSecretStore: SecretStore { - public static let service = "dev.openab.instance-mcp.grant" + public static let service = "dev.openab.computer.grant" public init() {} private func query(_ account: String) -> [String: Any] { @@ -180,7 +180,7 @@ public struct KeychainSecretStore: SecretStore { var add = query(account) add[kSecValueData as String] = Data(secret.utf8) add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly - add[kSecAttrLabel as String] = "oab-instance-mcp attach grant" + add[kSecAttrLabel as String] = "oab-computer attach grant" return SecItemAdd(add as CFDictionary, nil) == errSecSuccess } diff --git a/Sources/InstanceMCPCore/MCPHTTPEndpoint.swift b/Sources/InstanceMCPCore/MCPHTTPEndpoint.swift index 5a0e035..2389371 100644 --- a/Sources/InstanceMCPCore/MCPHTTPEndpoint.swift +++ b/Sources/InstanceMCPCore/MCPHTTPEndpoint.swift @@ -157,7 +157,7 @@ extension MCPHTTPEndpoint { public final class LoopbackHTTPServer: @unchecked Sendable { private let listener: NWListener private let endpoint: MCPHTTPEndpoint - private let queue = DispatchQueue(label: "oab-instance-mcp.http", qos: .userInitiated) + private let queue = DispatchQueue(label: "oab-computer.http", qos: .userInitiated) private let log: @Sendable (String) -> Void public init(host: String, port: UInt16, endpoint: MCPHTTPEndpoint, log: @escaping @Sendable (String) -> Void) throws { diff --git a/Sources/InstanceMCPCore/MenuModel.swift b/Sources/InstanceMCPCore/MenuModel.swift index 149fa4c..bde04c1 100644 --- a/Sources/InstanceMCPCore/MenuModel.swift +++ b/Sources/InstanceMCPCore/MenuModel.swift @@ -10,7 +10,7 @@ public enum LaunchdJob { /// The variable is inherited by every child of a job, so a shell inside another LaunchAgent (an Actions runner, a /// coding agent) carries that job's label. Only this app's own label counts: booting out any other would stop an /// unrelated service, and a restart would wait for a KeepAlive that does not exist. - public static let ownLabel = "dev.openab.instance-mcp" + public static let ownLabel = "dev.openab.computer" /// A child of this app (a second copy started through the exec tool) inherits the label too, but its parent is the /// app, not launchd (pid 1), so the parent pid must be 1 as well. diff --git a/Sources/InstanceMCPCore/ProfileAudit.swift b/Sources/InstanceMCPCore/ProfileAudit.swift index 1febc72..5c62e7a 100644 --- a/Sources/InstanceMCPCore/ProfileAudit.swift +++ b/Sources/InstanceMCPCore/ProfileAudit.swift @@ -6,7 +6,7 @@ import Foundation /// `desktop` and the daemon serves `observe` instead, it appends one JSON line here /// as well as logging it. The file is append-only and separate from the daemon log, /// so the record survives log rotation and is easy to inspect: -/// `~/Library/Application Support/oab-instance-mcp/audit.jsonl`, mode 600 in a 0700 +/// `~/Library/Application Support/oab-computer/audit.jsonl`, mode 600 in a 0700 /// directory, beside `grants.json`. The Linux node writes the same record shape. /// /// Writing is best-effort: a failure is logged and never blocks the grant. @@ -41,7 +41,7 @@ public final class FileAuditLog: AuditLog, @unchecked Sendable { public static var defaultURL: URL { FileManager.default.homeDirectoryForCurrentUser - .appendingPathComponent("Library/Application Support/oab-instance-mcp/audit.jsonl") + .appendingPathComponent("Library/Application Support/oab-computer/audit.jsonl") } public init(url: URL = FileAuditLog.defaultURL, log: @escaping @Sendable (String) -> Void = { _ in }) { diff --git a/Sources/InstanceMCPCore/SwitchboardConfig.swift b/Sources/InstanceMCPCore/SwitchboardConfig.swift index ba9344e..b874257 100644 --- a/Sources/InstanceMCPCore/SwitchboardConfig.swift +++ b/Sources/InstanceMCPCore/SwitchboardConfig.swift @@ -156,9 +156,9 @@ public struct SwitchboardConfigStore: Sendable { } } - /// `~/.config/oab-instance-mcp`, the directory the installer reads. + /// `~/.config/oab-computer`, the directory the installer reads. public static var defaultDirectory: URL { - FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".config/oab-instance-mcp") + FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".config/oab-computer") } public let directory: URL diff --git a/Sources/InstanceMCPCore/ToolProfile.swift b/Sources/InstanceMCPCore/ToolProfile.swift index 6178dbf..988e861 100644 --- a/Sources/InstanceMCPCore/ToolProfile.swift +++ b/Sources/InstanceMCPCore/ToolProfile.swift @@ -2,7 +2,7 @@ import Foundation /// Which tools a connection may see and call. Chosen by *this* side — the Mac is /// the MCP server, so scoping is a per-connection tool list here, never MCP -/// parsing in a proxy (instance-mcp `docs/adr/reverse-attach.md`). +/// parsing in a proxy (oab-computer `docs/adr/reverse-attach.md`). /// /// Two profiles (openab-computer#59), named after the one boundary that is real: /// shell-equivalent or not. diff --git a/Sources/InstanceMCPCore/Tools/AsyncExecTools.swift b/Sources/InstanceMCPCore/Tools/AsyncExecTools.swift index 22c5bb4..4e0c5a1 100644 --- a/Sources/InstanceMCPCore/Tools/AsyncExecTools.swift +++ b/Sources/InstanceMCPCore/Tools/AsyncExecTools.swift @@ -76,7 +76,7 @@ public struct ExecPollTool: Tool { byte offsets returned by the previous poll. When `state` is `exited` or `killed`, \ `exit_code` is set and no more output will appear. Output is read from the job's log \ files (never truncated); terminal-job metadata is retained ~10 min, and the log files \ - themselves persist on disk (~/Library/Logs/oab-instance-mcp/jobs/.out/.err). + themselves persist on disk (~/Library/Logs/oab-computer/jobs/.out/.err). """ public let inputSchema: JSONValue = [ "type": "object", @@ -92,7 +92,7 @@ public struct ExecPollTool: Tool { public func call(arguments: JSONValue) async throws -> ToolResult { guard let id = arguments["job_id"]?.stringValue else { throw JSONRPCError.invalidParams("job_id is required") } guard let job = await JobRegistry.shared.job(id) else { - return .error("unknown job_id: \(id) (its metadata may have been garbage-collected; the log files may still exist under ~/Library/Logs/oab-instance-mcp/jobs/)") + return .error("unknown job_id: \(id) (its metadata may have been garbage-collected; the log files may still exist under ~/Library/Logs/oab-computer/jobs/)") } let outSince = arguments["stdout_since"]?.intValue ?? 0 let errSince = arguments["stderr_since"]?.intValue ?? 0 diff --git a/Sources/InstanceMCPCore/Tools/ExecSupport.swift b/Sources/InstanceMCPCore/Tools/ExecSupport.swift index 154eaa1..b2830de 100644 --- a/Sources/InstanceMCPCore/Tools/ExecSupport.swift +++ b/Sources/InstanceMCPCore/Tools/ExecSupport.swift @@ -69,7 +69,7 @@ public enum ExecCwd { static func externalVolumeHint(_ path: String, verb: String) -> String { "cwd \(path) is on an external volume and \(verb). If this daemon runs as a LaunchAgent, " - + "macOS TCC blocks external-volume access unless the app bundle (dev.openab.instance-mcp) is granted " + + "macOS TCC blocks external-volume access unless the app bundle (dev.openab.computer) is granted " + "Full Disk Access in System Settings → Privacy & Security → Full Disk Access. " + "Grant it and restart the agent, or use an internal-disk path." } diff --git a/Sources/InstanceMCPCore/Tools/ExecTool.swift b/Sources/InstanceMCPCore/Tools/ExecTool.swift index 6daeae9..e4e38e6 100644 --- a/Sources/InstanceMCPCore/Tools/ExecTool.swift +++ b/Sources/InstanceMCPCore/Tools/ExecTool.swift @@ -88,7 +88,7 @@ public struct ExecTool: Tool { let pid = proc.pid let collector = OutputCollector(cap: cap) - let ioQueue = DispatchQueue(label: "oab-instance-mcp.exec.io", attributes: .concurrent) + let ioQueue = DispatchQueue(label: "oab-computer.exec.io", attributes: .concurrent) let group = DispatchGroup() for (fd, stream) in [(proc.stdoutFD, 0), (proc.stderrFD, 1)] { group.enter() diff --git a/Sources/InstanceMCPCore/Tools/Input.swift b/Sources/InstanceMCPCore/Tools/Input.swift index bd04de6..f1a94a9 100644 --- a/Sources/InstanceMCPCore/Tools/Input.swift +++ b/Sources/InstanceMCPCore/Tools/Input.swift @@ -9,7 +9,7 @@ enum Input { static func requireAccessibility() throws { guard AXIsProcessTrusted() else { throw ToolError("Accessibility not granted: input events would be silently dropped. " + - "Grant Accessibility to OpenAB Computer (oab-instance-mcp) in System Settings → Privacy & Security → Accessibility, then restart the agent.") + "Grant Accessibility to OpenAB Computer (oab-computer) in System Settings → Privacy & Security → Accessibility, then restart the agent.") } } diff --git a/Sources/InstanceMCPCore/Tools/JobRegistry.swift b/Sources/InstanceMCPCore/Tools/JobRegistry.swift index be8f931..f2cac2d 100644 --- a/Sources/InstanceMCPCore/Tools/JobRegistry.swift +++ b/Sources/InstanceMCPCore/Tools/JobRegistry.swift @@ -84,7 +84,7 @@ public actor JobRegistry { private let maxAgeDays: Double // startup GC threshold for log *files* public init(logDir: String? = nil, retention: TimeInterval = 600, maxJobs: Int = 64, maxAgeDays: Double = 7) { - self.logDir = logDir ?? (NSHomeDirectory() + "/Library/Logs/oab-instance-mcp/jobs") + self.logDir = logDir ?? (NSHomeDirectory() + "/Library/Logs/oab-computer/jobs") self.retention = retention self.maxJobs = maxJobs self.maxAgeDays = maxAgeDays @@ -180,7 +180,7 @@ public actor JobRegistry { /// Drain both pipes into the job's log files and reap the child on a detached task, so /// the spawning MCP call returns immediately. A timeout (> 0) kills the process group. private nonisolated func superviseDetached(job: Job, proc: SpawnedProcess, timeout: TimeInterval, outFile: Int32, errFile: Int32) { - let ioQueue = DispatchQueue(label: "oab-instance-mcp.job.io", attributes: .concurrent) + let ioQueue = DispatchQueue(label: "oab-computer.job.io", attributes: .concurrent) let group = DispatchGroup() for (readFD, writeFD) in [(proc.stdoutFD, outFile), (proc.stderrFD, errFile)] { group.enter() diff --git a/Sources/InstanceMCPCore/Tools/ScreenshotTool.swift b/Sources/InstanceMCPCore/Tools/ScreenshotTool.swift index 2f83056..b4fa748 100644 --- a/Sources/InstanceMCPCore/Tools/ScreenshotTool.swift +++ b/Sources/InstanceMCPCore/Tools/ScreenshotTool.swift @@ -43,7 +43,7 @@ public struct ScreenshotTool: Tool { content = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true) } catch { throw ToolError("ScreenCaptureKit unavailable: \(error.localizedDescription). " + - "Grant Screen Recording to OpenAB Computer (oab-instance-mcp) in System Settings → Privacy & Security, then restart the agent.") + "Grant Screen Recording to OpenAB Computer (oab-computer) in System Settings → Privacy & Security, then restart the agent.") } // Main display first so index 0 is predictable; shared with mouse and ui_snapshot. let ordered = DisplayOrder.sorted(content.displays, mainID: CGMainDisplayID()) { $0.displayID } diff --git a/Sources/InstanceMCPCore/Tools/SysInfoTool.swift b/Sources/InstanceMCPCore/Tools/SysInfoTool.swift index f150415..e946b04 100644 --- a/Sources/InstanceMCPCore/Tools/SysInfoTool.swift +++ b/Sources/InstanceMCPCore/Tools/SysInfoTool.swift @@ -59,7 +59,7 @@ public struct SysInfoTool: Tool { let tail = tailnetAddresses() let structured: JSONValue = [ - "agent": ["name": "oab-instance-mcp", "version": .string(agentVersion), "pid": .number(Double(pi.processIdentifier))], + "agent": ["name": "oab-computer", "version": .string(agentVersion), "pid": .number(Double(pi.processIdentifier))], "host": .string(host), "os": .string("macOS \(os.majorVersion).\(os.minorVersion).\(os.patchVersion)"), "hardware": .object(hw), @@ -85,9 +85,9 @@ public struct SysInfoTool: Tool { }.joined(separator: ", ")) lines.append("tailscale: \(tail.isEmpty ? "none" : tail.joined(separator: ", "))") lines.append("permissions: screen_recording=\(screenRecording) accessibility=\(accessibility) full_disk_access=\(fullDiskAccessState)") - if !screenRecording { lines.append("→ screenshot will fail until Screen Recording is granted to OpenAB Computer (oab-instance-mcp)") } - if !accessibility { lines.append("→ mouse/key/ui_snapshot/ui_action will fail until Accessibility is granted to OpenAB Computer (oab-instance-mcp)") } - if !fullDiskAccess { lines.append("→ protected Mail/Messages/Safari files will fail until Full Disk Access is granted to OpenAB Computer (oab-instance-mcp)") } + if !screenRecording { lines.append("→ screenshot will fail until Screen Recording is granted to OpenAB Computer (oab-computer)") } + if !accessibility { lines.append("→ mouse/key/ui_snapshot/ui_action will fail until Accessibility is granted to OpenAB Computer (oab-computer)") } + if !fullDiskAccess { lines.append("→ protected Mail/Messages/Safari files will fail until Full Disk Access is granted to OpenAB Computer (oab-computer)") } lines.append("agent \(agentVersion)") return ToolResult(content: [.text(lines.joined(separator: "\n"))], structured: structured) } diff --git a/Sources/InstanceMCPCore/Tools/UIActionTool.swift b/Sources/InstanceMCPCore/Tools/UIActionTool.swift index 055b366..95dd886 100644 --- a/Sources/InstanceMCPCore/Tools/UIActionTool.swift +++ b/Sources/InstanceMCPCore/Tools/UIActionTool.swift @@ -47,7 +47,7 @@ public struct UIActionTool: Tool { /// Error texts, pinned by tests. None of them ever contains the input `value`. public enum Errors { - public static let notTrusted = "Accessibility not granted: ui_action cannot act on other apps' windows. Grant Accessibility to OpenAB Computer (oab-instance-mcp, bundle id dev.openab.instance-mcp) in System Settings → Privacy & Security → Accessibility, then restart the agent." + public static let notTrusted = "Accessibility not granted: ui_action cannot act on other apps' windows. Grant Accessibility to OpenAB Computer (oab-computer, bundle id dev.openab.computer) in System Settings → Privacy & Security → Accessibility, then restart the agent." public static let selfApp = "ui_action cannot act on OpenAB Computer's own windows" public static let busy = "another ui_action is still running and the timeout_ms budget ran out waiting for it; nothing was done. Retry" public static func staleSnapshot(_ id: String) -> String { diff --git a/Sources/InstanceMCPCore/Tools/UISnapshotTool.swift b/Sources/InstanceMCPCore/Tools/UISnapshotTool.swift index 1a78dd7..29cea07 100644 --- a/Sources/InstanceMCPCore/Tools/UISnapshotTool.swift +++ b/Sources/InstanceMCPCore/Tools/UISnapshotTool.swift @@ -41,7 +41,7 @@ public struct UISnapshotTool: Tool { /// Error texts, pinned by tests. public enum Errors { - public static let notTrusted = "Accessibility not granted: ui_snapshot cannot read other apps' windows. Grant Accessibility to OpenAB Computer (oab-instance-mcp, bundle id dev.openab.instance-mcp) in System Settings → Privacy & Security → Accessibility, then restart the agent." + public static let notTrusted = "Accessibility not granted: ui_snapshot cannot read other apps' windows. Grant Accessibility to OpenAB Computer (oab-computer, bundle id dev.openab.computer) in System Settings → Privacy & Security → Accessibility, then restart the agent." public static let noFrontmost = "no frontmost app (the screen may be locked or at the login window); pass app or pid" public static let budgetSpent = "the timeout_ms budget ran out before the target app was chosen; raise timeout_ms, or pass app or pid" public static let selfApp = "ui_snapshot cannot read OpenAB Computer's own windows" diff --git a/Sources/InstanceMCPCore/UpstreamMCP.swift b/Sources/InstanceMCPCore/UpstreamMCP.swift index 84c8a96..1bc70f9 100644 --- a/Sources/InstanceMCPCore/UpstreamMCP.swift +++ b/Sources/InstanceMCPCore/UpstreamMCP.swift @@ -94,7 +94,7 @@ public actor UpstreamMCP { let (status, body, headers) = try await postRaw([ "jsonrpc": "2.0", "id": 0, "method": "initialize", "params": ["protocolVersion": "2025-06-18", "capabilities": [:], - "clientInfo": ["name": "oab-instance-mcp", "version": "upstream"]], + "clientInfo": ["name": "oab-computer", "version": "upstream"]], ]) guard (200..<300).contains(status) else { throw UpstreamError.http(status, body) } sessionID = headers.first { $0.key.lowercased() == "mcp-session-id" }?.value diff --git a/Sources/oab-instance-mcp/PermissionSetupWindowController.swift b/Sources/oab-computer/PermissionSetupWindowController.swift similarity index 100% rename from Sources/oab-instance-mcp/PermissionSetupWindowController.swift rename to Sources/oab-computer/PermissionSetupWindowController.swift diff --git a/Sources/oab-instance-mcp/StatusItemController.swift b/Sources/oab-computer/StatusItemController.swift similarity index 100% rename from Sources/oab-instance-mcp/StatusItemController.swift rename to Sources/oab-computer/StatusItemController.swift diff --git a/Sources/oab-instance-mcp/SwitchboardMenu.swift b/Sources/oab-computer/SwitchboardMenu.swift similarity index 100% rename from Sources/oab-instance-mcp/SwitchboardMenu.swift rename to Sources/oab-computer/SwitchboardMenu.swift diff --git a/Sources/oab-instance-mcp/main.swift b/Sources/oab-computer/main.swift similarity index 97% rename from Sources/oab-instance-mcp/main.swift rename to Sources/oab-computer/main.swift index 02e078f..980fc1c 100644 --- a/Sources/oab-instance-mcp/main.swift +++ b/Sources/oab-computer/main.swift @@ -39,9 +39,9 @@ struct Options { func usage() -> Never { print(""" - oab-instance-mcp \(version) — MCP server exposing this Mac (exec / screenshot / ui_snapshot / ui_action / mouse / key / osascript / sys_info) + oab-computer \(version) — MCP server exposing this Mac (exec / screenshot / ui_snapshot / ui_action / mouse / key / osascript / sys_info) - USAGE: oab-instance-mcp [--host 127.0.0.1] [--port 8795] [--path /mcp] + USAGE: oab-computer [--host 127.0.0.1] [--port 8795] [--path /mcp] [--allow-login ]... [--token | --token-file ] [--insecure-local] [--quiet] [--menu-bar] [--public-url ] @@ -66,7 +66,7 @@ func usage() -> Never { Mac to one openab-pty session (this Mac dials the pod; see the ADR). --no-grant-persistence Keep reverse-attach grants in memory only. By default live grants are - saved (record: ~/Library/Application Support/oab-instance-mcp/grants.json, + saved (record: ~/Library/Application Support/oab-computer/grants.json, mode 600; secret: login Keychain) and re-dialled after a restart. --switchboard Dial an openab-sb switchboard and serve this computer's tools on that socket, so @@ -175,11 +175,11 @@ if firstRunLaunch { FirstRunSetup.redirectStderr(toLogAt: FirstRunSetup.logPath(home: home)) #if DEBUG // Test seam, compiled into debug builds only (swift test). Release builds have no override and always use 8795. - if let raw = ProcessInfo.processInfo.environment["OAB_INSTANCE_MCP_FIRST_RUN_PORT"] { + if let raw = ProcessInfo.processInfo.environment["OAB_COMPUTER_FIRST_RUN_PORT"] { if let p = Int(raw), (1...65535).contains(p) { opts.port = UInt16(p) } else { - fputs("no-flag launch: OAB_INSTANCE_MCP_FIRST_RUN_PORT=\(raw) rejected (not 1 to 65535); using \(opts.port)\n", stderr) + fputs("no-flag launch: OAB_COMPUTER_FIRST_RUN_PORT=\(raw) rejected (not 1 to 65535); using \(opts.port)\n", stderr) } } #endif @@ -200,7 +200,7 @@ if firstRunLaunch { firstRunNotice = plan.notice firstRunLines = plan.logLines } catch { - showFirstRunAlert("Could not prepare the token file: \(error). Quit any other oab-instance-mcp that is running, " + + showFirstRunAlert("Could not prepare the token file: \(error). Quit any other oab-computer that is running, " + "remove \(FirstRunSetup.lockPath(for: FirstRunSetup.tokenPath(home: home))) if it is left over from a crash, " + "then open the app again. Details are in \(FirstRunSetup.logPath(home: home)).") exit(73) @@ -255,7 +255,7 @@ let log: @Sendable (String) -> Void = { msg in } let server = MCPServer( - name: "oab-instance-mcp", + name: "oab-computer", version: version, instructions: """ You are operating a real Mac (\(Host.current().localizedName ?? "unknown")) through its logged-in \ @@ -301,7 +301,7 @@ let attachManager: AttachManager? = opts.attach // be read from Tailscale now; a configured allow list alone is not proof that a login exists at this launch. // (Skipped under the debug-only test switch below, so test-launched binaries never run the Tailscale CLI.) #if DEBUG -let exposureProbeSkipped = ProcessInfo.processInfo.environment["OAB_INSTANCE_MCP_NO_TAILNET_EXPOSURE"] != nil +let exposureProbeSkipped = ProcessInfo.processInfo.environment["OAB_COMPUTER_NO_TAILNET_EXPOSURE"] != nil #else let exposureProbeSkipped = false #endif @@ -319,7 +319,7 @@ let installerOwnsOtherPort = !firstRunLaunch && TailnetExposure.installerOwnsOth // Debug builds only: the process tests set an environment variable so a test-launched binary never touches Tailscale. // A release build has no such switch, so nothing in the environment can skip the no-login withdrawal below. #if DEBUG -let tailnetExposureDisabledForTests = ProcessInfo.processInfo.environment["OAB_INSTANCE_MCP_NO_TAILNET_EXPOSURE"] != nil +let tailnetExposureDisabledForTests = ProcessInfo.processInfo.environment["OAB_COMPUTER_NO_TAILNET_EXPOSURE"] != nil #else let tailnetExposureDisabledForTests = false #endif @@ -359,7 +359,7 @@ do { fputs("failed to start listener: \(error)\n", stderr); exit(2) } let startupPermissions = PermissionProbe.current() -log("oab-instance-mcp \(version) starting on http://\(opts.host):\(opts.port)\(opts.path) " + +log("oab-computer \(version) starting on http://\(opts.host):\(opts.port)\(opts.path) " + "auth=[logins:\(opts.allowLogins.sorted().joined(separator: ",")) token:\(opts.token != nil) insecure-local:\(opts.insecureLocal)] " + "attach=\(opts.attach) upstreams=[\(opts.upstreams.map { $0.0 }.joined(separator: ","))] " + "screen_recording=\(startupPermissions.screenRecording.isGranted) " + diff --git a/Tests/InstanceMCPCoreTests/FirstRunSetupTests.swift b/Tests/InstanceMCPCoreTests/FirstRunSetupTests.swift index 236d781..69caf5c 100644 --- a/Tests/InstanceMCPCoreTests/FirstRunSetupTests.swift +++ b/Tests/InstanceMCPCoreTests/FirstRunSetupTests.swift @@ -332,19 +332,37 @@ final class FirstRunSetupTests: XCTestCase { func testAgentLogIsTheFileTheMenuOpensUnderTheSameHome() { XCTAssertEqual(FirstRunSetup.logPath(home: "/Users/x"), - "/Users/x/Library/Logs/oab-instance-mcp/agent.log") + "/Users/x/Library/Logs/oab-computer/agent.log") XCTAssertEqual(FirstRunSetup.grantsURL(home: "/Users/x").path, - "/Users/x/Library/Application Support/oab-instance-mcp/grants.json") - XCTAssertEqual(FirstRunSetup.configDirectory(home: "/Users/x"), "/Users/x/.config/oab-instance-mcp") + "/Users/x/Library/Application Support/oab-computer/grants.json") + XCTAssertEqual(FirstRunSetup.configDirectory(home: "/Users/x"), "/Users/x/.config/oab-computer") + } + + /// The Swift half of scripts/test-name-consistency.sh: every user-visible name the app derives is + /// the 0.12.0 name, and none is the old one. `legacyTokenPath` is the one deliberate exception. + func testUserVisibleNamesAreOabComputer() { + let names = [ + FirstRunSetup.configDirectory(home: "/h"), + FirstRunSetup.logPath(home: "/h"), + FirstRunSetup.grantsURL(home: "/h").path, + FirstRunSetup.tokenPath(home: "/h"), + KeychainSecretStore.service, + LaunchdJob.ownLabel, + ] + for name in names { + XCTAssertTrue(name.contains("computer"), name) + XCTAssertFalse(name.contains("instance-mcp"), name) + } + XCTAssertEqual(KeychainSecretStore.service, LaunchdJob.ownLabel + ".grant") } - // MARK: flag launch (process level, the built `oab-instance-mcp` binary) + // MARK: flag launch (process level, the built `oab-computer` binary) private func builtBinary() throws -> URL { let dir = Bundle(for: FirstRunSetupTests.self).bundleURL.deletingLastPathComponent() - let url = dir.appendingPathComponent("oab-instance-mcp") + let url = dir.appendingPathComponent("oab-computer") guard FileManager.default.isExecutableFile(atPath: url.path) else { - throw XCTSkip("oab-instance-mcp is not built next to the test bundle: \(url.path)") + throw XCTSkip("oab-computer is not built next to the test bundle: \(url.path)") } return url } @@ -377,7 +395,7 @@ final class FirstRunSetupTests: XCTestCase { "--no-attach", "--no-grant-persistence", "--quiet"] var environment = ProcessInfo.processInfo.environment environment["HOME"] = home - environment["OAB_INSTANCE_MCP_NO_TAILNET_EXPOSURE"] = "1" + environment["OAB_COMPUTER_NO_TAILNET_EXPOSURE"] = "1" child.environment = environment child.standardOutput = FileHandle.nullDevice child.standardError = FileHandle.nullDevice @@ -420,9 +438,9 @@ final class FirstRunSetupTests: XCTestCase { var environment = ProcessInfo.processInfo.environment environment["HOME"] = home // Moves only the no-flag launch's port, for the free-port test. Unset means the default 8795. - environment["OAB_INSTANCE_MCP_FIRST_RUN_PORT"] = port.map(String.init) + environment["OAB_COMPUTER_FIRST_RUN_PORT"] = port.map(String.init) // Structural guard: the test-launched binary builds no TailnetExposure, so it can never run `tailscale serve`. - environment["OAB_INSTANCE_MCP_NO_TAILNET_EXPOSURE"] = "1" + environment["OAB_COMPUTER_NO_TAILNET_EXPOSURE"] = "1" let child = Process() child.executableURL = binary child.arguments = [] diff --git a/Tests/InstanceMCPCoreTests/MenuModelTests.swift b/Tests/InstanceMCPCoreTests/MenuModelTests.swift index bb3b3e3..cea9b91 100644 --- a/Tests/InstanceMCPCoreTests/MenuModelTests.swift +++ b/Tests/InstanceMCPCoreTests/MenuModelTests.swift @@ -44,14 +44,14 @@ final class MenuModelTests: XCTestCase { } func testLaunchdDetectionUsesAJobLabelOnly() { - XCTAssertTrue(LaunchdJob.isRunByLaunchd(environment: ["XPC_SERVICE_NAME": "dev.openab.instance-mcp"], parentPID: 1)) - XCTAssertEqual(LaunchdJob.jobLabel(environment: ["XPC_SERVICE_NAME": "dev.openab.instance-mcp"], parentPID: 1), "dev.openab.instance-mcp") - XCTAssertFalse(LaunchdJob.isRunByLaunchd(environment: ["XPC_SERVICE_NAME": "dev.openab.instance-mcp"], parentPID: 4242), + XCTAssertTrue(LaunchdJob.isRunByLaunchd(environment: ["XPC_SERVICE_NAME": "dev.openab.computer"], parentPID: 1)) + XCTAssertEqual(LaunchdJob.jobLabel(environment: ["XPC_SERVICE_NAME": "dev.openab.computer"], parentPID: 1), "dev.openab.computer") + XCTAssertFalse(LaunchdJob.isRunByLaunchd(environment: ["XPC_SERVICE_NAME": "dev.openab.computer"], parentPID: 4242), "a child of the app inherits the label but its parent is not launchd") XCTAssertFalse(LaunchdJob.isRunByLaunchd(environment: [:])) XCTAssertFalse(LaunchdJob.isRunByLaunchd(environment: ["XPC_SERVICE_NAME": "0"]), "a terminal session") XCTAssertFalse(LaunchdJob.isRunByLaunchd(environment: ["XPC_SERVICE_NAME": ""])) - XCTAssertFalse(LaunchdJob.isRunByLaunchd(environment: ["XPC_SERVICE_NAME": "application.dev.openab.instance-mcp.1234.5678"]), + XCTAssertFalse(LaunchdJob.isRunByLaunchd(environment: ["XPC_SERVICE_NAME": "application.dev.openab.computer.1234.5678"]), "an app opened from Finder is not a job that can be booted out") } @@ -110,8 +110,8 @@ final class MenuModelTests: XCTestCase { MenuModel.quit(jobLabel: nil, bootout: { booted.append($0) }, exitProcess: { exits += 1 }) XCTAssertEqual(booted, []) XCTAssertEqual(exits, 1) - MenuModel.quit(jobLabel: "dev.openab.instance-mcp", bootout: { booted.append($0) }, exitProcess: { exits += 1 }) - XCTAssertEqual(booted, ["dev.openab.instance-mcp"]) + MenuModel.quit(jobLabel: "dev.openab.computer", bootout: { booted.append($0) }, exitProcess: { exits += 1 }) + XCTAssertEqual(booted, ["dev.openab.computer"]) XCTAssertEqual(exits, 1) } diff --git a/Tests/InstanceMCPCoreTests/PermissionRelaunchTests.swift b/Tests/InstanceMCPCoreTests/PermissionRelaunchTests.swift index 02623b9..a378abb 100644 --- a/Tests/InstanceMCPCoreTests/PermissionRelaunchTests.swift +++ b/Tests/InstanceMCPCoreTests/PermissionRelaunchTests.swift @@ -265,7 +265,7 @@ final class HiddenPermissionCheckTests: XCTestCase { /// Runs the real built executable. Needs the executable target built next to the test bundle. func testBuiltExecutableExitsZeroOrOneWithoutOutput() throws { let dir = Bundle(for: Self.self).bundleURL.deletingLastPathComponent() - let exe = dir.appendingPathComponent("oab-instance-mcp") + let exe = dir.appendingPathComponent("oab-computer") try XCTSkipUnless(FileManager.default.isExecutableFile(atPath: exe.path), "executable not built beside the tests") let p = Process() p.executableURL = exe diff --git a/Tests/InstanceMCPCoreTests/TailnetExposureTests.swift b/Tests/InstanceMCPCoreTests/TailnetExposureTests.swift index 44a3284..3ae5efb 100644 --- a/Tests/InstanceMCPCoreTests/TailnetExposureTests.swift +++ b/Tests/InstanceMCPCoreTests/TailnetExposureTests.swift @@ -551,10 +551,10 @@ final class TailnetExposureTests: XCTestCase { func testTheReleaseBinaryDoesNotContainTheTestOverrideName() throws { let dir = Bundle(for: TailnetExposureTests.self).bundleURL.deletingLastPathComponent() // .../.build//debug/ -> .../release/ - let release = dir.deletingLastPathComponent().appendingPathComponent("release/oab-instance-mcp") + let release = dir.deletingLastPathComponent().appendingPathComponent("release/oab-computer") guard FileManager.default.fileExists(atPath: release.path) else { throw XCTSkip("no release binary built") } let data = try Data(contentsOf: release) - XCTAssertNil(data.range(of: Data("OAB_INSTANCE_MCP_NO_TAILNET_EXPOSURE".utf8))) + XCTAssertNil(data.range(of: Data("OAB_COMPUTER_NO_TAILNET_EXPOSURE".utf8))) } func testWithdrawReportsAMappingThatWillNotGoAway() { diff --git a/Tests/InstanceMCPCoreTests/TailscaleDetectionTests.swift b/Tests/InstanceMCPCoreTests/TailscaleDetectionTests.swift index 74ac983..5678cca 100644 --- a/Tests/InstanceMCPCoreTests/TailscaleDetectionTests.swift +++ b/Tests/InstanceMCPCoreTests/TailscaleDetectionTests.swift @@ -105,8 +105,8 @@ final class TailscaleDetectionTests: XCTestCase { } func testTheCLIGetsTERM_PROGRAMBecauseTheAppBinaryNeedsItOutsideATerminal() { - XCTAssertEqual(FirstRunSetup.tailscaleEnvironment([:])["TERM_PROGRAM"], "oab-instance-mcp") - XCTAssertEqual(FirstRunSetup.tailscaleEnvironment(["TERM_PROGRAM": ""])["TERM_PROGRAM"], "oab-instance-mcp") + XCTAssertEqual(FirstRunSetup.tailscaleEnvironment([:])["TERM_PROGRAM"], "oab-computer") + XCTAssertEqual(FirstRunSetup.tailscaleEnvironment(["TERM_PROGRAM": ""])["TERM_PROGRAM"], "oab-computer") XCTAssertEqual(FirstRunSetup.tailscaleEnvironment(["TERM_PROGRAM": "ghostty", "A": "b"]), ["TERM_PROGRAM": "ghostty", "A": "b"], "an existing value is kept") } diff --git a/Tests/InstanceMCPCoreTests/UISnapshotTests.swift b/Tests/InstanceMCPCoreTests/UISnapshotTests.swift index 65a5964..3f9e048 100644 --- a/Tests/InstanceMCPCoreTests/UISnapshotTests.swift +++ b/Tests/InstanceMCPCoreTests/UISnapshotTests.swift @@ -342,7 +342,7 @@ final class UISnapshotWalkerTests: XCTestCase { XCTAssertThrowsError(try walk([el("AXWindow", "w", a)])) { e in XCTAssertEqual((e as? ToolError)?.description, UISnapshotTool.Errors.notTrusted) } - XCTAssertTrue(UISnapshotTool.Errors.notTrusted.contains("dev.openab.instance-mcp")) + XCTAssertTrue(UISnapshotTool.Errors.notTrusted.contains("dev.openab.computer")) XCTAssertTrue(UISnapshotTool.Errors.notTrusted.contains("Privacy & Security → Accessibility")) } @@ -608,7 +608,7 @@ final class UISnapshotTargetTests: XCTestCase { UIRunningApp(pid: 30, bundleID: "a.helper", name: "Helper"), UIRunningApp(pid: 31, bundleID: "b.helper", name: "Helper"), UIRunningApp(pid: 40, bundleID: "com.apple.AirPlayUIAgent", name: "AirPlayUIAgent", regular: false), - UIRunningApp(pid: 99, bundleID: "dev.openab.instance-mcp", name: "oab-instance-mcp", regular: false), + UIRunningApp(pid: 99, bundleID: "dev.openab.computer", name: "oab-computer", regular: false), ] private func resolve(app: String? = nil, pid: Int32? = nil, front: Int32? = nil, @@ -632,7 +632,7 @@ final class UISnapshotTargetTests: XCTestCase { XCTAssertEqual(failure(resolve(app: "Nope")), "app not found: no running app has bundle id or name 'Nope'. Running apps: Finder, Helper, TextEdit") XCTAssertEqual(failure(resolve(pid: 99)), UISnapshotTool.Errors.selfApp) - XCTAssertEqual(failure(resolve(app: "dev.openab.instance-mcp")), UISnapshotTool.Errors.selfApp) + XCTAssertEqual(failure(resolve(app: "dev.openab.computer")), UISnapshotTool.Errors.selfApp) XCTAssertEqual(failure(resolve()), UISnapshotTool.Errors.noFrontmost) XCTAssertEqual(try resolve(front: 10).get().name, "Finder") XCTAssertEqual(try resolve(front: 55).get(), UIRunningApp(pid: 55, bundleID: nil, name: nil)) diff --git a/docs/adr/linux-rust-port.md b/docs/adr/linux-rust-port.md index 3bca2ed..cf4ff9f 100644 --- a/docs/adr/linux-rust-port.md +++ b/docs/adr/linux-rust-port.md @@ -1,5 +1,7 @@ # ADR: Linux-first Rust port — one core, per-OS backends +**Renamed** from `oab-instance-mcp` to `oab-computer` in 0.12.0 (the product name is OpenAB Computer). This is a dated record: paths, unit, label and artifact names below are the 0.8–0.11 names (`oab-instance-mcp`, `dev.openab.instance-mcp`, `oab-pw-mcp.service`). + - **Status:** Proposed - **Date:** 2026-09-27 - **Related:** [openabdev/instance-mcp#15](https://github.com/openabdev/instance-mcp/issues/15) (FR + phased plan + arch diagrams), [`reverse-attach.md`](reverse-attach.md), [`sandbox-adapter.md`](sandbox-adapter.md) diff --git a/docs/adr/reverse-attach.md b/docs/adr/reverse-attach.md index ce04360..6777654 100644 --- a/docs/adr/reverse-attach.md +++ b/docs/adr/reverse-attach.md @@ -1,5 +1,7 @@ # ADR: Reverse Attach — macmini dials the sandbox +**Renamed** from `oab-instance-mcp` to `oab-computer` in 0.12.0 (the product name is OpenAB Computer). This is a dated record: paths, unit, label and artifact names below are the 0.8–0.11 names (`oab-instance-mcp`, `dev.openab.instance-mcp`, `oab-pw-mcp.service`). + - **Status:** Proposed - **Date:** 2026-09-25 - **Requirement:** [`docs/requirements/reverse-attach.md`](../requirements/reverse-attach.md) (full design, threat model, phasing) diff --git a/docs/adr/sandbox-adapter.md b/docs/adr/sandbox-adapter.md index 09da30b..934d210 100644 --- a/docs/adr/sandbox-adapter.md +++ b/docs/adr/sandbox-adapter.md @@ -1,5 +1,7 @@ # ADR: Sandbox Adapter +**Renamed** from `oab-instance-mcp` to `oab-computer` in 0.12.0 (the product name is OpenAB Computer). This is a dated record: paths, unit, label and artifact names below are the 0.8–0.11 names (`oab-instance-mcp`, `dev.openab.instance-mcp`, `oab-pw-mcp.service`). + - **Status:** Proposed - **Date:** 2026-09-24 - **Issue:** [#1](https://github.com/openabdev/instance-mcp/issues/1) diff --git a/docs/linux-setup.md b/docs/linux-setup.md index d6016b8..326a4b1 100644 --- a/docs/linux-setup.md +++ b/docs/linux-setup.md @@ -1,6 +1,6 @@ # Linux hands node setup -How to turn a Linux desktop box into an `oab-instance-mcp` hands node with the Rust PoC in +How to turn a Linux desktop box into an `oab-computer` hands node with the Rust PoC in [`poc/reverse-attach-linux/`](../poc/reverse-attach-linux/). Written from two deployments, both verified end to end on 2026-09-28 (lending to an openab-pty session, the OpenAB Connect Screens pane, browser tools): @@ -29,8 +29,8 @@ flowchart LR subgraph node["Linux hands node — one box"] ts["tailscale serve :8444
TLS + injects Tailscale-User-Login"] subgraph seat["Desktop seat · systemd --user · WAYLAND_DISPLAY=wayland-0"] - mcp["oab-instance-mcp.service
reverse-attach binary · 127.0.0.1:8795
auth = allow-login AND bearer
/mcp · /attach · /healthz
sys_info · screenshot · bash · mouse · key
exec_start · exec_poll · exec_list · exec_cancel"] - pw["oab-pw-mcp.service
@playwright/mcp · 127.0.0.1:8794
headed distro Chromium, persistent profile"] + mcp["oab-computer.service
reverse-attach binary · 127.0.0.1:8795
auth = allow-login AND bearer
/mcp · /attach · /healthz
sys_info · screenshot · bash · mouse · key
exec_start · exec_poll · exec_list · exec_cancel"] + pw["oab-computer-pw-mcp.service
@playwright/mcp · 127.0.0.1:8794
headed distro Chromium, persistent profile"] end subgraph hands["What the tools touch"] grim["grim → screenshot"] @@ -93,17 +93,17 @@ with no URL at all, check `curl -4 -m 8 https://controlplane.tailscale.com/` — ## 2. Get the daemon -**Prebuilt (preferred).** Every `v*` release ships `oab-instance-mcp-VERSION-linux-arm64.tar.gz` +**Prebuilt (preferred).** Every `v*` release ships `oab-computer-VERSION-linux-arm64.tar.gz` and `-linux-amd64.tar.gz` (built and smoke-tested in CI on native runners) beside the macOS installer, with a `.sha256` each: ```sh -V=$(curl -fsSL https://api.github.com/repos/openabdev/instance-mcp/releases/latest | python3 -c 'import sys,json;print(json.load(sys.stdin)["tag_name"][1:])') +V=$(curl -fsSL https://api.github.com/repos/openabdev/openab-computer/releases/latest | python3 -c 'import sys,json;print(json.load(sys.stdin)["tag_name"][1:])') A=$(dpkg --print-architecture) # arm64 or amd64 -curl -fsSLO "https://github.com/openabdev/instance-mcp/releases/download/v$V/oab-instance-mcp-$V-linux-$A.tar.gz" -curl -fsSLO "https://github.com/openabdev/instance-mcp/releases/download/v$V/oab-instance-mcp-$V-linux-$A.tar.gz.sha256" -sha256sum -c "oab-instance-mcp-$V-linux-$A.tar.gz.sha256" -tar xzf "oab-instance-mcp-$V-linux-$A.tar.gz" && cd "oab-instance-mcp-$V-linux-$A" +curl -fsSLO "https://github.com/openabdev/openab-computer/releases/download/v$V/oab-computer-$V-linux-$A.tar.gz" +curl -fsSLO "https://github.com/openabdev/openab-computer/releases/download/v$V/oab-computer-$V-linux-$A.tar.gz.sha256" +sha256sum -c "oab-computer-$V-linux-$A.tar.gz.sha256" +tar xzf "oab-computer-$V-linux-$A.tar.gz" && cd "oab-computer-$V-linux-$A" ./install-linux.sh # does steps 3–5 below; --allow-login / --no-browser / --port ./install-linux.sh --headless-seat # same, on a box with no desktop (creates oab-seat.service) ``` @@ -116,8 +116,8 @@ steps 3–5 describe what it did. **From source** (needs Rust ≥ 1.85): ```sh -git clone https://github.com/openabdev/instance-mcp ~/repo/instance-mcp -cd ~/repo/instance-mcp/poc/reverse-attach-linux +git clone https://github.com/openabdev/openab-computer ~/repo/oab-computer +cd ~/repo/oab-computer/poc/reverse-attach-linux cargo build --release # ~30 s cold on a Pi 4/5; 2.2 MB binary bash smoke.sh # 55 checks against the bundled mock runtime; all must pass ``` @@ -129,23 +129,23 @@ smoke runs in CI on amd64 and arm64 for every push. ## 3. Bearer token and the daemon unit ```sh -mkdir -p ~/.config/oab-instance-mcp -head -c 32 /dev/urandom | base64 | tr -d '=+/\n' | head -c 40 > ~/.config/oab-instance-mcp/token -chmod 600 ~/.config/oab-instance-mcp/token +mkdir -p ~/.config/oab-computer +head -c 32 /dev/urandom | base64 | tr -d '=+/\n' | head -c 40 > ~/.config/oab-computer/token +chmod 600 ~/.config/oab-computer/token LOGIN=$(sudo tailscale status --json | python3 -c \ 'import sys,json;d=json.load(sys.stdin);print(d["User"][str(d["Self"]["UserID"])]["LoginName"])') mkdir -p ~/.config/systemd/user -cat > ~/.config/systemd/user/oab-instance-mcp.service < ~/.config/systemd/user/oab-computer.service < ~/.config/systemd/user/oab-pw-mcp.service <<'EOF' +cat > ~/.config/systemd/user/oab-computer-pw-mcp.service <<'EOF' [Unit] -Description=Playwright MCP for the oab-instance-mcp hands node (loopback :8794) +Description=Playwright MCP for the oab-computer hands node (loopback :8794) After=graphical-session.target [Service] -ExecStart=%h/.local/oab-instance-mcp/pw-mcp.sh +ExecStart=%h/.local/oab-computer/pw-mcp.sh Environment=XDG_RUNTIME_DIR=/run/user/%U Environment=WAYLAND_DISPLAY=wayland-0 Environment=DISPLAY=:0 @@ -199,19 +199,19 @@ RestartSec=3 WantedBy=default.target EOF systemctl --user daemon-reload -systemctl --user enable --now oab-pw-mcp.service +systemctl --user enable --now oab-computer-pw-mcp.service # Block camera/mic/notification/geolocation prompts: YouTube raised an xdg-desktop-portal # "Allow app to use the Camera?" dialog that nothing but `mouse` could dismiss. sudo mkdir -p /etc/chromium/policies/managed echo '{"DefaultMediaStreamSetting": 2, "DefaultNotificationsSetting": 2, "DefaultGeolocationSetting": 2}' \ - | sudo tee /etc/chromium/policies/managed/oab-instance-mcp.json >/dev/null -systemctl --user restart oab-pw-mcp + | sudo tee /etc/chromium/policies/managed/oab-computer.json >/dev/null +systemctl --user restart oab-computer-pw-mcp ``` `pw-mcp.sh` uses the distro Chromium (`--executable-path /usr/bin/chromium`) so nothing is downloaded, launches it **headed** into the seat, keeps a persistent profile under -`~/.local/oab-instance-mcp/pw-profile`, and lists `--allowed-hosts` in both `host` and +`~/.local/oab-computer/pw-profile`, and lists `--allowed-hosts` in both `host` and `host:port` forms (Playwright answers 403 to everything otherwise). Chromium runs with its sandbox on; check with `tr '\0' '\n' cat ~/.config/oab-instance-mcp/token) +TOKEN=$(ssh cat ~/.config/oab-computer/token) U=https://..ts.net:8444 curl -s $U/healthz # ok curl -s -o /dev/null -w '%{http_code}\n' -X POST $U/mcp -H 'Authorization: Bearer nope' -d '{}' # 401 @@ -291,22 +291,22 @@ exec_cancel` + 32 `browser_*` + the runtime's `instance_status`. ## Operate ```sh -systemctl --user status oab-instance-mcp oab-pw-mcp -journalctl --user -u oab-instance-mcp -f # one line per deny / upstream error -systemctl --user restart oab-instance-mcp # grants are in memory: re-lend afterwards +systemctl --user status oab-computer oab-computer-pw-mcp +journalctl --user -u oab-computer -f # one line per deny / upstream error +systemctl --user restart oab-computer # grants are in memory: re-lend afterwards sudo tailscale serve --https=8444 off # stop exposing WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/$(id -u) wlr-randr # outputs the tools see ``` Grants survive a daemon restart (#12): live grants are kept in -`~/.local/state/oab-instance-mcp/grants.json` (mode 600; override with `MCP_GRANTS_FILE`, or +`~/.local/state/oab-computer/grants.json` (mode 600; override with `MCP_GRANTS_FILE`, or `MCP_GRANTS_FILE=off` to disable), and on start every grant still inside its deadline is re-dialled -under its original id — `journalctl --user -u oab-instance-mcp` shows `grants: resuming `. The +under its original id — `journalctl --user -u oab-computer` shows `grants: resuming `. The file holds the attach secrets, at the same trust level as the bearer token file. A grant whose runtime has forgotten it (pod replaced) ends with `handshake_rejected_401`; lend again. A request or stored grant that names the retired `desktop` profile runs as `observe` (#59). Each -such mapping is logged and appended to `~/.local/state/oab-instance-mcp/audit.jsonl` (mode 600; +such mapping is logged and appended to `~/.local/state/oab-computer/audit.jsonl` (mode 600; override with `MCP_AUDIT_FILE`, or `MCP_AUDIT_FILE=off` to disable). See [Upgrading from 0.8.x](tool-profiles.md#upgrading-from-08x-59). @@ -319,7 +319,7 @@ long call: same arguments and `structuredContent` as on macOS (README Tools tabl point the agent here. Under `observe` neither `bash` nor the job tools exist, and a long `bash` call is an *unknown tool*, not a refusal that names it. -- **Logs**: `$XDG_STATE_HOME/oab-instance-mcp/jobs/.out` / `.err` (default +- **Logs**: `$XDG_STATE_HOME/oab-computer/jobs/.out` / `.err` (default `~/.local/state/…`; directory mode 700, files 600). The job writes to them directly, so they are never truncated and can be followed with `tail -f`. `exec_poll` reads them by byte offset (`stdout_since` / `stderr_since` → `stdout_next` / `stderr_next`), at most 1 MiB per stream per @@ -342,8 +342,8 @@ call is an *unknown tool*, not a refusal that names it. whatever their age. GC runs at start-up and on each `exec_start`. ```sh -ls -lt ~/.local/state/oab-instance-mcp/jobs/ # .out / .err -tail -f ~/.local/state/oab-instance-mcp/jobs/.out +ls -lt ~/.local/state/oab-computer/jobs/ # .out / .err +tail -f ~/.local/state/oab-computer/jobs/.out ``` ## Headless server notes (Ubuntu 24.04 on `black`) @@ -417,7 +417,7 @@ What a box with no desktop needed beyond the Pi, all handled by `install-linux.s use the `tailscale serve` URL even from the node itself. - **`pkill -f` inside `ssh host 'sh -c …'` kills the `sh -c`** whose command line matches. Use `pkill -x ` — but `-x` matches at most 15 characters of the process name, so - `pkill -x oab-instance-mcp` silently matches nothing. + `pkill -x oab-computer` silently matches nothing. - **Chromium `No usable sandbox!` / `Trace/breakpoint trap`** on Ubuntu 24.04+: AppArmor userns restriction; see [Headless server notes](#headless-server-notes-ubuntu-2404-on-black). - **Headed browser "without having a XServer"** on a headless seat: Chromium picked X11; add diff --git a/docs/operators.md b/docs/operators.md index 713227d..ecfb0ab 100644 --- a/docs/operators.md +++ b/docs/operators.md @@ -41,7 +41,7 @@ written as placeholders or as "an M4 Mac mini". - **A LaunchAgent cannot read external volumes without Full Disk Access.** A `cwd` on `/Volumes/…` (e.g. a RAID where builds live) does not fail with EPERM — the syscall *blocks*, so an un-guarded `exec` would hang to its timeout. Two fixes: `ExecCwd` time-boxes - a readability probe and returns a clear "grant Full Disk Access to dev.openab.instance-mcp" + a readability probe and returns a clear "grant Full Disk Access to dev.openab.computer" error instead of hanging; and granting FDA to the bundle in System Settings → Privacy & Security → Full Disk Access makes the volume readable (verified 2026-09-23: same `ls /Volumes/…` went from a 15 s timeout to 39 ms). The grant sticks across re-signing like the diff --git a/docs/oracle-cloud-refarch.md b/docs/oracle-cloud-refarch.md index 08c7775..f77839e 100644 --- a/docs/oracle-cloud-refarch.md +++ b/docs/oracle-cloud-refarch.md @@ -1,5 +1,15 @@ # Reference architecture: instance-mcp on an Oracle Cloud Ubuntu VM +**Renamed** from `oab-instance-mcp` to `oab-computer` in 0.12.0 (the product name is OpenAB Computer). This is a dated record: paths, unit, label and artifact names below are the 0.8–0.11 names (`oab-instance-mcp`, `dev.openab.instance-mcp`, `oab-pw-mcp.service`). + +| Old (0.8–0.11) | New (0.12.0) | +|---|---| +| `oab-instance-mcp` (binary, app, directories, unit, tarball) | `oab-computer` | +| `dev.openab.instance-mcp` (bundle id, LaunchAgent) | `dev.openab.computer` | +| `dev.openab.instance-mcp.pw-mcp`, `oab-pw-mcp.service` | `dev.openab.computer.pw-mcp`, `oab-computer-pw-mcp.service` | +| `oab-instance-mcp.service` | `oab-computer.service` | +| `OAB_INSTANCE_MCP_*` | `OAB_COMPUTER_*` | + > Agent-friendly prompt block (copy-paste to set up a new VM) > > ``` diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index 63e31b0..6af1951 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -119,7 +119,7 @@ local tool is served under `owner` only, except `sys_info` and `screenshot`, whi | `osascript` | `OsascriptTool` | no (macOS only) | `shell`; `owner` only | none | | `ui_snapshot` | `UISnapshotTool` (`UISnapshot/`, AXUIElement) | no (Linux AT-SPI deferred, #102) | `observe`-class on macOS; `owner` only (not in `observe` in phase 1) | `profile_vectors.json` `macos` lists; Swift `UISnapshotWalkerTests`/`UISnapshotTargetTests`/`UISnapshotToolTests` (fake AX provider, no GUI); Rust `profile_conformance::macos_only_tools_are_not_served_on_linux` | | `ui_action` | `UIActionTool` (`UISnapshot/UIAction.swift`, `UISnapshotCache.swift`; AX actions, no pointer movement) | no (Linux AT-SPI deferred, #102) | `shell` on macOS; `owner` only | `profile_vectors.json` `macos` lists; Swift `UIActionToolTests`/`UISnapshotCacheTests` (fake AX provider, no GUI), `ProfileBoundaryTests.testUIActionIsOwnerOnlyShellClass`; Rust `profile_conformance::macos_only_tools_are_not_served_on_linux` | -| `exec_start`, `exec_poll`, `exec_list`, `exec_cancel` | `AsyncExecTools`, `JobRegistry` | yes (`src/jobs/`, #32 item 4): same names, arguments and `structuredContent`; `bash -c` instead of `zsh -f -c`; logs under `$XDG_STATE_HOME/oab-instance-mcp/jobs/`; `shell`-class, `owner` only (Swift classes `exec_start` as `shell`, `exec_poll`/`exec_list` as `observe` and `exec_cancel` as `act`; both expose the four to `owner` only, so the gating is the same) | Swift: `exec_start` `shell`, `exec_poll`/`exec_list` `observe`, `exec_cancel` `act`; Rust: all `shell`. `owner` only on both | Rust unit tests `jobs::tests` and `smoke.sh` (start, offset polls, survives an attach drop, cancel → `killed`); no shared vectors. Differences: Rust keeps the 10 most recent finished jobs (Swift: finished metadata for ~10 min), pages `exec_poll` at 1 MiB per stream, and refuses a `signal` other than `KILL`/`TERM`. A job stays `running` until its shell has exited and its process group is empty (Swift: until its output pipes close), so a child the shell left behind can still be cancelled or timed out | +| `exec_start`, `exec_poll`, `exec_list`, `exec_cancel` | `AsyncExecTools`, `JobRegistry` | yes (`src/jobs/`, #32 item 4): same names, arguments and `structuredContent`; `bash -c` instead of `zsh -f -c`; logs under `$XDG_STATE_HOME/oab-computer/jobs/`; `shell`-class, `owner` only (Swift classes `exec_start` as `shell`, `exec_poll`/`exec_list` as `observe` and `exec_cancel` as `act`; both expose the four to `owner` only, so the gating is the same) | Swift: `exec_start` `shell`, `exec_poll`/`exec_list` `observe`, `exec_cancel` `act`; Rust: all `shell`. `owner` only on both | Rust unit tests `jobs::tests` and `smoke.sh` (start, offset polls, survives an attach drop, cancel → `killed`); no shared vectors. Differences: Rust keeps the 10 most recent finished jobs (Swift: finished metadata for ~10 min), pages `exec_poll` at 1 MiB per stream, and refuses a `signal` other than `KILL`/`TERM`. A job stays `running` until its shell has exited and its process group is empty (Swift: until its output pipes close), so a child the shell left behind can still be cancelled or timed out | | `browser_*` (upstream Playwright) | `UpstreamMCP` | yes (`mcp.rs` `Upstream`, `MCP_UPSTREAM`) | not classified locally; every upstream tool is `owner` only on both, `observe` gets none (#59) | `profile_vectors.json` membership on both; the full 32-tool fixture through a fake upstream: `BrowserProfileTests` (Swift), `profile_conformance::owner_reaches_every_upstream_tool_and_observe_none` (Rust) | ## 6. Profile rows diff --git a/docs/releasing.md b/docs/releasing.md index 52a9f1e..0acff9e 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,4 +1,4 @@ -# Releasing oab-instance-mcp for macOS +# Releasing oab-computer for macOS A release is a **universal (arm64 + x86_64), Developer-ID-signed and Apple-notarized** app plus a signed/notarized installer package. The package is the normal download; the app zip is for advanced @@ -10,21 +10,21 @@ A `vMAJOR.MINOR.PATCH` tag publishes a GitHub Release titled `oab-computer VERSI | Artifact | Use | |---|---| -| `oab-instance-mcp-VERSION-universal.pkg` | Recommended. Double-click; installs/configures the app for the logged-in desktop user | -| `oab-instance-mcp-VERSION-universal.app.zip` | Pre-signed app, no package receipt. Contains `Contents/Resources/install-prebuilt.sh` for manual installation | +| `oab-computer-VERSION-universal.pkg` | Recommended. Double-click; installs/configures the app for the logged-in desktop user | +| `oab-computer-VERSION-universal.app.zip` | Pre-signed app, no package receipt. Contains `Contents/Resources/install-prebuilt.sh` for manual installation | | `SHA256SUMS` | SHA-256 of both artifacts | -Only the release title uses the new name: asset names (`oab-instance-mcp-VERSION-*`), the binary, -the bundle id and the install paths stay `oab-instance-mcp` on purpose (see #63). +Since 0.12.0 the asset names (`oab-computer-VERSION-*`), the binary, the bundle id and the install +paths all use `oab-computer` (up to 0.11 they were `oab-instance-mcp`). The installer: 1. verifies bundle id, code signature and team `6LPQNY95AQ` **before** stopping the running service; 2. detects the current Tailscale login and MagicDNS name from structured `tailscale status --json`; -3. installs to `~/.local/oab-instance-mcp/oab-instance-mcp.app` without re-signing it; -4. creates a bearer token once at `~/.config/oab-computer/token`, mode 600, and preserves it on updates (the first 0.12 install copies a 0.11 token from `~/.config/oab-instance-mcp/token` once; Linux keeps `~/.config/oab-instance-mcp`); -5. writes/starts the Aqua-user LaunchAgent `dev.openab.instance-mcp`; -6. adds the Playwright upstream when `dev.openab.instance-mcp.pw-mcp` is installed; and +3. installs to `~/.local/oab-computer/oab-computer.app` without re-signing it; +4. creates a bearer token once at `~/.config/oab-computer/token`, mode 600, and preserves it on updates (the first 0.12 install copies a 0.11 token from `~/.config/oab-instance-mcp/token` once; the Linux installer generates a new token and copies nothing); +5. writes/starts the Aqua-user LaunchAgent `dev.openab.computer`; +6. adds the Playwright upstream when `dev.openab.computer.pw-mcp` is installed; and 7. configures `tailscale serve --https=8444` to loopback port 8795. A logged-in GUI user and a logged-in Tailscale app are prerequisites. Package scripts run as root, @@ -34,8 +34,8 @@ LaunchAgent or config is written to root's home. ## One-time TCC grant After the first Developer-ID release install, enable **OpenAB Computer** once under System Settings → -Privacy & Security (older docs and builds before 0.9.0 list it as **oab-instance-mcp**; the bundle id -`dev.openab.instance-mcp` is unchanged, so it is the same entry): +Privacy & Security (builds before 0.12.0 used the bundle id `dev.openab.instance-mcp`, so their +entries do not carry over and must be granted again): - Full Disk Access - Screen & System Audio Recording @@ -76,7 +76,7 @@ deleted in an `always()` cleanup step. ## Cut a release -1. Update `let version = "…"` in `Sources/oab-instance-mcp/main.swift` and merge with green CI. +1. Update `let version = "…"` in `Sources/oab-computer/main.swift` and merge with green CI. This is the only version to bump. The Linux binary takes its version from the tag at build time (see [Linux version](#linux-version)); do not bump `poc/reverse-attach-linux/Cargo.toml`. 2. Ensure the matching active-team release secrets above exist. @@ -88,7 +88,7 @@ deleted in an `always()` cleanup step. ``` 4. The `Release macOS installer` workflow builds/tests, signs, notarizes and publishes the macOS - artifacts, and (since 0.6.7) `oab-instance-mcp-VERSION-linux-{arm64,amd64}.tar.gz` + `.sha256` + artifacts, and (since 0.6.7) `oab-computer-VERSION-linux-{arm64,amd64}.tar.gz` + `.sha256` from the same run. All three jobs wait on the `release` environment approval. A manual dispatch can retry an existing tag; it is not a way to release an untagged commit, and it **must run on the tag ref** — the environment only allows `v*` refs, so a dispatch from @@ -102,8 +102,8 @@ deleted in an `always()` cleanup step. ```sh scripts/verify-release.sh \ - oab-instance-mcp-0.8.4-universal.app.zip \ - oab-instance-mcp-0.8.4-universal.pkg + oab-computer-0.8.4-universal.app.zip \ + oab-computer-0.8.4-universal.pkg shasum -a 256 -c SHA256SUMS ``` @@ -125,7 +125,7 @@ The Linux hands node reports the release version in `sys_info.agent.version` and `serverInfo.version` (#74). It is not read from `poc/reverse-attach-linux/Cargo.toml`: - The release workflow's Linux job checks that the tag equals `let version` in - `Sources/oab-instance-mcp/main.swift`, the same gate as the macOS job, then builds with + `Sources/oab-computer/main.swift`, the same gate as the macOS job, then builds with `OAB_RELEASE_VERSION=`, which `option_env!` compiles into the binary. Both jobs and PR CI read the version with `scripts/release-version.sh [tag]`, which requires one `MAJOR.MINOR.PATCH` line and exact string equality with the tag. @@ -140,7 +140,7 @@ CI runs this on every PR: ```sh swift build -scripts/test-packaging.sh .build/debug/oab-instance-mcp +scripts/test-packaging.sh .build/debug/oab-computer ``` It assembles the app, proves unsigned input is rejected by the production installer, exercises the @@ -153,12 +153,12 @@ To build a universal unsigned artifact manually on a Mac: ```sh swift build -c release --arch arm64 --scratch-path /tmp/imcp-arm64 swift build -c release --arch x86_64 --scratch-path /tmp/imcp-x86_64 -lipo -create /tmp/imcp-arm64/release/oab-instance-mcp \ - /tmp/imcp-x86_64/release/oab-instance-mcp \ - -output /tmp/oab-instance-mcp -chmod +x /tmp/oab-instance-mcp -scripts/assemble-app.sh /tmp/oab-instance-mcp /tmp/oab-instance-mcp.app 0.8.4 -ALLOW_UNSIGNED=1 scripts/package-pkg.sh /tmp/oab-instance-mcp.app /tmp/oab-instance-mcp.pkg 0.8.4 +lipo -create /tmp/imcp-arm64/release/oab-computer \ + /tmp/imcp-x86_64/release/oab-computer \ + -output /tmp/oab-computer +chmod +x /tmp/oab-computer +scripts/assemble-app.sh /tmp/oab-computer /tmp/oab-computer.app 0.8.4 +ALLOW_UNSIGNED=1 scripts/package-pkg.sh /tmp/oab-computer.app /tmp/oab-computer.pkg 0.8.4 ``` Unsigned artifacts are testing inputs only; do not install or publish them. diff --git a/docs/requirements/authn.md b/docs/requirements/authn.md index 0c7d1e5..a8a56b3 100644 --- a/docs/requirements/authn.md +++ b/docs/requirements/authn.md @@ -1,5 +1,7 @@ # Design — bearer-token authn as defence-in-depth for the MCP endpoint +**Renamed** from `oab-instance-mcp` to `oab-computer` in 0.12.0 (the product name is OpenAB Computer). This is a dated record: paths, unit, label and artifact names below are the 0.8–0.11 names (`oab-instance-mcp`, `dev.openab.instance-mcp`, `oab-pw-mcp.service`). + > Recorded 2026-09-23. Implements the "two callers, two trust levels, one daemon" > constraint from [`connect-closed-loop.md`](connect-closed-loop.md), first slice: add a > shared bearer token so a leaked tailnet credential alone cannot reach the agent. diff --git a/docs/requirements/connect-closed-loop.md b/docs/requirements/connect-closed-loop.md index b30b8ef..e096e99 100644 --- a/docs/requirements/connect-closed-loop.md +++ b/docs/requirements/connect-closed-loop.md @@ -1,5 +1,7 @@ # Requirement — OpenAB Connect closed loop: agent operates the Mac mini, human observes +**Renamed** from `oab-instance-mcp` to `oab-computer` in 0.12.0 (the product name is OpenAB Computer). This is a dated record: paths, unit, label and artifact names below are the 0.8–0.11 names (`oab-instance-mcp`, `dev.openab.instance-mcp`, `oab-pw-mcp.service`). + > Recorded 2026-09-22 (mid-Step-1 PoC). Not scheduled; captured so Step 2/3 design > choices do not foreclose it. Parent design: openabdev/openab#1544. diff --git a/docs/requirements/reverse-attach.md b/docs/requirements/reverse-attach.md index ee7802f..8b9d8b8 100644 --- a/docs/requirements/reverse-attach.md +++ b/docs/requirements/reverse-attach.md @@ -1,5 +1,7 @@ # Requirement — reverse attach: macmini dials the sandbox, the sandbox has no tailnet egress +**Renamed** from `oab-instance-mcp` to `oab-computer` in 0.12.0 (the product name is OpenAB Computer). This is a dated record: paths, unit, label and artifact names below are the 0.8–0.11 names (`oab-instance-mcp`, `dev.openab.instance-mcp`, `oab-pw-mcp.service`). + > Recorded 2026-09-24. Implements **Phase 4** of > [`connect-closed-loop.md`](connect-closed-loop.md): "Connect's agent (in the `openab-pty` > sandbox) reaches macmini MCP over the tailnet." Supersedes the earlier *sandbox-egress* diff --git a/docs/tool-profiles.md b/docs/tool-profiles.md index 7d839a3..88f2833 100644 --- a/docs/tool-profiles.md +++ b/docs/tool-profiles.md @@ -142,10 +142,10 @@ the change is reported to the granter. | `POST /attach {"profile":"desktop"}` | a `desktop` grant | 202, runs as `observe`. The grant JSON has `"profile":"observe"`, `"requested_profile":"desktop"` and a `warning`. The daemon log and the audit file record the mapping | | `POST /attach` without `profile` | macOS `desktop`, Linux `observe` | `observe` on both | | `POST /attach` with `sandbox` or any other unknown name | 400 (two different messages) | 400 `profile must be one of [owner, observe]` on both | -| A grant stored as `desktop` (macOS `~/Library/Application Support/oab-instance-mcp/grants.json`, Linux `grants.json`) | resumed as `desktop` | kept and re-dialled under its id, as `observe`. `GET /attach` shows `profile: observe`, `requested_profile: desktop` and the `warning`. The file is rewritten with `profile: observe` and the requested name | +| A grant stored as `desktop` (macOS `~/Library/Application Support/oab-computer/grants.json`, Linux `grants.json`) | resumed as `desktop` | kept and re-dialled under its id, as `observe`. `GET /attach` shows `profile: observe`, `requested_profile: desktop` and the `warning`. The file is rewritten with `profile: observe` and the requested name | | A grant stored as `sandbox` or an unknown name | dropped | dropped (unchanged, #46) | | macOS `--switchboard-profile desktop` (a flag in the LaunchAgent plist) | `desktop` | serves `observe`, prints the warning to the agent log, and logs `switchboard: dialling … as observe (requested desktop)`. Any other unknown value still exits with status 64 | -| macOS `~/.config/oab-instance-mcp/switchboard.profile` containing `desktop` | copied into the plist | the installer writes `--switchboard-profile observe` and prints a warning. It does not edit your file | +| macOS `~/.config/oab-computer/switchboard.profile` containing `desktop` | copied into the plist | the installer writes `--switchboard-profile observe` and prints a warning. It does not edit your file | | Linux environment and config | no profile setting exists | nothing to migrate | `requested_profile` is in every grant JSON (it equals `profile` when nothing was mapped). @@ -161,8 +161,8 @@ string counts as absent on both platforms: the row keeps its stored profile. **Audit file.** Every mapping is also appended as one JSON line to an audit file, separate from the daemon log so it survives log rotation: macOS -`~/Library/Application Support/oab-instance-mcp/audit.jsonl`, Linux -`~/.local/state/oab-instance-mcp/audit.jsonl` (`$XDG_STATE_HOME` if set; override with +`~/Library/Application Support/oab-computer/audit.jsonl`, Linux +`~/.local/state/oab-computer/audit.jsonl` (`$XDG_STATE_HOME` if set; override with `MCP_AUDIT_FILE`, or `MCP_AUDIT_FILE=off` to disable). The file is mode 600 in a 0700 directory and is only appended to. A record looks like this: @@ -177,7 +177,7 @@ no `grant`, `session` or `principal`). A write failure is logged and never block **To restore shell or browser access**, grant `owner` explicitly: pick Owner in OpenAB Connect or OpenAB Remote, or send `POST /attach` with `"profile":"owner"`. The new grant replaces the old one for the same runtime and session. For a switchboard, choose `owner` in the menu bar's Configure… -(it asks for confirmation), or write `owner` to `~/.config/oab-instance-mcp/switchboard.profile` +(it asks for confirmation), or write `owner` to `~/.config/oab-computer/switchboard.profile` and re-run the installer, which bakes the file into the LaunchAgent plist. A hand-edited profile file alone does not apply: the daemon ignores it until the menu bar saves again, so a restart serves the profile in the plist. To keep `observe` and silence the warning, write `observe` there diff --git a/images/toolchain/Dockerfile b/images/toolchain/Dockerfile index cbd0a55..7ab9188 100644 --- a/images/toolchain/Dockerfile +++ b/images/toolchain/Dockerfile @@ -35,7 +35,7 @@ FROM debian:bookworm-slim@sha256:3783cc01769c7b2b1b83a5c5ad96c815348e28ed7da68e2e3687004faa906251 -LABEL org.opencontainers.image.source="https://github.com/openabdev/instance-mcp" \ +LABEL org.opencontainers.image.source="https://github.com/openabdev/openab-computer" \ org.opencontainers.image.title="oab-toolchain" \ org.opencontainers.image.description="Sandbox toolchain image for the sandbox adapter (docs/adr/sandbox-adapter.md)" \ org.opencontainers.image.base.name="docker.io/library/debian:bookworm-slim" diff --git a/poc/pw-mcp/README.md b/poc/pw-mcp/README.md index 6321af0..03402f2 100644 --- a/poc/pw-mcp/README.md +++ b/poc/pw-mcp/README.md @@ -6,7 +6,7 @@ > > **Playwright MCP is loopback only (`127.0.0.1:8794`) and has no authentication.** It is not > mapped onto the tailnet and has no `:8443` access path (#115). Browser tools are reached only -> through `oab-instance-mcp` (`--upstream browser=http://127.0.0.1:8794/mcp`), which adds the +> through `oab-computer` (`--upstream browser=http://127.0.0.1:8794/mcp`), which adds the > Tailscale login check, the bearer token and the owner-only tool profile. The first version of > this PoC mapped `:8443` with `tailscale serve`; see > [Older installs](#older-installs-may-still-have-a-8443-mapping). @@ -15,12 +15,12 @@ | Piece | Location | |---|---| -| `@playwright/mcp@0.0.82` (pinned, `npm install --save-exact`) | `~/.local/oab-instance-mcp/pw-mcp/` | -| Wrapper script | `~/.local/oab-instance-mcp/pw-mcp.sh` (from `pw-mcp.sh` here) | -| LaunchAgent (`gui/`) | `~/Library/LaunchAgents/dev.openab.instance-mcp.pw-mcp.plist` (rendered by `install.sh`) | -| Persistent browser profile | `~/.local/oab-instance-mcp/pw-profile/` | -| Screenshot / snapshot output | `~/.local/oab-instance-mcp/pw-output/` | -| Logs | `~/Library/Logs/oab-instance-mcp/pw-mcp.{log,err}` | +| `@playwright/mcp@0.0.82` (pinned, `npm install --save-exact`) | `~/.local/oab-computer/pw-mcp/` | +| Wrapper script | `~/.local/oab-computer/pw-mcp.sh` (from `pw-mcp.sh` here) | +| LaunchAgent (`gui/`) | `~/Library/LaunchAgents/dev.openab.computer.pw-mcp.plist` (rendered by `install.sh`) | +| Persistent browser profile | `~/.local/oab-computer/pw-profile/` | +| Screenshot / snapshot output | `~/.local/oab-computer/pw-output/` | +| Logs | `~/Library/Logs/oab-computer/pw-mcp.{log,err}` | | Listener | `127.0.0.1:8794` (loopback only; the wrapper passes `--host 127.0.0.1`) | | Tailnet URL | None. `install.sh` creates no `tailscale serve` mapping | @@ -28,20 +28,20 @@ Browser: Playwright's bundled Chromium (`~/Library/Caches/ms-playwright/chromium **headed**, spawned as a child of the LaunchAgent pid, so it is a real window in the logged-in desktop session. No Google Chrome is installed on macmini. -Client side: do not point a client at Playwright directly. Connect to `oab-instance-mcp`, which +Client side: do not point a client at Playwright directly. Connect to `oab-computer`, which re-serves the `browser_*` tools under its `owner` profile (top-level README, "Browser tools for lent sessions"). ## Install / operate ```sh -ssh macmini 'cd ~/src/oab-instance-mcp && bash poc/pw-mcp/install.sh' # idempotent +ssh macmini 'cd ~/src/oab-computer && bash poc/pw-mcp/install.sh' # idempotent ``` ```sh -ssh macmini launchctl print gui/501/dev.openab.instance-mcp.pw-mcp | grep -E 'state|pid' -ssh macmini launchctl kickstart -k gui/501/dev.openab.instance-mcp.pw-mcp # restart -ssh macmini tail -20 ~/Library/Logs/oab-instance-mcp/pw-mcp.err +ssh macmini launchctl print gui/501/dev.openab.computer.pw-mcp | grep -E 'state|pid' +ssh macmini launchctl kickstart -k gui/501/dev.openab.computer.pw-mcp # restart +ssh macmini tail -20 ~/Library/Logs/oab-computer/pw-mcp.err ssh macmini lsof -nP -iTCP:8794 -sTCP:LISTEN # one listener, on 127.0.0.1 ``` @@ -60,7 +60,7 @@ tailscale serve status # look for :8443 pointing at http://127.0.0. tailscale serve --https=8443 off # remove only that mapping ``` -Browser tools keep working through `oab-instance-mcp`, which reaches Playwright over loopback. +Browser tools keep working through `oab-computer`, which reaches Playwright over loopback. ## Measurements (first PoC, laptop → macmini through the since-removed :8443 mapping) @@ -80,7 +80,7 @@ Latency is not the bottleneck; the LLM round trip is. Good enough for the thin-e 1. **`--allowed-hosts` is an exact string match on the `Host` header, port included.** `127.0.0.1` does not cover `127.0.0.1:8794`. List every form a client will send. - (oab-instance-mcp sends the bare `127.0.0.1`, which the wrapper allows.) + (oab-computer sends the bare `127.0.0.1`, which the wrapper allows.) 2. **Default browser channel is `chrome`, not bundled Chromium.** Without `--browser chromium` the first tool call fails with `Chromium distribution 'chrome' is not found at /Applications/Google Chrome.app`. The @@ -103,7 +103,7 @@ Latency is not the bottleneck; the LLM round trip is. Good enough for the thin-e - Playwright MCP has no authentication. It listens on `127.0.0.1:8794` only, so only processes on this Mac reach it. There is no `tailscale serve` mapping for it and no `:8443` access path. -- Browser tools reach other machines only through `oab-instance-mcp`'s `--upstream`, behind its +- Browser tools reach other machines only through `oab-computer`'s `--upstream`, behind its login check and bearer token, and only under the `owner` profile: `observe` gets no browser tool (#59). A switchboard's per-caller allowlist applies on top. - `browser_run_code_unsafe` is in the tool list: arbitrary Playwright JS in the logged-in @@ -112,12 +112,12 @@ Latency is not the bottleneck; the LLM round trip is. Good enough for the thin-e ## What this proves for the design - The Aqua-session LaunchAgent + loopback listener shape works and needs no custom transport. - The Swift `oab-instance-mcp` re-serves the browser tools to remote callers. + The Swift `oab-computer` re-serves the browser tools to remote callers. - Playwright MCP covers the browser half completely; the Swift daemon does not need to touch CDP. `connectOverCDP` from the sandbox was never needed. ## Next -The Swift `oab-instance-mcp` daemon (see top-level README) runs beside this on `127.0.0.1:8795`, +The Swift `oab-computer` daemon (see top-level README) runs beside this on `127.0.0.1:8795`, same LaunchAgent pattern, with Tailscale-identity auth, and re-serves these tools through `--upstream`. Lent callers get the browser only under `owner` (#59). diff --git a/poc/pw-mcp/check-legacy-mapping.sh b/poc/pw-mcp/check-legacy-mapping.sh index 1b91a5e..5fe65f8 100755 --- a/poc/pw-mcp/check-legacy-mapping.sh +++ b/poc/pw-mcp/check-legacy-mapping.sh @@ -12,7 +12,7 @@ warn_legacy_8443_mapping() { echo "Tailscale :8443 check: could not check (Tailscale CLI not found)"; return 0 fi # TERM_PROGRAM makes the Tailscale.app binary act as a CLI. - if ! out=$(TERM_PROGRAM=oab-pw-mcp-installer "$cli" serve status 2>/dev/null) || [ -z "$out" ]; then + if ! out=$(TERM_PROGRAM=oab-computer-pw-mcp-installer "$cli" serve status 2>/dev/null) || [ -z "$out" ]; then echo "Tailscale :8443 check: could not check (serve status unreadable)"; return 0 fi # A mapping is a non-indented "https://...:8443" header followed by a handler line to 8794. @@ -23,7 +23,7 @@ warn_legacy_8443_mapping() { cat <<'MSG' WARNING: this Mac still maps tailnet port :8443 to 127.0.0.1:8794 (Playwright MCP). Playwright MCP has no authentication, so any device on your tailnet can drive the browser, - including browser_run_code_unsafe. Browser tools do not need it: oab-instance-mcp reaches + including browser_run_code_unsafe. Browser tools do not need it: oab-computer reaches Playwright over loopback. This installer does not remove it. To remove it, run: tailscale serve --https=8443 off MSG diff --git a/poc/pw-mcp/install.sh b/poc/pw-mcp/install.sh index b0cd935..8b9fa77 100755 --- a/poc/pw-mcp/install.sh +++ b/poc/pw-mcp/install.sh @@ -1,12 +1,12 @@ #!/bin/bash -# Install the Playwright MCP LaunchAgent beside oab-instance-mcp (run ON the target Mac). +# Install the Playwright MCP LaunchAgent beside oab-computer (run ON the target Mac). # Renders the plist with this user's $HOME (launchd does not expand ~), installs the # wrapper and pins @playwright/mcp. Playwright MCP listens on 127.0.0.1:8794 only and is -# NOT exposed to the tailnet; browser tools are reached through oab-instance-mcp (#115). +# NOT exposed to the tailnet; browser tools are reached through oab-computer (#115). set -euo pipefail -LABEL=dev.openab.instance-mcp.pw-mcp -BASE="$HOME/.local/oab-instance-mcp" -LOGS="$HOME/Library/Logs/oab-instance-mcp" +LABEL=dev.openab.computer.pw-mcp +BASE="$HOME/.local/oab-computer" +LOGS="$HOME/Library/Logs/oab-computer" HERE="$(cd "$(dirname "$0")" && pwd)" TS=/Applications/Tailscale.app/Contents/MacOS/Tailscale # read by warn_legacy_8443_mapping # shellcheck source=check-legacy-mapping.sh disable=SC2034 @@ -38,4 +38,4 @@ for _ in $(seq 1 40); do launchctl print "gui/$UID_/$LABEL" >/dev/null 2>&1 || b launchctl bootstrap "gui/$UID_" "$PLIST" launchctl print "gui/$UID_/$LABEL" | grep -E 'state|pid =' warn_legacy_8443_mapping -echo "Browser tools: reach them through oab-instance-mcp with --upstream browser=http://127.0.0.1:8794/mcp (the main installer adds it when this agent is loaded)." +echo "Browser tools: reach them through oab-computer with --upstream browser=http://127.0.0.1:8794/mcp (the main installer adds it when this agent is loaded)." diff --git a/poc/pw-mcp/pw-mcp.sh b/poc/pw-mcp/pw-mcp.sh index 756f01c..263eec2 100755 --- a/poc/pw-mcp/pw-mcp.sh +++ b/poc/pw-mcp/pw-mcp.sh @@ -1,7 +1,7 @@ #!/bin/zsh -# Playwright MCP for oab-instance-mcp step-1 PoC. Loopback only (127.0.0.1:8794); never exposed with tailscale serve (#115). +# Playwright MCP for oab-computer step-1 PoC. Loopback only (127.0.0.1:8794); never exposed with tailscale serve (#115). export PATH=/opt/homebrew/bin:/usr/bin:/bin -BASE="$HOME/.local/oab-instance-mcp" +BASE="$HOME/.local/oab-computer" DNSNAME="${TS_DNSNAME:-$(/Applications/Tailscale.app/Contents/MacOS/Tailscale status --self --peers=false --json | python3 -c 'import json,sys;print(json.load(sys.stdin)["Self"]["DNSName"].rstrip("."))')}" cd "$BASE/pw-mcp" exec ./node_modules/.bin/playwright-mcp \ diff --git a/poc/reverse-attach-linux/README.md b/poc/reverse-attach-linux/README.md index 5bc001e..3284907 100644 --- a/poc/reverse-attach-linux/README.md +++ b/poc/reverse-attach-linux/README.md @@ -43,7 +43,7 @@ mirroring the Swift `ReverseAttachClient` / `POST /attach` contract: `exec_start` + `exec_poll`. Under `observe` a long `bash` is an *unknown tool*. - Background jobs (`src/jobs/`, #32 item 4, `owner` only): `exec_start` / `exec_poll` / `exec_list` / `exec_cancel`, same arguments and `structuredContent` as the Swift tools, with - `bash -c` in a new session. stdout/stderr go to `$XDG_STATE_HOME/oab-instance-mcp/jobs/.out` + `bash -c` in a new session. stdout/stderr go to `$XDG_STATE_HOME/oab-computer/jobs/.out` / `.err` (dir 700, files 600, never truncated); `exec_poll` reads by byte offset, at most 1 MiB per stream per poll. A job stays `running` until its shell has exited and its process group is empty; the shell is reaped only then, so cancel and timeout can signal the group @@ -70,7 +70,7 @@ bash smoke.sh # RESULT: 55 passed, 0 failed | mint path | `ttl_secs=60` seen by the runtime's mint endpoint; `expires_in_secs=60` in the grant | | control contract | `POST /attach` 202 with every Connect-required grant field; `GET /attach` wrapper; `GET /attach/{id}`; `DELETE` 204 + removal; principal and dynamic expiry | | redial / stop | close `1000` → redialed after 1 s; close `4010` → `state=ended, ended=revoked`; wrong secret → `ended=handshake_rejected_401` with exactly one attempt; unreachable runtime → `ended=deadline` | -| MCP | `serverInfo.name=instance-mcp-rpi`; owner list = `sys_info,screenshot,exec`; `sys_info.hostname=`; `exec` ran on the node; unknown tool / method → `-32601` at the time (since #78: unknown tool `-32602`, method `-32601`); notification silent; ping → pong (since #78: `{}`) | +| MCP | `serverInfo.name=oab-computer-rpi`; owner list = `sys_info,screenshot,exec`; `sys_info.hostname=`; `exec` ran on the node; unknown tool / method → `-32601` at the time (since #78: unknown tool `-32602`, method `-32601`); notification silent; ping → pong (since #78: `{}`) | | desktop (removed in #59) | list = `sys_info,screenshot`; forced `exec` → error, not executed (2026-09-28 run). Since #59 `smoke.sh` checks that a `desktop` request runs as `observe` with `requested_profile` and a `warning`, and that a stored `desktop` grant resumes as `observe` | | close handshake | client echoes the Close frame (was a bare EOF before the `socket.flush()` fix in `dial_loop`) | @@ -96,7 +96,7 @@ JPEG in 0.07 s vs a 2.0 MB PNG in 1.0 s. `sys_info` carries the `host` / `displays` / `permissions.screen_recording` / `agent.version` fields the Screens pane reads. -On a Raspberry Pi: systemd user unit `oab-instance-mcp.service` (BIND 127.0.0.1:8795, seat env +On a Raspberry Pi: systemd user unit `oab-computer.service` (BIND 127.0.0.1:8795, seat env `WAYLAND_DISPLAY=wayland-0`, linger on) + `tailscale serve --bg --https=8444 http://127.0.0.1:8795` → `https://..ts.net:8444/mcp`. Verified from the laptop: wrong token 401, initialize 200 with `Mcp-Session-Id`, `sys_info` host= displays=1, Connect-shaped screenshot @@ -118,7 +118,7 @@ config. Verified from the laptop: click into a field → `type` → screenshot s Same pattern as the Mac: `@playwright/mcp` runs beside the daemon on loopback and the daemon re-serves its tools under its own `tools/list`, filtered by the connection's profile. -- `pw-mcp.sh` + systemd user unit `oab-pw-mcp.service`: `@playwright/mcp@0.0.82` pinned (same +- `pw-mcp.sh` + systemd user unit `oab-computer-pw-mcp.service`: `@playwright/mcp@0.0.82` pinned (same as `poc/pw-mcp`), **headed** Chromium launched into the labwc seat (`WAYLAND_DISPLAY`, `XDG_RUNTIME_DIR`, `DISPLAY=:0` for labwc's Xwayland), using the distro Chromium (`--executable-path /usr/bin/chromium`, arm64-native, no Playwright download), persistent @@ -131,7 +131,7 @@ re-serves its tools under its own `tools/list`, filtered by the connection's pro - Profile filter (#59): `owner` sees every upstream tool (all 32, and any added later); `observe` sees none. `profile_conformance` checks both against the 32-tool fixture in `conformance/profile_vectors.json` through a fake upstream. -- Chromium managed policy `/etc/chromium/policies/managed/oab-instance-mcp.json` blocks +- Chromium managed policy `/etc/chromium/policies/managed/oab-computer.json` blocks camera/mic/notification/geolocation prompts: YouTube triggered the xdg-desktop-portal "Allow app to use the Camera?" dialog, which nothing but `mouse` could dismiss. @@ -144,14 +144,14 @@ on the Pi's desktop, so `screenshot` / Connect's Screens pane show what the agen ## Grant persistence (#12) -Live grants are written to `MCP_GRANTS_FILE` (default `$XDG_STATE_HOME/oab-instance-mcp/grants.json`, +Live grants are written to `MCP_GRANTS_FILE` (default `$XDG_STATE_HOME/oab-computer/grants.json`, `off` disables), mode 600 in a 0700 directory, replaced atomically on every create / replace / `DELETE` / terminal end. On start, grants still inside their deadline are re-dialled under the same id. Smoke covers `kill -9` → restart → redial, same id, secret absent from `GET /attach`, and a revoked grant not resumed. A `desktop` request or stored row runs as `observe` (#59), and each mapping is appended as one -JSON line to `MCP_AUDIT_FILE` (default `$XDG_STATE_HOME/oab-instance-mcp/audit.jsonl`, `off` +JSON line to `MCP_AUDIT_FILE` (default `$XDG_STATE_HOME/oab-computer/audit.jsonl`, `off` disables), mode 600. Smoke checks the `attach` and `resume` records. ## Not yet (vs the Swift implementation) diff --git a/poc/reverse-attach-linux/pw-mcp.sh b/poc/reverse-attach-linux/pw-mcp.sh index 946ca8d..e614232 100644 --- a/poc/reverse-attach-linux/pw-mcp.sh +++ b/poc/reverse-attach-linux/pw-mcp.sh @@ -12,7 +12,7 @@ # - PW_NODE_BIN: the directory of a non-system node (fnm/nvm) so npm/npx resolve. export PATH="${PW_NODE_BIN:+$PW_NODE_BIN:}/usr/local/bin:/usr/bin:/bin" export LANG="${LANG:-en_US.UTF-8}" -BASE="$HOME/.local/oab-instance-mcp" +BASE="$HOME/.local/oab-computer" export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" export WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-wayland-0}" export DISPLAY="${DISPLAY:-:0}" diff --git a/poc/reverse-attach-linux/smoke.sh b/poc/reverse-attach-linux/smoke.sh index 3f4bd84..1ba0a86 100755 --- a/poc/reverse-attach-linux/smoke.sh +++ b/poc/reverse-attach-linux/smoke.sh @@ -115,7 +115,7 @@ if q=="killed": sc=byid(13)["result"]["structuredContent"]; print(f'{sc["state"]},{sc.get("exit_code")}') EOF } -check "initialize → serverInfo instance-mcp-rpi" "[[ \$(py init) == instance-mcp-rpi ]]" +check "initialize → serverInfo oab-computer-rpi" "[[ \$(py init) == oab-computer-rpi ]]" check "owner tools/list = sys_info,screenshot,bash,mouse,key,exec_*" "[[ \$(py tools) == sys_info,screenshot,bash,mouse,key,exec_start,exec_poll,exec_list,exec_cancel ]]" check "sys_info hostname = $(hostname)" "[[ \$(py sys) == $(hostname) ]]" # Release builds set OAB_RELEASE_VERSION at compile time and EXPECT_AGENT_VERSION here (#74); @@ -134,7 +134,7 @@ check "ping → pong" "[[ \$(py pong) == True ]]" JOB=$(py job) check "exec_start → job_id" "[[ '$JOB' == job-* ]]" check "exec_poll by offset returns only new output" "[[ \$(py poll2) == True ]]" -check "job log tee'd under XDG_STATE_HOME" "[[ -s '$GRANTS_DIR/oab-instance-mcp/jobs/$JOB.out' ]]" +check "job log tee'd under XDG_STATE_HOME" "[[ -s '$GRANTS_DIR/oab-computer/jobs/$JOB.out' ]]" check "job survived the attach drop (polled on attach 2)" "[[ \$(py survived) == running ]]" check "exec_cancel → killed with exit code" "[[ \$(py killed) == killed,137 ]]" diff --git a/poc/reverse-attach-linux/src/attach/store.rs b/poc/reverse-attach-linux/src/attach/store.rs index 1f4296a..69366f5 100644 --- a/poc/reverse-attach-linux/src/attach/store.rs +++ b/poc/reverse-attach-linux/src/attach/store.rs @@ -10,8 +10,8 @@ //! at the same trust level as the bearer token file beside it (both 600 in the //! daemon user's home). Ended, cancelled and expired grants are never written. //! -//! Path: `MCP_GRANTS_FILE`, else `$XDG_STATE_HOME/oab-instance-mcp/grants.json`, -//! else `~/.local/state/oab-instance-mcp/grants.json`. `MCP_GRANTS_FILE=off` +//! Path: `MCP_GRANTS_FILE`, else `$XDG_STATE_HOME/oab-computer/grants.json`, +//! else `~/.local/state/oab-computer/grants.json`. `MCP_GRANTS_FILE=off` //! disables persistence. use std::fs; @@ -65,7 +65,7 @@ fn default_path() -> Option { let base = std::env::var_os("XDG_STATE_HOME") .map(PathBuf::from) .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".local/state")))?; - Some(base.join("oab-instance-mcp/grants.json")) + Some(base.join("oab-computer/grants.json")) } /// Snapshot the registry's live grants to disk. No-op when persistence is off. diff --git a/poc/reverse-attach-linux/src/audit.rs b/poc/reverse-attach-linux/src/audit.rs index 8e237bc..f119d48 100644 --- a/poc/reverse-attach-linux/src/audit.rs +++ b/poc/reverse-attach-linux/src/audit.rs @@ -5,8 +5,8 @@ //! it. The file is append-only and separate from the journal, so the record //! survives log rotation. Same record shape as the macOS daemon. //! -//! Path: `MCP_AUDIT_FILE`, else `$XDG_STATE_HOME/oab-instance-mcp/audit.jsonl`, -//! else `~/.local/state/oab-instance-mcp/audit.jsonl`. `MCP_AUDIT_FILE=off` +//! Path: `MCP_AUDIT_FILE`, else `$XDG_STATE_HOME/oab-computer/audit.jsonl`, +//! else `~/.local/state/oab-computer/audit.jsonl`. `MCP_AUDIT_FILE=off` //! disables it. Mode 600 in a 0700 directory. Writing is best-effort: a failure //! is logged and never blocks the grant. @@ -36,7 +36,7 @@ fn default_path() -> Option { let base = std::env::var_os("XDG_STATE_HOME") .map(PathBuf::from) .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".local/state")))?; - Some(base.join("oab-instance-mcp/audit.jsonl")) + Some(base.join("oab-computer/audit.jsonl")) } /// One `profile_mapped` record. `source` is `attach` or `resume`. diff --git a/poc/reverse-attach-linux/src/jobs/mod.rs b/poc/reverse-attach-linux/src/jobs/mod.rs index 9f3b421..46c32ed 100644 --- a/poc/reverse-attach-linux/src/jobs/mod.rs +++ b/poc/reverse-attach-linux/src/jobs/mod.rs @@ -2,8 +2,8 @@ //! (same contract as the Swift `JobRegistry`). //! //! Each job's stdout and stderr go straight to files, `

/.out` and -//! `.err` (`$XDG_STATE_HOME/oab-instance-mcp/jobs/`, else -//! `~/.local/state/oab-instance-mcp/jobs/`). The files are the single source of +//! `.err` (`$XDG_STATE_HOME/oab-computer/jobs/`, else +//! `~/.local/state/oab-computer/jobs/`). The files are the single source of //! truth: `exec_poll` reads them by byte offset, nothing is kept in memory or //! truncated, and a human can `tail -f` them. //! @@ -304,7 +304,7 @@ fn default_dir() -> Option { .filter(|v| !v.is_empty()) .map(PathBuf::from) .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".local/state")))?; - Some(base.join("oab-instance-mcp/jobs")) + Some(base.join("oab-computer/jobs")) } /// Unit tests never write into the real state directory. diff --git a/poc/reverse-attach-linux/src/jobs/tools.rs b/poc/reverse-attach-linux/src/jobs/tools.rs index 6bfa424..e70f05c 100644 --- a/poc/reverse-attach-linux/src/jobs/tools.rs +++ b/poc/reverse-attach-linux/src/jobs/tools.rs @@ -48,7 +48,7 @@ pub(crate) fn tool_specs() -> Vec { a process that left the group, e.g. with setsid). Output is read from the \ job's log files (never truncated; at most 1 MiB per stream per poll, poll \ again from the returned offsets for the rest). The files are \ - $XDG_STATE_HOME/oab-instance-mcp/jobs/.out/.err (default \ + $XDG_STATE_HOME/oab-computer/jobs/.out/.err (default \ ~/.local/state/...); `out_path`/`err_path` give the exact paths.", "inputSchema": { "type": "object", diff --git a/poc/reverse-attach-linux/src/mcp.rs b/poc/reverse-attach-linux/src/mcp.rs index b7ae9f4..dfe2d56 100644 --- a/poc/reverse-attach-linux/src/mcp.rs +++ b/poc/reverse-attach-linux/src/mcp.rs @@ -66,7 +66,7 @@ impl Upstream { let r = self.post(&json!({ "jsonrpc": "2.0", "id": 0, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}, - "clientInfo": {"name": "instance-mcp-rpi", "version": AGENT_VERSION}} + "clientInfo": {"name": "oab-computer-rpi", "version": AGENT_VERSION}} }))?; if !(200..300).contains(&r.status) { return Err(format!( @@ -476,7 +476,7 @@ pub(crate) fn answer_request( "protocolVersion": negotiated, "capabilities": { "tools": { "listChanged": false } }, "serverInfo": { - "name": "instance-mcp-rpi", + "name": "oab-computer-rpi", "version": AGENT_VERSION }, "instructions": instructions(profile) diff --git a/poc/reverse-attach-linux/src/tools/sysinfo.rs b/poc/reverse-attach-linux/src/tools/sysinfo.rs index 0ac01a9..848318e 100644 --- a/poc/reverse-attach-linux/src/tools/sysinfo.rs +++ b/poc/reverse-attach-linux/src/tools/sysinfo.rs @@ -5,10 +5,10 @@ use serde_json::{json, Value}; use crate::platform; -pub const AGENT_NAME: &str = "instance-mcp-rpi"; +pub const AGENT_NAME: &str = "oab-computer-rpi"; /// The release version this binary reports (`sys_info.agent.version`, MCP `serverInfo`). /// The release workflow sets `OAB_RELEASE_VERSION` from the tag, which it requires to equal -/// `let version` in `Sources/oab-instance-mcp/main.swift`, so the tag is the only source. +/// `let version` in `Sources/oab-computer/main.swift`, so the tag is the only source. /// A build without it (local, `cargo test`) reports `-dev`, never a release /// number; the crate version in Cargo.toml is not bumped per release (#74). pub const AGENT_VERSION: &str = match option_env!("OAB_RELEASE_VERSION") { diff --git a/scripts/assemble-app.sh b/scripts/assemble-app.sh index 415a8b0..b10a187 100755 --- a/scripts/assemble-app.sh +++ b/scripts/assemble-app.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Assemble the minimal oab-instance-mcp.app bundle around a prebuilt binary. +# Assemble the minimal oab-computer.app bundle around a prebuilt binary. # # This script does NOT sign. Local deploy.sh signs with Apple Development; the # release workflow signs with Developer ID Application. Keeping assembly separate @@ -12,7 +12,7 @@ set -euo pipefail BIN="${1:?usage: assemble-app.sh [version]}" OUT="${2:?usage: assemble-app.sh [version]}" VERSION="${3:-}" -BUNDLE_ID="${BUNDLE_ID:-dev.openab.instance-mcp}" +BUNDLE_ID="${BUNDLE_ID:-dev.openab.computer}" [ -f "$BIN" ] && [ -x "$BIN" ] || { echo "assemble-app: binary is missing or not executable: $BIN" >&2 @@ -49,8 +49,8 @@ TMP=$(mktemp -d "$PARENT/.${NAME}.assemble.XXXXXX") trap 'rm -rf "$TMP"' EXIT APP="$TMP/$NAME" mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" -/usr/bin/ditto "$BIN" "$APP/Contents/MacOS/oab-instance-mcp" -chmod 755 "$APP/Contents/MacOS/oab-instance-mcp" +/usr/bin/ditto "$BIN" "$APP/Contents/MacOS/oab-computer" +chmod 755 "$APP/Contents/MacOS/oab-computer" # Makes the advanced .app.zip artifact self-contained: after unzipping, run # app/Contents/Resources/install-prebuilt.sh app # The signed/notarized .pkg remains the normal one-click install path. @@ -58,11 +58,11 @@ SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) cp "$SCRIPT_DIR/install-prebuilt.sh" "$APP/Contents/Resources/install-prebuilt.sh" chmod 755 "$APP/Contents/Resources/install-prebuilt.sh" -# User-visible name. The bundle id, executable and .app file name stay -# oab-instance-mcp (TCC grants and installed paths depend on them, see #63). +# User-visible name. The bundle id, executable and .app file name are +# oab-computer (TCC grants and installed paths depend on them, see #63). # macOS shows a name other than the file name only when the bundle localizes # it AND the file name equals the base CFBundleDisplayName, so the base names -# stay oab-instance-mcp and en.lproj carries "OpenAB Computer". +# stay oab-computer and en.lproj carries "OpenAB Computer". DISPLAY_NAME="OpenAB Computer" mkdir -p "$APP/Contents/Resources/en.lproj" cat >"$APP/Contents/Resources/en.lproj/InfoPlist.strings" <"$APP/Contents/Info.plist" < CFBundleIdentifier$BUNDLE_ID - CFBundleNameoab-instance-mcp - CFBundleDisplayNameoab-instance-mcp + CFBundleNameoab-computer + CFBundleDisplayNameoab-computer LSHasLocalizedDisplayName CFBundleDevelopmentRegionen - CFBundleExecutableoab-instance-mcp + CFBundleExecutableoab-computer CFBundlePackageTypeAPPL CFBundleShortVersionString$VERSION CFBundleVersion$VERSION diff --git a/scripts/deploy.sh b/scripts/deploy.sh index dd5655c..e4cd828 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -16,7 +16,7 @@ set -euo pipefail LOGIN="${1:?usage: deploy.sh }" IDENTITY="${2:?usage: deploy.sh }" ROOT=$(cd "$(dirname "$0")/.." && pwd) -BIN="$ROOT/.build/release/oab-instance-mcp" +BIN="$ROOT/.build/release/oab-computer" EXPECT_TEAM="${EXPECT_TEAM:-6LPQNY95AQ}" [ -x "$BIN" ] || { echo "build first: swift build -c release" >&2; exit 1; } @@ -25,7 +25,7 @@ NEWEST_SRC=$(find "$ROOT/Sources" -name '*.swift' -newer "$BIN" | head -1) VERSION=$("$BIN" --version) TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT -APP="$TMP/oab-instance-mcp.app" +APP="$TMP/oab-computer.app" "$ROOT/scripts/assemble-app.sh" "$BIN" "$APP" "$VERSION" @@ -37,7 +37,7 @@ if [ -n "${KEYCHAIN:-}" ]; then KC_ARGS=(--keychain "$KEYCHAIN") fi codesign --force --options runtime --timestamp=none \ - ${KC_ARGS[@]+"${KC_ARGS[@]}"} --sign "$IDENTITY" --identifier dev.openab.instance-mcp "$APP" + ${KC_ARGS[@]+"${KC_ARGS[@]}"} --sign "$IDENTITY" --identifier dev.openab.computer "$APP" codesign --verify --deep --strict "$APP" TEAM=$(codesign -dvv "$APP" 2>&1 | sed -n 's/^TeamIdentifier=//p') diff --git a/scripts/install-linux.sh b/scripts/install-linux.sh index 812e02f..4d8b056 100755 --- a/scripts/install-linux.sh +++ b/scripts/install-linux.sh @@ -1,20 +1,20 @@ #!/bin/bash # Install the Linux hands node from a release tarball (run ON the target, as the desktop user). # -# curl -fsSL https://github.com/openabdev/instance-mcp/releases/latest/download/oab-instance-mcp-VERSION-linux-$(dpkg --print-architecture).tar.gz | tar xz -# cd oab-instance-mcp-*-linux-* && ./install-linux.sh [--allow-login you@example.com] [--no-browser] [--headless-seat] +# curl -fsSL https://github.com/openabdev/openab-computer/releases/latest/download/oab-computer-VERSION-linux-$(dpkg --print-architecture).tar.gz | tar xz +# cd oab-computer-*-linux-* && ./install-linux.sh [--allow-login you@example.com] [--no-browser] [--headless-seat] # # --headless-seat: for a server install with no desktop (Ubuntu Server, a mini PC in a closet): # installs sway and runs it headless (one 1920x1080 virtual output) as user unit oab-seat.service. # Boxes that already run a wlroots desktop (Raspberry Pi OS labwc, sway) do not need it. # -# Installs the binary + units under ~/.local/oab-instance-mcp, generates or keeps the bearer +# Installs the binary + units under ~/.local/oab-computer, generates or keeps the bearer # token, detects the Tailscale login, installs @playwright/mcp if node is present, enables # linger and `tailscale serve --https=8444`. Idempotent. Full walkthrough: docs/linux-setup.md. set -euo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" -BASE="$HOME/.local/oab-instance-mcp" -CFG="$HOME/.config/oab-instance-mcp" +BASE="$HOME/.local/oab-computer" +CFG="$HOME/.config/oab-computer" UNITS="$HOME/.config/systemd/user" ALLOW_LOGIN=""; BROWSER=1; PORT=8795; SERVE_PORT=8444; HEADLESS_SEAT=0 while [ $# -gt 0 ]; do @@ -32,6 +32,25 @@ need() { command -v "$1" >/dev/null || { echo "missing: $1 (apt-get install $2)" need tailscale tailscale; need grim grim; need wlrctl wlrctl; need wtype wtype; need python3 python3 [ "$(id -u)" != 0 ] || { echo "run as the desktop user, not root" >&2; exit 64; } +# Warn only (read-only: never stops, disables or removes the old units). A 0.11 or earlier node runs +# oab-instance-mcp.service on the same port; it would fight the new oab-computer.service for it. +LEGACY_FOUND=() +for U in oab-instance-mcp.service oab-pw-mcp.service; do + if systemctl --user is-active --quiet "$U" >/dev/null 2>&1 || systemctl --user is-enabled --quiet "$U" >/dev/null 2>&1; then + LEGACY_FOUND+=("$U") + fi +done +if [ "${#LEGACY_FOUND[@]}" -gt 0 ]; then + { + echo "install: WARNING: the old unit(s) ${LEGACY_FOUND[*]} (0.11 or earlier) are still active or enabled." + echo "oab-instance-mcp.service listens on port $PORT like the new oab-computer.service and will fight it for that port." + echo "This installer does not touch them. To stop and remove them, run:" + echo " systemctl --user disable --now ${LEGACY_FOUND[*]}" + echo " rm ${LEGACY_FOUND[*]/#/$UNITS/}" + echo " systemctl --user daemon-reload" + } >&2 +fi + # node installed via fnm/nvm is often a symlink with npm/npx only beside the real binary. NODE_BIN="" if command -v node >/dev/null; then NODE_BIN="$(dirname "$(readlink -f "$(command -v node)")")"; export PATH="$NODE_BIN:$PATH"; fi @@ -41,14 +60,14 @@ if [ "$HEADLESS_SEAT" = 1 ]; then need sway sway mkdir -p "$HOME/.config/sway" [ -f "$HOME/.config/sway/config" ] || cat >"$HOME/.config/sway/config" <<'SWAY' -# Headless seat for the oab-instance-mcp hands node: one virtual 1080p output, no input devices. +# Headless seat for the oab-computer hands node: one virtual 1080p output, no input devices. output HEADLESS-1 resolution 1920x1080 position 0,0 default_border none focus_follows_mouse no SWAY cat >"$UNITS/oab-seat.service" <<'UNIT' [Unit] -Description=Headless sway seat for the oab-instance-mcp hands node +Description=Headless sway seat for the oab-computer hands node [Service] Environment=WLR_BACKENDS=headless @@ -81,7 +100,7 @@ if [ -z "$ALLOW_LOGIN" ]; then [ -n "$ALLOW_LOGIN" ] || { echo "cannot detect Tailscale login; is this node logged in? (or pass --allow-login)" >&2; exit 78; } fi -install -m 755 "$HERE/oab-instance-mcp" "$BASE/oab-instance-mcp" +install -m 755 "$HERE/oab-computer" "$BASE/oab-computer" if [ ! -s "$CFG/token" ]; then head -c 32 /dev/urandom | base64 | tr -d '=+/\n' | head -c 40 > "$CFG/token"; chmod 600 "$CFG/token" echo "generated bearer token at $CFG/token" @@ -125,9 +144,9 @@ JSON sudo -n apt-get install -y -qq fonts-dejavu-core fonts-liberation fonts-noto-cjk fonts-noto-color-emoji >/dev/null 2>&1 || \ echo "note: install fonts (fonts-dejavu-core fonts-noto-cjk) or pages render as boxes" >&2 fi - cat >"$UNITS/oab-pw-mcp.service" <"$UNITS/oab-computer-pw-mcp.service" </dev/null; then sudo mkdir -p /etc/chromium/policies/managed echo '{"DefaultMediaStreamSetting": 2, "DefaultNotificationsSetting": 2, "DefaultGeolocationSetting": 2}' \ - | sudo tee /etc/chromium/policies/managed/oab-instance-mcp.json >/dev/null + | sudo tee /etc/chromium/policies/managed/oab-computer.json >/dev/null else echo "note: could not write Chromium policy (no sudo); camera/mic prompts will need a mouse click" >&2 fi @@ -157,13 +176,13 @@ fi # Unquoted heredoc so $VARS expand: escape any literal backtick or $ in it (#90). # scripts/test-install-linux.sh diffs the written units against their expected text. -cat >"$UNITS/oab-instance-mcp.service" <"$UNITS/oab-computer.service" </dev/null || loginctl enable-linger "$USER" 2>/dev/null || echo "note: enable linger manually: sudo loginctl enable-linger $USER" >&2 if sudo -n true 2>/dev/null; then diff --git a/scripts/install-prebuilt.sh b/scripts/install-prebuilt.sh index de4f401..0970445 100755 --- a/scripts/install-prebuilt.sh +++ b/scripts/install-prebuilt.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Install a PREBUILT, already-signed oab-instance-mcp.app for the logged-in user. +# Install a PREBUILT, already-signed oab-computer.app for the logged-in user. # # This script never signs or modifies the bundle. That is load-bearing: an ad-hoc # re-sign changes the app's designated requirement and silently drops the human's @@ -9,7 +9,7 @@ # Usage: install-prebuilt.sh [--allow-login ] # # Test seams (not used by the package): INSTALL_HOME, ALLOW_UNSIGNED=1, -# SKIP_LAUNCH=1, SKIP_TAILSCALE=1. +# SKIP_LAUNCH=1, SKIP_TAILSCALE=1, LEGACY_LAUNCHCTL (a stub launchctl for the old-agent warning). set -euo pipefail SOURCE="${1:?usage: install-prebuilt.sh [--allow-login ]}" @@ -27,22 +27,22 @@ while [ "$#" -gt 0 ]; do esac done -LABEL=dev.openab.instance-mcp +LABEL=dev.openab.computer OLD_LABEL=dev.openab.mac-agent -BUNDLE_ID=dev.openab.instance-mcp +BUNDLE_ID=dev.openab.computer EXPECT_TEAM="${EXPECT_TEAM:-6LPQNY95AQ}" HOME_DIR="${INSTALL_HOME:-$HOME}" -BASE="$HOME_DIR/.local/oab-instance-mcp" -APP="$BASE/oab-instance-mcp.app" +BASE="$HOME_DIR/.local/oab-computer" +APP="$BASE/oab-computer.app" TOKEN_FILE="$HOME_DIR/.config/oab-computer/token" LEGACY_TOKEN_FILE="$HOME_DIR/.config/oab-instance-mcp/token" OLDEST_TOKEN_FILE="$HOME_DIR/.config/oab-mac-agent/token" PLIST="$HOME_DIR/Library/LaunchAgents/$LABEL.plist" -LOG_DIR="$HOME_DIR/Library/Logs/oab-instance-mcp" +LOG_DIR="$HOME_DIR/Library/Logs/oab-computer" UID_=$(id -u) case "$SOURCE" in *.app) ;; *) echo "installer: source must be an .app bundle" >&2; exit 64 ;; esac -[ -x "$SOURCE/Contents/MacOS/oab-instance-mcp" ] || { +[ -x "$SOURCE/Contents/MacOS/oab-computer" ] || { echo "installer: app has no executable: $SOURCE" >&2; exit 66 } [ "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$SOURCE/Contents/Info.plist" 2>/dev/null)" = "$BUNDLE_ID" ] || { @@ -62,6 +62,31 @@ if [ "${ALLOW_UNSIGNED:-0}" != "1" ]; then } fi +# Warn only (read-only: never stops, removes or changes the old agent). A 0.11 or earlier agent is +# named dev.openab.instance-mcp; it listens on the same port 8795 and would fight this one. +# Test seam: LEGACY_LAUNCHCTL replaces /bin/launchctl for this check (and enables it under SKIP_LAUNCH=1). +LEGACY_LABEL=dev.openab.instance-mcp +LEGACY_FOUND=() +for L in "$LEGACY_LABEL" "$LEGACY_LABEL.pw-mcp"; do + if [ -n "${LEGACY_LAUNCHCTL:-}" ] || [ "${SKIP_LAUNCH:-0}" != "1" ]; then + if "${LEGACY_LAUNCHCTL:-/bin/launchctl}" print "gui/$UID_/$L" >/dev/null 2>&1; then + LEGACY_FOUND+=("$L"); continue + fi + fi + if [ -f "$HOME_DIR/Library/LaunchAgents/$L.plist" ]; then LEGACY_FOUND+=("$L"); fi +done +if [ "${#LEGACY_FOUND[@]}" -gt 0 ]; then + { + echo "installer: WARNING: the old LaunchAgent ${LEGACY_FOUND[*]} (0.11 or earlier) is still installed." + echo "It listens on port $PORT like the new agent and will fight it for that port." + echo "This installer does not touch it. To stop and remove it, run:" + for L in "${LEGACY_FOUND[@]}"; do + echo " launchctl bootout gui/$UID_/$L" + echo " rm \"$HOME_DIR/Library/LaunchAgents/$L.plist\"" + done + } >&2 +fi + TS="${TAILSCALE_CLI:-}" if [ "${SKIP_TAILSCALE:-0}" != "1" ]; then if [ -z "$TS" ]; then @@ -170,8 +195,8 @@ rm -f "$HOME_DIR/Library/LaunchAgents/$OLD_LABEL.plist" # Copy to a sibling and verify there, then replace atomically. `ditto` preserves # the Developer ID signature and notarization ticket; we never run codesign here. -STAGED="$BASE/.oab-instance-mcp.app.installing.$$" -BACKUP="$BASE/.oab-instance-mcp.app.previous.$$" +STAGED="$BASE/.oab-computer.app.installing.$$" +BACKUP="$BASE/.oab-computer.app.previous.$$" rm -rf "$STAGED" "$BACKUP" /usr/bin/ditto "$SOURCE" "$STAGED" if [ "${ALLOW_UNSIGNED:-0}" != "1" ]; then @@ -189,14 +214,14 @@ PB=/usr/libexec/PlistBuddy "$PB" -c "Add :Label string $LABEL" "$PLIST" "$PB" -c 'Add :ProgramArguments array' "$PLIST" ARGS=( - "$APP/Contents/MacOS/oab-instance-mcp" + "$APP/Contents/MacOS/oab-computer" --port "$PORT" --allow-login "$LOGIN" --token-file "$TOKEN_FILE" --menu-bar --public-url "https://$DNSNAME:$HTTPS_PORT/mcp" ) -if [ "${SKIP_LAUNCH:-0}" != "1" ] && /bin/launchctl print "gui/$UID_/dev.openab.instance-mcp.pw-mcp" >/dev/null 2>&1; then +if [ "${SKIP_LAUNCH:-0}" != "1" ] && /bin/launchctl print "gui/$UID_/dev.openab.computer.pw-mcp" >/dev/null 2>&1; then ARGS+=(--upstream browser=http://127.0.0.1:8794/mcp) elif [ "${TEST_WITH_UPSTREAM:-0}" = "1" ]; then ARGS+=(--upstream browser=http://127.0.0.1:8794/mcp) @@ -206,7 +231,7 @@ fi # switchboard.profile (observe|owner) optionally picks the profile. The retired # `desktop` (#59) is written as `observe`, never `owner`, with a warning; the # file itself is left as it is. -SB_DIR="$HOME_DIR/.config/oab-instance-mcp" +SB_DIR="$HOME_DIR/.config/oab-computer" if [ -s "$SB_DIR/switchboard.url" ] && [ -s "$SB_DIR/switchboard.secret" ]; then SB_URL=$(/usr/bin/head -n 1 "$SB_DIR/switchboard.url" | /usr/bin/tr -d '[:space:]') ARGS+=(--switchboard "$SB_URL" --switchboard-secret-file "$SB_DIR/switchboard.secret") @@ -259,7 +284,7 @@ if [ -n "$OLD_REQ" ] && [ -n "$NEW_REQ" ] && [ "$OLD_REQ" != "$NEW_REQ" ]; then echo "Future Developer-ID releases keep this requirement stable." fi -echo "installed oab-instance-mcp $VERSION" +echo "installed oab-computer $VERSION" echo " app: $APP" echo " login: $LOGIN" echo " MCP: https://$DNSNAME:$HTTPS_PORT/mcp" diff --git a/scripts/package-pkg.sh b/scripts/package-pkg.sh index bfd7df3..dbbe89f 100755 --- a/scripts/package-pkg.sh +++ b/scripts/package-pkg.sh @@ -1,7 +1,7 @@ #!/bin/bash # Build a macOS installer package around an ALREADY-SIGNED app. # -# Usage: package-pkg.sh [version] +# Usage: package-pkg.sh [version] # Env: PKG_SIGN_IDENTITY="Developer ID Installer: ..." (optional for smoke; # required for release) # EXPECT_TEAM=6LPQNY95AQ @@ -13,7 +13,7 @@ OUT="${2:?usage: package-pkg.sh [version]}" VERSION="${3:-}" ROOT=$(cd "$(dirname "$0")/.." && pwd) EXPECT_TEAM="${EXPECT_TEAM:-6LPQNY95AQ}" -IDENTIFIER="${PKG_IDENTIFIER:-dev.openab.instance-mcp.installer}" +IDENTIFIER="${PKG_IDENTIFIER:-dev.openab.computer.installer}" [ -d "$APP" ] || { echo "package-pkg: app not found: $APP" >&2; exit 66; } case "$OUT" in *.pkg) ;; *) echo "package-pkg: output must end in .pkg" >&2; exit 64 ;; esac @@ -35,8 +35,8 @@ trap 'rm -rf "$TMP"' EXIT PAYLOAD="$TMP/payload" SCRIPTS="$TMP/scripts" COMPONENTS="$TMP/components.plist" -mkdir -p "$PAYLOAD/Library/Application Support/OpenAB/instance-mcp" "$SCRIPTS" "$(dirname "$OUT")" -/usr/bin/ditto "$APP" "$PAYLOAD/Library/Application Support/OpenAB/instance-mcp/oab-instance-mcp.app" +mkdir -p "$PAYLOAD/Library/Application Support/OpenAB/computer" "$SCRIPTS" "$(dirname "$OUT")" +/usr/bin/ditto "$APP" "$PAYLOAD/Library/Application Support/OpenAB/computer/oab-computer.app" cp "$ROOT/scripts/install-prebuilt.sh" "$SCRIPTS/install-prebuilt.sh" cp "$ROOT/scripts/pkg/postinstall" "$SCRIPTS/postinstall" chmod 755 "$SCRIPTS/install-prebuilt.sh" "$SCRIPTS/postinstall" @@ -68,7 +68,7 @@ rm -f "$OUT" /usr/bin/pkgbuild "${ARGS[@]}" "$OUT" /usr/sbin/pkgutil --payload-files "$OUT" | /usr/bin/grep -q \ - '^\./Library/Application Support/OpenAB/instance-mcp/oab-instance-mcp.app/Contents/MacOS/oab-instance-mcp$' + '^\./Library/Application Support/OpenAB/computer/oab-computer.app/Contents/MacOS/oab-computer$' if [ -n "${PKG_SIGN_IDENTITY:-}" ]; then /usr/sbin/pkgutil --check-signature "$OUT" | /usr/bin/grep -q 'Developer ID Installer' fi diff --git a/scripts/pkg/postinstall b/scripts/pkg/postinstall index 31a9214..18b4e3e 100755 --- a/scripts/pkg/postinstall +++ b/scripts/pkg/postinstall @@ -7,14 +7,14 @@ set -euo pipefail CONSOLE_USER=$(/usr/bin/stat -f '%Su' /dev/console) case "$CONSOLE_USER" in ''|root|loginwindow|_mbsetupuser) - echo "oab-instance-mcp: no logged-in desktop user; sign in, then run the package again" >&2 + echo "oab-computer: no logged-in desktop user; sign in, then run the package again" >&2 exit 1 ;; esac UID_=$(/usr/bin/id -u "$CONSOLE_USER") HOME_DIR=$(/usr/bin/dscl . -read "/Users/$CONSOLE_USER" NFSHomeDirectory | \ /usr/bin/sed 's/^NFSHomeDirectory: //') -[ -d "$HOME_DIR" ] || { echo "oab-instance-mcp: home not found for $CONSOLE_USER" >&2; exit 1; } +[ -d "$HOME_DIR" ] || { echo "oab-computer: home not found for $CONSOLE_USER" >&2; exit 1; } GID_=$(/usr/bin/id -g "$CONSOLE_USER") # Repair only our own per-user paths before dropping root. v0.6.0's relocatable @@ -23,18 +23,18 @@ GID_=$(/usr/bin/id -g "$CONSOLE_USER") # This also makes a partially interrupted package retry idempotent. Do not chown # a parent directory owned by the user, and never touch unrelated files. for owned in \ - "$HOME_DIR/.local/oab-instance-mcp" \ - "$HOME_DIR/.config/oab-instance-mcp" \ - "$HOME_DIR/Library/Logs/oab-instance-mcp" \ - "$HOME_DIR/Library/LaunchAgents/dev.openab.instance-mcp.plist"; do + "$HOME_DIR/.local/oab-computer" \ + "$HOME_DIR/.config/oab-computer" \ + "$HOME_DIR/Library/Logs/oab-computer" \ + "$HOME_DIR/Library/LaunchAgents/dev.openab.computer.plist"; do if [ -e "$owned" ]; then /usr/sbin/chown -R "$UID_:$GID_" "$owned"; fi done SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) -SOURCE="/Library/Application Support/OpenAB/instance-mcp/oab-instance-mcp.app" +SOURCE="/Library/Application Support/OpenAB/computer/oab-computer.app" HELPER="$SCRIPT_DIR/install-prebuilt.sh" -[ -d "$SOURCE" ] || { echo "oab-instance-mcp: package payload app is missing" >&2; exit 1; } -[ -x "$HELPER" ] || { echo "oab-instance-mcp: package installer helper is missing" >&2; exit 1; } +[ -d "$SOURCE" ] || { echo "oab-computer: package payload app is missing" >&2; exit 1; } +[ -x "$HELPER" ] || { echo "oab-computer: package installer helper is missing" >&2; exit 1; } # `asuser` places launchctl/Tailscale in the GUI bootstrap namespace; sudo drops # filesystem/process identity. The helper itself never needs root. diff --git a/scripts/release-version.sh b/scripts/release-version.sh index 19b769e..a0e2d7f 100755 --- a/scripts/release-version.sh +++ b/scripts/release-version.sh @@ -6,7 +6,7 @@ # RELEASE_VERSION_SOURCE overrides the source file (tests only). set -euo pipefail -SRC="${RELEASE_VERSION_SOURCE:-$(cd "$(dirname "$0")/.." && pwd)/Sources/oab-instance-mcp/main.swift}" +SRC="${RELEASE_VERSION_SOURCE:-$(cd "$(dirname "$0")/.." && pwd)/Sources/oab-computer/main.swift}" [ -f "$SRC" ] || { echo "release-version: $SRC not found" >&2; exit 66; } N=$(grep -c '^let version = "[^"]*"$' "$SRC" || true) [ "$N" = 1 ] || { echo "release-version: expected one 'let version = \"…\"' line in $SRC, found $N" >&2; exit 65; } diff --git a/scripts/smoke.sh b/scripts/smoke.sh index 5d6033e..34a621d 100755 --- a/scripts/smoke.sh +++ b/scripts/smoke.sh @@ -1,8 +1,8 @@ #!/bin/bash -# Loopback smoke test for oab-instance-mcp. Every step is time-bounded; the server it +# Loopback smoke test for oab-computer. Every step is time-bounded; the server it # starts is killed on exit. Usage: scripts/smoke.sh [path-to-binary] [port] set -u -B="${1:-.build/release/oab-instance-mcp}" +B="${1:-.build/release/oab-computer}" PORT="${2:-8797}" URL="http://127.0.0.1:$PORT/mcp" LOG="$(mktemp)" @@ -15,7 +15,7 @@ post() { # post [extra curl args...] } field() { python3 -c 'import json,sys; r=json.load(sys.stdin); print(eval(sys.argv[1]))' "$1"; } -pkill -f "oab-instance-mcp --port $PORT" 2>/dev/null || true +pkill -f "oab-computer --port $PORT" 2>/dev/null || true echo "--- no auth flags (expect refusal, exit 64) ---" t 5 "$B" --port "$PORT"; echo "exit=$?" diff --git a/scripts/test-install-linux.sh b/scripts/test-install-linux.sh index b62e238..988fc50 100755 --- a/scripts/test-install-linux.sh +++ b/scripts/test-install-linux.sh @@ -20,8 +20,8 @@ fail() { echo "FAIL: $*" >&2; FAIL=1; } PKG="$T/pkg"; mkdir -p "$PKG" cp "$HERE/install-linux.sh" "$PKG/" cp "$HERE/../poc/reverse-attach-linux/pw-mcp.sh" "$PKG/" -printf '#!/bin/sh\nexit 0\n' >"$PKG/oab-instance-mcp" -chmod 755 "$PKG/install-linux.sh" "$PKG/pw-mcp.sh" "$PKG/oab-instance-mcp" +printf '#!/bin/sh\nexit 0\n' >"$PKG/oab-computer" +chmod 755 "$PKG/install-linux.sh" "$PKG/pw-mcp.sh" "$PKG/oab-computer" STUBS="$T/stubs"; mkdir -p "$STUBS" LOG="$T/calls.log" @@ -42,9 +42,9 @@ stub ls 'case "$1" in /run/user/*) printf "wayland-0\nwayland-1\n";; *) exec '"$ run() { local name="$1"; shift local home="$T/$name/home" - mkdir -p "$home/.local/oab-instance-mcp/pw-mcp/node_modules/.bin" - printf '#!/bin/sh\n' >"$home/.local/oab-instance-mcp/pw-mcp/node_modules/.bin/playwright-mcp" - chmod 755 "$home/.local/oab-instance-mcp/pw-mcp/node_modules/.bin/playwright-mcp" + mkdir -p "$home/.local/oab-computer/pw-mcp/node_modules/.bin" + printf '#!/bin/sh\n' >"$home/.local/oab-computer/pw-mcp/node_modules/.bin/playwright-mcp" + chmod 755 "$home/.local/oab-computer/pw-mcp/node_modules/.bin/playwright-mcp" : >"$LOG" # stdin is /dev/null: a command substitution such as `bash` in a heredoc must not wait on it. if ! (cd "$PKG" && HOME="$home" USER="${USER:-$(id -un)}" PATH="$STUBS:$PATH" ./install-linux.sh "$@") "$T/$name/out" 2>&1; then @@ -56,7 +56,7 @@ run() { # expect NAME UNIT LOGIN PORT: diff the written unit against the expected text below. expect() { local name="$1" unit="$2" login="$3" port="$4" home="$T/$1/home" - local base="$home/.local/oab-instance-mcp" cfg="$home/.config/oab-instance-mcp" + local base="$home/.local/oab-computer" cfg="$home/.config/oab-computer" local got="$home/.config/systemd/user/$unit" [ -f "$got" ] || { fail "$name: $unit not written"; return; } sed -e "s|@BASE@|$base|g" -e "s|@CFG@|$cfg|g" -e "s|@LOGIN@|$login|g" -e "s|@PORT@|$port|g" \ @@ -66,13 +66,13 @@ expect() { # Expected units. @UPSTREAM@ becomes the browser line or an empty line (the installer writes # an empty $UPSTREAM line with --no-browser). -cat >"$T/expected-oab-instance-mcp.service.in" <<'EOF' +cat >"$T/expected-oab-computer.service.in" <<'EOF' [Unit] -Description=oab-instance-mcp (Linux hands node: /mcp + /attach) +Description=oab-computer (Linux hands node: /mcp + /attach) After=graphical-session.target [Service] -ExecStart=@BASE@/oab-instance-mcp +ExecStart=@BASE@/oab-computer Environment=BIND=127.0.0.1:@PORT@ Environment=MCP_TOKEN_FILE=@CFG@/token Environment=MCP_ALLOW_LOGIN=@LOGIN@ @@ -87,9 +87,9 @@ KillMode=process [Install] WantedBy=default.target EOF -cat >"$T/expected-oab-pw-mcp.service" <<'EOF' +cat >"$T/expected-oab-computer-pw-mcp.service" <<'EOF' [Unit] -Description=Playwright MCP for the oab-instance-mcp hands node (loopback :8794) +Description=Playwright MCP for the oab-computer hands node (loopback :8794) After=graphical-session.target oab-seat.service [Service] @@ -107,7 +107,7 @@ WantedBy=default.target EOF cat >"$T/expected-oab-seat.service" <<'EOF' [Unit] -Description=Headless sway seat for the oab-instance-mcp hands node +Description=Headless sway seat for the oab-computer hands node [Service] Environment=WLR_BACKENDS=headless @@ -126,31 +126,31 @@ EOF # 1. Defaults plus a headless seat: browser unit, login detected from `tailscale status`. run browser --headless-seat sed 's|@UPSTREAM@|Environment=MCP_UPSTREAM=browser=http://127.0.0.1:8794/mcp|' \ - "$T/expected-oab-instance-mcp.service.in" >"$T/expected-oab-instance-mcp.service" -expect browser oab-instance-mcp.service ci-user 8795 -expect browser oab-pw-mcp.service ci-user 8795 + "$T/expected-oab-computer.service.in" >"$T/expected-oab-computer.service" +expect browser oab-computer.service ci-user 8795 +expect browser oab-computer-pw-mcp.service ci-user 8795 expect browser oab-seat.service ci-user 8795 for want in "systemctl --user enable --now oab-seat.service" \ - "systemctl --user enable --now oab-pw-mcp.service" \ - "systemctl --user enable --now oab-instance-mcp.service" \ - "systemctl --user restart oab-instance-mcp.service" \ + "systemctl --user enable --now oab-computer-pw-mcp.service" \ + "systemctl --user enable --now oab-computer.service" \ + "systemctl --user restart oab-computer.service" \ "loginctl enable-linger"; do grep -qF "$want" "$T/browser/calls" || fail "browser: no call: $want" done # 2. --no-browser, explicit login and port: no browser unit, empty upstream line. run nobrowser --no-browser --allow-login ci-other --port 9000 -sed 's|@UPSTREAM@||' "$T/expected-oab-instance-mcp.service.in" >"$T/expected-oab-instance-mcp.service" -expect nobrowser oab-instance-mcp.service ci-other 9000 -for u in oab-pw-mcp.service oab-seat.service; do +sed 's|@UPSTREAM@||' "$T/expected-oab-computer.service.in" >"$T/expected-oab-computer.service" +expect nobrowser oab-computer.service ci-other 9000 +for u in oab-computer-pw-mcp.service oab-seat.service; do [ ! -e "$T/nobrowser/home/.config/systemd/user/$u" ] || fail "nobrowser: $u written" done -if grep -qF "oab-pw-mcp" "$T/nobrowser/calls"; then fail "nobrowser: oab-pw-mcp.service enabled"; fi +if grep -qF "oab-computer-pw-mcp" "$T/nobrowser/calls"; then fail "nobrowser: oab-computer-pw-mcp.service enabled"; fi for name in browser nobrowser; do - unit="$T/$name/home/.config/systemd/user/oab-instance-mcp.service" + unit="$T/$name/home/.config/systemd/user/oab-computer.service" grep -qF '# Apps opened via `bash` belong to the desktop' "$unit" || fail "$name: KillMode comment mangled" - tok="$T/$name/home/.config/oab-instance-mcp/token" + tok="$T/$name/home/.config/oab-computer/token" if [ ! -s "$tok" ] || [ "$(stat -c %a "$tok")" != 600 ]; then fail "$name: token missing or not mode 600"; fi done @@ -158,8 +158,8 @@ done # installed here, and verify rejects a missing executable. if command -v systemd-analyze >/dev/null; then for name in browser nobrowser; do - units=("$T/$name/home/.config/systemd/user/oab-instance-mcp.service") - [ "$name" = nobrowser ] || units+=("$T/$name/home/.config/systemd/user/oab-pw-mcp.service") + units=("$T/$name/home/.config/systemd/user/oab-computer.service") + [ "$name" = nobrowser ] || units+=("$T/$name/home/.config/systemd/user/oab-computer-pw-mcp.service") if ! out="$(HOME="$T/$name/home" systemd-analyze --user verify "${units[@]}" 2>&1)" || [ -n "$out" ]; then echo "$out" >&2; fail "$name: systemd-analyze verify" fi diff --git a/scripts/test-legacy-agent.sh b/scripts/test-legacy-agent.sh new file mode 100755 index 0000000..60947a9 --- /dev/null +++ b/scripts/test-legacy-agent.sh @@ -0,0 +1,155 @@ +#!/bin/bash +# The installers only WARN about a 0.11-or-earlier agent (macOS LaunchAgent dev.openab.instance-mcp, +# Linux unit oab-instance-mcp.service). They must print the commands to remove it and must never run +# them. This test drives both installers against stub launchctl / systemctl that log every call and +# fail any call other than a read-only query, in a fake HOME. It touches no real launchd, systemd, +# Tailscale or user directory. +# +# scripts/test-legacy-agent.sh +# +# macOS cases run only where /usr/libexec/PlistBuddy exists; Linux cases run wherever bash does. +# shellcheck disable=SC2016 # stub bodies are literal on purpose +# shellcheck disable=SC2015 # `A && ok || fail`: ok() always succeeds, so this is an if/else +set -uo pipefail +HERE="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(cd "$HERE/.." && pwd)" +T="$(mktemp -d)" +trap 'rm -rf "$T"' EXIT +FAIL=0 +fail() { echo "FAIL: $*" >&2; FAIL=1; } +ok() { echo "ok: $*"; } +has() { grep -qF -- "$2" "$1"; } +has_line() { grep -qxF -- "$2" "$1"; } + +OLD_LABEL=dev.openab.instance-mcp +OLD_PW_LABEL=dev.openab.instance-mcp.pw-mcp +OLD_SENTINEL='OLD-AGENT-FILE-MUST-STAY-BYTE-IDENTICAL' + +# ---------------------------------------------------------------- macOS +if [ -x /usr/libexec/PlistBuddy ]; then + UID_=$(id -u) + printf '#!/bin/sh\necho 0.0.0\n' >"$T/fakebin"; chmod 755 "$T/fakebin" + APP="$T/oab-computer.app" + "$ROOT/scripts/assemble-app.sh" "$T/fakebin" "$APP" 0.0.0 >/dev/null || { echo "assemble-app failed" >&2; exit 1; } + # The stub answers `print gui//