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
@@ -4,6 +4,7 @@ import { join, resolve } from "node:path";
4
4
  import { assertStateMaintenanceAvailable, ensureOwnerOnlyDir } from "./state.mjs";
5
5
  import { createToolAuthorizer } from "./policy.mjs";
6
6
  import { BridgeError } from "./errors.mjs";
7
+ import { assertOwnedByContext, principalBinding, visibleToContext } from "./authority-context.mjs";
7
8
  import { inspectResourceFile, normalizeResourceRegistry, validatePlan } from "./managed-job-plan.mjs";
8
9
  export { inspectResourceFile, publicResourceRegistry, validateResourceName } from "./managed-job-plan.mjs";
9
10
  import { clampInteger } from "./numbers.mjs";
@@ -13,18 +14,22 @@ import {
13
14
  atomicWriteJson, readBoundedFile, readJson, readRequiredJson, resourceErrorClass, safeReadDir,
14
15
  } from "./managed-job-storage.mjs";
15
16
  import { launchRunner, runnerProcessIsCurrent } from "./managed-job-runner.mjs";
17
+ import {
18
+ isTerminalManagedJobResult, scrubManagedJobArtifacts, terminalStatusFromResult,
19
+ } from "./managed-job-terminal.mjs";
16
20
  export { launchRunner } from "./managed-job-runner.mjs";
17
21
 
18
22
  const JOB_ID = /^job_[A-Za-z0-9_-]{24,}$/;
19
23
  const MAX_JOBS = 50;
20
24
  const JOB_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
25
+ const STAGED_PLAN_RETENTION_MS = 24 * 60 * 60 * 1000;
21
26
  const MAX_PLAN_BYTES = 1024 * 1024;
22
27
  const MAX_RECOVERY_ATTEMPTS = 3;
23
28
  const ACTIVE_JOB_STATES = new Set(["queued", "running", "cleaning", "interrupted"]);
24
29
  const PLAN_RETAINING_STATES = new Set(["staged", ...ACTIVE_JOB_STATES]);
25
30
 
26
31
  export class ManagedJobManager {
27
- constructor({ jobRoot, workspace, policy, authorizeTool = null, resources = {}, resourceStatePath = "", stateRoot = "", logger = console, recover = true }) {
32
+ constructor({ jobRoot, workspace, policy, authorizeTool = null, policyForContext = null, resources = {}, resourceStatePath = "", stateRoot = "", logger = console, recover = true }) {
28
33
  const jobRootInput = resolve(jobRoot);
29
34
  ensureOwnerOnlyDir(jobRootInput);
30
35
  this.jobRoot = realpathSync.native ? realpathSync.native(jobRootInput) : realpathSync(jobRootInput);
@@ -32,6 +37,7 @@ export class ManagedJobManager {
32
37
  this.workspace = realpathSync.native ? realpathSync.native(workspaceInput) : realpathSync(workspaceInput);
33
38
  this.policy = policy;
34
39
  this.authorizeTool = createToolAuthorizer(this.policy, authorizeTool);
40
+ this.policyForContext = typeof policyForContext === "function" ? policyForContext : () => this.policy;
35
41
  this.resources = normalizeResourceRegistry(resources);
36
42
  this.resourceStatePath = resourceStatePath ? resolve(resourceStatePath) : "";
37
43
  this.stateRoot = stateRoot ? resolve(stateRoot) : "";
@@ -62,7 +68,7 @@ export class ManagedJobManager {
62
68
  };
63
69
  }
64
70
 
65
- listResources() {
71
+ listResources(_context = {}) {
66
72
  this.authorizeTool("list_local_resources");
67
73
  this.assertMaintenanceAvailable();
68
74
  const resources = [];
@@ -94,16 +100,16 @@ export class ManagedJobManager {
94
100
  }
95
101
 
96
102
 
97
- stage(args = {}) {
103
+ stage(args = {}, context = {}) {
98
104
  this.authorizeTool("stage_job");
99
105
  this.assertMaintenanceAvailable();
100
- return this.createJob(args, { launch: false });
106
+ return this.createJob(args, { launch: false }, context);
101
107
  }
102
108
 
103
- start(args = {}) {
109
+ start(args = {}, context = {}) {
104
110
  this.authorizeTool("start_job");
105
111
  this.assertMaintenanceAvailable();
106
- return this.createJob(args, { launch: true });
112
+ return this.createJob(args, { launch: true }, context);
107
113
  }
108
114
 
109
115
  approve(args = {}, { localOperator = false } = {}) {
@@ -125,7 +131,7 @@ export class ManagedJobManager {
125
131
  status.cleanup_guarantee = "best-effort-finally-and-recovery";
126
132
  atomicWriteJson(statusFile, status, 256 * 1024);
127
133
  try {
128
- launchRunner(dir, false, "", { logger: this.logger });
134
+ launchRunner(dir, false, "", { logger: this.logger, fullEnv: plan.full_env === true });
129
135
  } catch (error) {
130
136
  failRunnerLaunch(dir, status, error);
131
137
  throw error;
@@ -150,15 +156,16 @@ export class ManagedJobManager {
150
156
  }
151
157
  }
152
158
 
153
- createJob(args, { launch }) {
159
+ createJob(args, { launch }, context = {}) {
154
160
  this.prune();
155
161
  const retained = safeReadDir(this.jobRoot).filter((entry) => entry.isDirectory() && JOB_ID.test(entry.name)).length;
156
162
  if (retained >= MAX_JOBS) throw new Error(`managed job limit reached (${MAX_JOBS})`);
163
+ const effectivePolicy = this.policyForContext(context);
157
164
  const plan = validatePlan(args, {
158
165
  workspace: this.workspace,
159
166
  resources: this.currentResources(),
160
- fullEnv: this.policy.minimalEnv === false,
161
- unrestrictedPaths: this.policy.unrestrictedPaths === true,
167
+ fullEnv: effectivePolicy.minimalEnv === false,
168
+ unrestrictedPaths: effectivePolicy.unrestrictedPaths === true,
162
169
  });
163
170
  const planSha256 = createHash("sha256").update(JSON.stringify(plan)).digest("hex");
164
171
  const id = `job_${randomBytes(24).toString("base64url")}`;
@@ -178,11 +185,12 @@ export class ManagedJobManager {
178
185
  approval: launch ? "mcp" : "pending-local-operator",
179
186
  plan_sha256: planSha256,
180
187
  cleanup_guarantee: launch ? "best-effort-finally-and-recovery" : "not-started",
188
+ ...principalBinding(context),
181
189
  };
182
190
  atomicWriteJson(join(dir, "status.json"), status, 256 * 1024);
183
191
  if (launch) {
184
192
  try {
185
- launchRunner(dir, false, "", { logger: this.logger });
193
+ launchRunner(dir, false, "", { logger: this.logger, fullEnv: plan.full_env === true });
186
194
  } catch (error) {
187
195
  failRunnerLaunch(dir, status, error);
188
196
  throw error;
@@ -209,14 +217,13 @@ export class ManagedJobManager {
209
217
  status: "staged",
210
218
  execution_started: false,
211
219
  plan_sha256: planSha256,
212
- local_inspection_command: `machine-mcp job inspect ${id}`,
213
- local_approval_command: `machine-mcp job approve ${id}`,
214
- plan_expires_after_days: 7,
220
+ continuation: "start the staged job through the same authenticated client when ready",
221
+ plan_expires_after_hours: 24,
215
222
  };
216
223
  }
217
224
 
218
225
 
219
- list(args = {}) {
226
+ list(args = {}, context = {}) {
220
227
  this.authorizeTool("list_jobs");
221
228
  this.assertMaintenanceAvailable();
222
229
  this.prune();
@@ -228,23 +235,24 @@ export class ManagedJobManager {
228
235
  try {
229
236
  this.reconcileStatus(dir);
230
237
  const status = readJson(join(dir, "status.json"), 256 * 1024, "job status");
231
- if (!status) continue;
238
+ if (!status || !visibleToContext(status, context)) continue;
232
239
  jobs.push(publicStatus(status));
233
240
  } catch (error) {
234
241
  this.logger.warn?.("managed job status is unreadable; retaining it for inspection", { error_class: resourceErrorClass(error) });
235
- jobs.push({ job_id: entry.name, name: "unavailable", status: "unreadable", error_class: resourceErrorClass(error) });
242
+ if (context?.authority?.owner !== false) jobs.push({ job_id: entry.name, name: "unavailable", status: "unreadable", error_class: resourceErrorClass(error) });
236
243
  }
237
244
  }
238
245
  jobs.sort((a, b) => String(b.created_at).localeCompare(String(a.created_at)));
239
246
  return { jobs: jobs.slice(0, limit), retained: jobs.length, maximum: MAX_JOBS };
240
247
  }
241
248
 
242
- read(args = {}) {
249
+ read(args = {}, context = {}) {
243
250
  this.authorizeTool("read_job");
244
251
  this.assertMaintenanceAvailable();
245
252
  const dir = this.jobDir(args.job_id);
246
253
  this.reconcileStatus(dir);
247
254
  const status = readRequiredJson(join(dir, "status.json"), 256 * 1024, "job status");
255
+ assertOwnedByContext(status, context, "managed job");
248
256
  const result = readJson(join(dir, "result.json"), 4 * 1024 * 1024);
249
257
  return {
250
258
  ...publicStatus(status),
@@ -266,7 +274,7 @@ export class ManagedJobManager {
266
274
  };
267
275
  }
268
276
 
269
- cancel(args = {}) {
277
+ cancel(args = {}, context = {}) {
270
278
  this.authorizeTool("cancel_job");
271
279
  this.assertMaintenanceAvailable();
272
280
  const dir = this.jobDir(args.job_id);
@@ -275,6 +283,7 @@ export class ManagedJobManager {
275
283
  try {
276
284
  this.reconcileStatus(dir);
277
285
  const status = readRequiredJson(join(dir, "status.json"), 256 * 1024, "job status");
286
+ assertOwnedByContext(status, context, "managed job");
278
287
  if (status.status === "staged") {
279
288
  const now = new Date().toISOString();
280
289
  status.status = "cancelled_before_start";
@@ -342,6 +351,15 @@ export class ManagedJobManager {
342
351
  return;
343
352
  }
344
353
  if (runnerProcessIsCurrent(initial, dir)) return;
354
+ const terminalResult = readJson(join(dir, "result.json"), 4 * 1024 * 1024, "job result");
355
+ if (isTerminalManagedJobResult(terminalResult, initial.job_id)) {
356
+ const recoveredStatus = terminalStatusFromResult(initial, terminalResult, {
357
+ resultPersisted: true, updatedAt: new Date().toISOString(),
358
+ });
359
+ atomicWriteJson(file, recoveredStatus, 256 * 1024);
360
+ scrubFinishedPlan(dir, recoveredStatus);
361
+ return;
362
+ }
345
363
  const updated = Date.parse(initial.updated_at || initial.created_at || "");
346
364
  if (Number.isFinite(updated) && Date.now() - updated < 10_000) return;
347
365
 
@@ -399,6 +417,7 @@ export class ManagedJobManager {
399
417
  continue;
400
418
  }
401
419
  }
420
+ if (status?.status === "staged" && stagedPlanExpired(status, mtime)) status = expireStagedJob(dir, status);
402
421
  if (status && !PLAN_RETAINING_STATES.has(status.status)) scrubFinishedPlan(dir, status);
403
422
  entries.push({ dir, status, mtime });
404
423
  }
@@ -515,6 +534,8 @@ function markRecoveryExhausted(dir, statusFile, status, recoveryAttempts) {
515
534
  }
516
535
 
517
536
  function relaunchInterruptedJob(dir, statusFile, status, recoveryAttempts, recoveryToken, logger) {
537
+ const plan = readRequiredJson(join(dir, "plan.json"), MAX_PLAN_BYTES, "job plan");
538
+ assertPlanIntegrity(plan, status);
518
539
  status.status = "interrupted";
519
540
  status.updated_at = new Date().toISOString();
520
541
  status.finished_at = status.updated_at;
@@ -523,7 +544,40 @@ function relaunchInterruptedJob(dir, statusFile, status, recoveryAttempts, recov
523
544
  atomicWriteJson(statusFile, status, 256 * 1024);
524
545
  rmSync(join(dir, "runtime"), { recursive: true, force: true });
525
546
  rmSync(join(dir, "runner.pid"), { force: true });
526
- return launchRunner(dir, true, recoveryToken, { logger });
547
+ return launchRunner(dir, true, recoveryToken, { logger, fullEnv: plan.full_env === true });
548
+ }
549
+
550
+ function stagedPlanExpired(status, fallbackMtime) {
551
+ const createdAt = Date.parse(String(status?.created_at || ""));
552
+ const baseline = Number.isFinite(createdAt) ? createdAt : fallbackMtime;
553
+ return Number.isFinite(baseline) && Date.now() - baseline > STAGED_PLAN_RETENTION_MS;
554
+ }
555
+
556
+ function expireStagedJob(dir, status) {
557
+ const now = new Date().toISOString();
558
+ const expired = {
559
+ ...status,
560
+ status: "expired_before_start",
561
+ updated_at: now,
562
+ finished_at: now,
563
+ current_phase: null,
564
+ current_step: null,
565
+ error_class: "expired",
566
+ cleanup_guarantee: "not-started",
567
+ };
568
+ atomicWriteJson(join(dir, "status.json"), expired, 256 * 1024);
569
+ atomicWriteJson(join(dir, "result.json"), {
570
+ job_id: expired.job_id,
571
+ name: expired.name,
572
+ status: expired.status,
573
+ steps: [],
574
+ finally_steps: [],
575
+ error_class: "expired",
576
+ cleanup_error_class: null,
577
+ finished_at: now,
578
+ }, 4 * 1024 * 1024);
579
+ scrubFinishedPlan(dir, expired);
580
+ return expired;
527
581
  }
528
582
 
529
583
  function planSha256(plan) {
@@ -540,16 +594,18 @@ function assertPlanIntegrity(plan, status) {
540
594
  }
541
595
 
542
596
  function scrubFinishedPlan(dir, status) {
543
- if (PLAN_RETAINING_STATES.has(status.status)) return;
544
- const safeRm = (path) => {
545
- try {
546
- rmSync(path, { force: true });
547
- } catch (error) {
548
- if (!["ENOENT", "EPERM", "EACCES"].includes(error.code)) throw error;
549
- }
550
- };
551
- safeRm(join(dir, "plan.json"));
552
- safeRm(join(dir, "runner.pid"));
553
- safeRm(join(dir, "recovery.lock"));
554
- safeRm(join(dir, "transition.lock"));
597
+ if (PLAN_RETAINING_STATES.has(status.status)) return { scrubbed: false, errorClass: null, failureCount: 0 };
598
+ const cleanup = scrubManagedJobArtifacts([
599
+ join(dir, "runtime"), join(dir, "plan.json"), join(dir, "runner.pid"), join(dir, "cancel"),
600
+ join(dir, "recovery.lock"), join(dir, "transition.lock"),
601
+ ], (file) => rmSync(file, { recursive: true, force: true }), resourceErrorClass);
602
+ const pending = !cleanup.scrubbed;
603
+ if (status.artifact_cleanup_pending !== pending
604
+ || (status.artifact_cleanup_error_class || null) !== cleanup.errorClass) {
605
+ status.artifact_cleanup_pending = pending;
606
+ status.artifact_cleanup_error_class = cleanup.errorClass;
607
+ status.updated_at = new Date().toISOString();
608
+ atomicWriteJson(join(dir, "status.json"), status, 256 * 1024);
609
+ }
610
+ return cleanup;
555
611
  }