machine-bridge-mcp 1.2.7 → 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.
- package/CHANGELOG.md +23 -0
- package/CONTRIBUTING.md +37 -19
- package/README.md +120 -410
- package/SECURITY.md +2 -0
- package/browser-extension/manifest.json +2 -2
- package/docs/ARCHITECTURE.md +13 -4
- package/docs/AUDIT.md +22 -0
- package/docs/ENGINEERING.md +2 -2
- package/docs/OVERVIEW.md +113 -0
- package/docs/PROJECT_STANDARDS.md +9 -5
- package/docs/RELEASING.md +78 -33
- package/docs/TESTING.md +14 -7
- package/docs/THREAT_MODEL.md +142 -0
- package/package.json +18 -6
- package/scripts/check-plan.mjs +91 -0
- package/scripts/coverage-check.mjs +22 -0
- package/scripts/github-push.mjs +49 -0
- package/scripts/github-release.mjs +15 -8
- package/scripts/local-release-acceptance.mjs +186 -0
- package/scripts/release-acceptance.mjs +181 -0
- package/scripts/release-impact-check.mjs +19 -3
- package/scripts/release-state.mjs +1 -1
- package/scripts/run-checks.mjs +29 -0
- package/scripts/start-release-candidate.mjs +113 -0
- package/src/local/agent-context-projection.mjs +158 -0
- package/src/local/agent-context.mjs +23 -332
- package/src/local/agent-skill-discovery.mjs +230 -0
- package/src/local/agent-text-file.mjs +41 -0
- package/src/local/browser-bridge-http.mjs +48 -0
- package/src/local/browser-bridge.mjs +48 -222
- package/src/local/browser-broker-routes.mjs +136 -0
- package/src/local/browser-broker-server.mjs +59 -0
- package/src/local/browser-request-registry.mjs +67 -0
- package/src/local/managed-job-lock.mjs +99 -0
- package/src/local/managed-job-projection.mjs +68 -0
- package/src/local/managed-job-runner.mjs +73 -0
- package/src/local/managed-job-storage.mjs +93 -0
- package/src/local/managed-jobs.mjs +12 -297
- package/src/local/runtime-paths.mjs +107 -0
- package/src/local/runtime-relay.mjs +73 -0
- package/src/local/runtime-tool-handlers.mjs +66 -0
- package/src/local/runtime.mjs +22 -204
- package/src/local/windows-launcher.mjs +57 -0
- package/src/local/windows-service.mjs +7 -56
- package/src/worker/index.ts +9 -118
- package/src/worker/mcp-jsonrpc.ts +94 -0
- package/src/worker/oauth-authorization-page.ts +70 -0
- package/src/worker/oauth-controller.ts +9 -58
- package/src/worker/websocket-protocol.ts +24 -0
- package/tsconfig.local.json +6 -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.
|
|
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
|
|
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",
|
|
@@ -117,12 +118,23 @@
|
|
|
117
118
|
"typecheck:local": "tsc -p tsconfig.local.json --noEmit",
|
|
118
119
|
"runtime-boundaries:test": "node tests/runtime-boundaries-test.mjs",
|
|
119
120
|
"worker-oauth-controller:test": "node tests/worker-oauth-controller-test.mjs",
|
|
120
|
-
"cli-service:test": "node tests/cli-service-test.mjs"
|
|
121
|
+
"cli-service:test": "node tests/cli-service-test.mjs",
|
|
122
|
+
"release:acceptance:test": "node tests/release-acceptance-test.mjs",
|
|
123
|
+
"release:candidate": "npm run check && node scripts/local-release-acceptance.mjs --prepare",
|
|
124
|
+
"release:accept": "node scripts/local-release-acceptance.mjs --record",
|
|
125
|
+
"release:acceptance:verify": "node scripts/local-release-acceptance.mjs --verify",
|
|
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"
|
|
121
133
|
},
|
|
122
134
|
"dependencies": {
|
|
123
135
|
"https-proxy-agent": "9.1.0",
|
|
124
136
|
"proxy-from-env": "2.1.0",
|
|
125
|
-
"wrangler": "4.
|
|
137
|
+
"wrangler": "4.112.0",
|
|
126
138
|
"ws": "8.21.1"
|
|
127
139
|
},
|
|
128
140
|
"devDependencies": {
|
|
@@ -161,8 +173,8 @@
|
|
|
161
173
|
"allowScripts": {
|
|
162
174
|
"esbuild@0.28.1": true,
|
|
163
175
|
"sharp@0.34.5": true,
|
|
164
|
-
"
|
|
165
|
-
"
|
|
176
|
+
"fsevents": false,
|
|
177
|
+
"workerd@1.20260714.1": true
|
|
166
178
|
},
|
|
167
179
|
"packageManager": "npm@12.0.1",
|
|
168
180
|
"devEngines": {
|
|
@@ -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)) {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { verifyCurrentReleaseAcceptance } from "./release-acceptance.mjs";
|
|
7
|
+
|
|
8
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const status = output("git", ["status", "--porcelain"]);
|
|
12
|
+
if (status) throw new Error(`working tree is not clean:\n${status}`);
|
|
13
|
+
const branch = output("git", ["branch", "--show-current"]);
|
|
14
|
+
if (!branch) throw new Error("cannot push from a detached HEAD");
|
|
15
|
+
if (branch === "main") throw new Error("direct pushes to main are prohibited; push a reviewed branch and merge through a pull request");
|
|
16
|
+
|
|
17
|
+
const acceptance = verifyCurrentReleaseAcceptance(root);
|
|
18
|
+
if (acceptance.required) {
|
|
19
|
+
const path = `release-acceptance/v${acceptance.metadata.package_version}.json`;
|
|
20
|
+
run("git", ["ls-files", "--error-unmatch", path], { capture: true });
|
|
21
|
+
console.log(`Verified interactive local candidate acceptance for ${acceptance.metadata.filename}.`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
run("git", ["push", "--set-upstream", "origin", "HEAD"]);
|
|
25
|
+
console.log(`Pushed accepted branch ${branch} to origin.`);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error(`GitHub push blocked: ${error?.message || error}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function output(command, args) {
|
|
32
|
+
return String(run(command, args, { capture: true }).stdout || "").trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function run(command, args, options = {}) {
|
|
36
|
+
const result = spawnSync(command, args, {
|
|
37
|
+
cwd: root,
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
env: process.env,
|
|
40
|
+
stdio: options.capture ? "pipe" : "inherit",
|
|
41
|
+
windowsHide: true,
|
|
42
|
+
});
|
|
43
|
+
if (result.error) throw result.error;
|
|
44
|
+
if (result.status !== 0) {
|
|
45
|
+
const detail = options.capture ? String(result.stderr || result.stdout).trim() : "";
|
|
46
|
+
throw new Error(`${command} ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`);
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
@@ -12,6 +12,7 @@ import { spawnSync } from "node:child_process";
|
|
|
12
12
|
import { runNetworkCommand } from "./network-retry.mjs";
|
|
13
13
|
import { requireSuccessfulWorkflowRun } from "./release-ci.mjs";
|
|
14
14
|
import { tagSyncError } from "./release-state.mjs";
|
|
15
|
+
import { verifyCurrentReleaseAcceptance } from "./release-acceptance.mjs";
|
|
15
16
|
import { fileURLToPath } from "node:url";
|
|
16
17
|
|
|
17
18
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -178,6 +179,7 @@ function releaseInfo(tag) {
|
|
|
178
179
|
|
|
179
180
|
function assertCoreSync({ requireReleaseAsset }) {
|
|
180
181
|
const pkg = packageMetadata();
|
|
182
|
+
assertLocalAcceptance();
|
|
181
183
|
const tag = `v${pkg.version}`;
|
|
182
184
|
const head = output("git", ["rev-parse", "HEAD"]);
|
|
183
185
|
const originMain = output("git", ["rev-parse", "origin/main"]);
|
|
@@ -299,18 +301,12 @@ function publishCurrent() {
|
|
|
299
301
|
run("npm", ["run", "check"]);
|
|
300
302
|
run("npm", ["run", "version:check"]);
|
|
301
303
|
ensureClean();
|
|
304
|
+
assertLocalAcceptance();
|
|
302
305
|
|
|
303
306
|
const head = output("git", ["rev-parse", "HEAD"]);
|
|
304
307
|
const originMain = output("git", ["rev-parse", "origin/main"]);
|
|
305
308
|
if (head !== originMain) {
|
|
306
|
-
|
|
307
|
-
capture: true,
|
|
308
|
-
allowFailure: true,
|
|
309
|
-
});
|
|
310
|
-
if (ancestor.status !== 0) {
|
|
311
|
-
fail("origin/main is not an ancestor of HEAD; refusing a non-fast-forward release");
|
|
312
|
-
}
|
|
313
|
-
runNetwork("git", ["push", "origin", "HEAD:main"]);
|
|
309
|
+
fail("HEAD does not match origin/main; local acceptance must be committed, pushed through npm run github:push, reviewed, and merged before release publication");
|
|
314
310
|
}
|
|
315
311
|
assertSuccessfulCi(head);
|
|
316
312
|
|
|
@@ -342,6 +338,17 @@ function publishCurrent() {
|
|
|
342
338
|
assertCoreSync({ requireReleaseAsset: true });
|
|
343
339
|
}
|
|
344
340
|
|
|
341
|
+
function assertLocalAcceptance() {
|
|
342
|
+
try {
|
|
343
|
+
const result = verifyCurrentReleaseAcceptance(root);
|
|
344
|
+
if (result.required) {
|
|
345
|
+
console.log(`Interactive local candidate acceptance matches ${result.metadata.filename} (${result.metadata.shasum}).`);
|
|
346
|
+
}
|
|
347
|
+
} catch (error) {
|
|
348
|
+
fail(String(error?.message || error));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
345
352
|
function backfillMissingReleases() {
|
|
346
353
|
ensureClean();
|
|
347
354
|
fetchRemote();
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
mkdirSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { dirname, join, resolve } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import {
|
|
15
|
+
ACCEPTANCE_CONFIRMATION,
|
|
16
|
+
ACCEPTANCE_SCHEMA_VERSION,
|
|
17
|
+
acceptancePath,
|
|
18
|
+
packProject,
|
|
19
|
+
requiresLocalAcceptance,
|
|
20
|
+
verifyAcceptanceRecord,
|
|
21
|
+
verifyCurrentReleaseAcceptance,
|
|
22
|
+
verifyTarball,
|
|
23
|
+
} from "./release-acceptance.mjs";
|
|
24
|
+
|
|
25
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
26
|
+
const candidateDirectory = join(root, ".release-candidate");
|
|
27
|
+
const candidateManifestPath = join(candidateDirectory, "manifest.json");
|
|
28
|
+
const mode = process.argv[2] || "--verify";
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
if (mode === "--prepare") prepareCandidate();
|
|
32
|
+
else if (mode === "--record") recordAcceptance();
|
|
33
|
+
else if (mode === "--verify") verifyAcceptance();
|
|
34
|
+
else fail("usage: node scripts/local-release-acceptance.mjs [--prepare|--record|--verify]");
|
|
35
|
+
} catch (error) {
|
|
36
|
+
fail(error?.message || error);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function prepareCandidate() {
|
|
40
|
+
const pkg = readPackage();
|
|
41
|
+
if (!requiresLocalAcceptance(pkg.version)) {
|
|
42
|
+
throw new Error(`local acceptance policy begins at ${pkg.name} 1.2.8; current version is ${pkg.version}`);
|
|
43
|
+
}
|
|
44
|
+
rmSync(candidateDirectory, { recursive: true, force: true });
|
|
45
|
+
mkdirSync(candidateDirectory, { recursive: true });
|
|
46
|
+
const metadata = packProject(root, candidateDirectory);
|
|
47
|
+
const manifest = {
|
|
48
|
+
schema_version: ACCEPTANCE_SCHEMA_VERSION,
|
|
49
|
+
result: "pending",
|
|
50
|
+
...metadata,
|
|
51
|
+
prepared_at: new Date().toISOString(),
|
|
52
|
+
};
|
|
53
|
+
writeFileSync(candidateManifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
54
|
+
const phrase = confirmationPhrase(pkg.name, pkg.version);
|
|
55
|
+
console.log(`Release candidate created: ${join(candidateDirectory, metadata.filename)}`);
|
|
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:");
|
|
60
|
+
console.log(`npm run release:accept -- --confirm \"${phrase}\"`);
|
|
61
|
+
console.log("Automated tests alone do not authorize acceptance or the first GitHub push.");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function recordAcceptance() {
|
|
65
|
+
const pkg = readPackage();
|
|
66
|
+
const supplied = argumentValue("--confirm");
|
|
67
|
+
const expected = confirmationPhrase(pkg.name, pkg.version);
|
|
68
|
+
if (supplied !== expected) {
|
|
69
|
+
throw new Error(`interactive candidate verification confirmation must exactly match: ${expected}`);
|
|
70
|
+
}
|
|
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
|
+
}
|
|
75
|
+
verifyTarball(join(candidateDirectory, pending.filename), pending);
|
|
76
|
+
|
|
77
|
+
const verificationDirectory = join(candidateDirectory, "verification");
|
|
78
|
+
rmSync(verificationDirectory, { recursive: true, force: true });
|
|
79
|
+
mkdirSync(verificationDirectory, { recursive: true });
|
|
80
|
+
const current = packProject(root, verificationDirectory);
|
|
81
|
+
for (const key of ["package_name", "package_version", "filename", "shasum", "integrity"]) {
|
|
82
|
+
if (pending[key] !== current[key]) {
|
|
83
|
+
throw new Error(`source changed after candidate preparation: ${key} no longer matches`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const record = {
|
|
88
|
+
schema_version: ACCEPTANCE_SCHEMA_VERSION,
|
|
89
|
+
result: "passed",
|
|
90
|
+
confirmation: ACCEPTANCE_CONFIRMATION,
|
|
91
|
+
package_name: current.package_name,
|
|
92
|
+
package_version: current.package_version,
|
|
93
|
+
filename: current.filename,
|
|
94
|
+
shasum: current.shasum,
|
|
95
|
+
integrity: current.integrity,
|
|
96
|
+
accepted_at: new Date().toISOString(),
|
|
97
|
+
package_content_sha256: computePortablePackageDigest(),
|
|
98
|
+
};
|
|
99
|
+
verifyAcceptanceRecord(record, current);
|
|
100
|
+
const path = acceptancePath(root, pkg.version);
|
|
101
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
102
|
+
writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
103
|
+
console.log(`Interactive local candidate acceptance recorded: ${path}`);
|
|
104
|
+
console.log("Commit this record with the candidate. Any packaged-file change invalidates it.");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function verifyAcceptance() {
|
|
108
|
+
const result = verifyCurrentReleaseAcceptance(root);
|
|
109
|
+
if (!result.required) {
|
|
110
|
+
console.log(`Local release acceptance is not required for ${result.version}.`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
console.log(`Local release acceptance matches ${result.metadata.filename} (${result.metadata.shasum}).`);
|
|
114
|
+
}
|
|
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
|
+
|
|
160
|
+
function readPackage() {
|
|
161
|
+
return readJson(join(root, "package.json"), "package.json");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function readJson(path, label) {
|
|
165
|
+
try {
|
|
166
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
167
|
+
} catch (error) {
|
|
168
|
+
throw new Error(`${label} is unavailable or invalid: ${error.message}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function argumentValue(name) {
|
|
173
|
+
const exact = process.argv.find((value) => value.startsWith(`${name}=`));
|
|
174
|
+
if (exact) return exact.slice(name.length + 1);
|
|
175
|
+
const index = process.argv.indexOf(name);
|
|
176
|
+
return index >= 0 ? process.argv[index + 1] : "";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function confirmationPhrase(name, version) {
|
|
180
|
+
return `I VERIFIED ${name} ${version} CANDIDATE ON THE OWNER MACHINE AND IT WORKS`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function fail(message) {
|
|
184
|
+
console.error(`local release acceptance failed: ${message}`);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|