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

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 (147) hide show
  1. package/CHANGELOG.md +120 -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 +28 -13
  8. package/docs/AUDIT.md +101 -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 +14 -3
  14. package/docs/MANAGED_JOBS.md +24 -29
  15. package/docs/MULTI_ACCOUNT.md +98 -37
  16. package/docs/OPERATIONS.md +34 -23
  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 +21 -9
  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 +37 -6
  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 +119 -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 +18 -44
  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-resource-service.mjs +77 -0
  101. package/src/local/runtime-tool-handlers.mjs +8 -8
  102. package/src/local/runtime.mjs +85 -95
  103. package/src/local/secure-file.mjs +5 -1
  104. package/src/local/security-audit-log.mjs +204 -0
  105. package/src/local/service-convergence.mjs +17 -1
  106. package/src/local/service-ownership.mjs +17 -0
  107. package/src/local/service-restart-handoff.mjs +50 -0
  108. package/src/local/service-restart-scheduler.mjs +49 -0
  109. package/src/local/service-status.mjs +47 -0
  110. package/src/local/service.mjs +142 -43
  111. package/src/local/state-inventory.mjs +21 -3
  112. package/src/local/state.mjs +155 -17
  113. package/src/local/stdio.mjs +5 -0
  114. package/src/local/tool-executor.mjs +39 -6
  115. package/src/local/tool-result-boundary.mjs +51 -0
  116. package/src/local/tools.mjs +1 -0
  117. package/src/local/trusted-executable.mjs +53 -0
  118. package/src/local/trusted-git-executable.mjs +34 -0
  119. package/src/local/trusted-github-cli.mjs +24 -0
  120. package/src/local/windows-service.mjs +20 -0
  121. package/src/local/worker-deployment.mjs +3 -4
  122. package/src/local/worker-secret-file.mjs +2 -2
  123. package/src/local/workspace-file-service.mjs +34 -31
  124. package/src/shared/admin-auth.d.mts +2 -1
  125. package/src/shared/admin-auth.mjs +3 -3
  126. package/src/shared/device-session-auth.d.mts +20 -0
  127. package/src/shared/device-session-auth.mjs +51 -0
  128. package/src/shared/log-redaction.d.mts +9 -0
  129. package/src/shared/log-redaction.mjs +51 -0
  130. package/src/shared/server-metadata.json +1 -1
  131. package/src/shared/tool-catalog.json +2 -2
  132. package/src/worker/account-admin.ts +23 -22
  133. package/src/worker/authority.ts +23 -4
  134. package/src/worker/daemon-auth.ts +45 -67
  135. package/src/worker/daemon-sockets.ts +12 -2
  136. package/src/worker/device-session-verifier.ts +129 -0
  137. package/src/worker/dpop.ts +151 -0
  138. package/src/worker/http.ts +47 -16
  139. package/src/worker/index.ts +53 -16
  140. package/src/worker/nonce-store.ts +1 -5
  141. package/src/worker/oauth-client-admin.ts +45 -0
  142. package/src/worker/oauth-controller.ts +55 -6
  143. package/src/worker/oauth-refresh-families.ts +21 -7
  144. package/src/worker/oauth-state.ts +6 -0
  145. package/src/worker/oauth-tokens.ts +28 -8
  146. package/src/worker/observability.ts +4 -2
  147. package/src/worker/websocket-protocol.ts +8 -2
@@ -6,7 +6,7 @@ import { isRelayReadyContext } from "./relay-connection.mjs";
6
6
  import { ProcessSessionManager } from "./process-sessions.mjs";
7
7
  import { MAX_CONCURRENT_TOOL_CALLS } from "./execution-limits.mjs";
8
8
  export { MAX_COMMAND_BYTES } from "./process-contract.mjs";
9
- import { normalizePolicy, PolicyGate } from "./tools.mjs";
9
+ import { normalizePolicy, PolicyGate, toolNamesForPolicy } from "./tools.mjs";
10
10
  import { publicError } from "./errors.mjs";
11
11
  import { ProcessTracker } from "./process-tracker.mjs";
12
12
  import { CallRegistry } from "./call-registry.mjs";
@@ -14,27 +14,29 @@ import { RuntimeObservability } from "./observability.mjs";
14
14
  import { ToolExecutor } from "./tool-executor.mjs";
15
15
  import { boundedErrorMessage, ProcessExecutionService } from "./process-execution.mjs";
16
16
  import { GitService } from "./git-service.mjs";
17
+ import { createTrustedGitResolver } from "./trusted-git-executable.mjs";
17
18
  import { LifecycleController } from "./lifecycle.mjs";
18
19
  import { MAX_WRITE_BYTES, sha256, WorkspaceFileService } from "./workspace-file-service.mjs";
19
20
  export { MAX_WRITE_BYTES, sha256 } from "./workspace-file-service.mjs";
20
21
  import { classifyOperationalError } from "./log.mjs";
21
- import { inspectResourceFile, ManagedJobManager } from "./managed-jobs.mjs";
22
- import { generateRegisteredSshKey } from "./resource-operations.mjs";
23
- import { expandHome } from "./state.mjs";
22
+ import { ManagedJobManager } from "./managed-jobs.mjs";
24
23
  import { AgentContextManager } from "./agent-context.mjs";
25
24
  import { AppAutomationManager } from "./app-automation.mjs";
26
25
  import { BrowserBridgeManager } from "./browser-bridge.mjs";
27
26
  import { CapabilityObserver } from "./capability-observer.mjs";
28
- import { readBoundedRegularFileSync } from "./secure-file.mjs";
29
27
  import { isPlainRecord } from "./records.mjs";
30
28
  import { AccountAccessGate } from "./account-access.mjs";
31
29
  import { buildProjectOverview, buildRuntimeInfo } from "./runtime-reporting.mjs";
32
30
  import { diagnoseRuntime as runRuntimeDiagnostics } from "./runtime-diagnostics.mjs";
33
31
  import { bindRuntimeToolHandlers, runtimeToolHandlerNames as registeredRuntimeToolHandlerNames } from "./runtime-tool-handlers.mjs";
34
32
  import { OperationAuthorizer } from "./operation-authorization.mjs";
33
+ import { SecurityAuditLog } from "./security-audit-log.mjs";
34
+ import { delegatedProcessIsolationStatus } from "./delegated-process-sandbox.mjs";
35
+ import { policyForContext } from "./authority-context.mjs";
35
36
  import { createRuntimeRelayConnection, normalizeRelayResumeCalls, normalizeRelayToolCall } from "./runtime-relay.mjs";
36
37
  import { RelayCallRecovery } from "./relay-call-recovery.mjs";
37
- import { assertContainedPath, createRuntimeDir, redactRuntimeErrorMessage, stateRootFromProfileStatePath } from "./runtime-paths.mjs";
38
+ import { RuntimeResourceService } from "./runtime-resource-service.mjs";
39
+ import { assertContainedPath, createRuntimeDir, redactRuntimeErrorMessage } from "./runtime-paths.mjs";
38
40
  import {
39
41
  resolveTaskCapabilities as resolveRuntimeTaskCapabilities,
40
42
  sessionBootstrap as buildRuntimeSessionBootstrap,
@@ -47,7 +49,7 @@ export function runtimeToolHandlerNames() {
47
49
  }
48
50
 
49
51
  export class LocalRuntime {
50
- constructor({ workerUrl = "", deviceIdentity = null, expectedRelayVersion = "", workspace, policy, logger = console, onSuperseded = null, onFatal = null, jobRoot = "", approvalRoot = "", resources = {}, resourceStatePath = "", browserStateRoot = "", agentHome = process.env.HOME || process.env.USERPROFILE || "", codexHome = process.env.CODEX_HOME || "", recoverJobs = true, applicationAutomation = {} }) {
52
+ constructor({ workerUrl = "", deviceIdentity = null, expectedRelayVersion = "", workspace, policy, logger = console, onSuperseded = null, onFatal = null, jobRoot = "", approvalRoot = "", resources = {}, resourceStatePath = "", browserStateRoot = "", agentHome = process.env.HOME || process.env.USERPROFILE || "", codexHome = process.env.CODEX_HOME || "", recoverJobs = true, applicationAutomation = {}, deviceRootStatus = null, resolveGitExecutable = null }) {
51
53
  const remoteWorkerUrl = workerUrl ? String(workerUrl) : "";
52
54
  this.workspaceInput = resolve(workspace || process.cwd());
53
55
  this.workspace = realpathSync.native ? realpathSync.native(this.workspaceInput) : realpathSync(this.workspaceInput);
@@ -58,6 +60,7 @@ export class LocalRuntime {
58
60
  this.logger = logger;
59
61
  this.onSuperseded = typeof onSuperseded === "function" ? onSuperseded : null;
60
62
  this.resourceStatePath = resourceStatePath ? resolve(resourceStatePath) : "";
63
+ this.deviceRootStatus = deviceRootStatus && typeof deviceRootStatus === "object" ? Object.freeze({ ...deviceRootStatus }) : null;
61
64
  this.processTracker = new ProcessTracker();
62
65
  this.lifecycle = new LifecycleController("local runtime");
63
66
  this.observability = new RuntimeObservability();
@@ -80,13 +83,15 @@ export class LocalRuntime {
80
83
  this.mutationQueue = Promise.resolve();
81
84
  this.capabilityObserver = new CapabilityObserver();
82
85
  this.runtimeDir = createRuntimeDir();
86
+ this.resolveGitExecutable = createTrustedGitResolver({ resolve: resolveGitExecutable, workspace: this.workspace, stateRoot: browserStateRoot, runtimeDir: this.runtimeDir, home: agentHome });
83
87
  this.workspaceFileService = new WorkspaceFileService({
84
88
  workspace: this.workspace,
85
89
  policy: this.policy,
86
90
  policyGate: this.policyGate,
87
- resolveExistingPath: (value) => this.resolveExistingPath(value),
88
- resolveWritePath: (value) => this.resolveWritePath(value),
89
- displayPath: (value) => this.displayPath(value),
91
+ policyForContext: (context) => this.effectivePolicy(context),
92
+ resolveExistingPath: (value, context) => this.resolveExistingPath(value, context),
93
+ resolveWritePath: (value, context) => this.resolveWritePath(value, context),
94
+ displayPath: (value, context) => this.displayPath(value, context),
90
95
  throwIfCancelled: (context) => this.throwIfCancelled(context),
91
96
  withMutationLock: (operation) => this.withMutationLock(operation),
92
97
  });
@@ -96,6 +101,7 @@ export class LocalRuntime {
96
101
  workspace: this.workspace,
97
102
  policy: this.policy,
98
103
  authorizeTool: (tool) => this.policyGate.assert(tool),
104
+ policyForContext: (context) => this.effectivePolicy(context),
99
105
  resources,
100
106
  resourceStatePath,
101
107
  stateRoot: browserStateRoot,
@@ -106,21 +112,23 @@ export class LocalRuntime {
106
112
  workspace: this.workspace,
107
113
  policy: this.policy,
108
114
  authorizeTool: (tool) => this.policyGate.assert(tool),
115
+ policyForContext: (context) => this.effectivePolicy(context),
109
116
  runtimeDir: this.runtimeDir,
110
117
  processTracker: this.processTracker,
111
- resolveCwd: async (input) => {
112
- const cwd = await this.resolveExistingPath(input);
118
+ resolveCwd: async (input, context) => {
119
+ const cwd = await this.resolveExistingPath(input, context);
113
120
  if (!(await stat(cwd)).isDirectory()) throw new Error("cwd is not a directory");
114
121
  return cwd;
115
122
  },
116
- displayPath: (value) => this.displayPath(value),
123
+ displayPath: (value, context) => this.displayPath(value, context),
117
124
  throwIfCancelled: (context) => this.throwIfCancelled(context),
118
125
  });
119
126
  this.agentContextManager = new AgentContextManager({
120
127
  workspace: this.workspace,
121
128
  policy: this.policy,
122
- displayPath: (value) => this.displayPath(value),
123
- resolveExistingPath: (value) => this.resolveExistingPath(value),
129
+ policyForContext: (context) => this.effectivePolicy(context),
130
+ displayPath: (value, context) => this.displayPath(value, context),
131
+ resolveExistingPath: (value, context) => this.resolveExistingPath(value, context),
124
132
  throwIfCancelled: (context) => this.throwIfCancelled(context),
125
133
  home: agentHome,
126
134
  codexHome,
@@ -129,37 +137,47 @@ export class LocalRuntime {
129
137
  workspace: this.workspace,
130
138
  policy: this.policy,
131
139
  policyGate: this.policyGate,
140
+ policyForContext: (context) => this.effectivePolicy(context),
132
141
  runtimeDir: this.runtimeDir,
133
142
  processTracker: this.processTracker,
134
- resolveExistingPath: (value) => this.resolveExistingPath(value),
143
+ resolveExistingPath: (value, context) => this.resolveExistingPath(value, context),
135
144
  resolveLocalCommand: (args, context) => this.agentContextManager.resolveLocalCommand(args, context),
136
- displayPath: (value) => this.displayPath(value),
145
+ displayPath: (value, context) => this.displayPath(value, context),
137
146
  throwIfCancelled: (context) => this.throwIfCancelled(context),
138
- retainCompletedOutput: (value) => this.processSessionManager.retainCompletedOutput(value),
147
+ retainCompletedOutput: (value, context) => this.processSessionManager.retainCompletedOutput(value, context),
139
148
  });
140
149
  this.gitService = new GitService({
141
- resolveExistingPath: (value) => this.resolveExistingPath(value),
142
- displayPath: (value) => this.displayPath(value),
143
- runProcess: (...args) => this.processExecutionService.run(...args),
150
+ resolveExistingPath: (value, context) => this.resolveExistingPath(value, context),
151
+ displayPath: (value, context) => this.displayPath(value, context),
152
+ runInternalProcess: (...args) => this.processExecutionService.runFixedInternal(...args),
153
+ gitExecutable: () => this.resolveGitExecutable(),
144
154
  maximumBytes: MAX_WRITE_BYTES,
145
155
  });
156
+ this.runtimeResourceService = new RuntimeResourceService({
157
+ workspace: this.workspace,
158
+ resourceStatePath: this.resourceStatePath,
159
+ currentResources: () => this.managedJobManager.currentResources(),
160
+ authorizeTool: (tool) => this.policyGate.assert(tool),
161
+ });
146
162
  const runProcess = (cmd, argv, timeoutMs, allowFailure, maxOutputBytes, context, cwd, stdin) => this.runProcess(cmd, argv, timeoutMs, allowFailure, maxOutputBytes, context, cwd, stdin);
147
- const readResourceText = (name) => this.readLocalResourceText(name);
148
- const readResourceBinary = (name) => this.readLocalResourceBinary(name);
163
+ const readResourceText = (name) => this.runtimeResourceService.readText(name);
164
+ const readResourceBinary = (name) => this.runtimeResourceService.readBinary(name);
149
165
  this.appAutomationManager = new AppAutomationManager({
150
166
  ...applicationAutomation,
151
167
  policy: this.policy,
152
168
  authorizeTool: (tool) => this.policyGate.assert(tool),
153
- displayPath: (value) => this.displayPath(value),
169
+ displayPath: (value, context) => this.displayPath(value, context),
154
170
  runProcess,
155
171
  readResourceText,
156
172
  throwIfCancelled: (context) => this.throwIfCancelled(context),
157
173
  });
174
+ this.securityAudit = new SecurityAuditLog({ root: approvalRoot });
158
175
  this.operationAuthorizer = new OperationAuthorizer({
159
176
  workspace: this.workspace,
160
177
  root: approvalRoot,
161
- resolveExistingPath: (value) => this.resolveExistingPath(value),
162
- resolveWritePath: (value) => this.resolveWritePath(value),
178
+ resolveExistingPath: (value, context) => this.resolveExistingPath(value, context),
179
+ resolveWritePath: (value, context) => this.resolveWritePath(value, context),
180
+ protectedRoots: [approvalRoot, browserStateRoot],
163
181
  });
164
182
  this.browserBridgeManager = new BrowserBridgeManager({
165
183
  policy: this.policy,
@@ -178,8 +196,9 @@ export class LocalRuntime {
178
196
  operationAuthorizer: this.operationAuthorizer,
179
197
  callRegistry: this.callRegistry,
180
198
  observability: this.observability,
199
+ securityAudit: this.securityAudit,
181
200
  logger: this.logger,
182
- safeMessage: (error, args) => this.safeErrorMessage(error, args),
201
+ safeMessage: (error, args, context) => this.safeErrorMessage(error, args, context),
183
202
  slowMs: SLOW_TOOL_CALL_MS,
184
203
  });
185
204
  this.relay = createRuntimeRelayConnection(this, {
@@ -198,11 +217,11 @@ export class LocalRuntime {
198
217
 
199
218
  tools() { return this.policyGate.names().filter((name) => name !== "server_info"); }
200
219
 
201
- runtimeInfo() {
202
- return buildRuntimeInfo({
220
+ runtimeInfo(context = {}) {
221
+ const info = buildRuntimeInfo({
203
222
  workspace: this.workspace,
204
- displayPath: (value) => this.displayPath(value),
205
- policy: this.policy,
223
+ displayPath: (value) => this.displayPath(value, context),
224
+ policy: this.effectivePolicy(context),
206
225
  toolNames: this.tools(),
207
226
  capabilityObserver: this.capabilityObserver,
208
227
  observability: this.observability,
@@ -214,6 +233,16 @@ export class LocalRuntime {
214
233
  processSessionManager: this.processSessionManager,
215
234
  managedJobManager: this.managedJobManager,
216
235
  });
236
+ return {
237
+ ...info,
238
+ security_audit: this.securityAudit.snapshot(),
239
+ trust: {
240
+ device_root: this.deviceRootStatus,
241
+ daemon_session: { ephemeral: true, certificate_lifetime_seconds: 86400, reconnect_prompts: false },
242
+ delegated_process_isolation: delegatedProcessIsolationStatus(),
243
+ routine_operation_prompts: false,
244
+ },
245
+ };
217
246
  }
218
247
 
219
248
  async start() {
@@ -427,20 +456,24 @@ export class LocalRuntime {
427
456
  }
428
457
 
429
458
  async projectOverview(context = {}) {
459
+ const effectivePolicy = this.effectivePolicy(context);
430
460
  return buildProjectOverview({
431
461
  workspace: this.workspace,
432
- displayPath: (value) => this.displayPath(value),
433
- policy: this.policy,
434
- toolNames: this.tools(),
462
+ displayPath: (value) => this.displayPath(value, context),
463
+ policy: effectivePolicy,
464
+ toolNames: toolNamesForPolicy(effectivePolicy).filter((name) => name !== "server_info"),
465
+ daemonPolicy: this.policy,
466
+ daemonToolNames: this.tools(),
435
467
  capabilityObserver: this.capabilityObserver,
436
468
  listTopLevel: (callContext) => this.listDir(".", callContext),
437
- runProcess: (...args) => this.runProcess(...args),
438
- safeErrorMessage: (error) => this.safeErrorMessage(error),
469
+ runInternalProcess: (...args) => this.processExecutionService.runFixedInternal(...args),
470
+ gitExecutable: () => this.resolveGitExecutable(),
471
+ safeErrorMessage: (error) => this.safeErrorMessage(error, {}, context),
439
472
  throwIfCancelled: (callContext) => this.throwIfCancelled(callContext),
440
473
  }, context);
441
474
  }
442
475
 
443
- listRoots() { return this.workspaceFileService.listRoots(); }
476
+ listRoots(context = {}) { return this.workspaceFileService.listRoots(context); }
444
477
 
445
478
  listDir(pathValue, context = {}) { return this.workspaceFileService.listDir(pathValue, context); }
446
479
 
@@ -482,43 +515,14 @@ export class LocalRuntime {
482
515
  runProcess: (...args) => this.runProcess(...args),
483
516
  probeShell: (callContext) => this.processExecutionService.probeShell(callContext),
484
517
  managedJobManager: this.managedJobManager,
518
+ relayStatus: () => this.relay?.status?.() || null,
485
519
  throwIfCancelled: (callContext) => this.throwIfCancelled(callContext),
486
520
  }, context);
487
521
  }
488
522
 
489
523
  async generateSshKeyResource(args = {}, context = {}) {
490
524
  this.throwIfCancelled(context);
491
- this.policyGate.assert("generate_ssh_key_resource");
492
- if (!this.resourceStatePath) throw new Error("local resource state is unavailable in this runtime");
493
- const home = process.env.HOME || process.env.USERPROFILE;
494
- if (!home) throw new Error("HOME or USERPROFILE is required to choose a default SSH key path");
495
- const target = args.path
496
- ? resolve(expandHome(String(args.path)))
497
- : resolve(home, ".ssh", `machine-mcp-${args.name}-ed25519`);
498
- const key = await generateRegisteredSshKey({
499
- workspace: this.workspace,
500
- stateDir: stateRootFromProfileStatePath(this.resourceStatePath),
501
- name: args.name,
502
- targetPath: target,
503
- comment: args.comment || `machine-mcp:${args.name}`,
504
- });
505
- const exposePaths = args.expose_paths === true;
506
- return {
507
- name: key.name,
508
- created: key.created,
509
- registered: key.registered,
510
- fingerprint: key.fingerprint,
511
- key_type: key.keyType,
512
- private_mode: key.privateMode,
513
- public_mode: key.publicMode,
514
- private_key_content_exposed: key.privateKeyContentExposed,
515
- available_to_new_jobs_immediately: key.availableToNewJobsImmediately,
516
- paths_exposed: exposePaths,
517
- ...(exposePaths ? {
518
- private_key_path: resolve(key.privateKeyPath),
519
- public_key_path: resolve(key.publicKeyPath),
520
- } : {}),
521
- };
525
+ return this.runtimeResourceService.generateSshKey(args);
522
526
  }
523
527
 
524
528
  async sessionBootstrap(args = {}, context = {}) {
@@ -539,24 +543,6 @@ export class LocalRuntime {
539
543
  }, args, context);
540
544
  }
541
545
 
542
- readLocalResourceBinary(name) {
543
- const registry = this.managedJobManager.currentResources();
544
- const resource = Object.hasOwn(registry, name) ? registry[name] : null;
545
- if (!resource) throw new Error(`unknown local resource: ${name}`);
546
- const inspected = inspectResourceFile(resource.path, { allowInsecurePermissions: resource.allowInsecurePermissions === true });
547
- if (inspected.size > 1024 * 1024) throw new Error("local resource exceeds 1 MiB browser injection limit");
548
- return { buffer: readBoundedRegularFileSync(resource.path, 1024 * 1024), path: resource.path, size: inspected.size };
549
- }
550
-
551
- readLocalResourceText(name) {
552
- const { buffer } = this.readLocalResourceBinary(name);
553
- try {
554
- return new TextDecoder("utf-8", { fatal: true }).decode(buffer);
555
- } catch {
556
- throw new Error(`local resource is not valid UTF-8 text: ${name}`);
557
- }
558
- }
559
-
560
546
  runDirectProcess(args, context = {}) {
561
547
  return this.processExecutionService.runDirect(args, context);
562
548
  }
@@ -577,6 +563,10 @@ export class LocalRuntime {
577
563
  return this.processExecutionService.run(cmd, args, timeoutMs, allowFailure, maxOutputBytes, context, cwd, stdin);
578
564
  }
579
565
 
566
+ effectivePolicy(context = {}) {
567
+ return policyForContext(context, this.policy);
568
+ }
569
+
580
570
  resolvePath(inputPath = ".") {
581
571
  const raw = String(inputPath || ".");
582
572
  if (raw.includes("\0")) throw new Error("path contains a NUL byte");
@@ -599,14 +589,14 @@ export class LocalRuntime {
599
589
  return this.workspaceCanonicalPromise;
600
590
  }
601
591
 
602
- async resolveExistingPath(inputPath = ".") {
592
+ async resolveExistingPath(inputPath = ".", context = {}) {
603
593
  const candidate = this.resolvePath(inputPath);
604
594
  const [workspace, canonical] = await Promise.all([this.canonicalWorkspace(), realpath(candidate)]);
605
- if (!this.policy.unrestrictedPaths) assertContainedPath(workspace, canonical);
595
+ if (!this.effectivePolicy(context).unrestrictedPaths) assertContainedPath(workspace, canonical);
606
596
  return canonical;
607
597
  }
608
598
 
609
- async resolveWritePath(inputPath = ".") {
599
+ async resolveWritePath(inputPath = ".", context = {}) {
610
600
  const candidate = this.resolvePath(inputPath);
611
601
  const candidateInfo = await lstat(candidate).catch(() => null);
612
602
  if (candidateInfo?.isSymbolicLink()) throw new Error("refusing to overwrite a symbolic link");
@@ -617,23 +607,23 @@ export class LocalRuntime {
617
607
  ancestor = parent;
618
608
  }
619
609
  const [workspace, canonicalAncestor] = await Promise.all([this.canonicalWorkspace(), realpath(ancestor)]);
620
- if (!this.policy.unrestrictedPaths) assertContainedPath(workspace, canonicalAncestor);
610
+ if (!this.effectivePolicy(context).unrestrictedPaths) assertContainedPath(workspace, canonicalAncestor);
621
611
  const suffix = relative(ancestor, candidate);
622
612
  return suffix ? resolve(canonicalAncestor, suffix) : canonicalAncestor;
623
613
  }
624
614
 
625
- displayPath(fullPath) {
615
+ displayPath(fullPath, context = {}) {
626
616
  const absolute = resolve(fullPath);
627
- if (this.policy.exposeAbsolutePaths) return absolute;
617
+ if (this.effectivePolicy(context).exposeAbsolutePaths) return absolute;
628
618
  const shown = relative(this.workspace, absolute);
629
619
  const insideWorkspace = shown === "" || (!shown.startsWith(`..${sep}`) && shown !== ".." && !isAbsolute(shown));
630
620
  if (insideWorkspace) return shown ? shown.split(sep).join("/") : ".";
631
621
  return `<external-path:${sha256(absolute).slice(0, 12)}>`;
632
622
  }
633
623
 
634
- safeErrorMessage(error, toolArgs = {}) {
624
+ safeErrorMessage(error, toolArgs = {}, context = {}) {
635
625
  const message = boundedErrorMessage(error);
636
- if (this.policy.exposeAbsolutePaths) return message;
626
+ if (this.effectivePolicy(context).exposeAbsolutePaths) return message;
637
627
  return redactRuntimeErrorMessage(message, {
638
628
  error,
639
629
  toolArgs,
@@ -641,7 +631,7 @@ export class LocalRuntime {
641
631
  workspaceInput: this.workspaceInput,
642
632
  runtimeDir: this.runtimeDir,
643
633
  home: process.env.HOME || process.env.USERPROFILE || "",
644
- displayPath: (value) => this.displayPath(value),
634
+ displayPath: (value) => this.displayPath(value, context),
645
635
  });
646
636
  }
647
637
 
@@ -16,6 +16,9 @@ export function openRegularFileSync(file, flags, options = {}) {
16
16
  try {
17
17
  const info = fstatSync(fd);
18
18
  if (!info.isFile()) throw new Error(`${label} is not a regular file`);
19
+ if (options.rejectMultipleLinks === true && Number(info.nlink) > 1) {
20
+ throw new Error(`${label} must not have multiple hard links`);
21
+ }
19
22
  if (options.verifyPathIdentity === true) {
20
23
  const pathInfo = lstatSync(file);
21
24
  if (pathInfo.isSymbolicLink() || !pathInfo.isFile()) throw new Error(`${label} must be a regular file and not a symbolic link`);
@@ -39,7 +42,7 @@ function withRegularFileSync(file, flags, options, callback) {
39
42
  }
40
43
 
41
44
  export function chmodRegularFileSync(file, mode, label = "path") {
42
- return withRegularFileSync(file, fsConstants.O_RDONLY, { label, chmod: mode }, () => undefined);
45
+ return withRegularFileSync(file, fsConstants.O_RDONLY, { label, chmod: mode, rejectMultipleLinks: true }, () => undefined);
43
46
  }
44
47
 
45
48
  export function readBoundedRegularFileSync(file, maxBytes, label = "path", options = {}) {
@@ -52,6 +55,7 @@ export function readBoundedRegularFileWithInfoSync(file, maxBytes, label = "path
52
55
  return withRegularFileSync(file, fsConstants.O_RDONLY, {
53
56
  label,
54
57
  verifyPathIdentity: options.verifyPathIdentity === true,
58
+ rejectMultipleLinks: options.rejectMultipleLinks === true,
55
59
  }, (fd, info) => {
56
60
  if (info.size > limit) throw new Error(`file exceeds ${limit} bytes`);
57
61
  options.afterOpen?.({ fd, info });
@@ -0,0 +1,204 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { replaceFileAtomicallySync } from "./exclusive-file.mjs";
5
+ import { ensureOwnerOnlyDirectorySync, readBoundedRegularFileSync } from "./secure-file.mjs";
6
+ import { withOwnerStateLock } from "./owner-state-lock.mjs";
7
+
8
+ const SCHEMA_VERSION = 1;
9
+ const MAX_EVENTS = 4096;
10
+ const MAX_BYTES = 4 * 1024 * 1024;
11
+ const HASH_PATTERN = /^[a-f0-9]{64}$/;
12
+
13
+ export class SecurityAuditLog {
14
+ constructor({ root = "", now = Date.now } = {}) {
15
+ this.root = root ? path.resolve(root) : "";
16
+ this.file = this.root ? path.join(this.root, "security-audit.json") : "";
17
+ this.now = typeof now === "function" ? now : Date.now;
18
+ this.queue = Promise.resolve();
19
+ this.lastError = "";
20
+ if (this.file && existsSync(this.file)) {
21
+ try { this.readVerifiedState(); } catch (error) { this.lastError = errorClass(error); }
22
+ }
23
+ }
24
+
25
+ record(input = {}) {
26
+ if (!this.file) return Promise.resolve(false);
27
+ const operation = async () => {
28
+ try {
29
+ return await withOwnerStateLock(this.root, async () => {
30
+ const state = this.readVerifiedState();
31
+ const event = buildEvent(input, state, this.now());
32
+ state.events.push(event);
33
+ while (state.events.length > MAX_EVENTS) {
34
+ const removed = state.events.shift();
35
+ state.anchor = removed.hash;
36
+ }
37
+ writeState(this.file, state);
38
+ this.lastError = "";
39
+ return true;
40
+ }, {
41
+ purpose: "security-audit", fileName: "security-audit.lock", label: "security audit",
42
+ });
43
+ } catch (error) {
44
+ this.lastError = errorClass(error);
45
+ return false;
46
+ }
47
+ };
48
+ const result = this.queue.then(operation, operation);
49
+ this.queue = result.then(() => undefined, () => undefined);
50
+ return result;
51
+ }
52
+
53
+ snapshot() {
54
+ if (!this.file) return { enabled: false, healthy: true, retained: 0, maximum: MAX_EVENTS };
55
+ try {
56
+ const state = this.readVerifiedState();
57
+ return {
58
+ enabled: true,
59
+ healthy: !this.lastError,
60
+ retained: state.events.length,
61
+ maximum: MAX_EVENTS,
62
+ last_event_at: state.events.at(-1)?.timestamp || null,
63
+ last_error_class: this.lastError || null,
64
+ content_logged: false,
65
+ chain_verified: true,
66
+ };
67
+ } catch (error) {
68
+ return {
69
+ enabled: true,
70
+ healthy: false,
71
+ retained: 0,
72
+ maximum: MAX_EVENTS,
73
+ last_event_at: null,
74
+ last_error_class: errorClass(error),
75
+ content_logged: false,
76
+ chain_verified: false,
77
+ };
78
+ }
79
+ }
80
+
81
+ readVerifiedState() {
82
+ if (!existsSync(this.file)) return emptyState();
83
+ const raw = readBoundedRegularFileSync(this.file, MAX_BYTES, "security audit state");
84
+ let state;
85
+ try { state = JSON.parse(raw.toString("utf8")); } catch (error) {
86
+ throw new Error("security audit state is not valid JSON", { cause: error });
87
+ }
88
+ validateState(state);
89
+ let previous = state.anchor;
90
+ for (const event of state.events) {
91
+ if (event.previous_hash !== previous || event.hash !== eventHash(event)) {
92
+ throw new Error("security audit hash chain verification failed");
93
+ }
94
+ previous = event.hash;
95
+ }
96
+ return state;
97
+ }
98
+ }
99
+
100
+ function buildEvent(input, state, nowMs) {
101
+ const timestamp = new Date(Number(nowMs)).toISOString();
102
+ if (!Number.isFinite(Date.parse(timestamp))) throw new Error("security audit timestamp is invalid");
103
+ const principal = input.principal && typeof input.principal === "object" ? input.principal : {};
104
+ const event = {
105
+ sequence: state.next_sequence,
106
+ timestamp,
107
+ outcome: boundedToken(input.outcome, "unknown"),
108
+ tool: boundedToken(input.tool, "unknown"),
109
+ risk_category: boundedText(input.riskCategory, "ordinary operation", 160),
110
+ target_hash: HASH_PATTERN.test(String(input.targetHash || "")) ? String(input.targetHash) : null,
111
+ account_ref: principal.accountId ? privateReference(state.identity_salt, principal.accountId) : null,
112
+ client_ref: principal.clientId ? privateReference(state.identity_salt, principal.clientId) : null,
113
+ family_ref: principal.familyId ? privateReference(state.identity_salt, principal.familyId) : null,
114
+ account_version: Number.isSafeInteger(principal.accountVersion) ? principal.accountVersion : null,
115
+ role: boundedToken(principal.role, principal.kind === "local" ? "local" : "unknown"),
116
+ duration_ms: boundedNumber(input.durationMs),
117
+ input_bytes: boundedNumber(input.inputBytes),
118
+ output_bytes: boundedNumber(input.outputBytes),
119
+ error_code: input.errorCode ? boundedToken(input.errorCode, "unknown") : null,
120
+ previous_hash: state.events.at(-1)?.hash || state.anchor,
121
+ };
122
+ const completed = { ...event, hash: eventHash(event) };
123
+ state.next_sequence += 1;
124
+ return completed;
125
+ }
126
+
127
+ function emptyState() {
128
+ return {
129
+ schemaVersion: SCHEMA_VERSION,
130
+ identity_salt: randomBytes(32).toString("hex"),
131
+ anchor: randomBytes(32).toString("hex"),
132
+ next_sequence: 1,
133
+ events: [],
134
+ };
135
+ }
136
+
137
+ function validateState(state) {
138
+ if (!plainRecord(state) || state.schemaVersion !== SCHEMA_VERSION) throw new Error("security audit state schema is invalid");
139
+ if (!HASH_PATTERN.test(state.identity_salt) || !HASH_PATTERN.test(state.anchor)) throw new Error("security audit state identity is invalid");
140
+ if (!Number.isSafeInteger(state.next_sequence) || state.next_sequence < 1) throw new Error("security audit sequence is invalid");
141
+ if (!Array.isArray(state.events) || state.events.length > MAX_EVENTS || !state.events.every(validEvent)) {
142
+ throw new Error("security audit events are invalid");
143
+ }
144
+ if (state.events.length && state.next_sequence <= state.events.at(-1).sequence) throw new Error("security audit sequence did not advance");
145
+ }
146
+
147
+ function validEvent(event) {
148
+ return plainRecord(event)
149
+ && Number.isSafeInteger(event.sequence)
150
+ && event.sequence > 0
151
+ && Number.isFinite(Date.parse(String(event.timestamp || "")))
152
+ && typeof event.outcome === "string"
153
+ && typeof event.tool === "string"
154
+ && typeof event.risk_category === "string"
155
+ && (event.target_hash === null || HASH_PATTERN.test(event.target_hash))
156
+ && ["account_ref", "client_ref", "family_ref"].every((key) => event[key] === null || HASH_PATTERN.test(event[key]))
157
+ && (event.account_version === null || Number.isSafeInteger(event.account_version))
158
+ && typeof event.role === "string"
159
+ && Number.isFinite(event.duration_ms)
160
+ && Number.isFinite(event.input_bytes)
161
+ && Number.isFinite(event.output_bytes)
162
+ && (event.error_code === null || typeof event.error_code === "string")
163
+ && HASH_PATTERN.test(event.previous_hash)
164
+ && HASH_PATTERN.test(event.hash);
165
+ }
166
+
167
+ function eventHash(event) {
168
+ const value = { ...event };
169
+ delete value.hash;
170
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
171
+ }
172
+
173
+ function privateReference(salt, value) {
174
+ return createHash("sha256").update(salt).update("\0").update(String(value)).digest("hex");
175
+ }
176
+
177
+ function writeState(file, state) {
178
+ ensureOwnerOnlyDirectorySync(path.dirname(file));
179
+ const content = `${JSON.stringify(state)}\n`;
180
+ if (Buffer.byteLength(content) > MAX_BYTES) throw new Error("security audit state exceeds its size limit");
181
+ replaceFileAtomicallySync(file, content, { mode: 0o600 });
182
+ }
183
+
184
+ function boundedToken(value, fallback) {
185
+ const text = String(value || fallback).replace(/[^A-Za-z0-9._:-]/g, "_").slice(0, 128);
186
+ return text || fallback;
187
+ }
188
+
189
+ function boundedText(value, fallback, maximum) {
190
+ return String(value || fallback).replace(/[\r\n\t\u0000-\u001f\u007f]/g, " ").trim().slice(0, maximum) || fallback;
191
+ }
192
+
193
+ function boundedNumber(value) {
194
+ const number = Number(value);
195
+ return Number.isFinite(number) && number >= 0 ? Math.min(Math.floor(number), Number.MAX_SAFE_INTEGER) : 0;
196
+ }
197
+
198
+ function errorClass(error) {
199
+ return String(error?.code || error?.name || "audit_error").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 80);
200
+ }
201
+
202
+ function plainRecord(value) {
203
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
204
+ }
@@ -1,14 +1,30 @@
1
1
  const DEFAULT_ATTEMPTS = 20;
2
2
  const DEFAULT_DELAY_MS = 100;
3
3
 
4
+ export async function waitForActiveStatus(
5
+ readStatus,
6
+ { attempts = DEFAULT_ATTEMPTS, delayMs = DEFAULT_DELAY_MS, sleep = delay } = {},
7
+ ) {
8
+ return waitForStatus(readStatus, (status) => status?.active === true, { attempts, delayMs, sleep });
9
+ }
10
+
4
11
  export async function waitForInactiveStatus(
5
12
  readStatus,
6
13
  { attempts = DEFAULT_ATTEMPTS, delayMs = DEFAULT_DELAY_MS, sleep = delay } = {},
14
+ ) {
15
+ return waitForStatus(readStatus, (status) => status?.active !== true, { attempts, delayMs, sleep });
16
+ }
17
+
18
+ export async function waitForStatus(
19
+ readStatus,
20
+ predicate,
21
+ { attempts = DEFAULT_ATTEMPTS, delayMs = DEFAULT_DELAY_MS, sleep = delay } = {},
7
22
  ) {
8
23
  if (typeof readStatus !== "function") throw new TypeError("readStatus must be a function");
24
+ if (typeof predicate !== "function") throw new TypeError("predicate must be a function");
9
25
  const maximum = Number.isInteger(attempts) && attempts > 0 ? attempts : DEFAULT_ATTEMPTS;
10
26
  let status = await readStatus();
11
- for (let attempt = 1; status?.active === true && attempt < maximum; attempt += 1) {
27
+ for (let attempt = 1; !predicate(status) && attempt < maximum; attempt += 1) {
12
28
  await sleep(delayMs);
13
29
  status = await readStatus();
14
30
  }
@@ -0,0 +1,17 @@
1
+ export async function stopOwnedPlatformService({
2
+ state,
3
+ inspectWorkspaceDaemon,
4
+ ownsPlatformAutostart,
5
+ stopPlatformService,
6
+ } = {}) {
7
+ if (!state) throw new TypeError("service ownership check requires state");
8
+ if (typeof inspectWorkspaceDaemon !== "function") throw new TypeError("service ownership check requires inspectWorkspaceDaemon");
9
+ if (typeof ownsPlatformAutostart !== "function") throw new TypeError("service ownership check requires ownsPlatformAutostart");
10
+ if (typeof stopPlatformService !== "function") throw new TypeError("service ownership check requires stopPlatformService");
11
+ const daemon = inspectWorkspaceDaemon(state);
12
+ if (!ownsPlatformAutostart(daemon)) {
13
+ return { owned: false, stopped: false, daemon, provider: null };
14
+ }
15
+ const provider = await stopPlatformService();
16
+ return { owned: true, stopped: provider?.ok !== false, daemon, provider };
17
+ }