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-computerLSHasLocalizedDisplayNameCFBundleDevelopmentRegionen
- CFBundleExecutableoab-instance-mcp
+ CFBundleExecutableoab-computerCFBundlePackageTypeAPPLCFBundleShortVersionString$VERSIONCFBundleVersion$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//