machine-bridge-mcp 2.0.0 → 3.0.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/CONTRIBUTING.md +20 -25
  3. package/README.md +21 -14
  4. package/SECURITY.md +147 -123
  5. package/browser-extension/manifest.json +3 -2
  6. package/browser-extension/service-worker.js +31 -12
  7. package/docs/ARCHITECTURE.md +27 -13
  8. package/docs/AUDIT.md +85 -2
  9. package/docs/CLIENTS.md +2 -2
  10. package/docs/ENGINEERING.md +9 -3
  11. package/docs/GETTING_STARTED.md +10 -9
  12. package/docs/LOCAL_AUTHORIZATION.md +115 -72
  13. package/docs/LOGGING.md +12 -3
  14. package/docs/MANAGED_JOBS.md +24 -29
  15. package/docs/MULTI_ACCOUNT.md +98 -37
  16. package/docs/OPERATIONS.md +33 -22
  17. package/docs/OVERVIEW.md +7 -7
  18. package/docs/PROJECT_STANDARDS.md +9 -5
  19. package/docs/RELEASING.md +171 -64
  20. package/docs/TESTING.md +19 -7
  21. package/docs/THREAT_MODEL.md +138 -67
  22. package/docs/TOOL_REFERENCE.md +2 -2
  23. package/docs/UPGRADING.md +102 -18
  24. package/native/macos/MachineBridgeTrustBroker.swift +233 -0
  25. package/package.json +36 -5
  26. package/scripts/candidate-runtime-store.mjs +38 -0
  27. package/scripts/check-plan.mjs +19 -0
  28. package/scripts/check-runner.mjs +4 -1
  29. package/scripts/commit-message-check.mjs +5 -2
  30. package/scripts/coverage-check.mjs +30 -2
  31. package/scripts/github-push.mjs +11 -0
  32. package/scripts/github-release.mjs +38 -7
  33. package/scripts/install-published-prerelease.mjs +167 -0
  34. package/scripts/local-release-acceptance.mjs +23 -12
  35. package/scripts/npm-publication-policy.mjs +16 -0
  36. package/scripts/prerelease-activation.mjs +79 -0
  37. package/scripts/privacy-check.mjs +7 -5
  38. package/scripts/promotion-digest.mjs +121 -0
  39. package/scripts/publish-npm.mjs +51 -0
  40. package/scripts/published-release.mjs +87 -0
  41. package/scripts/release-acceptance.mjs +30 -1
  42. package/scripts/release-candidate-manifest.mjs +40 -0
  43. package/scripts/release-channel.mjs +106 -0
  44. package/scripts/release-impact-check.mjs +12 -22
  45. package/scripts/release-soak.mjs +237 -0
  46. package/scripts/start-release-candidate.mjs +113 -7
  47. package/src/local/account-access.mjs +6 -0
  48. package/src/local/account-admin.mjs +34 -16
  49. package/src/local/agent-context.mjs +45 -42
  50. package/src/local/app-automation.mjs +9 -4
  51. package/src/local/authority-context.mjs +106 -0
  52. package/src/local/browser-bridge-http.mjs +4 -1
  53. package/src/local/browser-bridge.mjs +14 -9
  54. package/src/local/browser-broker-routes.mjs +4 -0
  55. package/src/local/browser-broker-server.mjs +5 -4
  56. package/src/local/browser-extension-identity.mjs +50 -0
  57. package/src/local/browser-extension-protocol.mjs +9 -4
  58. package/src/local/browser-operation-service.mjs +7 -0
  59. package/src/local/browser-pairing-http.mjs +36 -0
  60. package/src/local/browser-pairing-store.mjs +56 -47
  61. package/src/local/cli-account-admin.mjs +41 -7
  62. package/src/local/cli-activate.mjs +90 -0
  63. package/src/local/cli-approval.mjs +14 -58
  64. package/src/local/cli-local-admin.mjs +5 -15
  65. package/src/local/cli-options.mjs +11 -7
  66. package/src/local/cli-service.mjs +35 -5
  67. package/src/local/cli.mjs +112 -58
  68. package/src/local/daemon-process.mjs +6 -0
  69. package/src/local/delegated-process-sandbox.mjs +147 -0
  70. package/src/local/device-identity.mjs +157 -48
  71. package/src/local/device-root-provider.mjs +87 -0
  72. package/src/local/errors.mjs +25 -3
  73. package/src/local/git-service.mjs +16 -15
  74. package/src/local/job-runner.mjs +35 -27
  75. package/src/local/log.mjs +14 -1
  76. package/src/local/macos-trust-broker.mjs +320 -0
  77. package/src/local/managed-job-lock.mjs +18 -0
  78. package/src/local/managed-job-projection.mjs +9 -1
  79. package/src/local/managed-job-runner.mjs +13 -1
  80. package/src/local/managed-job-storage.mjs +2 -1
  81. package/src/local/managed-job-terminal.mjs +138 -0
  82. package/src/local/managed-jobs.mjs +88 -32
  83. package/src/local/operation-authorization.mjs +75 -239
  84. package/src/local/operation-risk.mjs +28 -0
  85. package/src/local/operation-state-lock.mjs +7 -86
  86. package/src/local/owner-state-lock.mjs +104 -0
  87. package/src/local/policy.mjs +19 -0
  88. package/src/local/process-execution.mjs +21 -9
  89. package/src/local/process-sessions.mjs +27 -17
  90. package/src/local/process-tracker.mjs +30 -6
  91. package/src/local/process-tree-ownership.mjs +59 -0
  92. package/src/local/process-tree.mjs +22 -18
  93. package/src/local/relay-connection.mjs +47 -35
  94. package/src/local/relay-diagnostics.mjs +65 -0
  95. package/src/local/remote-configuration.mjs +48 -0
  96. package/src/local/runtime-activation.mjs +141 -0
  97. package/src/local/runtime-diagnostics.mjs +21 -0
  98. package/src/local/runtime-relay.mjs +9 -8
  99. package/src/local/runtime-reporting.mjs +8 -3
  100. package/src/local/runtime-tool-handlers.mjs +8 -8
  101. package/src/local/runtime.mjs +73 -39
  102. package/src/local/secure-file.mjs +5 -1
  103. package/src/local/security-audit-log.mjs +204 -0
  104. package/src/local/service-convergence.mjs +17 -1
  105. package/src/local/service-ownership.mjs +17 -0
  106. package/src/local/service-restart-handoff.mjs +50 -0
  107. package/src/local/service-restart-scheduler.mjs +49 -0
  108. package/src/local/service-status.mjs +47 -0
  109. package/src/local/service.mjs +142 -43
  110. package/src/local/state-inventory.mjs +21 -3
  111. package/src/local/state.mjs +155 -17
  112. package/src/local/stdio.mjs +5 -0
  113. package/src/local/tool-executor.mjs +39 -6
  114. package/src/local/tool-result-boundary.mjs +51 -0
  115. package/src/local/tools.mjs +1 -0
  116. package/src/local/trusted-executable.mjs +53 -0
  117. package/src/local/trusted-git-executable.mjs +34 -0
  118. package/src/local/trusted-github-cli.mjs +24 -0
  119. package/src/local/windows-service.mjs +20 -0
  120. package/src/local/worker-deployment.mjs +3 -4
  121. package/src/local/worker-secret-file.mjs +2 -2
  122. package/src/local/workspace-file-service.mjs +34 -31
  123. package/src/shared/admin-auth.d.mts +2 -1
  124. package/src/shared/admin-auth.mjs +3 -3
  125. package/src/shared/device-session-auth.d.mts +20 -0
  126. package/src/shared/device-session-auth.mjs +51 -0
  127. package/src/shared/server-metadata.json +1 -1
  128. package/src/shared/tool-catalog.json +2 -2
  129. package/src/worker/account-admin.ts +23 -22
  130. package/src/worker/authority.ts +9 -2
  131. package/src/worker/daemon-auth.ts +45 -67
  132. package/src/worker/daemon-sockets.ts +12 -2
  133. package/src/worker/device-session-verifier.ts +129 -0
  134. package/src/worker/dpop.ts +151 -0
  135. package/src/worker/http.ts +47 -16
  136. package/src/worker/index.ts +53 -16
  137. package/src/worker/nonce-store.ts +1 -5
  138. package/src/worker/oauth-client-admin.ts +45 -0
  139. package/src/worker/oauth-controller.ts +55 -6
  140. package/src/worker/oauth-refresh-families.ts +21 -7
  141. package/src/worker/oauth-state.ts +6 -0
  142. package/src/worker/oauth-tokens.ts +28 -8
  143. package/src/worker/websocket-protocol.ts +8 -2
@@ -3,7 +3,6 @@ importScripts("devtools-input.js", "browser-operations.js");
3
3
  let socket = null;
4
4
  let reconnectTimer = null;
5
5
  let reconnectAttempt = 0;
6
- let keepaliveTimer = null;
7
6
  const MAX_RESULT_BYTES = 7 * 1024 * 1024;
8
7
  const BROWSER_EXTENSION_PROTOCOL = 3;
9
8
  const HANDSHAKE_TIMEOUT_MS = 3000;
@@ -91,8 +90,10 @@ function closeSocketQuietly(ws, code, reason) {
91
90
  function sendSocketQuietly(ws, payload) {
92
91
  try {
93
92
  ws.send(payload);
93
+ return true;
94
94
  } catch {
95
95
  // A response cannot be recovered after the broker socket closes.
96
+ return false;
96
97
  }
97
98
  }
98
99
 
@@ -185,6 +186,10 @@ function connect(endpoint, token, { reconnect = true } = {}) {
185
186
  clearTimeout(reconnectTimer);
186
187
  reconnectTimer = null;
187
188
  if (socket) {
189
+ socket.reconnectEnabled = false;
190
+ clearInterval(socket.keepaliveTimer);
191
+ socket.keepaliveTimer = null;
192
+ cancelRequestsForSocket(socket);
188
193
  closeSocketQuietly(socket);
189
194
  }
190
195
  const ws = new WebSocket(endpoint, [`mbm.${token}`]);
@@ -194,6 +199,7 @@ function connect(endpoint, token, { reconnect = true } = {}) {
194
199
  ws.machineBridgeEndpoint = endpoint;
195
200
  ws.machineBridgeToken = token;
196
201
  ws.reconnectEnabled = reconnect;
202
+ ws.keepaliveTimer = null;
197
203
  setConnectionState("connecting");
198
204
  return new Promise((resolvePromise, rejectPromise) => {
199
205
  let settled = false;
@@ -216,11 +222,11 @@ function connect(endpoint, token, { reconnect = true } = {}) {
216
222
  ws.onerror = () => {};
217
223
  ws.onclose = () => {
218
224
  clearTimeout(ws.handshakeTimer);
225
+ clearInterval(ws.keepaliveTimer);
226
+ ws.keepaliveTimer = null;
227
+ cancelRequestsForSocket(ws);
219
228
  if (!ws.bridgeReady) settle(new Error("browser broker handshake failed"));
220
229
  if (socket !== ws) return;
221
- clearInterval(keepaliveTimer);
222
- keepaliveTimer = null;
223
- cancelRequestsForSocket(ws);
224
230
  socket = null;
225
231
  setConnectionState("disconnected");
226
232
  if (ws.reconnectEnabled) scheduleReconnect(endpoint, token);
@@ -247,15 +253,17 @@ async function handleMessage(ws, raw, onReady = () => {}) {
247
253
  }
248
254
  ws.serverHelloSeen = true;
249
255
  const manifest = chrome.runtime.getManifest();
250
- ws.send(JSON.stringify({
256
+ const helloSent = sendSocketQuietly(ws, JSON.stringify({
251
257
  type: "hello",
252
258
  role: "extension",
253
259
  protocol: BROWSER_EXTENSION_PROTOCOL,
254
260
  version: manifest.version_name || manifest.version,
261
+ extension_id: chrome.runtime.id,
255
262
  capabilities: [
256
263
  "semantic_snapshot_refs", "actionability_waits", "trusted_input", "tab_management", "explicit_waits",
257
264
  ],
258
265
  }));
266
+ if (!helloSent) closeSocketQuietly(ws, 1011, "browser extension hello failed");
259
267
  return;
260
268
  }
261
269
  if (message?.type === "hello_ack") {
@@ -267,9 +275,12 @@ async function handleMessage(ws, raw, onReady = () => {}) {
267
275
  ws.bridgeReady = true;
268
276
  reconnectAttempt = 0;
269
277
  setConnectionState("connected");
270
- clearInterval(keepaliveTimer);
271
- keepaliveTimer = setInterval(() => {
272
- if (socket === ws && ws.bridgeReady && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: "ping" }));
278
+ clearInterval(ws.keepaliveTimer);
279
+ ws.keepaliveTimer = setInterval(() => {
280
+ if (socket === ws && ws.bridgeReady && ws.readyState === WebSocket.OPEN
281
+ && !sendSocketQuietly(ws, JSON.stringify({ type: "ping" }))) {
282
+ closeSocketQuietly(ws, 1011, "browser extension keepalive failed");
283
+ }
273
284
  }, 20000);
274
285
  onReady();
275
286
  return;
@@ -284,15 +295,23 @@ async function handleMessage(ws, raw, onReady = () => {}) {
284
295
  return;
285
296
  }
286
297
  if (message?.type !== "request" || typeof message.id !== "string" || typeof message.method !== "string") return;
298
+ if (activeRequests.has(message.id)) {
299
+ closeSocketQuietly(ws, 1002, "duplicate browser request id");
300
+ return;
301
+ }
287
302
  const state = { cancelled: false, timeoutMs: browserOperations().boundedRequestTimeout(message.timeout_ms), socket: ws };
288
303
  activeRequests.set(message.id, state);
289
304
  try {
290
305
  throwIfCancelled(state);
291
306
  const result = await dispatch(message.method, message.params || {}, state);
292
307
  throwIfCancelled(state);
293
- sendResponse(ws, message.id, true, result);
308
+ if (!sendResponse(ws, message.id, true, result)) {
309
+ closeSocketQuietly(ws, 1011, "browser response delivery failed");
310
+ }
294
311
  } catch (error) {
295
- if (!state.cancelled) sendResponse(ws, message.id, false, null, String(error?.message || error).slice(0, 2000));
312
+ if (!state.cancelled && !sendResponse(ws, message.id, false, null, String(error?.message || error).slice(0, 2000))) {
313
+ closeSocketQuietly(ws, 1011, "browser error delivery failed");
314
+ }
296
315
  } finally {
297
316
  activeRequests.delete(message.id);
298
317
  }
@@ -309,12 +328,12 @@ function throwIfCancelled(state) {
309
328
  }
310
329
 
311
330
  function sendResponse(ws, id, ok, result, error = "") {
312
- if (ws.readyState !== WebSocket.OPEN) return;
331
+ if (ws.readyState !== WebSocket.OPEN) return false;
313
332
  let payload = JSON.stringify({ type: "response", id, ok, ...(ok ? { result } : { error }) });
314
333
  if (new TextEncoder().encode(payload).byteLength > MAX_RESULT_BYTES) {
315
334
  payload = JSON.stringify({ type: "response", id, ok: false, error: "browser result exceeds maximum size" });
316
335
  }
317
- sendSocketQuietly(ws, payload);
336
+ return sendSocketQuietly(ws, payload);
318
337
  }
319
338
 
320
339
 
@@ -69,7 +69,7 @@ See [Local application and browser automation](LOCAL_AUTOMATION.md).
69
69
 
70
70
  ### Managed job runner
71
71
 
72
- `ManagedJobManager` persists bounded per-workspace job envelopes below the owner-only profile directory. `start_job` validates the complete plan, snapshots referenced resource metadata/hashes, writes an owner-only plan/status, and launches `job-runner.mjs` as a detached process with runner-level logs redirected to owner-only files. `stage_job` performs the same acceptance validation but writes a non-running `staged` envelope; only local `job approve` transitions it to queued and launches the runner.
72
+ `ManagedJobManager` persists bounded per-workspace job envelopes below the owner-only profile directory. `start_job` validates the complete plan, snapshots referenced resource metadata/hashes, writes an owner-only plan/status, and launches `job-runner.mjs` as a detached process with runner-level logs redirected to owner-only files. `stage_job` performs the same acceptance validation but writes a non-running `staged` envelope; it remains non-running; execution requires a separate trusted `start_job` request or an explicit local `job submit` plan.
73
73
 
74
74
  The runner:
75
75
 
@@ -83,7 +83,7 @@ The runner:
83
83
  - writes bounded redacted results and terminal status;
84
84
  - deletes the full execution plan and runner PID file after terminal commit.
85
85
 
86
- Running managed jobs do not belong to an MCP socket or daemon call ID and snapshot the accepted environment mode/resources. Later profile changes govern new direct submissions; accepted running jobs require explicit cancellation. Staged plans launch no process and require explicit local approval. Daemon disconnect/replacement does not terminate them. Dead runner PIDs are detected on the next daemon or local job-CLI start; stale private runtime data is removed and the finally phase is retried in recovery mode. Recovery deliberately reruns all finally steps, so cleanup must be idempotent.
86
+ Running managed jobs do not belong to an MCP socket or daemon call ID and snapshot the accepted environment mode/resources. Later profile changes govern new direct submissions; accepted running jobs require explicit cancellation. Staged plans launch no process and have no terminal promotion path. Daemon disconnect/replacement does not terminate them. Dead runner PIDs are detected on the next daemon or local job-CLI start; stale private runtime data is removed and the finally phase is retried in recovery mode. Recovery deliberately reruns all finally steps, so cleanup must be idempotent.
87
87
 
88
88
  Local resource registrations remain in owner-only state and are reloaded for every new job. MCP-visible resource inventory includes aliases and validation status but never source paths, hashes, or contents.
89
89
 
@@ -93,7 +93,7 @@ Policy revision 5 is loaded from one shared JSON contract by both local and Work
93
93
 
94
94
  Named profiles are normalized to their complete capability sets. In particular, `full` always means write enabled, shell execution, unrestricted paths, complete parent environment, absolute-path display, and every catalog tool. CLI flags that alter an individual capability deliberately change the profile identity to `custom`. Policy revision 5 defines the only accepted persisted policy shape. Named profiles are normalized to their canonical capability sets, and persisted data from another revision is rejected rather than interpreted. Compound availability requirements come from the shared contract.
95
95
 
96
- Remote authority has a separate final layer. After the Worker intersects account role with daemon policy and the local runtime rechecks that role, `OperationAuthorizer` permits an authenticated owner directly within the daemon policy ceiling. For delegated non-owner accounts it classifies the normalized operation effect. Workspace-contained reads and ordinary edits, project inspection, and non-content browser/application status proceed automatically. Higher-impact delegated effects may require several independent scopes: browser upload combines profile access with data export, application resource input combines application control with data export, and external sensitive paths combine location and sensitivity scopes. Existing account/client-bound leases may satisfy those scopes independently; only missing scopes become a short-lived local pending approval. A `full` lease covers every transaction scope for at most eight hours but does not alter the canonical saved `full` policy. Risk classification is a pure domain module. Lease persistence is a separate owner-only, bounded, atomically replaced state boundary, and an owner-only process-identity lock serializes daemon and CLI mutations so concurrent approval activity cannot lose updates. Every patch destination is canonicalized through the execution resolver before classification. See [LOCAL_AUTHORIZATION.md](LOCAL_AUTHORIZATION.md).
96
+ Remote authority has a separate final layer. After the Worker intersects account role with daemon policy, the local runtime independently rebuilds authority from account ID, account version, OAuth client ID, refresh-family ID, and role. `OperationAuthorizer` classifies normalized effects and enforces hard invariants: generic path-based tools cannot target control-plane roots; sensitive and persistence targets, browser and application control, data export, credential operations, and persistent-plan creation are owner-only; external paths and process execution remain inside the effective policy. Classification produces audit metadata, not an approval or privilege-escalation workflow. Every patch destination is canonicalized through the execution resolver before classification. Long-lived processes, output sessions, and jobs retain the creating principal binding. Legacy lease storage remains only for local migration cleanup and is not consulted by runtime authorization. See [LOCAL_AUTHORIZATION.md](LOCAL_AUTHORIZATION.md).
97
97
 
98
98
  The full-only `generate_ssh_key_resource` operation is implemented locally. The Worker only filters and relays its shared catalog definition. Local generation uses `ssh-keygen`, verifies public/private correspondence, registers the private file through the same owner-only state transaction as the CLI, and rolls back a newly created pair if state persistence fails.
99
99
 
@@ -119,34 +119,36 @@ The Worker verifies OAuth, validates MCP envelopes and optional protocol headers
119
119
 
120
120
  ### Daemon device authentication
121
121
 
122
- The local daemon owns a P-256 device identity. Worker deployment receives only the public JWK; the private JWK remains in owner-only local state. Every WebSocket attempt first carries a short-lived signed preflight transcript bound to Worker origin, package version, nonce, and timestamp. The nonce is consumed once through bounded Durable Object transaction state before upgrade, so neither an unauthenticated client nor a captured signed preflight can accumulate or replace daemon candidates. After upgrade, the Worker issues a fresh challenge and accepts daemon tools only after a second signature binds the challenge, Worker origin, package version, daemon instance ID, and timestamp. End-to-end readiness still requires an ordinary relay probe before a verified candidate replaces the incumbent. The removed long-lived `X-Bridge-Token` protocol is not retained as a fallback.
122
+ The local daemon uses a long-term P-256 device root to certify a 24-hour ephemeral session key. Worker deployment receives only the root public JWK. The default provider on every platform is an owner-only portable private JWK. macOS can instead use non-exportable Secure Enclave material only when `MBM_MACOS_TRUST_BROKER` identifies an app-like broker whose Apple signature, Team ID, canonical non-symlink executable path with no group/other write access, provisioning-backed Keychain capability, and actual create/delete key probe all validate. The enrolled state binds that broker identity, Keychain tag, and public key. One root signature at daemon startup certifies the in-memory session key. Every WebSocket attempt then carries a short-lived preflight signed by the session key and bound to Worker origin, package version, nonce, timestamp, and the root certificate. The nonce is consumed once through bounded Durable Object state. After upgrade, the Worker issues a fresh challenge and accepts tools only after a second session signature binds the challenge, origin, package version, daemon instance ID, and timestamp. Reconnects reuse the in-memory key without touching the root. End-to-end readiness still requires an ordinary relay probe before a verified candidate replaces the incumbent.
123
123
 
124
- The primary OAuth store separates client registrations and named accounts from authorization codes and access-token records. A separate versioned Durable Object key owns refresh-token families, consumed-token markers, and family revocation. A `client_id` identifies an MCP application and redirect URIs; account records identify the authorized human or service identity. Codes and tokens bind client ID, account ID, account version, role, scope, resource, deployment token version, family identity, and expiration where applicable. Only hashes of bearer tokens are persisted. Access tokens last fifteen minutes; refresh tokens rotate, expire after fourteen idle days or thirty absolute family days, and reuse of a consumed refresh token revokes the complete family, including active access tokens. The Worker carries the authenticated client ID with every relayed call so local leases cannot cross clients. One bridge-specific Durable Object and one local runtime remain the normal topology for a workspace/trust domain; see [MULTI_ACCOUNT.md](MULTI_ACCOUNT.md).
124
+ The primary OAuth store separates trusted client registrations and named accounts from authorization codes and access-token records. A separate versioned Durable Object key owns refresh-token families, consumed-token markers, and family revocation. A `client_id` identifies an MCP application and redirect URIs; account records identify the authorized human or service identity. Codes and tokens bind client ID, account ID, account version, role, scope, resource, deployment token version, family identity, and expiration where applicable. Only hashes of bearer tokens are persisted. Access tokens last fifteen minutes; refresh tokens rotate, expire after fourteen idle days or thirty absolute family days, and reuse of a consumed refresh token revokes the complete family, including active access tokens. The Worker carries account, client, and refresh-family identity with every relayed call so long-lived runtime objects cannot cross principals. One bridge-specific Durable Object and one local runtime remain the normal topology for a workspace/trust domain; see [MULTI_ACCOUNT.md](MULTI_ACCOUNT.md).
125
125
 
126
126
  The daemon attachment deliberately omits workspace path/name/hash and process ID. Explicit authenticated tools may return workspace metadata according to local path-display policy.
127
127
 
128
128
  ### Autostart layer
129
129
 
130
- The service layer emits launchd, systemd-user, or Windows Scheduled Task definitions. Worker/account credentials are not embedded in service definitions; the daemon loads owner-only state. The exact policy is stored in owner-only state. An allowlisted `service-environment.json` separately persists only network-proxy and custom-CA variables needed by a background daemon, because shell-session environment is not inherited reliably by login managers; values are loaded only for `--daemon-only`, never logged, and status exposes key names only. launchd/systemd definitions contain the workspace/state-root selectors, `warn` plus JSON log settings, and a sanitized absolute-only PATH captured at installation so background `full` mode can resolve the same developer tools without accepting relative PATH entries.
130
+ The service layer emits launchd, systemd-user, or Windows Scheduled Task definitions. Worker/account credentials are not embedded in service definitions; the daemon loads owner-only state. The exact policy is stored in owner-only state. An allowlisted `service-environment.json` separately persists only network-proxy and custom-CA variables needed by a background daemon, because shell-session environment is not inherited reliably by login managers; values are loaded only for `--daemon-only`, never logged, and status exposes key names only. launchd/systemd definitions contain the workspace/state-root selectors, `warn` plus JSON log settings, and a sanitized absolute-only PATH captured at installation so background `full` mode can resolve the same developer tools without accepting relative PATH entries. The current Node and package bin are explicit; all nested npm run-script prefixes and inactive candidate-runtime entries are removed so the service definition does not depend on whether installation was invoked from an npm lifecycle or a prior candidate daemon.
131
131
 
132
132
  The Windows adapter owns a short private restart launcher because Task Scheduler's `/TR` action is substantially smaller than the Windows process command-line limit and the full installed Node/CLI/workspace argv can exceed it. The scheduled action therefore contains only the launcher path. The launcher performs the full quoted invocation, redirects to service logs, and restarts only nonzero exits. A fixed PowerShell object query supplies language-independent `Ready`/`Running` state; provider installation is not equated with process activity. The trigger is least-privilege current-user logon, not boot-time `SYSTEM` execution.
133
133
 
134
134
  The platform adapters normalize launchd, systemd, and Windows Scheduled Task operations to one `{ok, provider}` result contract. Removal is not a provider-specific sequence: `service-lifecycle.mjs` first stops the provider, then every verified workspace daemon in scope, and only then removes the definition. A failed stop or unverifiable process prevents definition/state deletion.
135
135
 
136
+ The autostart provider name is machine-global, but daemon locks are state/workspace-scoped. Foreground startup therefore does not stop the provider merely because a Machine Bridge service exists. It first verifies that the current state's daemon lock identifies a live `service` process whose command line matches the same canonical workspace and state root. Only that proven owner may trigger provider stop; isolated installs, another workspace, another state root, and foreground locks leave the existing service untouched.
137
+
136
138
  ## Trust boundaries
137
139
 
138
140
  ```mermaid
139
141
  flowchart LR
140
- C[Remote MCP client] -->|HTTPS + OAuth bearer token| W[Worker / Durable Object]
141
- W -->|device-authenticated bounded WebSocket calls| R[Local runtime]
142
+ C[Remote MCP client] -->|HTTPS + OAuth Bearer or DPoP| W[Worker / Durable Object]
143
+ W -->|root-certified ephemeral WebSocket calls| R[Local runtime]
142
144
  L[Local MCP client] -->|stdio JSON-RPC| R
143
- R --> T[Local transaction gate]
145
+ R --> T[Effective-authority and ownership gate]
144
146
  T -->|canonical workspace tools| F[Selected workspace]
145
- T -->|leased direct/shell processes| P[Local user / OS / network]
146
- T -->|leased Accessibility actions| A[Local applications]
147
+ T -->|owner or verified delegated sandbox| P[Local user / OS / network]
148
+ T -->|owner-only structured actions| A[Local applications]
147
149
  T -->|authenticated loopback broker| B[Existing-profile browser extension]
148
150
  B -->|DOM and visible UI authority| WB[Web pages and browser tabs]
149
- T -->|leased durable accepted plan| J[Detached managed-job runner]
151
+ T -->|owner-only durable accepted plan| J[Detached managed-job runner]
150
152
  J -->|private copies| LR[Local resource files]
151
153
  J -->|argv/stdin/env| P
152
154
  CLI[CLI + owner-only state] --> W
@@ -219,6 +221,8 @@ This is a process-level transaction, not a filesystem-wide atomic transaction ac
219
221
 
220
222
  `run_process` and process sessions use argv arrays and do not invoke a shell. `exec_command` invokes the platform shell and is available only in `shell` mode. Both inherit local-user OS authority.
221
223
 
224
+ Fixed implementation-owned metadata commands are not routed through that arbitrary-process capability. Project-root detection and read-only Git status/diff/log/show use code-constructed argv, no shell, a minimal isolated environment, bounded output/deadlines, and the same cancellation/process-tree accounting. The caller cannot choose the executable or inject a command string. Request-effective path visibility and role filtering still apply to the returned data.
225
+
222
226
  The default `full` profile passes the complete parent environment. Isolated environment mode, used by the narrower named profiles unless overridden, creates private runtime HOME, temporary, and cache directories and passes only a small set of path/locale/platform variables. It reduces accidental credential inheritance but cannot prevent explicit access to known filesystem paths, credential stores, network services, or other user resources.
223
227
 
224
228
  `execution-limits.mjs` is the shared source for local tool-call concurrency, one-shot process timeout/stdin/output limits, and process-session count/stdin/output/retention limits. `server_info.runtime.execution_guardrails` reports those enforced limits together with explicit `not-enforced` values for CPU quota, memory quota, and network isolation. Public one-shot commands inline at most 32 KiB per stream. When either stream exceeds that preview, the runtime keeps up to 1 MiB per stream in a closed in-memory process session for thirty minutes and returns an `output_session_id`; `read_process` then reads monotonic byte-offset pages. The oldest exited session is evicted before an active session is refused, so continuation retention is explicitly best effort rather than durable. The continuation stores command basename and cwd metadata but not argv or shell text. It is memory-only and disappears on runtime stop or daemon replacement.
@@ -277,12 +281,22 @@ Cloudflare sampling is size control rather than an audit log. The project intent
277
281
 
278
282
  ## Release integrity
279
283
 
280
- Repository-local automated checks are necessary but cannot prove that the maintainer's ordinary installation path works. `scripts/local-release-acceptance.mjs` builds the exact npm tarball, while `scripts/start-release-candidate.mjs` installs that tarball into an ignored isolated prefix and starts it in the foreground. The helper requires `--allow-worker-deploy` because normal startup may update the configured same-name Worker before relay verification. After explicit owner authorization in the active conversation, the coding agent starts the candidate and verifies its live Worker version/hash, health, relay connection, local version, readiness, and representative behavior through Machine Bridge before recording both npm hashes outside the package. `scripts/github-push.mjs`, pull-request CI, and `scripts/github-release.mjs` rebuild the package and reject any mismatch. The release helper also requires `HEAD === origin/main`; it cannot silently push `main`.
284
+ Repository-local checks cannot prove the ordinary deployed path. `local-release-acceptance.mjs` builds the exact tarball and promotion-content digest. The owner executes `release:candidate:activate`, which installs the tarball under the private state root and invokes the extracted `runtime-activation` state machine. That state machine stops only a verified service owner, authenticates the candidate daemon through the real Worker, proves relay readiness, installs the service definition, releases startup/daemon ownership in a defined order, starts the login service, and verifies the exact Worker and daemon versions. Fault-injection tests cover foreground-owner conflict, installation failure cleanup, failed service start, and convergence timeout.
285
+
286
+ Accepted prereleases use explicit npm/GitHub channels and a registry-verified activation record. `release-soak.mjs` enforces elapsed major/minor/patch observation windows. `promotion-digest.mjs` hashes the npm package inventory, file modes, and bytes while normalizing only synchronized version metadata; stable release is blocked if any functional packaged content differs. Guarded push, portable CI acceptance, GitHub source release, npm publication, and stable publication all validate the relevant acceptance/soak evidence. Release commands require `HEAD === origin/main` and never push `main` implicitly.
281
287
 
282
288
  Cross-platform evidence remains independent. `scripts/github-release.mjs` queries CI, CodeQL, Governance, and Scorecard for the exact `origin/main` commit and requires the newest push-triggered run for each workflow to be completed with `success` before it creates or verifies a version tag, GitHub Release, or package asset. Pull-request runs, older successful runs, pending runs, and successful runs for another SHA do not satisfy the gate. The workflow selection policy is isolated in `scripts/release-ci.mjs` and tested independently.
283
289
 
284
290
  Third-party workflow actions are pinned to immutable commit SHAs. Dependabot groups GitHub Action updates into one reviewed PR so coupled action families cannot drift across versions. `architecture:test` rejects movable action tags, split update policy, loss of local acceptance verification, or removal of the reachable-history package-audit step.
285
291
 
292
+ ### Recovery and delivery boundaries
293
+
294
+ The relay connection owns transport lifecycle, while `relay-diagnostics.mjs` owns the bounded public snapshot and outage/recovery fields. Application proxy selection is intentionally separated from the operating-system network path. Reconnect backoff is bounded at fifteen seconds; heartbeat and Worker liveness remain independent checks.
295
+
296
+ Owner-state mutations use a shared process-identity lock primitive with separate lock files per concern. Security-audit writes, legacy authorization cleanup, daemon/startup ownership, and managed-job transition/recovery are visible to state inventory so uninstall cannot race an active mutation. Workspace trust-state recovery requires a complete canonical envelope before its recovery marker is removed.
297
+
298
+ Managed-job terminal persistence is an ordered recoverable protocol, not a collection of ignored cleanup calls. Browser broker storage, HTTP/Origin validation, route state, and extension identity are separate modules. Broker internals are not exposed as mutable public aliases; only bounded counts are projected.
299
+
286
300
  ## Explicit non-goals
287
301
 
288
302
  - operating-system sandboxing of arbitrary executables;
package/docs/AUDIT.md CHANGED
@@ -1,5 +1,88 @@
1
1
  # Security and privacy audit notes
2
2
 
3
+ ## 2026-07-22 version 3.0.0-beta.7 nested lifecycle PATH audit
4
+
5
+ Beta.6 passed its automated gates and exact live activation, but the launchd PATH still contained repository `node_modules/.bin` entries and npm's private `node-gyp-bin`. Source, tarball, and isolated runtime inspection proved that the intended sanitizer was packaged. A direct invocation over the installed plist PATH also produced the expected clean value. The remaining variable was the activation process environment.
6
+
7
+ A bounded live probe confirmed the topology: the beta.6 daemon inherited one npm run-script marker from beta.5 activation; starting another npm command prepended a second complete project-bin prefix and marker. The beta.6 algorithm selected the first marker, so it discarded only the outer activation layer and retained the inner inherited layer. This was a deterministic nested-lifecycle defect, not stale package bytes or a launchd rewrite.
8
+
9
+ Beta.7 selects the final npm run-script marker and inherits only entries after that boundary. Tests reproduce two nested Unix and Windows npm layers, then a stale candidate runtime and an ordinary user bin. Both npm layers and the stale runtime are rejected; current Node/package paths and the user bin remain. Beta.6 is blocked and has no acceptance record. No beta.7 publication or source release is implied by this correction.
10
+
11
+ ## 2026-07-22 version 3.0.0-beta.6 service-environment reproducibility audit
12
+
13
+ The exact beta.5 candidate completed its full automated gates, isolated installation, same-name Worker deployment, persistent launchd handoff, project/Git verification, idempotent `service start`, and detached `service restart`. The live restart changed the daemon PID, incremented the launchd run count, exited the prior process cleanly, and restored one ready same-version daemon with no pending calls. Schema-4 service logs remained owner-only, single-link, and free of mixed text. These checks validated the beta.5 lifecycle corrections.
14
+
15
+ Inspection of the installed launchd definition then found a separate reproducibility defect. Candidate activation is invoked through an npm lifecycle from the source repository and was itself launched by the previous daemon. npm prepended a chain of repository/ancestor `node_modules/.bin` directories and its private `@npmcli/run-script/lib/node-gyp-bin`; the previous daemon contributed its beta.4 candidate bin as the first inherited PATH entry. The service installer accepted every absolute entry. Activation then pruned the beta.4 runtime, leaving a dead path in the persistent launchd definition, while unrelated source-repository and npm-private paths remained ahead of the operator's ordinary PATH. Internal Git/release decisions were already pinned to trusted absolute executables, so no observed authority bypass occurred, but the service environment depended on the mechanism used to invoke installation and no longer represented a reproducible operator environment.
16
+
17
+ Beta.6 blocks beta.5 and repairs the common service-path constructor rather than special-casing candidate activation. Node and the current package bin are inserted explicitly. If npm's run-script `node-gyp-bin` marker exists, every preceding lifecycle-injected entry and the marker itself are discarded. Entries in the candidate runtime container are rejected unless they are the current package bin. The remaining absolute inherited user PATH and platform defaults retain their order and are deduplicated. A user who intentionally supplies a `node_modules/.bin` outside an npm lifecycle keeps it; the rule distinguishes provenance using the npm marker instead of banning a path shape globally.
18
+
19
+ The regression reproduces the owner-machine topology and proves that Node, the current candidate, and the inherited user bin remain, while the npm project prefix, npm-private marker, and stale prior candidate are removed. Existing launchd/systemd quoting and absolute-only tests remain in force. No beta.6 Worker deployment, persistent service replacement, acceptance record, Git push, tag, npm publication, or GitHub Release is implied by this source correction.
20
+
21
+ ## 2026-07-22 version 3.0.0-beta.5 relay, lifecycle, and recovery audit
22
+
23
+ Beta.4 successfully corrected the delegated-policy and fixed-Git defects found in beta.3, but it did not complete the release contract. During further owner-machine verification, the authenticated relay repeatedly disappeared and later recovered without a daemon PID change. The launchd daemon remained alive, retained the same process start time, and reauthenticated as the same installed beta.4 runtime. The evidence therefore excludes a local daemon crash for the observed incidents. The Worker route was resolved through the machine's system VPN/TUN and synthetic DNS mapping; direct `en0` probes to the public Cloudflare addresses were reset while the VPN path succeeded. The VPN service itself remained nominally connected, so the exact failing internal node cannot be proven from available logs. The supported conclusion is narrower: a system-level routed path can fail while the local process and VPN control plane remain alive, and the previous sixty-second reconnect cap unnecessarily prolonged recovery.
24
+
25
+ Beta.5 treats that distinction as an architectural contract. `network_route` now describes only Machine Bridge's application-level selection (`system-network-stack`, `application-http-proxy`, or invalid application proxy), and `network_route_scope` explicitly warns that an OS VPN/TUN may still intercept the first case. Relay status and `diagnose_runtime` retain bounded outage history, close category/code, transport error class, ready duration, and next-retry timing. Reconnect backoff is capped at fifteen seconds. Warn/info events are timestamped NDJSON and omit raw close reasons, proxy endpoints, tool arguments, commands, page data, and results. Heartbeat semantics remain conservative: the daemon sends every twenty-five seconds, requires inbound Worker traffic within seventy-five seconds, and the Worker reclaims a silent socket after ninety seconds.
26
+
27
+ The service review found that command semantics and ownership had been conflated. `service start` stopped and replaced an already running daemon, so invoking it through that daemon could destroy its own control path. Machine-global service control also accepted a workspace/state selector that did not necessarily own the installed service. Beta.5 makes start idempotent, isolates explicit restart, verifies the exact service daemon owner, and prevents unrelated state roots from stopping it. macOS/Linux use a detached service-manager handoff; Windows restart fails closed until a behaviorally verified helper boundary exists. Provider status is projected into a small structured record rather than returning launchd/systemd environment and path dumps.
28
+
29
+ The state and filesystem review found several variants of the same trust problem. Corrupt workspace state could be backed up and silently replaced on a later startup; an incomplete object could clear the recovery marker; and symlink-only checks did not prevent a workspace or state file from sharing an inode through a hard link. Beta.5 persists a validated recovery marker, requires a complete current-schema workspace/state envelope before unlocking, reads the marker once, and rejects multiple-link inodes at owner-only state, service logs, runner diagnostics, and existing-file path tools. The autostart log transition is strict: both active files are safely opened before mutation, schema 4 is committed only after reset/rotation succeeds, and a failed migration blocks startup rather than destroying incident evidence or mixing formats.
30
+
31
+ Long-lived execution received a failure-injection review rather than another happy-path test. Delayed SIGKILL now uses process-start/PGID membership snapshots so it neither misses surviving descendants nor targets a reused identity. Managed-job runners inherit only a minimal control environment unless the accepted plan explicitly requests full environment. Staged plans expire after twenty-four hours. Terminal result, terminal status, private runtime/plan cleanup, and cleanup confirmation are ordered; every write/delete failure is visible, and a valid terminal result can reconstruct status after a crash so finally steps are not repeated. Security-audit writes and legacy-state cleanup share a generic owner-state lock, and uninstall observes those locks plus job transition/recovery locks.
32
+
33
+ The browser and protocol review closed several same-machine amplification paths. The extension identity is fixed by the public manifest key and checked in both Origin and hello. Extension and runtime clients use different credentials. Replacing a socket clears its timer and requests, disables stale reconnect, and duplicate request IDs close the protocol. A failed hello, keepalive, result, error, Worker welcome, or Worker pong is treated as transport failure immediately. Mutable broker Sets/Maps are no longer exposed as public compatibility aliases; only bounded load counts are reported.
34
+
35
+ Release-control commands now use trusted absolute Git/GitHub CLI executables rather than npm lifecycle PATH. Unknown exceptions no longer export their raw message to a remote caller, and public structured details must be bounded JSON. Unauthorized MCP bodies are streamed to completion without retention before the 401 response, avoiding both buffering and response-after-stream races. Refresh-replay marker pressure revokes the affected family before marker eviction. Delegated macOS execution is enabled only after an allow/deny behavior matrix, not because `/usr/bin/sandbox-exec` can run `/usr/bin/true`.
36
+
37
+ The audit does not claim a distributed transaction across Cloudflare Worker deployment and the local service manager. Candidate activation verifies the foreground candidate before handoff and aggregates every local cleanup error, but an already changed Worker plus a failed service handoff may still require explicit operator rollback from the recorded previous runtime and deployment evidence. Likewise, system VPN/TUN node selection remains outside the repository. These residuals are now documented and observable rather than hidden behind labels such as `direct` or "rollback succeeded".
38
+
39
+ Beta.4 is blocked. No beta.5 package, Worker deployment, persistent service replacement, acceptance record, Git push, tag, npm publication, or GitHub Release is implied by this source audit. The complete repository gates, exact candidate activation, live same-version verification, and owner-machine acceptance remain mandatory.
40
+
41
+ ## 2026-07-22 version 3.0.0-beta.4 authority, release, and low-friction hardening audit
42
+
43
+
44
+ The first live candidate activation exposed a compatibility regression before any Worker or daemon handoff: the new shared public-JWK helper serialized members as `kty`, `crv`, `x`, `y`, while version 2 device identifiers used the RFC-style `crv`, `kty`, `x`, `y` order. Because JSON member order participates in the identifier hash, the intact version 2 key pair appeared to have an invalid identifier. Version `3.0.0-beta.2` restored the stable order, locked both local and Worker computations to the version 2 result, rejected the erroneous beta.1 ordering, and marked beta.1 as blocked.
45
+
46
+ The second live activation reached the next boundary and failed before Worker deployment or daemon handoff: the runtime-compiled helper was ad-hoc signed and therefore had no provisioning-profile-validated access group for the macOS data-protection Keychain. Persistent Secure Enclave key generation returned `errSecMissingEntitlement` (`-34018`). Version `3.0.0-beta.3` marks beta.2 as blocked, stops automatic Secure Enclave migration, keeps the portable root by default, and requires an explicitly configured provisioned app-like broker plus a real create/delete key probe before enrollment. Both failures validate the prerelease process: neither candidate was published or promoted as stable, and each blocking fix increments the beta version and restarts candidate verification.
47
+
48
+ The third live activation completed the exact beta.3 Worker and persistent launchd handoff, survived a controlled service restart, and completed ordinary owner read/write/process calls. The same live review then exposed a delegated-account boundary defect: local `project_overview` returned an already-intersected request policy while the Worker interpreted that field as the daemon ceiling, and fixed read-only Git metadata commands reused the arbitrary-process path, causing reviewer/editor metadata to fail whenever the delegated sandbox was unavailable. Version `3.0.0-beta.4` blocks beta.3, returns explicit effective and daemon fields, and moves implementation-owned Git probes to a validated no-shell minimal-environment internal process path while leaving caller-selected process execution sandbox-gated.
49
+
50
+ Repeated relay outages during beta.4 verification were traced to the package installation smoke test rather than the network or Cloudflare. Its isolated zero-argument startup reached foreground preparation, which unconditionally stopped the machine-global service label before the fake Wrangler boundary. The test therefore sent `SIGTERM` to the real beta.3 launchd daemon whenever the full plan reached `install:test`. Foreground startup now checks the exact state/workspace daemon lock and may stop the platform service only when that lock identifies a live, verified `service` process. The smoke test installs a service-manager trap and fails if isolated startup attempts any machine-level service control. A real post-fix installation smoke run preserved the same daemon PID, relay `connected_at`, and readiness state.
51
+
52
+ The same live review found an observability contract defect. The launchd definition correctly selected `--log-format json`, but only the structured `event()` path honored it; direct logger methods and the daemon-ready summary still emitted unstructured text without timestamps. Beta.4 makes JSON format authoritative across all logger methods and emits daemon readiness as a single redacted JSON lifecycle event, preserving newline-delimited ingestion and incident-timeline accuracy.
53
+
54
+ Immediately before candidate preparation, the registry audit database began reporting GHSA-f88m-g3jw-g9cj against Miniflare’s exact `sharp@0.34.5` dependency. The current and newly released Wrangler lines still selected that vulnerable version, so a Wrangler-only upgrade was insufficient. Beta.4 applies an exact npm override to patched `sharp@0.35.3`, updates the explicit lifecycle-script allowlist, verifies the resolved dependency tree, and requires complete Worker/Miniflare integration plus zero-vulnerability, registry-signature, and provenance-attestation checks before candidate generation.
55
+
56
+ The release-process review found a separate governance defect: the repository used the final semantic version while the package was still an unpublished local candidate, and successful live verification could flow directly into a stable tag and `latest` publication. That process had no persistent beta runtime, no registry-verified observation period, no defect-reset rule, and no machine-enforced proof that stable bytes matched the tested prerelease. Version 3 moved the package to the `3.0.0-beta` channel; at that stage the candidate under review was `3.0.0-beta.4`. The release process now has explicit `dev`, `beta`, `rc`, and stable channels. Candidate activation is now one owner-executed command that installs the exact tarball under owner-only state, updates the same-name Worker, proves candidate relay readiness, installs the login service, performs a lock-controlled handoff, verifies the exact background daemon and Worker versions, and exits while the service remains active.
57
+
58
+ Formal soak begins only after the exact accepted package is published under the correct npm dist-tag, marked as a GitHub Prerelease, installed from the registry, and reactivated. Major, minor, and patch releases require minimum seven-day, three-day, and one-day intervals. A blocking issue creates a new prerelease and restarts the clock. Stable promotion uses a digest over the npm file inventory with only synchronized version metadata normalized; any runtime, dependency, script, policy, extension, Worker, or packaged-documentation change forces another prerelease. Guarded push, GitHub release creation, npm publication, CI portable acceptance, and stable promotion all enforce these records.
59
+
60
+ The version 2 transaction-lease design solved several effect-classification gaps but retained the wrong privilege primitive for delegated accounts: a terminal lease could expand effective behavior after the account role had already been selected. It also required copy-an-ID, approve, and retry interaction, which increased prompt fatigue and encouraged broad temporary grants. Version 3 replaces that model with a request-scoped intersection of daemon capability, immutable account role, trusted OAuth client, account version, refresh-token family, operation invariants, and object ownership. Risk classification remains, but it now supplies hard enforcement and privacy-preserving audit metadata rather than temporary elevation. Legacy lease files remain readable only for migration cleanup and are not consumed by the runtime.
61
+
62
+ The review found cross-principal object gaps beyond initial tool dispatch. Interactive processes, retained one-shot output, and managed jobs could outlive the request that created them and were not uniformly bound to the originating account/client. Version 3 binds those objects to account ID, account version, OAuth client ID, and refresh-family ID. Another account, another client for the same account, or a newly issued family cannot read, continue, send input to, cancel, or terminate the object. The same principal is carried from Worker token verification through the relay envelope and local authority context.
63
+
64
+ Generic remote `full` file and process authority was also capable of reaching Machine Bridge's own state. Version 3 identifies protected control-plane roots and rejects generic path-based file access even for `owner`. Device-root material and profile state are no longer exposed through ordinary file APIs. This is defense against accidental/tool-level export, not a shell sandbox: an authorized owner interpreter still has the OS user authority and can reach same-user state unless an external OS boundary is used.
65
+
66
+ The long-lived account-administration HMAC was replaced by the same root-trust hierarchy used for daemon authentication. The default device root on every platform, including macOS, is the existing owner-only portable P-256 key. At daemon startup it signs a 24-hour ephemeral session certificate without Keychain access or user presence; the private session key remains in memory and signs WebSocket preflight, Worker challenge, reconnects, and account-administration requests. The Worker stores only the root public JWK. Account administration no longer deploys or stores `ACCOUNT_ADMIN_SECRET`; each request binds origin, method, path, body hash, session key ID, timestamp, and nonce to the certified session signature.
67
+
68
+ A non-exportable macOS Secure Enclave root is an optional deployment enhancement, not an npm runtime assumption. `MBM_MACOS_TRUST_BROKER` must name an app-like broker with a strict Apple code signature, stable Team ID, provisioning-profile-validated data-protection Keychain access, canonical non-symlink executable path with no group/other write access, and successful temporary create/delete key probe. The root state binds that broker identity and every later use revalidates it. The packaged Swift source and ad-hoc build remain development/protocol fixtures and are intentionally rejected by the production validator.
69
+
70
+ When a provisioned broker is configured, root migration and rotation remain two-phase. A candidate root is persisted as pending, its public key is deployed, and Worker health/version convergence is verified before local promotion. Failed probe, upload, interrupted startup, or failed health leaves the old active root intact. `--daemon-only` refuses to activate an undeployed pending root.
71
+
72
+ OAuth clients are now persistent trust objects rather than reusable registration shells. First authorization binds one client record to one account, account version, and role. Client revocation independently removes its codes and token records. Refresh rotation remains single-use and family-replay revocation remains fail closed. Optional DPoP ES256 binds supported client token families to a client key while Bearer remains for interoperability. The DPoP pass found that cryptographically valid proofs from arbitrary self-generated keys were consuming the global replay cache before OAuth credential validation, allowing unauthenticated cache exhaustion. Proof verification is now pure; the replay marker is consumed only after access-token or grant/client/account/resource validation succeeds. Unsupported critical JWS semantics are rejected.
73
+
74
+ Delegated direct execution received an explicit negative-boundary review. Restricting cwd, HOME, and environment does not prevent an ordinary same-user process from reading state, Keychain, or unrelated home files. Version 3 therefore requires a behaviorally verified OS sandbox. On the current macOS 27 beta host, deny-default `sandbox-exec` policies abort, so delegated process execution fails closed instead of using a misleading blacklist. Owner execution is unchanged. Hard mutually untrusted execution still requires a dedicated account, container, or VM.
75
+
76
+ The local security audit is now a bounded SHA-256 hash chain. Entries contain tool identity, risk class, result, timing, byte counts, target digest, and salted principal references. Commands, paths, file contents, form values, and output are excluded. Nonce stores no longer evict unexpired replay markers when full; they reject new entries until capacity is available. DPoP replay capacity is reserved for already authenticated/validated requests rather than arbitrary public proofs.
77
+
78
+ The normal interaction target is no prompt for ordinary tool calls, WebSocket reconnect, or portable-root daemon startup. A provisioned Secure Enclave deployment requests one user-presence operation when the root signs a daemon session certificate and may request one for an independent account-administration command or explicit root rotation. Terminal operation approval and `job approve` paths are removed. `stage_job` is a non-executing draft; execution requires owner-authorized `start_job` or explicit local `job submit`.
79
+
80
+ Regression evidence includes hard role ceilings under a canonical-full daemon; explicit effective versus daemon-ceiling project snapshots; reviewer/editor fixed Git metadata without arbitrary-process authority; minimal-environment internal process dispatch; cross-account/client/family process and job denial; control-plane path denial; root-certified session preflight/challenge tamper, expiry, wrong-root, and replay tests; signed account administration without a symmetric secret; trusted-client binding and independent revocation; DPoP proof/token binding, post-authorization replay consumption, invalid-grant non-consumption, and critical-header rejection; nonce saturation; audit-chain tamper detection; delegated sandbox behavior; macOS development-broker compilation, production rejection of ad-hoc signing, provisioned-broker path/signature/Team-ID binding, and create/delete probe cleanup; two-phase root deployment state; package/install/stdio/Worker integration; and critical-module coverage gates.
81
+
82
+ Residual limits remain explicit. A compromised active owner client can exercise the daemon ceiling without per-operation prompts. Bearer clients remain vulnerable to token possession until expiry or revocation. Portable roots are exportable. A same-user process can inspect daemon memory or interfere with local files and services. Owner execution has no universal CPU, memory, syscall, or network isolation. Public binary-store distribution, external audit anchoring, independent review, and protected publication identities remain optional external hardening rather than release blockers for the self-hosted package.
83
+
84
+ Live beta.3 Worker deployment, persistent daemon replacement, ordinary owner-tool verification, and one controlled launchd restart were performed on the owner machine. No account or device-root rotation, Git push, tag, npm publication, GitHub Release, or stable promotion was performed.
85
+
3
86
  ## 2026-07-21 version 2.0.0 remote-authority audit
4
87
 
5
88
  The critical question was whether canonical `full` could retain its automation value without allowing one long-lived remote bearer to remain equivalent to unattended local-user control. The pre-2.0 implementation correctly intersected account role with the daemon capability ceiling, but the final authorization decision was still tool-granular: once `exec_command`, unrestricted paths, browser tools, or managed jobs were exposed, the original arguments reached the handler without a local effect-level transaction boundary. The daemon WebSocket also authenticated with one long-lived shared bearer, access tokens lasted thirty days, and account administration sent its long-lived secret directly as a network bearer.
@@ -16,7 +99,7 @@ The review found and corrected several additional boundary gaps. Protecting only
16
99
 
17
100
  Regression evidence includes P-256 preflight/challenge generation and tamper/wrong-key/expiry rejection; real Worker candidate and readiness integration; fifteen-minute access tokens and whole-family revocation after refresh replay; signed administration success, legacy bearer rejection, nonce replay rejection, and malformed-state rejection; account/client lease isolation, automatic workspace behavior, scoped/full windows, compound browser/application authority, external-sensitive path composition, sensitive writes, process/job continuation, symbolic-link and patch-move targets, cross-process mutation serialization, owner-only state, and malformed-record rejection. The complete repository gates remain authoritative before release.
18
101
 
19
- Residual limits are explicit. Device and administration private material still live in owner-only cross-platform state rather than macOS Secure Enclave/Keychain; a determined same-user process can read or alter that state and can interfere with the daemon. `full` still passes the complete parent environment by contract once process authority is locally leased. Machine Bridge still does not provide kernel CPU/memory quotas, syscall filtering, or network egress isolation. DPoP remains optional future work because current hosted MCP clients do not uniformly support it. Logs remain privacy-oriented operational telemetry, not a remote tamper-proof forensic ledger. Signed/notarized root-owned distribution, external audit anchoring, and hard sandboxing remain separate distribution/deployment work rather than being simulated in application code.
102
+ At the time of version 2.0, residual limits were explicit. Device and administration private material still lived in owner-only cross-platform state rather than macOS Secure Enclave/Keychain; a determined same-user process can read or alter that state and can interfere with the daemon. `full` still passes the complete parent environment by contract once process authority is locally leased. Machine Bridge still does not provide kernel CPU/memory quotas, syscall filtering, or network egress isolation. DPoP was still future work because hosted MCP clients did not uniformly support it. Logs remain privacy-oriented operational telemetry, not a remote tamper-proof forensic ledger. Signed/notarized root-owned distribution, external audit anchoring, and hard sandboxing remain separate distribution/deployment work rather than being simulated in application code.
20
103
 
21
104
  No Worker deployment, daemon/service replacement, global installation, credential rotation in live state, push, tag, npm publication, or GitHub Release is performed by these source changes.
22
105
 
@@ -46,7 +129,7 @@ Regression evidence covers three layers: deterministic pending-registry detach/r
46
129
 
47
130
  The release-integrity mechanism correctly bound acceptance to exact package bytes, but the operational role split was wrong. Earlier documentation required the owner to execute candidate and approval commands in a terminal. The explicit owner decision is now that authorization is given in the active conversation; the coding agent starts the exact candidate through Machine Bridge, verifies its version, readiness, deployment identity, and representative functionality, then records acceptance and completes source release operations. This removes terminal authorization as a user-facing dependency without allowing automated tests alone to manufacture acceptance.
48
131
 
49
- Version 1.2.9 codifies that flow. `npm run release:candidate:start -- --allow-worker-deploy` verifies and installs the pending tarball into an ignored isolated prefix and launches it in the foreground without replacing the normal global installation. The explicit flag acknowledges that normal candidate startup may update the configured same-name Worker in place. After explicit owner authorization in the active conversation, the coding agent runs that command through Machine Bridge and must observe the live candidate through Machine Bridge; automated checks or an unobserved process are insufficient. After successful observation, the agent records the new `owner-authorized-agent-operated-local-candidate` marker, commits, pushes through the guarded command, completes the pull request, and runs `npm run release` to create or verify the annotated tag and GitHub Release. The old 1.2.8 owner-recorded marker remains accepted only for historical compatibility.
132
+ Version 1.2.9 codifies that flow. `npm run release:candidate:start -- --allow-worker-deploy` verifies and installs the pending tarball into an ignored isolated prefix and launches it in the foreground without replacing the normal global installation. The explicit flag acknowledges that normal candidate startup may update the configured same-name Worker in place. After explicit owner authorization in the active conversation, the coding agent runs that command through Machine Bridge and must observe the live candidate through Machine Bridge; automated checks or an unobserved process are insufficient. After successful observation, the agent records the new `owner-started-agent-verified-local-candidate` marker, commits, pushes through the guarded command, completes the pull request, and runs `npm run release` to create or verify the annotated tag and GitHub Release. The old 1.2.8 owner-recorded marker remains accepted only for historical compatibility.
50
133
 
51
134
  npm publication and Worker deployment remain owner-operated live release actions. The coding agent does not perform either as part of `npm run release`. Any packaged-byte change after candidate preparation invalidates the pending or recorded acceptance and requires another owner-authorized, agent-operated and observed candidate run.
52
135
 
package/docs/CLIENTS.md CHANGED
@@ -136,7 +136,7 @@ Several OAuth clients and named accounts can coexist. Accounts have independent
136
136
 
137
137
  ## Profile guidance
138
138
 
139
- - `full` is the default capability ceiling and prioritizes immediate usability. It is a canonical contract exposing every catalog tool, shell execution, unrestricted direct filesystem paths, absolute path output, and the full parent environment. Any individual narrowing is represented as `custom`. Remote high-impact transaction leases are a separate final authorization layer and do not change the profile identity.
139
+ - `full` is the default capability ceiling and prioritizes immediate usability. It is a canonical contract exposing every catalog tool, shell execution, unrestricted direct filesystem paths, absolute path output, and the full parent environment. Any individual narrowing is represented as `custom`. Remote accounts remain further constrained by immutable role, trusted-client, token-family, operation-invariant, and object-ownership checks.
140
140
  - `agent` retains file mutation and direct process execution but removes shell parsing, confines direct filesystem tools to the workspace, and isolates the process environment.
141
141
  - `edit` permits deterministic file mutation without process execution.
142
142
  - `review` is read-only and workspace-confined.
@@ -162,7 +162,7 @@ Machine Bridge itself does not block files because their names look sensitive. I
162
162
  Do not attempt to evade a host refusal by renaming, encoding, or switching to another arbitrary execution tool. Instead:
163
163
 
164
164
  1. register credentials locally as resource aliases so their values never enter MCP arguments;
165
- 2. submit a complete `start_job` plan before the workflow depends on later cleanup calls, or use `stage_job` plus local `job approve` when execution-class tools are unavailable;
165
+ 2. submit a complete owner-authorized `start_job` plan before the workflow depends on later cleanup calls; `stage_job` is only a non-running draft, while an explicit local operator may use `machine-mcp job submit PLAN.json`;
166
166
  3. use job-scoped temporary files or remote stdin scripts;
167
167
  4. put idempotent cleanup in `finally_steps`;
168
168
  5. inspect/cancel through `machine-mcp job ...` if the host later denies tools.
@@ -9,7 +9,7 @@ This document records project-wide decisions that must survive individual fixes,
9
9
  3. **Machine Bridge authority and host authority are separate.** `full` removes Machine Bridge's own policy, path, shell, and environment restrictions. It cannot override an MCP host, connector gateway, operating system, endpoint-security product, cloud IAM, remote authentication, or `sudo`.
10
10
  4. **Publication surfaces contain no real environment metadata.** Source, tests, fixtures, examples, release notes, filenames, package contents, tags, and release assets use synthetic identifiers and reserved example domains.
11
11
  5. **Secrets are never operational log data.** Tool arguments, command text, stdin, stdout, stderr, file content, OAuth bodies, credentials, and local resource values are not logged.
12
- 6. **A release is one owner-authorized, agent-operated and verified package with successful cross-platform evidence.** The repository owner explicitly authorizes the exact npm candidate and any same-name Worker update in the active conversation; the coding agent starts that candidate through Machine Bridge, verifies the connected candidate version, readiness, and representative functionality, and then records the package hashes before the first GitHub push. Package metadata, Worker version, browser-extension version/name, acceptance record, Git tag, GitHub Release, npm version, documentation, and deployed health version must agree, and the exact `origin/main` commit must have completed successful push-triggered CI, CodeQL, Governance, and OpenSSF Scorecard runs before a tag or release is created.
12
+ 6. **Stable release requires a published prerelease soak.** The owner runs one exact persistent candidate activation command; the coding agent verifies the real Worker and daemon before recording acceptance. Version 3 and later use `dev`, `beta`, or `rc`, publish under non-`latest` tags, and complete the policy minimum soak after registry-verified activation. Stable promotion may change only normalized release metadata and requires an identical promotion-content digest, a tracked soak record, repeated stable-candidate verification, and successful exact-commit CI, CodeQL, Governance, and Scorecard.
13
13
  7. **Generic local automation is structured, not arbitrary evaluation.** Browser/application features may expose broad user authority under canonical `full`, but must not accept caller-provided JavaScript, AppleScript, JXA, or extension code.
14
14
  8. **Daily-browser integration uses the existing profile.** The supported primary browser path is the packaged authenticated extension and machine-level loopback broker, preserving current tabs/login state; a separate automation profile is not an equivalent replacement.
15
15
  9. **Pairing and resource secrets are not conversation or log data.** Tokens and injected local-resource values must not be returned, embedded in URLs, or written to operational logs.
@@ -24,9 +24,15 @@ A proposed change that conflicts with an invariant requires an explicit owner de
24
24
 
25
25
  ## Change and release-operation ownership
26
26
 
27
- Repository implementation, interactive candidate acceptance, source release completion, and live release operations are separate responsibilities. Under `AGENTS.md`, coding automation may edit, test, and commit locally, generate an exact candidate tarball, and, after explicit owner authorization in the active conversation, start the exact isolated candidate through Machine Bridge. The agent verifies the deployed Worker version/hash, remote health, relay readiness, and connected local candidate through Machine Bridge and may then record `release-acceptance/v<version>.json`, push only through `npm run github:push`, complete the pull request through local `git`/`gh`, and create the annotated version tag plus final GitHub Release with `npm run release` after the accepted package hash and exact `main` checks pass. The owner is not required to copy approval IDs or run release-authorization commands in a terminal. Automated checks without an observed live candidate do not authorize acceptance. `npm run release` never pushes `main`. Automation must not publish or alter npm packages, install globally outside the isolated candidate prefix, deploy or reconfigure a Worker as a release operation, rotate credentials, mutate live deployment state, or replace daemon/service state without explicit user authorization.
27
+ Repository implementation, candidate preparation, observed live verification, acceptance recording, source pull requests, tags, and GitHub Releases are distinct from owner-operated live activation and npm publication.
28
28
 
29
- The normal handoff is: the repository owner publishes the reviewed npm version, then runs `npm install -g --omit=optional --allow-scripts=esbuild,workerd,sharp,fsevents machine-bridge-mcp@latest && machine-mcp`. The npm command updates the global CLI but cannot hot-reload an existing Node process. The subsequent normal foreground startup validates the Worker deployment hash, expected version, and health, requests shutdown of an active autostart daemon, waits a bounded interval for its lock, redeploys when necessary, and then takes over with the installed version. Live operations require explicit authorization even when they appear to be the obvious next release step.
29
+ For each package change, automation prepares the exact prerelease tarball and stops. The owner executes `npm run release:candidate:activate -- --allow-worker-deploy`. After that command updates the Worker, verifies candidate relay readiness, replaces the login daemon, and verifies service handoff, the coding agent inspects the connected system through Machine Bridge. Only then may it record candidate acceptance and push through `npm run github:push`.
30
+
31
+ After merge and exact-commit checks, automation creates a GitHub Prerelease with `npm run prerelease:release`. The owner explicitly publishes npm with `npm run prerelease:publish` and activates the registry package with `npm run prerelease:install -- --allow-worker-deploy`. The owner uses the prerelease for the required interval and explicitly reports whether blocking issues remain. Automation must not infer soak success from elapsed time.
32
+
33
+ Stable promotion is content-preserving. `release:soak:verify` compares the packaged functional digest with the accepted prerelease. A mismatch requires another prerelease and a restarted soak. After stable candidate activation and observed verification, automation may complete `npm run release`; the owner separately authorizes `npm run stable:publish`.
34
+
35
+ Live npm publication, global installation, Worker/service replacement, credential mutation, and unrelated live-state changes remain explicit owner decisions. `npm run release` never pushes `main`.
30
36
 
31
37
  ## Default instruction invariant
32
38
 
@@ -16,7 +16,7 @@ Remote and stdio modes use the same local runtime and policy model. The remote W
16
16
 
17
17
  ## 2. Understand the authority you are granting
18
18
 
19
- A new workspace uses the `full` profile unless another profile is selected explicitly. `full` preserves the complete tool catalog, shell execution, paths outside the selected workspace, absolute paths, browser/application automation, and the complete parent process environment. It is intended for a trusted owner using a trusted MCP host. An authenticated owner account may use the `full` ceiling directly without terminal approval. Delegated non-owner accounts require local time-bounded capability leases for high-impact effects; their normal workspace reads/edits and project inspection remain automatic. See [LOCAL_AUTHORIZATION.md](LOCAL_AUTHORIZATION.md).
19
+ A new workspace uses the `full` profile unless another profile is selected explicitly. `full` preserves the complete tool catalog, shell execution, paths outside the selected workspace, absolute paths, browser/application automation, and the complete parent process environment. It is intended for a trusted owner using a trusted MCP host. Delegated accounts remain permanently inside their role ceilings; unsupported effects are denied and cannot be enabled by a terminal approval or lease. The default portable root does not access Keychain or request user presence. A separately installed and provisioned macOS trust broker may request user presence once when its Secure Enclave root signs an ephemeral daemon session. See [LOCAL_AUTHORIZATION.md](LOCAL_AUTHORIZATION.md).
20
20
 
21
21
  For a first connection to an unfamiliar host, start with a narrower profile:
22
22
 
@@ -159,13 +159,14 @@ machine-mcp --workspace /path/to/project --profile review
159
159
  The first start performs these operations:
160
160
 
161
161
  1. canonicalizes and remembers the workspace;
162
- 2. creates owner-only per-workspace state and credentials;
163
- 3. opens the Wrangler/Cloudflare sign-in flow when required;
164
- 4. deploys a Worker and Durable Object for that workspace;
165
- 5. installs a platform-native login service unless `--no-autostart` is supplied;
166
- 6. starts an outbound authenticated WebSocket from the local daemon to the Worker;
167
- 7. creates the initial `owner` account when no account exists and prints its generated password once;
168
- 8. prints the remote `/mcp` URL.
162
+ 2. creates owner-only per-workspace state and a deployment token version;
163
+ 3. retains or creates the owner-only portable device root; when `MBM_MACOS_TRUST_BROKER` explicitly selects a validated provisioned broker, performs the optional two-phase Secure Enclave enrollment and requests user presence only when that root signs the ephemeral daemon session;
164
+ 4. opens the Wrangler/Cloudflare sign-in flow when required;
165
+ 5. deploys the Worker with the active or pending root public key and verifies health before promoting a pending root;
166
+ 6. installs a platform-native login service unless `--no-autostart` is supplied;
167
+ 7. starts an outbound WebSocket authenticated by the root-certified in-memory session key;
168
+ 8. creates the initial `owner` account when no account exists and prints its generated password once;
169
+ 9. prints the remote `/mcp` URL.
169
170
 
170
171
  The foreground command remains attached to the terminal. Keep it running while testing. The remote Worker cannot execute local tools when no authenticated daemon is connected.
171
172
 
@@ -336,7 +337,7 @@ Do not point two logical trust domains at one broad parent directory merely to r
336
337
 
337
338
  ## 13. Upgrade
338
339
 
339
- Version 1 supports only MCP protocol `2025-11-25`; upgrade or reconnect clients that still request an older date. The current 0.18.x state schema remains valid and should be preserved.
340
+ Version 3 supports the current MCP protocol contract advertised by the server and rejects incompatible mixed-version Worker/daemon combinations. Preserve the owner-only state root when upgrading from the immediately preceding supported release; malformed, obsolete, or ambiguous trust state fails closed instead of being guessed or silently regenerated. Follow [UPGRADING.md](UPGRADING.md) for blocked prereleases and coordinated Worker/daemon/browser-extension replacement.
340
341
 
341
342
 
342
343
  Repeat the isolated global installation, then start Machine Bridge in the target workspace: