machine-bridge-mcp 1.2.8 → 1.2.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 (49) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/CONTRIBUTING.md +16 -5
  3. package/README.md +115 -416
  4. package/SECURITY.md +2 -0
  5. package/browser-extension/manifest.json +2 -2
  6. package/docs/ARCHITECTURE.md +10 -3
  7. package/docs/AUDIT.md +10 -0
  8. package/docs/ENGINEERING.md +2 -2
  9. package/docs/OVERVIEW.md +113 -0
  10. package/docs/PROJECT_STANDARDS.md +4 -4
  11. package/docs/RELEASING.md +25 -12
  12. package/docs/TESTING.md +12 -8
  13. package/docs/THREAT_MODEL.md +142 -0
  14. package/package.json +10 -3
  15. package/scripts/check-plan.mjs +91 -0
  16. package/scripts/coverage-check.mjs +22 -0
  17. package/scripts/github-push.mjs +1 -1
  18. package/scripts/github-release.mjs +1 -1
  19. package/scripts/local-release-acceptance.mjs +56 -6
  20. package/scripts/release-acceptance.mjs +15 -3
  21. package/scripts/release-state.mjs +1 -1
  22. package/scripts/run-checks.mjs +29 -0
  23. package/scripts/start-release-candidate.mjs +113 -0
  24. package/src/local/agent-context-projection.mjs +158 -0
  25. package/src/local/agent-context.mjs +23 -332
  26. package/src/local/agent-skill-discovery.mjs +230 -0
  27. package/src/local/agent-text-file.mjs +41 -0
  28. package/src/local/browser-bridge-http.mjs +48 -0
  29. package/src/local/browser-bridge.mjs +48 -222
  30. package/src/local/browser-broker-routes.mjs +136 -0
  31. package/src/local/browser-broker-server.mjs +59 -0
  32. package/src/local/browser-request-registry.mjs +67 -0
  33. package/src/local/managed-job-lock.mjs +99 -0
  34. package/src/local/managed-job-projection.mjs +68 -0
  35. package/src/local/managed-job-runner.mjs +73 -0
  36. package/src/local/managed-job-storage.mjs +93 -0
  37. package/src/local/managed-jobs.mjs +12 -297
  38. package/src/local/runtime-paths.mjs +107 -0
  39. package/src/local/runtime-relay.mjs +73 -0
  40. package/src/local/runtime-tool-handlers.mjs +66 -0
  41. package/src/local/runtime.mjs +22 -204
  42. package/src/local/windows-launcher.mjs +57 -0
  43. package/src/local/windows-service.mjs +7 -56
  44. package/src/worker/index.ts +9 -118
  45. package/src/worker/mcp-jsonrpc.ts +94 -0
  46. package/src/worker/oauth-authorization-page.ts +70 -0
  47. package/src/worker/oauth-controller.ts +9 -58
  48. package/src/worker/websocket-protocol.ts +24 -0
  49. package/tsconfig.local.json +6 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "machine-bridge-mcp",
3
- "version": "1.2.8",
3
+ "version": "1.2.9",
4
4
  "description": "Cross-client MCP bridge for local agent context, structured browser and application automation, files, Git, processes, resources, and durable jobs over stdio or OAuth relay.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -47,7 +47,7 @@
47
47
  "worker:types": "node scripts/generate-worker-types.mjs",
48
48
  "typecheck": "npm run worker:types && tsc -p tsconfig.json --noEmit && npm run typecheck:local",
49
49
  "syntax": "node scripts/syntax-check.mjs",
50
- "check": "npm run privacy:check && npm run privacy:test && npm run release-impact:check && npm run release-impact:test && npm run release:acceptance:test && npm run release-state:test && npm run release-ci:test && npm run network-retry:test && npm run worker-deployment:test && npm run relay:test && npm run secure-file:test && npm run worker-secret-file:test && npm run sarif-security:test && npm run security-properties:test && npm run shell:test && npm run architecture:test && npm run markdown:test && npm run project-metadata:test && npm run numbers:test && npm run records:test && npm run state-inventory:test && npm run commit-message:test && npm run policy:test && npm run account:test && npm run worker-oauth-controller:test && npm run policy-docs:check && npm run tool-docs:check && npm run runtime-infrastructure:test && npm run runtime-boundaries:test && npm run runtime-handlers:test && npm run cli-entrypoint:test && npm run cli-service:test && npm run lifecycle:test && npm run logging-structure:test && npm run coverage:test && npm run worker-runtime-infrastructure:test && npm run lint:test && npm run lint && npm run typecheck && npm run syntax && npm run deadline:test && npm run process-lock:test && npm run service-lifecycle:test && npm run service-environment:test && npm run service-platform:test && npm run catalog:test && npm run agent-context:test && npm run capability-ranking:test && npm run browser-command:test && npm run browser-devtools-input:test && npm run browser-service-worker:test && npm run browser-page-automation:test && npm run browser-bridge:test && npm run app-automation:test && npm run self-test && npm run atomic-fs:test && npm run package:test && npm run install:test && npm run ssh-key:test && npm run full-access:test && npm run managed-jobs:test && npm run stdio:integration-test && npm run worker:integration-test && npm run oauth-browser:test",
50
+ "check": "npm run check:full",
51
51
  "worker:dry-run": "wrangler deploy --dry-run",
52
52
  "worker:integration-test": "node tests/worker-integration-test.mjs",
53
53
  "release:check": "node scripts/github-release.mjs --check",
@@ -79,6 +79,7 @@
79
79
  "release-state:test": "node tests/release-state-test.mjs",
80
80
  "release-ci:test": "node tests/release-ci-test.mjs",
81
81
  "agent-context:test": "node tests/agent-context-test.mjs",
82
+ "agent-boundaries:test": "node tests/agent-boundaries-test.mjs",
82
83
  "browser-bridge:test": "node tests/browser-bridge-test.mjs",
83
84
  "app-automation:test": "node tests/app-automation-test.mjs",
84
85
  "process-lock:test": "node tests/process-lock-test.mjs",
@@ -122,7 +123,13 @@
122
123
  "release:candidate": "npm run check && node scripts/local-release-acceptance.mjs --prepare",
123
124
  "release:accept": "node scripts/local-release-acceptance.mjs --record",
124
125
  "release:acceptance:verify": "node scripts/local-release-acceptance.mjs --verify",
125
- "github:push": "node scripts/github-push.mjs"
126
+ "github:push": "node scripts/github-push.mjs",
127
+ "check:fast": "node scripts/run-checks.mjs fast",
128
+ "check:full": "node scripts/run-checks.mjs full",
129
+ "check-plan:test": "node tests/check-plan-test.mjs",
130
+ "check:platform": "node scripts/run-checks.mjs platform",
131
+ "release": "node scripts/github-release.mjs --publish",
132
+ "release:candidate:start": "node scripts/start-release-candidate.mjs"
126
133
  },
127
134
  "dependencies": {
128
135
  "https-proxy-agent": "9.1.0",
@@ -0,0 +1,91 @@
1
+ export const FAST_CHECK_TASKS = Object.freeze([
2
+ "privacy:check",
3
+ "release-impact:check",
4
+ "release:acceptance:test",
5
+ "release-state:test",
6
+ "release-ci:test",
7
+ "network-retry:test",
8
+ "secure-file:test",
9
+ "worker-secret-file:test",
10
+ "sarif-security:test",
11
+ "shell:test",
12
+ "architecture:test",
13
+ "markdown:test",
14
+ "project-metadata:test",
15
+ "numbers:test",
16
+ "records:test",
17
+ "state-inventory:test",
18
+ "commit-message:test",
19
+ "policy:test",
20
+ "account:test",
21
+ "worker-oauth-controller:test",
22
+ "policy-docs:check",
23
+ "tool-docs:check",
24
+ "runtime-infrastructure:test",
25
+ "runtime-boundaries:test",
26
+ "runtime-handlers:test",
27
+ "cli-entrypoint:test",
28
+ "cli-service:test",
29
+ "lifecycle:test",
30
+ "logging-structure:test",
31
+ "worker-runtime-infrastructure:test",
32
+ "lint:test",
33
+ "lint",
34
+ "typecheck",
35
+ "syntax",
36
+ "deadline:test",
37
+ "catalog:test",
38
+ "capability-ranking:test",
39
+ "agent-boundaries:test",
40
+ "browser-command:test",
41
+ "check-plan:test",
42
+ ]);
43
+
44
+ export const PLATFORM_ONLY_CHECK_TASKS = Object.freeze([
45
+ "privacy:test",
46
+ "release-impact:test",
47
+ "worker-deployment:test",
48
+ "relay:test",
49
+ "security-properties:test",
50
+ "process-lock:test",
51
+ "service-lifecycle:test",
52
+ "service-environment:test",
53
+ "service-platform:test",
54
+ "agent-context:test",
55
+ "browser-devtools-input:test",
56
+ "browser-service-worker:test",
57
+ "browser-page-automation:test",
58
+ "app-automation:test",
59
+ "self-test",
60
+ "atomic-fs:test",
61
+ "ssh-key:test",
62
+ "full-access:test",
63
+ "managed-jobs:test",
64
+ ]);
65
+
66
+ export const FULL_ONLY_CHECK_TASKS = Object.freeze([
67
+ "coverage:test",
68
+ "browser-bridge:test",
69
+ "package:test",
70
+ "install:test",
71
+ "stdio:integration-test",
72
+ "worker:integration-test",
73
+ "oauth-browser:test",
74
+ ]);
75
+
76
+ export const PLATFORM_CHECK_TASKS = Object.freeze([
77
+ ...FAST_CHECK_TASKS,
78
+ ...PLATFORM_ONLY_CHECK_TASKS,
79
+ ]);
80
+
81
+ export const FULL_CHECK_TASKS = Object.freeze([
82
+ ...PLATFORM_CHECK_TASKS,
83
+ ...FULL_ONLY_CHECK_TASKS,
84
+ ]);
85
+
86
+ export function checkTasks(mode) {
87
+ if (mode === "fast") return FAST_CHECK_TASKS;
88
+ if (mode === "platform") return PLATFORM_CHECK_TASKS;
89
+ if (mode === "full") return FULL_CHECK_TASKS;
90
+ throw new Error(`unknown check mode: ${mode}`);
91
+ }
@@ -24,8 +24,12 @@ const tests = [
24
24
  "tests/state-inventory-test.mjs",
25
25
  "tests/worker-deployment-test.mjs",
26
26
  "tests/agent-context-test.mjs",
27
+ "tests/agent-boundaries-test.mjs",
27
28
  "tests/capability-ranking-test.mjs",
28
29
  "tests/browser-bridge-test.mjs",
30
+ "tests/relay-connection-test.mjs",
31
+ "tests/managed-jobs-test.mjs",
32
+ "tests/account-admin-test.mjs",
29
33
  "tests/monotonic-deadline-test.mjs",
30
34
  ];
31
35
 
@@ -54,6 +58,7 @@ try {
54
58
  "src/local/process-tracker.mjs": [65, 35],
55
59
  "src/local/log.mjs": [60, 40],
56
60
  "src/local/runtime.mjs": [75, 55],
61
+ "src/local/runtime-paths.mjs": [90, 50],
57
62
  "src/local/cli.mjs": [48, 21.9],
58
63
  "src/local/cli-service.mjs": [90, 65],
59
64
  "src/local/cli-options.mjs": [65, 35],
@@ -66,6 +71,9 @@ try {
66
71
  "src/local/worker-health.mjs": [85, 60],
67
72
  "src/local/worker-deployment.mjs": [80, 55],
68
73
  "src/local/agent-contract.mjs": [95, 40],
74
+ "src/local/agent-context-projection.mjs": [95, 60],
75
+ "src/local/agent-skill-discovery.mjs": [85, 60],
76
+ "src/local/agent-text-file.mjs": [90, 60],
69
77
  "src/local/capability-ranking.mjs": [95, 70],
70
78
  "src/local/browser-extension-protocol.mjs": [95, 35],
71
79
  "src/local/browser-operation-service.mjs": [80, 50],
@@ -73,9 +81,23 @@ try {
73
81
  "src/local/runtime-diagnostics.mjs": [75, 65],
74
82
  "src/local/runtime-capabilities.mjs": [75, 45],
75
83
  "src/local/monotonic-deadline.mjs": [100, 100],
84
+ "src/local/state.mjs": [85, 45],
85
+ "src/local/relay-connection.mjs": [90, 55],
86
+ "src/local/managed-jobs.mjs": [85, 50],
87
+ "src/local/managed-job-projection.mjs": [90, 60],
88
+ "src/local/managed-job-storage.mjs": [75, 50],
89
+ "src/local/managed-job-runner.mjs": [80, 50],
90
+ "src/local/browser-bridge.mjs": [80, 55],
91
+ "src/local/browser-request-registry.mjs": [95, 35],
92
+ "src/local/browser-broker-routes.mjs": [85, 55],
93
+ "src/local/browser-broker-server.mjs": [80, 50],
94
+ "src/worker/oauth-controller.ts": [85, 65],
95
+ "src/worker/oauth-authorization-page.ts": [90, 60],
76
96
  "src/worker/pending-calls.ts": [90, 35],
77
97
  "src/worker/policy.ts": [100, 25],
78
98
  "src/worker/errors.ts": [100, 40],
99
+ "src/worker/mcp-jsonrpc.ts": [95, 55],
100
+ "src/worker/websocket-protocol.ts": [100, 50],
79
101
  };
80
102
  const failures = [];
81
103
  for (const [file, [minimumFunctions, minimumBlocks]] of Object.entries(thresholds)) {
@@ -18,7 +18,7 @@ try {
18
18
  if (acceptance.required) {
19
19
  const path = `release-acceptance/v${acceptance.metadata.package_version}.json`;
20
20
  run("git", ["ls-files", "--error-unmatch", path], { capture: true });
21
- console.log(`Verified repository-owner local acceptance for ${acceptance.metadata.filename}.`);
21
+ console.log(`Verified interactive local candidate acceptance for ${acceptance.metadata.filename}.`);
22
22
  }
23
23
 
24
24
  run("git", ["push", "--set-upstream", "origin", "HEAD"]);
@@ -342,7 +342,7 @@ function assertLocalAcceptance() {
342
342
  try {
343
343
  const result = verifyCurrentReleaseAcceptance(root);
344
344
  if (result.required) {
345
- console.log(`Repository-owner local acceptance matches ${result.metadata.filename} (${result.metadata.shasum}).`);
345
+ console.log(`Interactive local candidate acceptance matches ${result.metadata.filename} (${result.metadata.shasum}).`);
346
346
  }
347
347
  } catch (error) {
348
348
  fail(String(error?.message || error));
@@ -2,10 +2,13 @@
2
2
 
3
3
  import {
4
4
  mkdirSync,
5
+ mkdtempSync,
5
6
  readFileSync,
6
7
  rmSync,
7
8
  writeFileSync,
8
9
  } from "node:fs";
10
+ import { spawnSync } from "node:child_process";
11
+ import { tmpdir } from "node:os";
9
12
  import { dirname, join, resolve } from "node:path";
10
13
  import { fileURLToPath } from "node:url";
11
14
  import {
@@ -50,10 +53,12 @@ function prepareCandidate() {
50
53
  writeFileSync(candidateManifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
51
54
  const phrase = confirmationPhrase(pkg.name, pkg.version);
52
55
  console.log(`Release candidate created: ${join(candidateDirectory, metadata.filename)}`);
53
- console.log("Test this exact tarball on the maintainer machine using the documented normal startup path.");
54
- console.log("After the repository owner confirms it works, record that confirmation with:");
56
+ console.log("The repository owner must start this exact candidate in a local terminal with:");
57
+ console.log("npm run release:candidate:start -- --allow-worker-deploy");
58
+ console.log("Leave the candidate running while the coding agent verifies connection readiness and representative functionality through Machine Bridge.");
59
+ console.log("After that observed live verification succeeds, the coding agent records acceptance with:");
55
60
  console.log(`npm run release:accept -- --confirm \"${phrase}\"`);
56
- console.log("Do not push this release-relevant branch before that command succeeds and its acceptance record is committed.");
61
+ console.log("Automated tests alone do not authorize acceptance or the first GitHub push.");
57
62
  }
58
63
 
59
64
  function recordAcceptance() {
@@ -61,7 +66,7 @@ function recordAcceptance() {
61
66
  const supplied = argumentValue("--confirm");
62
67
  const expected = confirmationPhrase(pkg.name, pkg.version);
63
68
  if (supplied !== expected) {
64
- throw new Error(`owner confirmation must exactly match: ${expected}`);
69
+ throw new Error(`interactive candidate verification confirmation must exactly match: ${expected}`);
65
70
  }
66
71
  const pending = readJson(candidateManifestPath, "release candidate manifest");
67
72
  if (pending.result !== "pending" || pending.package_name !== pkg.name || pending.package_version !== pkg.version) {
@@ -89,12 +94,13 @@ function recordAcceptance() {
89
94
  shasum: current.shasum,
90
95
  integrity: current.integrity,
91
96
  accepted_at: new Date().toISOString(),
97
+ package_content_sha256: computePortablePackageDigest(),
92
98
  };
93
99
  verifyAcceptanceRecord(record, current);
94
100
  const path = acceptancePath(root, pkg.version);
95
101
  mkdirSync(dirname(path), { recursive: true });
96
102
  writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
97
- console.log(`Local owner acceptance recorded: ${path}`);
103
+ console.log(`Interactive local candidate acceptance recorded: ${path}`);
98
104
  console.log("Commit this record with the candidate. Any packaged-file change invalidates it.");
99
105
  }
100
106
 
@@ -107,6 +113,50 @@ function verifyAcceptance() {
107
113
  console.log(`Local release acceptance matches ${result.metadata.filename} (${result.metadata.shasum}).`);
108
114
  }
109
115
 
116
+ function computePortablePackageDigest() {
117
+ const npmCli = process.env.npm_execpath;
118
+ if (!npmCli) throw new Error("portable package digest requires npm_execpath");
119
+ const temporary = mkdtempSync(join(tmpdir(), "mbm-release-index-"));
120
+ const indexPath = join(temporary, "index");
121
+ const env = { ...process.env, GIT_INDEX_FILE: indexPath };
122
+ try {
123
+ runChecked("git", ["read-tree", "HEAD"], { env });
124
+ runChecked("git", ["add", "--all", "--", "."], { env });
125
+ const packed = runChecked(process.execPath, [
126
+ npmCli,
127
+ "pack",
128
+ "--ignore-scripts",
129
+ "--silent",
130
+ "--dry-run",
131
+ "--json",
132
+ ], { env });
133
+ const verifier = runChecked(process.execPath, [
134
+ join(root, ".github", "scripts", "verify-release-acceptance.mjs"),
135
+ "--print-digest",
136
+ ], { env, input: packed.stdout });
137
+ const digest = verifier.stdout.trim();
138
+ if (!/^[0-9a-f]{64}$/.test(digest)) throw new Error("portable package digest output is invalid");
139
+ return digest;
140
+ } finally {
141
+ rmSync(temporary, { recursive: true, force: true });
142
+ }
143
+ }
144
+
145
+ function runChecked(file, args, { env = process.env, input } = {}) {
146
+ const result = spawnSync(file, args, {
147
+ cwd: root,
148
+ encoding: "utf8",
149
+ env,
150
+ input,
151
+ windowsHide: true,
152
+ });
153
+ if (result.error) throw result.error;
154
+ if (result.status !== 0) {
155
+ throw new Error(`${file} ${args[0] || ""} failed: ${String(result.stderr || result.stdout).trim()}`);
156
+ }
157
+ return result;
158
+ }
159
+
110
160
  function readPackage() {
111
161
  return readJson(join(root, "package.json"), "package.json");
112
162
  }
@@ -127,7 +177,7 @@ function argumentValue(name) {
127
177
  }
128
178
 
129
179
  function confirmationPhrase(name, version) {
130
- return `I TESTED ${name} ${version} LOCALLY AND IT WORKS`;
180
+ return `I VERIFIED ${name} ${version} CANDIDATE ON THE OWNER MACHINE AND IT WORKS`;
131
181
  }
132
182
 
133
183
  function fail(message) {
@@ -11,13 +11,21 @@ import { spawnSync } from "node:child_process";
11
11
 
12
12
  export const ACCEPTANCE_SCHEMA_VERSION = 1;
13
13
  export const ACCEPTANCE_POLICY_VERSION = "1.2.8";
14
- export const ACCEPTANCE_CONFIRMATION = "repository-owner-local-test";
14
+ export const AGENT_VERIFIED_ACCEPTANCE_VERSION = "1.2.9";
15
+ export const ACCEPTANCE_CONFIRMATION = "owner-started-agent-verified-local-candidate";
16
+ export const LEGACY_ACCEPTANCE_CONFIRMATION = "repository-owner-local-test";
15
17
  const MAX_ACCEPTANCE_BYTES = 64 * 1024;
16
18
 
17
19
  export function requiresLocalAcceptance(version) {
18
20
  return compareVersions(parseVersion(version), parseVersion(ACCEPTANCE_POLICY_VERSION)) >= 0;
19
21
  }
20
22
 
23
+ export function acceptanceConfirmationForVersion(version) {
24
+ return compareVersions(parseVersion(version), parseVersion(AGENT_VERIFIED_ACCEPTANCE_VERSION)) >= 0
25
+ ? ACCEPTANCE_CONFIRMATION
26
+ : LEGACY_ACCEPTANCE_CONFIRMATION;
27
+ }
28
+
21
29
  export function acceptancePath(root, version) {
22
30
  return join(root, "release-acceptance", `v${version}.json`);
23
31
  }
@@ -92,11 +100,15 @@ export function verifyAcceptanceRecord(record, metadata) {
92
100
  throw new Error(`unsupported release acceptance schema: ${record.schema_version}`);
93
101
  }
94
102
  if (record.result !== "passed") throw new Error("local release acceptance result is not passed");
95
- if (record.confirmation !== ACCEPTANCE_CONFIRMATION) {
96
- throw new Error("local release acceptance confirmation is missing");
103
+ const expectedConfirmation = acceptanceConfirmationForVersion(metadata.package_version);
104
+ if (record.confirmation !== expectedConfirmation) {
105
+ throw new Error("local release acceptance confirmation is missing or does not match the active verification workflow");
97
106
  }
98
107
  const acceptedAt = Date.parse(String(record.accepted_at || ""));
99
108
  if (!Number.isFinite(acceptedAt)) throw new Error("local release acceptance timestamp is invalid");
109
+ if (!/^[0-9a-f]{64}$/.test(String(record.package_content_sha256 || ""))) {
110
+ throw new Error("local release acceptance portable package-content digest is missing or invalid");
111
+ }
100
112
  for (const key of ["package_name", "package_version", "filename", "shasum", "integrity"]) {
101
113
  if (record[key] !== metadata[key]) {
102
114
  throw new Error(`local release acceptance ${key} does not match the current npm package`);
@@ -1,7 +1,7 @@
1
1
  export function tagSyncError({ scope = "local", tag, head, commit }) {
2
2
  const label = scope === "remote" ? "remote tag" : "local tag";
3
3
  if (!commit) {
4
- return `${label} ${tag} is missing; run npm run release:publish before npm publish`;
4
+ return `${label} ${tag} is missing; run npm run release before npm publish`;
5
5
  }
6
6
  if (commit !== head) {
7
7
  return `${label} ${tag} points to ${commit}, not HEAD ${head}`;
@@ -0,0 +1,29 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { performance } from "node:perf_hooks";
3
+ import { checkTasks } from "./check-plan.mjs";
4
+
5
+ const mode = process.argv[2] || "full";
6
+ const tasks = checkTasks(mode);
7
+ const npmCli = process.env.npm_execpath;
8
+ if (!npmCli) throw new Error("check runner must run through npm so npm_execpath is available");
9
+ const planStartedAt = performance.now();
10
+
11
+ console.log(`running ${mode} verification plan (${tasks.length} tasks)`);
12
+ for (const [index, task] of tasks.entries()) {
13
+ const taskStartedAt = performance.now();
14
+ console.log(`\n[${index + 1}/${tasks.length}] npm run ${task}`);
15
+ const result = spawnSync(process.execPath, [npmCli, "run", task], {
16
+ cwd: process.cwd(),
17
+ env: process.env,
18
+ stdio: "inherit",
19
+ });
20
+ const elapsedSeconds = ((performance.now() - taskStartedAt) / 1000).toFixed(1);
21
+ if (result.error) throw result.error;
22
+ if (result.status !== 0) {
23
+ console.error(`verification task failed after ${elapsedSeconds}s: ${task}`);
24
+ process.exit(result.status || 1);
25
+ }
26
+ console.log(`completed ${task} in ${elapsedSeconds}s`);
27
+ }
28
+ const totalSeconds = ((performance.now() - planStartedAt) / 1000).toFixed(1);
29
+ console.log(`\n${mode} verification plan passed in ${totalSeconds}s`);
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn, spawnSync } from "node:child_process";
4
+ import { mkdirSync, readFileSync, rmSync } from "node:fs";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { verifyTarball } from "./release-acceptance.mjs";
8
+
9
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
10
+ const candidateDirectory = join(root, ".release-candidate");
11
+ const manifestPath = join(candidateDirectory, "manifest.json");
12
+ const installPrefix = join(candidateDirectory, "runtime");
13
+ const npmCli = process.env.npm_execpath;
14
+
15
+ if (!npmCli) fail("candidate startup must run through npm so npm_execpath is available");
16
+
17
+ try {
18
+ const manifest = readJson(manifestPath, "release candidate manifest");
19
+ if (manifest.result !== "pending") throw new Error("release candidate manifest is not pending");
20
+ const tarball = join(candidateDirectory, String(manifest.filename || ""));
21
+ verifyTarball(tarball, manifest);
22
+
23
+ const npmVersion = runNpm(["--version"], root).stdout.trim();
24
+ if (Number(npmVersion.split(".")[0]) < 12) {
25
+ throw new Error(`candidate startup requires npm 12 or newer; current ${npmVersion}`);
26
+ }
27
+
28
+ rmSync(installPrefix, { recursive: true, force: true });
29
+ mkdirSync(installPrefix, { recursive: true });
30
+ runNpm([
31
+ "install",
32
+ "--global",
33
+ "--prefix", installPrefix,
34
+ "--omit=optional",
35
+ "--allow-scripts=esbuild,workerd,sharp,fsevents",
36
+ tarball,
37
+ ], root);
38
+
39
+ const globalRoot = runNpm(["root", "--global", "--prefix", installPrefix], root).stdout.trim();
40
+ const installedPackage = join(globalRoot, manifest.package_name);
41
+ const installed = readJson(join(installedPackage, "package.json"), "installed candidate package");
42
+ if (installed.version !== manifest.package_version) {
43
+ throw new Error(`installed candidate version ${installed.version} does not match ${manifest.package_version}`);
44
+ }
45
+
46
+ console.log(`Verified candidate tarball: ${manifest.filename} (${manifest.shasum})`);
47
+ console.log(`Installed isolated candidate: ${installedPackage}`);
48
+ const installOnly = process.argv.includes("--install-only");
49
+ const allowWorkerDeploy = process.argv.includes("--allow-worker-deploy");
50
+ if (installOnly) {
51
+ console.log("Candidate installation check passed; startup was skipped by --install-only.");
52
+ process.exit(0);
53
+ }
54
+
55
+ if (!allowWorkerDeploy) {
56
+ throw new Error("foreground candidate startup may update the configured same-name Worker; rerun with --allow-worker-deploy to authorize that live candidate deployment");
57
+ }
58
+
59
+ const forwardedArgs = process.argv.slice(2).filter((value) => value !== "--install-only" && value !== "--allow-worker-deploy");
60
+ const cli = join(installedPackage, "bin", "machine-mcp.mjs");
61
+ console.log("Authorized live candidate deployment: startup may update the configured same-name Worker when its version or deployment hash differs.");
62
+ console.log("Starting the exact candidate in the foreground. Leave this process running while the coding agent verifies the Worker, relay, and local runtime end to end.");
63
+ const child = spawn(process.execPath, [cli, ...forwardedArgs], {
64
+ cwd: root,
65
+ env: process.env,
66
+ stdio: "inherit",
67
+ windowsHide: true,
68
+ });
69
+ for (const signal of ["SIGINT", "SIGTERM"]) {
70
+ process.on(signal, () => {
71
+ try { child.kill(signal); } catch {
72
+ // The child may already have exited.
73
+ }
74
+ });
75
+ }
76
+ child.once("error", (error) => fail(error?.message || error));
77
+ child.once("exit", (code, signal) => {
78
+ if (signal) {
79
+ console.error(`release candidate exited from ${signal}`);
80
+ process.exitCode = 1;
81
+ return;
82
+ }
83
+ process.exitCode = code ?? 1;
84
+ });
85
+ } catch (error) {
86
+ fail(error?.message || error);
87
+ }
88
+
89
+ function runNpm(args, cwd) {
90
+ const result = spawnSync(process.execPath, [npmCli, ...args], {
91
+ cwd,
92
+ encoding: "utf8",
93
+ env: process.env,
94
+ timeout: 300_000,
95
+ windowsHide: true,
96
+ });
97
+ if (result.error) throw result.error;
98
+ if (result.status !== 0) throw new Error(`npm ${args[0]} failed: ${result.stderr || result.stdout}`);
99
+ return result;
100
+ }
101
+
102
+ function readJson(path, label) {
103
+ try {
104
+ return JSON.parse(readFileSync(path, "utf8"));
105
+ } catch (error) {
106
+ throw new Error(`${label} is unavailable or invalid: ${error.message}`);
107
+ }
108
+ }
109
+
110
+ function fail(message) {
111
+ console.error(`release candidate startup failed: ${message}`);
112
+ process.exit(1);
113
+ }
@@ -0,0 +1,158 @@
1
+ // @ts-check
2
+ import { createHash } from "node:crypto";
3
+
4
+ /** @typedef {(value: string) => string} DisplayPath */
5
+ /**
6
+ * @typedef {object} InstructionItem
7
+ * @property {string} [source]
8
+ * @property {string} [path]
9
+ * @property {string} scope
10
+ * @property {number} bytes
11
+ * @property {string} sha256
12
+ * @property {number} precedence
13
+ * @property {string} content
14
+ */
15
+ /**
16
+ * @typedef {object} SkillSummary
17
+ * @property {string} id
18
+ * @property {string} name
19
+ * @property {string} description
20
+ * @property {string} entrypoint
21
+ * @property {string} sourceRoot
22
+ * @property {number} bytes
23
+ * @property {string} sha256
24
+ */
25
+ /**
26
+ * @typedef {object} CommandSummary
27
+ * @property {string} name
28
+ * @property {string} description
29
+ * @property {string[]} argv
30
+ * @property {string} cwd
31
+ * @property {number} timeoutSeconds
32
+ * @property {boolean} allowExtraArgs
33
+ * @property {string} source
34
+ * @property {string} [sourceType]
35
+ * @property {string} [script]
36
+ */
37
+ /**
38
+ * @typedef {object} ProjectionState
39
+ * @property {string[]} configFiles
40
+ * @property {InstructionItem | null} builtinInstructions
41
+ * @property {InstructionItem | null} automaticProjectContext
42
+ * @property {InstructionItem | null} modelInstructions
43
+ * @property {InstructionItem[]} instructions
44
+ * @property {Map<string, CommandSummary>} commands
45
+ */
46
+
47
+ /** @param {ProjectionState} state @param {SkillSummary[]} skills */
48
+ export function capabilityFingerprint(state, skills) {
49
+ return sha256(JSON.stringify({
50
+ configs: state.configFiles,
51
+ instructions: [
52
+ state.builtinInstructions?.sha256 || "",
53
+ state.automaticProjectContext?.sha256 || "",
54
+ state.modelInstructions?.sha256 || "",
55
+ ...state.instructions.map((item) => item.sha256),
56
+ ],
57
+ skills: skills.map((skill) => [skill.id, skill.sha256]),
58
+ commands: [...state.commands.values()].map((command) => [command.name, command.argv]),
59
+ }));
60
+ }
61
+
62
+ /** @param {SkillSummary} skill @param {DisplayPath} displayPath */
63
+ export function publicSkill(skill, displayPath) {
64
+ return {
65
+ id: skill.id,
66
+ name: skill.name,
67
+ description: skill.description,
68
+ entrypoint: displayPath(skill.entrypoint),
69
+ source_root: displayPath(skill.sourceRoot),
70
+ bytes: skill.bytes,
71
+ sha256: skill.sha256,
72
+ };
73
+ }
74
+
75
+ /** @param {SkillSummary[]} skills @param {DisplayPath} displayPath @param {number} maxSkills @param {number} budgetChars */
76
+ export function contextSkillSummaries(skills, displayPath, maxSkills, budgetChars) {
77
+ /** @type {Array<ReturnType<typeof publicSkill> & {description_truncated?: boolean}>} */
78
+ const selected = [];
79
+ let used = 0;
80
+ for (const skill of skills) {
81
+ if (selected.length >= maxSkills) return { skills: selected, truncated: true };
82
+ const item = publicSkill(skill, displayPath);
83
+ const fullSize = JSON.stringify(item).length;
84
+ if (used + fullSize <= budgetChars) {
85
+ selected.push(item);
86
+ used += fullSize;
87
+ continue;
88
+ }
89
+ const withoutDescription = { ...item, description: "", description_truncated: true };
90
+ const baseSize = JSON.stringify(withoutDescription).length;
91
+ const available = budgetChars - used - baseSize;
92
+ if (available >= 32) selected.push({ ...withoutDescription, description: item.description.slice(0, available) });
93
+ return { skills: selected, truncated: true };
94
+ }
95
+ return { skills: selected, truncated: false };
96
+ }
97
+
98
+ /** @param {Array<{entrypoint: string, message: string}>} warnings @param {DisplayPath} displayPath */
99
+ export function publicSkillWarnings(warnings, displayPath) {
100
+ return warnings.map((warning) => ({ entrypoint: displayPath(warning.entrypoint), message: warning.message }));
101
+ }
102
+
103
+ /** @param {Map<string, CommandSummary>} commands @param {DisplayPath} displayPath */
104
+ export function publicCommands(commands, displayPath) {
105
+ return [...commands.values()]
106
+ .sort((left, right) => left.name.localeCompare(right.name))
107
+ .map((command) => ({
108
+ name: command.name,
109
+ description: command.description,
110
+ argv: [...command.argv],
111
+ cwd: displayPath(command.cwd),
112
+ timeout_seconds: command.timeoutSeconds,
113
+ allow_extra_args: command.allowExtraArgs,
114
+ source: displayPath(command.source),
115
+ source_type: command.sourceType || "agent-config",
116
+ ...(command.script ? { package_script: command.script } : {}),
117
+ }));
118
+ }
119
+
120
+ /** @param {ProjectionState} state */
121
+ export function effectiveInstructionItems(state) {
122
+ return [
123
+ ...(state.builtinInstructions ? [state.builtinInstructions] : []),
124
+ ...(state.automaticProjectContext ? [state.automaticProjectContext] : []),
125
+ ...(state.modelInstructions ? [state.modelInstructions] : []),
126
+ ...state.instructions,
127
+ ];
128
+ }
129
+
130
+ /** @param {InstructionItem | null} item @param {boolean} includeContent */
131
+ export function publicVirtualInstruction(item, includeContent) {
132
+ if (!item) return null;
133
+ return {
134
+ source: item.source,
135
+ scope: item.scope,
136
+ bytes: item.bytes,
137
+ sha256: item.sha256,
138
+ precedence: item.precedence,
139
+ ...(includeContent ? { content: item.content } : {}),
140
+ };
141
+ }
142
+
143
+ /** @param {InstructionItem[]} instructions @param {DisplayPath} displayPath */
144
+ export function renderEffectiveInstructions(instructions, displayPath) {
145
+ return instructions.map((item) => {
146
+ const source = item.source || displayPath(item.path || "");
147
+ return [
148
+ `--- BEGIN ${source} (precedence ${item.precedence}) ---`,
149
+ item.content,
150
+ `--- END ${source} ---`,
151
+ ].join("\n");
152
+ }).join("\n\n");
153
+ }
154
+
155
+ /** @param {unknown} value */
156
+ export function sha256(value) {
157
+ return createHash("sha256").update(String(value)).digest("hex");
158
+ }