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

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 +101 -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 +80 -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
package/src/local/cli.mjs CHANGED
@@ -3,7 +3,7 @@ import { join, resolve } from "node:path";
3
3
  import process from "node:process";
4
4
  import readline from "node:readline/promises";
5
5
  import { LocalRuntime } from "./runtime.mjs";
6
- import { acquireDaemonLockWithTakeover, stopWorkspaceServiceDaemon } from "./daemon-process.mjs";
6
+ import { acquireDaemonLockWithTakeover, inspectWorkspaceDaemon, stopWorkspaceServiceDaemon, workspaceDaemonOwnsPlatformAutostart } from "./daemon-process.mjs";
7
7
  import { inspectProcessInstance } from "./process-identity.mjs";
8
8
  import { runStdioServer } from "./stdio.mjs";
9
9
  import { assertCanonicalFullPolicy, POLICY_PROFILES, toolsForPolicy } from "./tools.mjs";
@@ -12,6 +12,7 @@ import { effectiveLogFormat, effectiveLogLevel, normalizeCommand, parseArgs, val
12
12
  import { createLocalAdminCommands } from "./cli-local-admin.mjs";
13
13
  import { createApprovalCommand } from "./cli-approval.mjs";
14
14
  import { createServiceCommand } from "./cli-service.mjs";
15
+ import { createActivateCommand } from "./cli-activate.mjs";
15
16
  import { generateAccountPassword } from "./account-admin.mjs";
16
17
  import { accountAdminClient, createAccountCommand } from "./cli-account-admin.mjs";
17
18
  export { resolvePolicy } from "./cli-policy.mjs";
@@ -20,8 +21,10 @@ import { classifyOperationalError, createLogger, sanitizeLogText } from "./log.m
20
21
  import { runExecutable, runWrangler } from "./shell.mjs";
21
22
  import { runFullAccessTest } from "./full-access-test.mjs";
22
23
  import { stopAndRemoveAutostart } from "./service-lifecycle.mjs";
24
+ import { stopOwnedPlatformService } from "./service-ownership.mjs";
23
25
  import { loadServiceEnvironment } from "./service-environment.mjs";
24
- import { ensureWorkerDeployment } from "./worker-deployment.mjs";
26
+ import { createDeviceSessionForRoot, deviceRootProviderStatus, ensurePreferredDeviceRoot } from "./device-root-provider.mjs";
27
+ import { convergeRemoteConfiguration } from "./remote-configuration.mjs";
25
28
  import { workerHealth } from "./worker-health.mjs";
26
29
  export { workerHealthUserReason } from "./worker-health.mjs";
27
30
  import { activeStateJobs, activeStateLocks, knownProfileStates, knownWorkerNames } from "./state-inventory.mjs";
@@ -52,9 +55,18 @@ const localAdminCommands = createLocalAdminCommands({ chooseWorkspace, confirm }
52
55
  const accountCommand = createAccountCommand({ chooseWorkspace, confirm });
53
56
  const approvalCommand = createApprovalCommand({ chooseWorkspace, confirm });
54
57
  const serviceCommand = createServiceCommand({ chooseWorkspace, stateRootFromArgs, structuredLogger });
58
+ const activateCommand = createActivateCommand({
59
+ chooseWorkspace,
60
+ prepareRemoteState,
61
+ createRemoteRuntime,
62
+ currentPackageVersion,
63
+ assertNodeVersion,
64
+ structuredLogger,
65
+ });
55
66
 
56
67
  const COMMAND_HANDLERS = new Map([
57
68
  ["start", startCommand],
69
+ ["activate", activateCommand],
58
70
  ["stdio", stdioCommand],
59
71
  ["client-config", clientConfigCommand],
60
72
  ["status", statusCommand],
@@ -206,11 +218,28 @@ async function prepareStartMode(args, state, logger) {
206
218
  trimAutostartLogs(state.paths.stateRoot);
207
219
  return { takeOverServiceOwner: false };
208
220
  }
209
- // A normal foreground start first asks the platform service manager to
210
- // unload the job, then independently reclaims a verified daemon-only
211
- // process. The second step handles orphaned service daemons that launchd or
212
- // another service manager no longer tracks.
213
- return stopAutostartBestEffort(logger);
221
+ // The platform service name is machine-global, while state/workspace locks
222
+ // are scoped. Stop the platform service only when the daemon lock proves
223
+ // that the loaded service belongs to this exact state/workspace. An
224
+ // unrelated foreground start (including install smoke tests with isolated
225
+ // HOME/state) must never unload another Machine Bridge deployment.
226
+ const ownership = await stopOwnedPlatformService({
227
+ state,
228
+ inspectWorkspaceDaemon,
229
+ ownsPlatformAutostart: workspaceDaemonOwnsPlatformAutostart,
230
+ stopPlatformService: () => stopAutostartBestEffort(logger),
231
+ });
232
+ if (!ownership.owned) {
233
+ logger.debug?.("foreground startup left unrelated machine autostart untouched", {
234
+ workspace_daemon_present: ownership.daemon.present === true,
235
+ workspace_daemon_alive: ownership.daemon.alive === true,
236
+ identity_reason: ownership.daemon.identity_reason || "not_running",
237
+ });
238
+ return { takeOverServiceOwner: true, provider: null };
239
+ }
240
+ // Once ownership is proven, unload the provider first so KeepAlive cannot
241
+ // respawn the service while the verified process lock is reclaimed.
242
+ return ownership.provider;
214
243
  }
215
244
 
216
245
  function reportExistingDaemon(args, state, owner, logger) {
@@ -255,9 +284,9 @@ async function startRemoteRuntime({ args, workspace, state, daemonLock, logger }
255
284
  let runtime = null;
256
285
  try {
257
286
  const readiness = await prepareRemoteState({ args, workspace, state, logger });
258
- runtime = createRemoteRuntime({ args, workspace, state, daemonLock });
287
+ runtime = createRemoteRuntime({ args, workspace, state, daemonLock, deviceSessionIdentity: readiness.deviceSessionIdentity });
259
288
  await runtime.start();
260
- reportRemoteReady(args, state, readiness);
289
+ reportRemoteReady(args, state, readiness, logger);
261
290
  keepProcessAlive({ daemon: runtime, lock: daemonLock, logger });
262
291
  } catch (error) {
263
292
  try { runtime?.stop?.(); } catch {}
@@ -267,32 +296,32 @@ async function startRemoteRuntime({ args, workspace, state, daemonLock, logger }
267
296
  }
268
297
 
269
298
  async function prepareRemoteState({ args, workspace, state, logger }) {
270
- const workerName = validateWorkerName(args.workerName);
271
- ensureWorkerSecrets(state, {
272
- rotateSecrets: Boolean(args.rotateSecrets),
273
- workerName,
274
- allowWorkerRename: Boolean(args.forceWorker),
275
- });
276
- state.policy = resolvePolicy(args, state.policy);
277
- state.policy.updatedAt = new Date().toISOString();
278
- saveState(state);
279
-
280
- let initialOwner = null;
281
299
  if (!args.daemonOnly) {
282
- await ensureWorker(state, args);
283
- initialOwner = await ensureInitialOwnerAccount(state);
300
+ await convergeRemoteConfiguration({ args, state });
284
301
  } else if (!state.worker.url) {
285
302
  throw new Error("--daemon-only requires an existing Worker URL; run start once without --daemon-only");
303
+ } else if (state.worker.pendingDeviceIdentity) {
304
+ throw new Error("--daemon-only cannot activate a pending device root; run a normal start once to deploy and promote it");
286
305
  }
287
306
 
307
+ const deviceSessionIdentity = await createDeviceSessionForRoot(
308
+ state.worker.deviceIdentity,
309
+ state.worker.url,
310
+ "machine-bridge-mcp",
311
+ currentPackageVersion(),
312
+ { profileDir: state.paths.profileDir, reason: "Authorize Machine Bridge startup" },
313
+ );
314
+ const initialOwner = args.daemonOnly ? null : await ensureInitialOwnerAccount(state, deviceSessionIdentity);
288
315
  if (!args.daemonOnly && !args.noAutostart) {
289
316
  await installAutostartBestEffort({ workspace, stateRoot: state.paths.stateRoot, entryScript: process.argv[1], logger });
290
317
  }
291
- return { initialOwner };
318
+ return { initialOwner, deviceSessionIdentity };
292
319
  }
293
320
 
294
- async function ensureInitialOwnerAccount(state) {
295
- const client = accountAdminClient(state);
321
+
322
+
323
+ async function ensureInitialOwnerAccount(state, deviceSessionIdentity) {
324
+ const client = await accountAdminClient(state, deviceSessionIdentity);
296
325
  const existing = await client.list();
297
326
  if (existing.accounts.length > 0) return null;
298
327
  const password = generateAccountPassword();
@@ -300,10 +329,11 @@ async function ensureInitialOwnerAccount(state) {
300
329
  return { ...created.account, password };
301
330
  }
302
331
 
303
- function createRemoteRuntime({ args, workspace, state, daemonLock }) {
304
- return new LocalRuntime({
332
+ function createRemoteRuntime({ args, workspace, state, daemonLock, deviceSessionIdentity, exitOnTerminal = true }) {
333
+ const terminalState = { error: null };
334
+ const runtime = new LocalRuntime({
305
335
  workerUrl: state.worker.url,
306
- deviceIdentity: state.worker.deviceIdentity,
336
+ deviceIdentity: deviceSessionIdentity,
307
337
  expectedRelayVersion: currentPackageVersion(),
308
338
  workspace,
309
339
  policy: state.policy,
@@ -313,18 +343,29 @@ function createRemoteRuntime({ args, workspace, state, daemonLock }) {
313
343
  resources: state.resources,
314
344
  resourceStatePath: state.paths.statePath,
315
345
  browserStateRoot: state.paths.stateRoot,
346
+ deviceRootStatus: deviceRootProviderStatus(state.worker.deviceIdentity),
316
347
  onSuperseded: () => {
348
+ if (!exitOnTerminal) {
349
+ terminalState.error ??= new Error("candidate daemon was superseded before service handoff");
350
+ return;
351
+ }
317
352
  daemonLock.release();
318
353
  process.exit(0);
319
354
  },
320
- onFatal: () => {
355
+ onFatal: (error) => {
356
+ if (!exitOnTerminal) {
357
+ terminalState.error ??= error instanceof Error ? error : new Error("candidate relay failed before service handoff");
358
+ return;
359
+ }
321
360
  daemonLock.release();
322
361
  process.exit(1);
323
362
  },
324
363
  });
364
+ if (!exitOnTerminal) runtime.terminalError = () => terminalState.error;
365
+ return runtime;
325
366
  }
326
367
 
327
- function reportRemoteReady(args, state, readiness) {
368
+ function reportRemoteReady(args, state, readiness, logger) {
328
369
  if (args.json) {
329
370
  printStartJson(state, { initialOwner: readiness.initialOwner });
330
371
  return;
@@ -333,6 +374,7 @@ function reportRemoteReady(args, state, readiness) {
333
374
  quiet: Boolean(args.quiet),
334
375
  verbose: Boolean(args.verbose),
335
376
  initialOwner: readiness.initialOwner,
377
+ logger,
336
378
  });
337
379
  }
338
380
 
@@ -350,6 +392,7 @@ async function stdioCommand(args) {
350
392
  resources: state.resources,
351
393
  resourceStatePath: state.paths.statePath,
352
394
  browserStateRoot: state.paths.stateRoot,
395
+ deviceRootStatus: state.worker?.deviceIdentity ? deviceRootProviderStatus(state.worker.deviceIdentity) : null,
353
396
  });
354
397
  }
355
398
 
@@ -379,10 +422,6 @@ async function clientConfigCommand(args) {
379
422
  }
380
423
  }
381
424
 
382
- async function ensureWorker(state, args) {
383
- const logger = createLogger({ level: args.json ? "error" : effectiveLogLevel(args), format: effectiveLogFormat(args), component: "worker" });
384
- return ensureWorkerDeployment(state, args, { logger });
385
- }
386
425
 
387
426
 
388
427
  function printStartJson(state, { requestedChangesApplied = true, notice = "", initialOwner = null } = {}) {
@@ -401,8 +440,21 @@ function printStartJson(state, { requestedChangesApplied = true, notice = "", in
401
440
  });
402
441
  }
403
442
 
404
- function printMcpConnection(state, { quiet = false, verbose = false, initialOwner = null } = {}) {
405
- const logger = createLogger({ component: "ready", quiet, level: quiet ? "error" : verbose ? "debug" : "info" });
443
+ function printMcpConnection(state, { quiet = false, verbose = false, initialOwner = null, logger: readyLogger = null } = {}) {
444
+ const output = readyLogger || createLogger({ component: "ready", quiet, level: quiet ? "error" : verbose ? "debug" : "info" });
445
+ if (output.format === "json" && !initialOwner) {
446
+ output.event("success", "daemon.ready", {
447
+ mcp_server_url: state.worker.mcpServerUrl,
448
+ worker_name: state.worker.name,
449
+ workspace_path: state.workspace.path,
450
+ policy_profile: state.policy.profile,
451
+ policy_origin: state.policy.origin,
452
+ write_enabled: state.policy.allowWrite,
453
+ exec_mode: state.policy.execMode,
454
+ }, "Remote MCP bridge is ready");
455
+ return;
456
+ }
457
+ const logger = output;
406
458
  logger.success("Remote MCP bridge is ready");
407
459
  logger.plain(` MCP Server URL: ${state.worker.mcpServerUrl}`);
408
460
  if (initialOwner) {
@@ -522,7 +574,12 @@ async function rotateSecretsCommand(args) {
522
574
  const operationLogger = createLogger({ level: args.quiet ? "error" : "warn", component: "service" });
523
575
  const startupLock = await acquireStartupLockWithWait(state, { operation: "rotate-secrets", logger: operationLogger });
524
576
  try {
525
- await stopAutostartBestEffort(operationLogger);
577
+ await stopOwnedPlatformService({
578
+ state,
579
+ inspectWorkspaceDaemon,
580
+ ownsPlatformAutostart: workspaceDaemonOwnsPlatformAutostart,
581
+ stopPlatformService: () => stopAutostartBestEffort(operationLogger),
582
+ });
526
583
  const stopped = await stopWorkspaceServiceDaemon(state, { logger: operationLogger, reason: "secret rotation" });
527
584
  if (stopped.found && !stopped.ok) {
528
585
  const pid = stopped.pid ? `pid ${stopped.pid}` : "unknown pid";
@@ -534,10 +591,17 @@ async function rotateSecretsCommand(args) {
534
591
  if (daemonIdentity?.current) {
535
592
  throw new Error(`refusing to rotate secrets while the daemon is active (pid ${daemonOwner.pid}); stop the foreground daemon and retry`);
536
593
  }
537
- ensureWorkerSecrets(state, { rotateSecrets: true });
594
+ if (state.worker.pendingDeviceIdentity) throw new Error("a device-root rotation is already pending; run machine-mcp to deploy it before rotating again");
595
+ ensureWorkerSecrets(state, { rotateSecrets: true, deferDeviceRotation: true });
596
+ state.worker.pendingDeviceIdentity = await ensurePreferredDeviceRoot({
597
+ profileDir: state.paths.profileDir,
598
+ workspaceHash: state.workspace.hash,
599
+ existing: null,
600
+ rotate: true,
601
+ });
538
602
  saveState(state);
539
- console.log("Rotated account administration, daemon, and global token-version secrets.");
540
- console.log("All account access tokens are invalid. Run machine-mcp to redeploy, then reconnect clients.");
603
+ console.log("Prepared a two-phase rotation for account administration, device root, and token-version secrets.");
604
+ console.log("All account access tokens are invalid. Run machine-mcp to deploy, verify, and atomically promote the pending device root.");
541
605
  } finally {
542
606
  startupLock.release();
543
607
  }
@@ -694,14 +758,7 @@ function escapeRegExp(value) {
694
758
  return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
695
759
  }
696
760
 
697
- function validateWorkerName(value) {
698
- if (value === undefined || value === null || value === false) return undefined;
699
- const name = String(value).trim();
700
- if (!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(name)) {
701
- throw new Error("--worker-name must be 1-63 lowercase letters, digits, or hyphens, and cannot start or end with a hyphen");
702
- }
703
- return name;
704
- }
761
+
705
762
 
706
763
  export function isSupportedNodeVersion(version = process.versions.node) {
707
764
  const major = Number(String(version || "").replace(/^v/, "").split(".")[0]);
@@ -737,28 +794,25 @@ Usage:
737
794
 
738
795
  Commands:
739
796
  start Deploy/update Worker, take over autostart, run foreground daemon
797
+ activate Deploy/update Worker, replace autostart, start and verify background daemon
740
798
  stdio Run a local MCP stdio server for Claude, Cursor, Codex, and compatible clients
741
799
  client-config Print stdio client configuration snippets
742
800
  workspace show Show remembered workspace
743
801
  workspace set Re-select workspace; prompts with current/default path
744
802
  service status Show autostart status
745
803
  service install Install login autostart for remembered/current workspace
746
- service start Start the installed autostart service now
804
+ service start Ensure the installed autostart service is running (idempotent)
805
+ service restart Schedule an explicit service-manager restart and return before handoff
747
806
  service stop Stop the installed autostart service
748
807
  service uninstall Remove only the autostart entry
749
808
  status Print redacted local profile state and Worker health
750
809
  doctor Check Node, Wrangler, Cloudflare login, Worker health
751
810
  full-test Run real local full-profile capability tests in a temporary sandbox
752
811
  rotate-secrets Rotate account-admin, device identity, and global token-version secrets
753
- account list|add|role|enable|disable|rotate-password|remove
754
- Manage isolated remote accounts and targeted revocation
755
- approval list Show pending high-impact operations and active capability leases
756
- approval approve APPROVAL_ID [--duration 1h] [--full]
757
- Approve its scope, or open an explicit full window (maximum 8h)
758
- approval grant SCOPE --account ACCOUNT_ID --client CLIENT_ID [--duration 1h]
759
- Pre-authorize a bounded scope; use * values only intentionally
760
- approval revoke LEASE_ID | approval clear
761
- Revoke one or all local capability leases
812
+ account list|clients|revoke-client|add|role|enable|disable|rotate-password|remove
813
+ Manage remote accounts, trusted clients, and targeted revocation
814
+ approval list|revoke|clear
815
+ Inspect or remove legacy leases; runtime calls are automatic within role ceilings
762
816
  resource generate-ssh-key NAME [PATH]
763
817
  Generate/reuse an Ed25519 key locally and register its private file by alias
764
818
  browser status Show browser-extension bridge and connection status
@@ -165,6 +165,12 @@ export async function stopWorkspaceServiceDaemon(state, options = {}) {
165
165
  };
166
166
  }
167
167
 
168
+ export function workspaceDaemonOwnsPlatformAutostart(status = {}) {
169
+ return status.alive === true
170
+ && status.verified_service_daemon === true
171
+ && status.mode === "service";
172
+ }
173
+
168
174
  export function inspectWorkspaceDaemon(state) {
169
175
  const owner = readDaemonLockOwner(daemonLockPathForState(state));
170
176
  if (!owner) return { present: false, alive: false, verified_service_daemon: false };
@@ -0,0 +1,147 @@
1
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { spawnSync } from "node:child_process";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import { BridgeError } from "./errors.mjs";
6
+
7
+ const MACOS_SANDBOX_EXEC = "/usr/bin/sandbox-exec";
8
+ let macosProbeResult;
9
+
10
+ export function delegatedProcessCommand({ command, args = [], workspace, runtimeDir, context = {}, platform = process.platform } = {}) {
11
+ const principal = context?.authority?.principal;
12
+ if (!principal || principal.kind !== "account" || principal.role === "owner") {
13
+ return { command, args, isolation: "owner-or-local-user" };
14
+ }
15
+ if (platform === "darwin" && macosSandboxAvailable({ platform })) {
16
+ const profile = macosProfile({ workspace, runtimeDir });
17
+ return {
18
+ command: MACOS_SANDBOX_EXEC,
19
+ args: ["-p", profile, String(command), ...args.map(String)],
20
+ isolation: "macos-sandbox-exec-workspace",
21
+ };
22
+ }
23
+ throw new BridgeError(
24
+ "policy_denied",
25
+ "delegated process execution requires a behavior-verified OS workspace sandbox; use the owner account or an isolated worker account/VM",
26
+ { details: { reason: "delegated_process_isolation_unavailable", platform } },
27
+ );
28
+ }
29
+
30
+ export function delegatedProcessIsolationStatus(platform = process.platform) {
31
+ if (platform === "darwin" && macosSandboxAvailable({ platform })) {
32
+ return {
33
+ available: true,
34
+ provider: "macos-sandbox-exec-deny-default",
35
+ network: "allowed",
36
+ filesystem: "workspace-and-runtime-write; system-runtime-read",
37
+ keychain: "common Keychain CLI access denied by the behavior probe; not a complete same-user tenancy boundary",
38
+ apple_events: "not reachable through the deny-default profile",
39
+ residual: "sandbox-exec is a compatibility boundary, not separate OS-user tenancy; choose a narrow workspace",
40
+ };
41
+ }
42
+ return {
43
+ available: false,
44
+ provider: null,
45
+ network: "unavailable",
46
+ filesystem: "unavailable",
47
+ keychain: "unavailable",
48
+ apple_events: "unavailable",
49
+ residual: platform === "darwin"
50
+ ? "sandbox-exec exists but the workspace read/write, outside-path denial, or Keychain-denial behavior probe failed; delegated execution fails closed"
51
+ : "no verified delegated process sandbox provider is installed; delegated execution fails closed",
52
+ };
53
+ }
54
+
55
+ export function macosDelegatedSandboxProfile({ workspace, runtimeDir } = {}) {
56
+ return macosProfile({ workspace, runtimeDir });
57
+ }
58
+
59
+ export function macosSandboxAvailable(options = {}) {
60
+ if (options.refresh === true) macosProbeResult = undefined;
61
+ if (macosProbeResult !== undefined) return macosProbeResult;
62
+ const platform = String(options.platform || process.platform);
63
+ const exists = typeof options.exists === "function" ? options.exists : existsSync;
64
+ if (platform !== "darwin" || !exists(MACOS_SANDBOX_EXEC)) {
65
+ macosProbeResult = false;
66
+ return false;
67
+ }
68
+ const probe = typeof options.behaviorProbe === "function" ? options.behaviorProbe : probeMacosDelegatedSandbox;
69
+ macosProbeResult = probe({ spawnSyncProcess: options.spawnSync || spawnSync }) === true;
70
+ return macosProbeResult;
71
+ }
72
+
73
+ export function probeMacosDelegatedSandbox(options = {}) {
74
+ const run = typeof options.spawnSyncProcess === "function" ? options.spawnSyncProcess : spawnSync;
75
+ const root = mkdtempSync(path.join(tmpdir(), "mbm-delegated-sandbox-probe-"));
76
+ const workspace = path.join(root, "workspace");
77
+ const runtimeDir = path.join(root, "runtime");
78
+ const outside = path.join(root, "outside.txt");
79
+ const outsideWrite = path.join(root, "outside-write.txt");
80
+ const workspaceWrite = path.join(workspace, "allowed-write.txt");
81
+ try {
82
+ mkdirSync(workspace, { mode: 0o700 });
83
+ mkdirSync(runtimeDir, { mode: 0o700 });
84
+ writeFileSync(path.join(workspace, "allowed.txt"), "allowed\n", { mode: 0o600 });
85
+ writeFileSync(outside, "blocked\n", { mode: 0o600 });
86
+ const profile = macosProfile({ workspace, runtimeDir });
87
+ const execute = (argv) => run(MACOS_SANDBOX_EXEC, ["-p", profile, ...argv], {
88
+ encoding: "utf8",
89
+ timeout: 5_000,
90
+ maxBuffer: 64 * 1024,
91
+ windowsHide: true,
92
+ env: { PATH: "/usr/bin:/bin:/usr/sbin:/sbin", HOME: workspace, TMPDIR: runtimeDir, LANG: "C", LC_ALL: "C" },
93
+ });
94
+ const allowedRead = execute(["/bin/cat", path.join(workspace, "allowed.txt")]);
95
+ const allowedWrite = execute(["/bin/sh", "-c", `printf allowed > ${shellQuote(workspaceWrite)}`]);
96
+ const blockedRead = execute(["/bin/cat", outside]);
97
+ const blockedWrite = execute(["/bin/sh", "-c", `printf blocked > ${shellQuote(outsideWrite)}`]);
98
+ const keychain = execute(["/usr/bin/security", "list-keychains"]);
99
+ return allowedRead.status === 0 && allowedWrite.status === 0 && existsSync(workspaceWrite)
100
+ && blockedRead.status !== 0 && blockedWrite.status !== 0 && !existsSync(outsideWrite)
101
+ && keychain.status !== 0;
102
+ } catch {
103
+ return false;
104
+ } finally {
105
+ rmSync(root, { recursive: true, force: true });
106
+ }
107
+ }
108
+
109
+ function macosProfile({ workspace, runtimeDir }) {
110
+ const readable = [
111
+ "/System",
112
+ "/usr",
113
+ "/bin",
114
+ "/sbin",
115
+ "/Library",
116
+ "/Applications",
117
+ "/opt/homebrew",
118
+ "/nix/store",
119
+ "/private/etc",
120
+ "/private/var/db",
121
+ "/dev",
122
+ workspace,
123
+ runtimeDir,
124
+ ].filter(Boolean);
125
+ const writable = [workspace, runtimeDir, "/private/tmp"].filter(Boolean);
126
+ return [
127
+ "(version 1)",
128
+ "(deny default)",
129
+ "(allow process*)",
130
+ "(allow signal (target self))",
131
+ "(allow sysctl-read)",
132
+ "(allow mach-lookup)",
133
+ "(allow ipc-posix-shm)",
134
+ "(allow network*)",
135
+ "(allow file-read-metadata)",
136
+ ...readable.map((value) => `(allow file-read* (subpath ${sandboxString(path.resolve(value))}))`),
137
+ ...writable.map((value) => `(allow file-write* (subpath ${sandboxString(path.resolve(value))}))`),
138
+ ].join(" ");
139
+ }
140
+
141
+ function sandboxString(value) {
142
+ return `"${String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
143
+ }
144
+
145
+ function shellQuote(value) {
146
+ return `'${String(value).replaceAll("'", `'\\''`)}'`;
147
+ }