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
@@ -16,6 +16,7 @@ const tests = [
16
16
  "tests/runtime-handler-matrix-test.mjs",
17
17
  "tests/cli-entrypoint-test.mjs",
18
18
  "tests/cli-service-test.mjs",
19
+ "tests/service-restart-handoff-test.mjs",
19
20
  "tests/local-self-test.mjs",
20
21
  "tests/runtime-self-test.mjs",
21
22
  "tests/numbers-test.mjs",
@@ -31,6 +32,12 @@ const tests = [
31
32
  "tests/managed-jobs-test.mjs",
32
33
  "tests/account-admin-test.mjs",
33
34
  "tests/monotonic-deadline-test.mjs",
35
+ "tests/device-auth-test.mjs",
36
+ "tests/operation-authorization-test.mjs",
37
+ "tests/security-audit-log-test.mjs",
38
+ "tests/delegated-process-sandbox-test.mjs",
39
+ "tests/dpop-test.mjs",
40
+ "tests/worker-security-boundaries-test.mjs",
34
41
  ];
35
42
 
36
43
  try {
@@ -50,17 +57,31 @@ try {
50
57
 
51
58
  const coverage = collectCoverage(coverageDir);
52
59
  const thresholds = {
60
+ "src/local/authority-context.mjs": [85, 55],
61
+ "src/local/device-identity.mjs": [90, null],
62
+ "src/local/operation-authorization.mjs": [85, 45],
63
+ "src/local/operation-risk.mjs": [85, 60],
64
+ "src/local/operation-state-lock.mjs": [100, 7],
65
+ "src/local/security-audit-log.mjs": [85, 55],
66
+ "src/local/delegated-process-sandbox.mjs": [80, 45],
67
+ "src/shared/device-session-auth.mjs": [100, null],
53
68
  "src/local/policy.mjs": [90, 65],
54
69
  "src/local/errors.mjs": [70, 50],
55
70
  "src/local/call-registry.mjs": [85, 55],
56
71
  "src/local/tool-executor.mjs": [90, 40],
72
+ "src/local/tool-result-boundary.mjs": [100, 75],
57
73
  "src/local/observability.mjs": [95, 40],
58
74
  "src/local/process-tracker.mjs": [65, 35],
59
75
  "src/local/log.mjs": [60, 40],
60
76
  "src/local/runtime.mjs": [75, 55],
61
77
  "src/local/runtime-paths.mjs": [90, 50],
62
78
  "src/local/cli.mjs": [48, 21.9],
63
- "src/local/cli-service.mjs": [90, 65],
79
+ "src/local/cli-service.mjs": [100, 75],
80
+ "src/local/service-convergence.mjs": [100, 75],
81
+ "src/local/service-status.mjs": [100, 75],
82
+ "src/local/service-ownership.mjs": [100, 75],
83
+ "src/local/service-restart-scheduler.mjs": [100, 70],
84
+ "src/local/service-restart-handoff.mjs": [100, 60],
64
85
  "src/local/cli-options.mjs": [65, 35],
65
86
  "src/local/cli-policy.mjs": [70, 35],
66
87
  "src/local/numbers.mjs": [100, 100],
@@ -91,7 +112,14 @@ try {
91
112
  "src/local/browser-request-registry.mjs": [95, 35],
92
113
  "src/local/browser-broker-routes.mjs": [85, 55],
93
114
  "src/local/browser-broker-server.mjs": [80, 50],
94
- "src/worker/oauth-controller.ts": [85, 65],
115
+ "src/worker/account-admin.ts": [70, 35],
116
+ "src/worker/daemon-auth.ts": [85, 45],
117
+ "src/worker/dpop.ts": [90, 30],
118
+ "src/worker/nonce-store.ts": [85, 50],
119
+ "src/worker/oauth-client-admin.ts": [75, 45],
120
+ "src/worker/oauth-refresh-families.ts": [70, 35],
121
+ "src/worker/oauth-tokens.ts": [70, 35],
122
+ "src/worker/oauth-controller.ts": [84, 65],
95
123
  "src/worker/oauth-authorization-page.ts": [90, 60],
96
124
  "src/worker/pending-calls.ts": [90, 35],
97
125
  "src/worker/policy.ts": [100, 25],
@@ -1,10 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawnSync } from "node:child_process";
4
+ import { readFileSync } from "node:fs";
4
5
  import { dirname, resolve } from "node:path";
5
6
  import { fileURLToPath } from "node:url";
6
7
  import { verifyCurrentReleaseAcceptance } from "./release-acceptance.mjs";
7
8
  import { assertGitHubBacklogReady } from "./github-backlog.mjs";
9
+ import { parseReleaseVersion, requiresSoakForStable } from "./release-channel.mjs";
10
+ import { verifyCurrentStableSoak } from "./release-soak.mjs";
8
11
 
9
12
  const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
10
13
 
@@ -25,6 +28,14 @@ try {
25
28
  console.log(`Verified interactive local candidate acceptance for ${acceptance.metadata.filename}.`);
26
29
  }
27
30
 
31
+ const releaseVersion = parseReleaseVersion(JSON.parse(readFileSync(resolve(root, "package.json"), "utf8")).version);
32
+ if (!releaseVersion.prerelease && requiresSoakForStable(releaseVersion.raw)) {
33
+ const soak = verifyCurrentStableSoak(root);
34
+ const path = `release-soak/v${releaseVersion.raw}.json`;
35
+ run("git", ["ls-files", "--error-unmatch", path], { capture: true });
36
+ console.log(`Verified stable promotion from soaked ${soak.record.prerelease_version}.`);
37
+ }
38
+
28
39
  run("git", ["push", "--set-upstream", "origin", "HEAD"]);
29
40
  console.log(`Pushed accepted branch ${branch} to origin.`);
30
41
  } catch (error) {
@@ -13,6 +13,8 @@ import { runNetworkCommand } from "./network-retry.mjs";
13
13
  import { requireSuccessfulWorkflowRun } from "./release-ci.mjs";
14
14
  import { tagSyncError } from "./release-state.mjs";
15
15
  import { verifyCurrentReleaseAcceptance } from "./release-acceptance.mjs";
16
+ import { parseReleaseVersion, requiresSoakForStable } from "./release-channel.mjs";
17
+ import { verifyCurrentStableSoak } from "./release-soak.mjs";
16
18
  import { fileURLToPath } from "node:url";
17
19
 
18
20
  const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
@@ -179,7 +181,9 @@ function releaseInfo(tag) {
179
181
 
180
182
  function assertCoreSync({ requireReleaseAsset }) {
181
183
  const pkg = packageMetadata();
184
+ const parsedVersion = parseReleaseVersion(pkg.version);
182
185
  assertLocalAcceptance();
186
+ if (requiresSoakForStable(pkg.version)) assertStableSoak();
183
187
  const tag = `v${pkg.version}`;
184
188
  const head = output("git", ["rev-parse", "HEAD"]);
185
189
  const originMain = output("git", ["rev-parse", "origin/main"]);
@@ -197,8 +201,10 @@ function assertCoreSync({ requireReleaseAsset }) {
197
201
  if (remoteTagError) fail(remoteTagError);
198
202
 
199
203
  const release = releaseInfo(tag);
200
- if (!release || release.isDraft || release.isPrerelease) {
201
- fail(`published GitHub Release ${tag} is missing or not final`);
204
+ if (!release || release.isDraft || release.isPrerelease !== parsedVersion.prerelease) {
205
+ fail(parsedVersion.prerelease
206
+ ? `published GitHub prerelease ${tag} is missing or not marked as prerelease`
207
+ : `published GitHub Release ${tag} is missing or not final`);
202
208
  }
203
209
 
204
210
  if (requireReleaseAsset) {
@@ -250,7 +256,7 @@ function normalizePackRecord(value, packageName) {
250
256
  return Object.values(value).find((item) => item && typeof item === "object") ?? null;
251
257
  }
252
258
 
253
- function ensureRelease(tag, version, assetPath, latest) {
259
+ function ensureRelease(tag, version, assetPath, { latest, prerelease }) {
254
260
  const temp = dirname(assetPath);
255
261
  const notes = writeNotesFile(temp, version);
256
262
  const existing = releaseInfo(tag);
@@ -266,6 +272,7 @@ function ensureRelease(tag, version, assetPath, latest) {
266
272
  "--title",
267
273
  title,
268
274
  latest ? "--latest" : "--latest=false",
275
+ prerelease ? "--prerelease" : "--prerelease=false",
269
276
  ];
270
277
  if (notes) args.push("--notes-file", notes);
271
278
  else args.push("--generate-notes");
@@ -279,13 +286,14 @@ function ensureRelease(tag, version, assetPath, latest) {
279
286
  "--title",
280
287
  title,
281
288
  latest ? "--latest" : "--latest=false",
289
+ prerelease ? "--prerelease" : "--prerelease=false",
282
290
  ...(notes ? ["--notes-file", notes] : []),
283
291
  ]);
284
292
  runNetwork("gh", ["release", "upload", tag, assetPath, "--clobber"]);
285
293
  }
286
294
  }
287
295
 
288
- function publishCurrent() {
296
+ function publishCurrent({ prereleaseMode = false } = {}) {
289
297
  ensureClean();
290
298
  fetchRemote();
291
299
 
@@ -293,6 +301,13 @@ function publishCurrent() {
293
301
  if (branch !== "main") fail(`release must run from main, not ${branch || "detached HEAD"}`);
294
302
 
295
303
  const pkg = packageMetadata();
304
+ const parsedVersion = parseReleaseVersion(pkg.version);
305
+ if (prereleaseMode !== parsedVersion.prerelease) {
306
+ fail(parsedVersion.prerelease
307
+ ? "prerelease versions must use npm run prerelease:release"
308
+ : "stable versions must use npm run release");
309
+ }
310
+ if (!parsedVersion.prerelease && requiresSoakForStable(pkg.version)) assertStableSoak();
296
311
  const tag = `v${pkg.version}`;
297
312
  if (!changelogBody(pkg.version)) {
298
313
  fail(`CHANGELOG.md has no section for ${pkg.version}`);
@@ -329,7 +344,10 @@ function publishCurrent() {
329
344
  const temp = mkdtempSync(join(tmpdir(), "machine-bridge-mcp-release-"));
330
345
  try {
331
346
  const assetPath = packReleaseAsset(temp, pkg);
332
- ensureRelease(tag, pkg.version, assetPath, true);
347
+ ensureRelease(tag, pkg.version, assetPath, {
348
+ latest: !parsedVersion.prerelease,
349
+ prerelease: parsedVersion.prerelease,
350
+ });
333
351
  } finally {
334
352
  rmSync(temp, { recursive: true, force: true });
335
353
  }
@@ -338,6 +356,17 @@ function publishCurrent() {
338
356
  assertCoreSync({ requireReleaseAsset: true });
339
357
  }
340
358
 
359
+ function assertStableSoak() {
360
+ try {
361
+ const result = verifyCurrentStableSoak(root);
362
+ if (result.required) {
363
+ console.log(`Stable promotion matches soaked ${result.record.prerelease_version} (${result.promotionDigest}).`);
364
+ }
365
+ } catch (error) {
366
+ fail(String(error?.message || error));
367
+ }
368
+ }
369
+
341
370
  function assertLocalAcceptance() {
342
371
  try {
343
372
  const result = verifyCurrentReleaseAcceptance(root);
@@ -405,9 +434,11 @@ if (mode === "--check") {
405
434
  fetchRemote();
406
435
  assertCoreSync({ requireReleaseAsset: true });
407
436
  } else if (mode === "--publish") {
408
- publishCurrent();
437
+ publishCurrent({ prereleaseMode: false });
438
+ } else if (mode === "--publish-prerelease") {
439
+ publishCurrent({ prereleaseMode: true });
409
440
  } else if (mode === "--backfill") {
410
441
  backfillMissingReleases();
411
442
  } else {
412
- fail("usage: node scripts/github-release.mjs [--check|--publish|--backfill]");
443
+ fail("usage: node scripts/github-release.mjs [--check|--publish|--publish-prerelease|--backfill]");
413
444
  }
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from "node:child_process";
4
+ import { createHash } from "node:crypto";
5
+ import { existsSync, readFileSync } from "node:fs";
6
+ import { dirname, join, resolve } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { defaultStateRoot, expandHome } from "../src/local/state.mjs";
9
+ import { writePrereleaseActivation } from "./prerelease-activation.mjs";
10
+ import { computePromotionContentDigest } from "./promotion-digest.mjs";
11
+ import { readPublishedNpmPrerelease } from "./published-release.mjs";
12
+ import { verifyCurrentReleaseAcceptance } from "./release-acceptance.mjs";
13
+ import { assertSoakEligiblePrerelease } from "./release-channel.mjs";
14
+
15
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
16
+ const npmCli = process.env.npm_execpath;
17
+ let previousInstallation = null;
18
+ let installedPrerelease = "";
19
+ if (!npmCli) fail("published prerelease installation must run through npm");
20
+
21
+ try {
22
+ if (!process.argv.includes("--allow-worker-deploy")) {
23
+ throw new Error("published prerelease activation updates the global package, same-name Worker, and login daemon; rerun with --allow-worker-deploy");
24
+ }
25
+ const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
26
+ const requested = argumentValue("--version") || pkg.version;
27
+ const prerelease = assertSoakEligiblePrerelease(requested);
28
+ if (pkg.version !== prerelease.raw) {
29
+ throw new Error(`source checkout version ${pkg.version} does not match requested prerelease ${prerelease.raw}`);
30
+ }
31
+ const acceptance = verifyCurrentReleaseAcceptance(root);
32
+ if (!acceptance.required || acceptance.metadata.package_version !== prerelease.raw) {
33
+ throw new Error("published prerelease installation requires the locally accepted exact prerelease source");
34
+ }
35
+ const promotionDigest = computePromotionContentDigest(root);
36
+ if (acceptance.record.promotion_content_sha256 !== promotionDigest) {
37
+ throw new Error("current source promotion digest does not match local candidate acceptance");
38
+ }
39
+ const published = readPublishedNpmPrerelease(pkg.name, prerelease.raw, prerelease.npmTag);
40
+ if (published.integrity !== acceptance.metadata.integrity || published.shasum !== acceptance.metadata.shasum) {
41
+ throw new Error("npm prerelease bytes do not match the locally accepted candidate");
42
+ }
43
+
44
+ previousInstallation = currentGlobalInstallation(pkg.name);
45
+ runNpm([
46
+ "install", "--global", "--omit=optional",
47
+ "--allow-scripts=esbuild,workerd,sharp,fsevents",
48
+ `${pkg.name}@${prerelease.raw}`,
49
+ ]);
50
+ const installed = currentGlobalInstallation(pkg.name);
51
+ if (!installed || installed.version !== prerelease.raw) {
52
+ throw new Error(`global prerelease installation did not converge on ${prerelease.raw}`);
53
+ }
54
+ installedPrerelease = prerelease.raw;
55
+
56
+ const stateRoot = resolve(expandHome(argumentValue("--state-dir") || defaultStateRoot()));
57
+ const forwarded = forwardedActivationArgs();
58
+ const activation = runActivation(installed.entry, ["activate", ...forwarded, "--state-dir", stateRoot, "--json"]);
59
+ if (activation.version !== prerelease.raw || activation.daemon?.version !== prerelease.raw || activation.worker?.health?.version !== prerelease.raw) {
60
+ throw new Error("published prerelease activation did not converge on the exact registry version");
61
+ }
62
+ const recordPath = writePrereleaseActivation({
63
+ schema_version: 1,
64
+ package_name: pkg.name,
65
+ package_version: prerelease.raw,
66
+ source: "npm-prerelease",
67
+ shasum: published.shasum,
68
+ integrity: published.integrity,
69
+ promotion_content_sha256: promotionDigest,
70
+ activated_at: new Date().toISOString(),
71
+ published_at: published.publishedAt,
72
+ npm_dist_tag: prerelease.npmTag,
73
+ workspace_hash: workspaceHash(activation.workspace),
74
+ runtime_entry: installed.entry,
75
+ ...(previousInstallation ? { previous: previousInstallation } : {}),
76
+ }, stateRoot);
77
+
78
+ console.log(`Published prerelease activated: ${prerelease.raw}`);
79
+ console.log(`Worker and login daemon version: ${activation.version}`);
80
+ console.log(`Soak activation record: ${recordPath}`);
81
+ console.log("Use this prerelease normally. Any blocking issue requires a new beta/rc version and restarts the soak clock.");
82
+ } catch (error) {
83
+ const message = boundedDiagnostic(error?.message || error);
84
+ if (installedPrerelease) {
85
+ const previousVersion = previousInstallation?.version || "unknown";
86
+ fail(`${message}. The global package is now ${installedPrerelease}, but Worker/service activation may not have converged. Previous global version: ${previousVersion}. Preserve state and logs; fix forward with the exact prerelease or restore package, Worker, service definition, browser extension, and state as one verified unit.`);
87
+ }
88
+ fail(message);
89
+ }
90
+
91
+ function currentGlobalInstallation(packageName) {
92
+ try {
93
+ const globalRoot = runNpm(["root", "--global"]).stdout.trim();
94
+ const packageRoot = join(globalRoot, packageName);
95
+ const packagePath = join(packageRoot, "package.json");
96
+ const entry = join(packageRoot, "bin", "machine-mcp.mjs");
97
+ if (!existsSync(packagePath) || !existsSync(entry)) return null;
98
+ const value = JSON.parse(readFileSync(packagePath, "utf8"));
99
+ return { version: String(value.version || ""), entry };
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+
105
+ function runActivation(entry, args) {
106
+ const result = spawnSync(process.execPath, [entry, ...args], {
107
+ cwd: root,
108
+ env: process.env,
109
+ encoding: "utf8",
110
+ timeout: 300_000,
111
+ windowsHide: true,
112
+ });
113
+ if (result.error) throw result.error;
114
+ if (result.status !== 0) throw new Error(`prerelease runtime activation failed: ${boundedDiagnostic(result.stderr || result.stdout)}`);
115
+ try { return JSON.parse(result.stdout); } catch { throw new Error("prerelease runtime activation did not return valid JSON"); }
116
+ }
117
+
118
+ function runNpm(args) {
119
+ const result = spawnSync(process.execPath, [npmCli, ...args], {
120
+ cwd: root,
121
+ env: process.env,
122
+ encoding: "utf8",
123
+ timeout: 300_000,
124
+ windowsHide: true,
125
+ });
126
+ if (result.error) throw result.error;
127
+ if (result.status !== 0) throw new Error(`npm ${args[0]} failed: ${boundedDiagnostic(result.stderr || result.stdout)}`);
128
+ return result;
129
+ }
130
+
131
+ function forwardedActivationArgs() {
132
+ const out = [];
133
+ const skipWithValue = new Set(["--version", "--state-dir"]);
134
+ for (let index = 2; index < process.argv.length; index += 1) {
135
+ const value = process.argv[index];
136
+ if (value === "--allow-worker-deploy") continue;
137
+ if (skipWithValue.has(value)) { index += 1; continue; }
138
+ if (["--version=", "--state-dir="].some((prefix) => value.startsWith(prefix))) continue;
139
+ if (value === "--json" || value.startsWith("--json=")) continue;
140
+ out.push(value);
141
+ }
142
+ return out;
143
+ }
144
+
145
+ function argumentValue(name) {
146
+ const exact = process.argv.find((value) => value.startsWith(`${name}=`));
147
+ if (exact) return exact.slice(name.length + 1);
148
+ const index = process.argv.indexOf(name);
149
+ return index >= 0 ? process.argv[index + 1] : "";
150
+ }
151
+
152
+ function workspaceHash(workspace) {
153
+ const value = process.platform === "win32" ? String(workspace).toLowerCase() : String(workspace);
154
+ return createHash("sha256").update(value).digest("hex").slice(0, 24);
155
+ }
156
+
157
+ function boundedDiagnostic(value) {
158
+ const home = String(process.env.HOME || process.env.USERPROFILE || "");
159
+ let text = String(value || "unknown error").replace(/[\r\n\t]+/g, " ").trim();
160
+ if (home) text = text.split(home).join("<home>");
161
+ return text.slice(0, 1600);
162
+ }
163
+
164
+ function fail(message) {
165
+ console.error(`published prerelease installation failed: ${message}`);
166
+ process.exit(1);
167
+ }
@@ -5,14 +5,13 @@ import {
5
5
  mkdtempSync,
6
6
  readFileSync,
7
7
  rmSync,
8
- writeFileSync,
9
8
  } from "node:fs";
10
9
  import { spawnSync } from "node:child_process";
11
10
  import { tmpdir } from "node:os";
12
11
  import { dirname, join, resolve } from "node:path";
13
12
  import { fileURLToPath } from "node:url";
14
13
  import {
15
- ACCEPTANCE_CONFIRMATION,
14
+ acceptanceConfirmationForVersion,
16
15
  ACCEPTANCE_SCHEMA_VERSION,
17
16
  acceptancePath,
18
17
  packProject,
@@ -21,6 +20,10 @@ import {
21
20
  verifyCurrentReleaseAcceptance,
22
21
  verifyTarball,
23
22
  } from "./release-acceptance.mjs";
23
+ import { computePromotionContentDigest } from "./promotion-digest.mjs";
24
+ import { parseReleaseVersion } from "./release-channel.mjs";
25
+ import { validateCandidateManifest } from "./release-candidate-manifest.mjs";
26
+ import { replaceFileAtomicallySync } from "../src/local/exclusive-file.mjs";
24
27
 
25
28
  const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
26
29
  const candidateDirectory = join(root, ".release-candidate");
@@ -48,14 +51,16 @@ function prepareCandidate() {
48
51
  schema_version: ACCEPTANCE_SCHEMA_VERSION,
49
52
  result: "pending",
50
53
  ...metadata,
54
+ promotion_content_sha256: computePromotionContentDigest(root),
51
55
  prepared_at: new Date().toISOString(),
52
56
  };
53
- writeFileSync(candidateManifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
57
+ replaceFileAtomicallySync(candidateManifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
54
58
  const phrase = confirmationPhrase(pkg.name, pkg.version);
55
59
  console.log(`Release candidate created: ${join(candidateDirectory, metadata.filename)}`);
56
- console.log("After the repository owner explicitly authorizes this exact candidate in the active conversation, the coding agent starts it through Machine Bridge with:");
57
- console.log("npm run release:candidate:start -- --allow-worker-deploy");
58
- console.log("The owner does not need to run a terminal authorization command. The coding agent keeps the candidate running while verifying connection readiness and representative functionality through Machine Bridge.");
60
+ console.log("The repository owner activates this exact candidate with one persistent owner-side command:");
61
+ parseReleaseVersion(pkg.version);
62
+ console.log("npm run release:candidate:activate -- --allow-worker-deploy");
63
+ console.log("The owner runs this one command. It installs the exact candidate in the private state root, updates the same-name Worker, verifies candidate relay readiness, replaces the login daemon, verifies the background handoff, and exits while the service remains active.");
59
64
  console.log("After that observed live verification succeeds, the coding agent records acceptance with:");
60
65
  console.log(`npm run release:accept -- --confirm \"${phrase}\"`);
61
66
  console.log("Automated tests alone do not authorize acceptance or the first GitHub push.");
@@ -68,10 +73,10 @@ function recordAcceptance() {
68
73
  if (supplied !== expected) {
69
74
  throw new Error(`interactive candidate verification confirmation must exactly match: ${expected}`);
70
75
  }
71
- const pending = readJson(candidateManifestPath, "release candidate manifest");
72
- if (pending.result !== "pending" || pending.package_name !== pkg.name || pending.package_version !== pkg.version) {
73
- throw new Error("release candidate manifest does not match the current package");
74
- }
76
+ const pending = validateCandidateManifest(readJson(candidateManifestPath, "release candidate manifest"), {
77
+ packageName: pkg.name,
78
+ packageVersion: pkg.version,
79
+ });
75
80
  verifyTarball(join(candidateDirectory, pending.filename), pending);
76
81
 
77
82
  const verificationDirectory = join(candidateDirectory, "verification");
@@ -84,10 +89,15 @@ function recordAcceptance() {
84
89
  }
85
90
  }
86
91
 
92
+ const promotionDigest = computePromotionContentDigest(root);
93
+ if (pending.promotion_content_sha256 !== promotionDigest) {
94
+ throw new Error("source changed after candidate preparation: promotion content digest no longer matches");
95
+ }
96
+
87
97
  const record = {
88
98
  schema_version: ACCEPTANCE_SCHEMA_VERSION,
89
99
  result: "passed",
90
- confirmation: ACCEPTANCE_CONFIRMATION,
100
+ confirmation: acceptanceConfirmationForVersion(pkg.version),
91
101
  package_name: current.package_name,
92
102
  package_version: current.package_version,
93
103
  filename: current.filename,
@@ -95,11 +105,12 @@ function recordAcceptance() {
95
105
  integrity: current.integrity,
96
106
  accepted_at: new Date().toISOString(),
97
107
  package_content_sha256: computePortablePackageDigest(),
108
+ promotion_content_sha256: promotionDigest,
98
109
  };
99
110
  verifyAcceptanceRecord(record, current);
100
111
  const path = acceptancePath(root, pkg.version);
101
112
  mkdirSync(dirname(path), { recursive: true });
102
- writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
113
+ replaceFileAtomicallySync(path, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o644 });
103
114
  console.log(`Interactive local candidate acceptance recorded: ${path}`);
104
115
  console.log("Commit this record with the candidate. Any packaged-file change invalidates it.");
105
116
  }
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+ import { assertPublishTag, parseReleaseVersion } from "./release-channel.mjs";
6
+
7
+ try {
8
+ const pkg = JSON.parse(readFileSync(resolve("package.json"), "utf8"));
9
+ const parsed = parseReleaseVersion(pkg.version);
10
+ const configured = String(process.env.npm_config_tag || "latest").trim().toLowerCase() || "latest";
11
+ assertPublishTag(parsed.raw, configured);
12
+ console.log(`npm publication channel verified: ${parsed.raw} -> ${parsed.npmTag}`);
13
+ } catch (error) {
14
+ console.error(`npm publication policy failed: ${error?.message || error}`);
15
+ process.exit(1);
16
+ }
@@ -0,0 +1,79 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, isAbsolute, join, resolve } from "node:path";
3
+ import { defaultStateRoot, expandHome } from "../src/local/state.mjs";
4
+ import { replaceFileAtomicallySync } from "../src/local/exclusive-file.mjs";
5
+ import { ensureOwnerOnlyDirectorySync, readBoundedRegularFileSync } from "../src/local/secure-file.mjs";
6
+ import { assertSoakEligiblePrerelease } from "./release-channel.mjs";
7
+
8
+ export const ACTIVATION_SCHEMA_VERSION = 1;
9
+ const MAX_ACTIVATION_BYTES = 64 * 1024;
10
+ const SOURCES = new Set(["local-candidate", "npm-prerelease"]);
11
+
12
+ export function prereleaseActivationPath(version, stateRoot = defaultStateRoot()) {
13
+ const parsed = assertSoakEligiblePrerelease(version);
14
+ return join(resolve(expandHome(stateRoot)), "release-channels", "activations", `v${parsed.raw}.json`);
15
+ }
16
+
17
+ export function writePrereleaseActivation(record, stateRoot = defaultStateRoot()) {
18
+ const normalized = validatePrereleaseActivation(record);
19
+ const file = prereleaseActivationPath(normalized.package_version, stateRoot);
20
+ ensureOwnerOnlyDirectorySync(dirname(file));
21
+ replaceFileAtomicallySync(file, `${JSON.stringify(normalized, null, 2)}\n`, { mode: 0o600 });
22
+ return file;
23
+ }
24
+
25
+ export function readPrereleaseActivation(version, stateRoot = defaultStateRoot()) {
26
+ const file = prereleaseActivationPath(version, stateRoot);
27
+ if (!existsSync(file)) throw new Error(`prerelease activation record is missing: ${file}`);
28
+ let value;
29
+ try { value = JSON.parse(readBoundedRegularFileSync(file, MAX_ACTIVATION_BYTES, "prerelease activation record", { verifyPathIdentity: true }).toString("utf8")); }
30
+ catch (error) { throw new Error(`prerelease activation record is unavailable or invalid: ${error.message}`); }
31
+ return validatePrereleaseActivation(value);
32
+ }
33
+
34
+ export function validatePrereleaseActivation(value) {
35
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("prerelease activation record must be an object");
36
+ if (value.schema_version !== ACTIVATION_SCHEMA_VERSION) throw new Error("unsupported prerelease activation schema");
37
+ const parsed = assertSoakEligiblePrerelease(value.package_version);
38
+ if (value.package_name !== "machine-bridge-mcp") throw new Error("prerelease activation package name is invalid");
39
+ if (!SOURCES.has(value.source)) throw new Error("prerelease activation source is invalid");
40
+ if (!/^[0-9a-f]{40}$/.test(String(value.shasum || ""))) throw new Error("prerelease activation SHA-1 is invalid");
41
+ if (!/^sha512-[A-Za-z0-9+/]+={0,2}$/.test(String(value.integrity || ""))) throw new Error("prerelease activation integrity is invalid");
42
+ if (!/^[0-9a-f]{64}$/.test(String(value.promotion_content_sha256 || ""))) throw new Error("prerelease activation promotion digest is invalid");
43
+ const activatedAt = Date.parse(String(value.activated_at || ""));
44
+ if (!Number.isFinite(activatedAt)) throw new Error("prerelease activation timestamp is invalid");
45
+ if (value.source === "npm-prerelease") {
46
+ if (value.npm_dist_tag !== parsed.npmTag) throw new Error("prerelease activation npm dist-tag is invalid");
47
+ const publishedAt = Date.parse(String(value.published_at || ""));
48
+ if (!Number.isFinite(publishedAt) || publishedAt > activatedAt + 5 * 60 * 1000) throw new Error("prerelease activation publication timestamp is invalid");
49
+ }
50
+ const workspaceHash = String(value.workspace_hash || "");
51
+ if (workspaceHash && !/^[0-9a-f]{24}$/.test(workspaceHash)) throw new Error("prerelease activation workspace hash is invalid");
52
+ const runtimeEntry = String(value.runtime_entry || "");
53
+ if (runtimeEntry && !isAbsolute(runtimeEntry)) throw new Error("prerelease activation runtime entry must be absolute");
54
+ let previous;
55
+ if (value.previous !== undefined) {
56
+ if (!value.previous || typeof value.previous !== "object" || Array.isArray(value.previous)) throw new Error("prerelease activation previous runtime is invalid");
57
+ const previousVersion = String(value.previous.version || "");
58
+ const previousEntry = String(value.previous.entry || "");
59
+ if (!previousVersion || !previousEntry || !isAbsolute(previousEntry)) throw new Error("prerelease activation previous runtime is invalid");
60
+ previous = { version: previousVersion, entry: previousEntry };
61
+ }
62
+ return Object.freeze({
63
+ schema_version: ACTIVATION_SCHEMA_VERSION,
64
+ package_name: value.package_name,
65
+ package_version: parsed.raw,
66
+ source: value.source,
67
+ shasum: value.shasum,
68
+ integrity: value.integrity,
69
+ promotion_content_sha256: value.promotion_content_sha256,
70
+ activated_at: new Date(activatedAt).toISOString(),
71
+ ...(value.source === "npm-prerelease" ? {
72
+ npm_dist_tag: value.npm_dist_tag,
73
+ published_at: new Date(Date.parse(value.published_at)).toISOString(),
74
+ } : {}),
75
+ ...(workspaceHash ? { workspace_hash: workspaceHash } : {}),
76
+ ...(runtimeEntry ? { runtime_entry: runtimeEntry } : {}),
77
+ ...(previous ? { previous } : {}),
78
+ });
79
+ }
@@ -3,9 +3,11 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { readBoundedRegularFileSync } from "../src/local/secure-file.mjs";
6
+ import { createTrustedGitResolver } from "../src/local/trusted-git-executable.mjs";
6
7
 
7
8
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
9
  const selfPath = "scripts/privacy-check.mjs";
10
+ const gitExecutable = createTrustedGitResolver({ workspace: root });
9
11
  const requestedOptions = new Set(process.argv.slice(2));
10
12
  for (const option of requestedOptions) {
11
13
  if (option !== "--history") throw new Error(`unknown privacy-check option: ${option}`);
@@ -68,7 +70,7 @@ process.stderr.write(`privacy check ok (${candidates.length} tracked/unignored f
68
70
 
69
71
  function collectCandidateFiles(directory) {
70
72
  try {
71
- return execFileSync("git", ["-C", directory, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
73
+ return execFileSync(gitExecutable(), ["-C", directory, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
72
74
  encoding: "buffer",
73
75
  maxBuffer: 32 * 1024 * 1024,
74
76
  stdio: ["ignore", "pipe", "ignore"],
@@ -98,7 +100,7 @@ function collectCandidateFiles(directory) {
98
100
  function scanReachableHistory(directory, entries, out) {
99
101
  let listing;
100
102
  try {
101
- listing = execFileSync("git", ["-C", directory, "rev-list", "--objects", "--all", "-z"], {
103
+ listing = execFileSync(gitExecutable(), ["-C", directory, "rev-list", "--objects", "--all", "-z"], {
102
104
  encoding: "buffer",
103
105
  maxBuffer: 64 * 1024 * 1024,
104
106
  stdio: ["ignore", "pipe", "pipe"],
@@ -130,7 +132,7 @@ function scanReachableHistory(directory, entries, out) {
130
132
  const hashes = [...objectPaths.keys()];
131
133
  let metadata;
132
134
  try {
133
- metadata = execFileSync("git", ["-C", directory, "cat-file", "--batch-check=%(objectname) %(objecttype) %(objectsize)"], {
135
+ metadata = execFileSync(gitExecutable(), ["-C", directory, "cat-file", "--batch-check=%(objectname) %(objecttype) %(objectsize)"], {
134
136
  input: `${hashes.join("\n")}\n`,
135
137
  encoding: "utf8",
136
138
  maxBuffer: 64 * 1024 * 1024,
@@ -162,7 +164,7 @@ function scanReachableHistory(directory, entries, out) {
162
164
  }
163
165
  let buffer;
164
166
  try {
165
- buffer = execFileSync("git", ["-C", directory, "cat-file", "blob", hash], {
167
+ buffer = execFileSync(gitExecutable(), ["-C", directory, "cat-file", "blob", hash], {
166
168
  encoding: "buffer",
167
169
  maxBuffer: 6 * 1024 * 1024,
168
170
  stdio: ["ignore", "pipe", "pipe"],
@@ -194,7 +196,7 @@ function scanReachableHistory(directory, entries, out) {
194
196
  function scanReachableCommitMessages(directory, entries, out) {
195
197
  let output;
196
198
  try {
197
- output = execFileSync("git", ["-C", directory, "log", "--all", "--format=%H%x00%B%x00"], {
199
+ output = execFileSync(gitExecutable(), ["-C", directory, "log", "--all", "--format=%H%x00%B%x00"], {
198
200
  encoding: "buffer",
199
201
  maxBuffer: 64 * 1024 * 1024,
200
202
  stdio: ["ignore", "pipe", "pipe"],