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.
- package/CHANGELOG.md +12 -0
- package/CONTRIBUTING.md +16 -5
- package/README.md +115 -416
- package/SECURITY.md +2 -0
- package/browser-extension/manifest.json +2 -2
- package/docs/ARCHITECTURE.md +10 -3
- package/docs/AUDIT.md +10 -0
- package/docs/ENGINEERING.md +2 -2
- package/docs/OVERVIEW.md +113 -0
- package/docs/PROJECT_STANDARDS.md +4 -4
- package/docs/RELEASING.md +25 -12
- package/docs/TESTING.md +12 -8
- package/docs/THREAT_MODEL.md +142 -0
- package/package.json +10 -3
- package/scripts/check-plan.mjs +91 -0
- package/scripts/coverage-check.mjs +22 -0
- package/scripts/github-push.mjs +1 -1
- package/scripts/github-release.mjs +1 -1
- package/scripts/local-release-acceptance.mjs +56 -6
- package/scripts/release-acceptance.mjs +15 -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
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",
|
|
@@ -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)) {
|
package/scripts/github-push.mjs
CHANGED
|
@@ -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
|
|
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(`
|
|
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("
|
|
54
|
-
console.log("
|
|
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("
|
|
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(`
|
|
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(`
|
|
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
|
|
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
|
|
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
|
-
|
|
96
|
-
|
|
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
|
|
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
|
+
}
|