machine-bridge-mcp 1.2.8 → 1.2.10

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 (57) hide show
  1. package/CHANGELOG.md +20 -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 +19 -9
  7. package/docs/AUDIT.md +20 -0
  8. package/docs/ENGINEERING.md +2 -2
  9. package/docs/LOGGING.md +1 -1
  10. package/docs/MULTI_ACCOUNT.md +2 -2
  11. package/docs/OPERATIONS.md +2 -2
  12. package/docs/OVERVIEW.md +113 -0
  13. package/docs/PROJECT_STANDARDS.md +4 -4
  14. package/docs/RELEASING.md +25 -12
  15. package/docs/TESTING.md +12 -8
  16. package/docs/THREAT_MODEL.md +142 -0
  17. package/package.json +10 -3
  18. package/scripts/check-plan.mjs +91 -0
  19. package/scripts/coverage-check.mjs +22 -0
  20. package/scripts/github-push.mjs +1 -1
  21. package/scripts/github-release.mjs +1 -1
  22. package/scripts/local-release-acceptance.mjs +56 -6
  23. package/scripts/release-acceptance.mjs +15 -3
  24. package/scripts/release-state.mjs +1 -1
  25. package/scripts/run-checks.mjs +29 -0
  26. package/scripts/start-release-candidate.mjs +113 -0
  27. package/src/local/agent-context-projection.mjs +158 -0
  28. package/src/local/agent-context.mjs +23 -332
  29. package/src/local/agent-skill-discovery.mjs +230 -0
  30. package/src/local/agent-text-file.mjs +41 -0
  31. package/src/local/browser-bridge-http.mjs +48 -0
  32. package/src/local/browser-bridge.mjs +48 -222
  33. package/src/local/browser-broker-routes.mjs +136 -0
  34. package/src/local/browser-broker-server.mjs +59 -0
  35. package/src/local/browser-request-registry.mjs +67 -0
  36. package/src/local/managed-job-lock.mjs +99 -0
  37. package/src/local/managed-job-projection.mjs +68 -0
  38. package/src/local/managed-job-runner.mjs +73 -0
  39. package/src/local/managed-job-storage.mjs +93 -0
  40. package/src/local/managed-jobs.mjs +12 -297
  41. package/src/local/relay-call-recovery.mjs +148 -0
  42. package/src/local/relay-connection.mjs +5 -0
  43. package/src/local/runtime-paths.mjs +107 -0
  44. package/src/local/runtime-relay.mjs +89 -0
  45. package/src/local/runtime-tool-handlers.mjs +66 -0
  46. package/src/local/runtime.mjs +80 -242
  47. package/src/local/windows-launcher.mjs +57 -0
  48. package/src/local/windows-service.mjs +7 -56
  49. package/src/worker/daemon-sockets.ts +10 -1
  50. package/src/worker/index.ts +51 -124
  51. package/src/worker/mcp-jsonrpc.ts +94 -0
  52. package/src/worker/oauth-authorization-page.ts +70 -0
  53. package/src/worker/oauth-controller.ts +9 -58
  54. package/src/worker/pending-call-contract.ts +26 -0
  55. package/src/worker/pending-calls.ts +41 -25
  56. package/src/worker/websocket-protocol.ts +24 -0
  57. package/tsconfig.local.json +7 -1
@@ -0,0 +1,142 @@
1
+ # Threat model
2
+
3
+ This document defines the security claims Machine Bridge is designed to make and the claims it explicitly does not make. It complements [SECURITY.md](../SECURITY.md) and the implementation detail in [ARCHITECTURE.md](ARCHITECTURE.md).
4
+
5
+ ## Scope
6
+
7
+ Machine Bridge combines a remote OAuth relay, a high-authority local runtime, optional local stdio transport, persistent managed jobs, and a browser extension. The primary security objective is to ensure that only explicitly authorized, policy-permitted operations cross those boundaries, with bounded inputs, fail-closed state transitions, and minimal sensitive observability.
8
+
9
+ The local runtime executes as the current OS user. Policy profiles restrict Machine Bridge behavior; they do not replace kernel, account, VM, container, browser-profile, or endpoint-security isolation.
10
+
11
+ ## Assets
12
+
13
+ The main protected assets are:
14
+
15
+ - workspace and local-user-accessible files;
16
+ - process execution authority and inherited environment values;
17
+ - daemon secret, account administration secret, account password verifiers, OAuth authorization codes, access tokens, and refresh tokens;
18
+ - Worker and Durable Object routing state;
19
+ - owner-only profile, lock, resource, service, and managed-job state;
20
+ - registered local resource contents and paths;
21
+ - browser pairing token, browser cookies, authenticated sessions, tabs, page content, and file-upload authority;
22
+ - release candidate hashes, observed-verification acceptance records, package contents, and source-release integrity;
23
+ - logs and diagnostics that could reveal paths, credentials, arguments, results, or user content.
24
+
25
+ ## Trust boundaries
26
+
27
+ ### Hosted client to Worker
28
+
29
+ The hosted client and its prompts, tools, extensions, and retrieved content are not automatically trusted. The Worker must validate OAuth state, PKCE, redirect and resource binding, account role/version, token expiry/rotation, MCP session state, request shape, body bounds, and allowed tool exposure.
30
+
31
+ ### Worker to local daemon
32
+
33
+ The Worker is a relay and authorization layer, not a source of local authority. The daemon accepts only an authenticated, version-compatible relay session. End-to-end readiness requires a local probe result through the active session. Every remote tool call carries bounded account authority that is rechecked locally.
34
+
35
+ ### MCP host to local stdio process
36
+
37
+ A local MCP host can invoke every tool allowed by the selected daemon policy. The host, model output, repository instructions, and retrieved content may be malicious or prompt-injected. A permissive policy therefore grants meaningful local-user authority.
38
+
39
+ ### Local runtime to operating system
40
+
41
+ The runtime relies on the OS account, filesystem permissions, macOS TCC/SIP, Windows ACLs, container/VM boundaries, and endpoint controls. Canonical workspace checks prevent path escape only when unrestricted paths are disabled. They do not protect against a malicious process already running with the same OS-user authority.
42
+
43
+ ### Local runtime to browser extension
44
+
45
+ The broker is loopback-only and pairing-token protected. The extension origin, protocol version, build version, capabilities, and handshake state are validated. The extension still runs inside a real browser profile and can reach the tabs, sessions, and origins granted by browser permissions.
46
+
47
+ ### Local runtime to persistent state
48
+
49
+ State paths, locks, plans, resources, and service definitions are security-sensitive. Reads are bounded and symlink-aware; writes use owner-only modes where supported and atomic replacement; locks bind tokens to process identity and start time; destructive removal validates ownership and expected layout.
50
+
51
+ ### Maintainer to release infrastructure
52
+
53
+ Repository checks reduce accidental or malicious package drift, but source hosting, npm ownership, protected environments, maintainer accounts, and human review remain external trust dependencies. Repository automation cannot manufacture independent review or claim live candidate success without observing the owner-started candidate. It may record acceptance after that observed verification.
54
+
55
+ ## Attacker models
56
+
57
+ Machine Bridge considers the following attackers and failure sources:
58
+
59
+ 1. **Malicious or compromised hosted MCP client** attempting unauthorized tools, token replay, OAuth redirect abuse, request smuggling, duplicate IDs, cancellation races, or excessive resource use.
60
+ 2. **Malicious local MCP host or model output** attempting to use allowed tools for destructive actions, credential discovery, shell injection, or persistence.
61
+ 3. **Malicious repository content** including instructions, package scripts, Git configuration, paths, symlinks, generated metadata, or files designed to influence an agent or execution boundary.
62
+ 4. **Malicious webpage or browser content** attempting prompt injection, deceptive selectors, sensitive-field disclosure, unsafe navigation, replay after ambiguous input dispatch, or extension-protocol abuse.
63
+ 5. **Local same-user process** attempting to race locks, replace files, impersonate stale processes, read local state, or interfere with loopback services.
64
+ 6. **Network attacker or misconfigured proxy** attempting interception, redirect manipulation, credential disclosure, stale deployment evidence, or ambiguous timeout outcomes.
65
+ 7. **Compromised dependency or build input** attempting package substitution, mutable workflow execution, install-script abuse, release drift, or sensitive artifact inclusion.
66
+ 8. **Operational failure** such as abrupt termination, PID reuse, clock rollback, partial writes, disk/permission errors, service-manager races, stale browser builds, or daemon replacement during active calls.
67
+
68
+ ## Security objectives
69
+
70
+ The implementation aims to preserve these invariants:
71
+
72
+ - deny unknown, malformed, stale, duplicated, unauthorized, or over-limit requests;
73
+ - intersect remote account authority with the daemon capability ceiling in both Worker and local runtime;
74
+ - keep transport authentication separate from tool authorization and OS authority;
75
+ - use direct argv execution without shell interpretation unless the explicit shell tool is authorized;
76
+ - canonicalize confined paths and reject symlink-based write escape;
77
+ - bound request bodies, messages, files, output, logs, state, retained results, and concurrency;
78
+ - ensure cancellation, timeout, disconnect, replacement, and shutdown have explicit process ownership and cleanup semantics;
79
+ - prevent a candidate daemon or extension from displacing a healthy incumbent before compatibility/readiness validation;
80
+ - avoid exposing secrets, arguments, results, resource contents, account credentials, or raw local paths in default logs and public metadata;
81
+ - make multi-stage state and file mutations atomic or recoverable without silent partial success;
82
+ - bind managed-job plans and release acceptance to cryptographic content hashes;
83
+ - fail closed when state, ownership, permissions, process identity, or destructive cleanup cannot be verified;
84
+ - keep supply-chain actions pinned, minimally permissioned, reviewed, and separately gated.
85
+
86
+ ## Non-goals
87
+
88
+ Machine Bridge does **not** claim to provide:
89
+
90
+ - kernel-enforced sandboxing, CPU/memory quotas, syscall filtering, or network isolation;
91
+ - hard multi-tenant isolation between mutually untrusted users sharing one daemon and OS account;
92
+ - protection from root, an administrator, or a fully compromised same-user account;
93
+ - complete prevention of prompt injection or malicious instructions when an authorized tool can perform the requested action;
94
+ - semantic detection of every secret, transformed credential, private document, or dangerous command;
95
+ - proof that the browser extension is loaded in a safe or isolated profile;
96
+ - rollback of a browser or external-system action after an ambiguous dispatch failure;
97
+ - guaranteed cleanup after power loss, kernel failure, filesystem corruption, or an uncooperative external platform;
98
+ - independent human code review while the project has only one active maintainer;
99
+ - npm trusted publishing until the package owner configures external OIDC trust and a protected publication environment.
100
+
101
+ ## Residual risks
102
+
103
+ ### Canonical `full` profile
104
+
105
+ `full` intentionally exposes the complete catalog, unrestricted local-user paths, shell execution, parent environment, and absolute path output. A malicious authorized client can use that authority destructively. The mitigation is a narrower profile or a separate low-privilege OS boundary, not additional warning text.
106
+
107
+ ### Application and browser automation
108
+
109
+ UI state can change between inspection and action. Pages can present deceptive content, and an authenticated browser profile may contain high-value sessions. Trusted input and replay controls reduce specific failure modes but do not make arbitrary web content trustworthy.
110
+
111
+ ### Package scripts and registered commands
112
+
113
+ Automatic command discovery does not inject script bodies into prompts, but invoking a package script still executes repository-controlled code. The active policy and human approval remain decisive.
114
+
115
+ ### Same-user interference
116
+
117
+ Owner-only permissions and process-identity locks reduce accidental and cross-account interference. They cannot reliably defend against a determined process running as the same OS user with equivalent filesystem and process rights.
118
+
119
+ ### Availability and resource exhaustion
120
+
121
+ Concurrency, message, output, and timeout limits bound many application-level resources. The daemon does not enforce OS CPU, memory, disk, or network quotas, so an authorized process can still consume host resources.
122
+
123
+ ### External governance and publication
124
+
125
+ Code cannot create a second independent reviewer, npm OIDC trust relationship, protected environment, or OpenSSF Best Practices registration. Those controls remain explicit external work rather than simulated repository compliance.
126
+
127
+ ## Validation map
128
+
129
+ The main regression suites cover:
130
+
131
+ - OAuth, account, policy, MCP session, daemon readiness, and relay replacement;
132
+ - path confinement, atomic writes, state locks, service lifecycle, managed-job recovery, and destructive cleanup;
133
+ - direct process boundaries, shell separation, process-tree termination, timeout, cancellation, and disconnect;
134
+ - browser pairing, version/capability handshake, owner/client broker routing, sensitive-field handling, trusted input, and CSP navigation;
135
+ - privacy redaction, package contents, release impact, interactive candidate acceptance, dependency integrity, CodeQL, and Scorecard findings;
136
+ - malformed, over-limit, concurrent, replayed, stale, and fault-injected inputs.
137
+
138
+ See [TESTING.md](TESTING.md) for the complete test inventory and [AUDIT.md](AUDIT.md) for reviewed failures and residual limitations.
139
+
140
+ ## Reporting
141
+
142
+ Report suspected vulnerabilities through the private process in [SECURITY.md](../SECURITY.md). Do not include real credentials, private user data, browser session material, or sensitive local paths in public issues or test fixtures.
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.10",
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
+ }