machine-bridge-mcp 2.0.0 → 3.0.0-beta.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 +101 -0
- package/CONTRIBUTING.md +20 -25
- package/README.md +21 -14
- package/SECURITY.md +147 -123
- package/browser-extension/manifest.json +3 -2
- package/browser-extension/service-worker.js +31 -12
- package/docs/ARCHITECTURE.md +27 -13
- package/docs/AUDIT.md +85 -2
- package/docs/CLIENTS.md +2 -2
- package/docs/ENGINEERING.md +9 -3
- package/docs/GETTING_STARTED.md +10 -9
- package/docs/LOCAL_AUTHORIZATION.md +115 -72
- package/docs/LOGGING.md +12 -3
- package/docs/MANAGED_JOBS.md +24 -29
- package/docs/MULTI_ACCOUNT.md +98 -37
- package/docs/OPERATIONS.md +33 -22
- package/docs/OVERVIEW.md +7 -7
- package/docs/PROJECT_STANDARDS.md +9 -5
- package/docs/RELEASING.md +171 -64
- package/docs/TESTING.md +19 -7
- package/docs/THREAT_MODEL.md +138 -67
- package/docs/TOOL_REFERENCE.md +2 -2
- package/docs/UPGRADING.md +102 -18
- package/native/macos/MachineBridgeTrustBroker.swift +233 -0
- package/package.json +36 -5
- package/scripts/candidate-runtime-store.mjs +38 -0
- package/scripts/check-plan.mjs +19 -0
- package/scripts/check-runner.mjs +4 -1
- package/scripts/commit-message-check.mjs +5 -2
- package/scripts/coverage-check.mjs +30 -2
- package/scripts/github-push.mjs +11 -0
- package/scripts/github-release.mjs +38 -7
- package/scripts/install-published-prerelease.mjs +167 -0
- package/scripts/local-release-acceptance.mjs +23 -12
- package/scripts/npm-publication-policy.mjs +16 -0
- package/scripts/prerelease-activation.mjs +79 -0
- package/scripts/privacy-check.mjs +7 -5
- package/scripts/promotion-digest.mjs +121 -0
- package/scripts/publish-npm.mjs +51 -0
- package/scripts/published-release.mjs +80 -0
- package/scripts/release-acceptance.mjs +30 -1
- package/scripts/release-candidate-manifest.mjs +40 -0
- package/scripts/release-channel.mjs +106 -0
- package/scripts/release-impact-check.mjs +12 -22
- package/scripts/release-soak.mjs +237 -0
- package/scripts/start-release-candidate.mjs +113 -7
- package/src/local/account-access.mjs +6 -0
- package/src/local/account-admin.mjs +34 -16
- package/src/local/agent-context.mjs +45 -42
- package/src/local/app-automation.mjs +9 -4
- package/src/local/authority-context.mjs +106 -0
- package/src/local/browser-bridge-http.mjs +4 -1
- package/src/local/browser-bridge.mjs +14 -9
- package/src/local/browser-broker-routes.mjs +4 -0
- package/src/local/browser-broker-server.mjs +5 -4
- package/src/local/browser-extension-identity.mjs +50 -0
- package/src/local/browser-extension-protocol.mjs +9 -4
- package/src/local/browser-operation-service.mjs +7 -0
- package/src/local/browser-pairing-http.mjs +36 -0
- package/src/local/browser-pairing-store.mjs +56 -47
- package/src/local/cli-account-admin.mjs +41 -7
- package/src/local/cli-activate.mjs +90 -0
- package/src/local/cli-approval.mjs +14 -58
- package/src/local/cli-local-admin.mjs +5 -15
- package/src/local/cli-options.mjs +11 -7
- package/src/local/cli-service.mjs +35 -5
- package/src/local/cli.mjs +112 -58
- package/src/local/daemon-process.mjs +6 -0
- package/src/local/delegated-process-sandbox.mjs +147 -0
- package/src/local/device-identity.mjs +157 -48
- package/src/local/device-root-provider.mjs +87 -0
- package/src/local/errors.mjs +25 -3
- package/src/local/git-service.mjs +16 -15
- package/src/local/job-runner.mjs +35 -27
- package/src/local/log.mjs +14 -1
- package/src/local/macos-trust-broker.mjs +320 -0
- package/src/local/managed-job-lock.mjs +18 -0
- package/src/local/managed-job-projection.mjs +9 -1
- package/src/local/managed-job-runner.mjs +13 -1
- package/src/local/managed-job-storage.mjs +2 -1
- package/src/local/managed-job-terminal.mjs +138 -0
- package/src/local/managed-jobs.mjs +88 -32
- package/src/local/operation-authorization.mjs +75 -239
- package/src/local/operation-risk.mjs +28 -0
- package/src/local/operation-state-lock.mjs +7 -86
- package/src/local/owner-state-lock.mjs +104 -0
- package/src/local/policy.mjs +19 -0
- package/src/local/process-execution.mjs +21 -9
- package/src/local/process-sessions.mjs +27 -17
- package/src/local/process-tracker.mjs +30 -6
- package/src/local/process-tree-ownership.mjs +59 -0
- package/src/local/process-tree.mjs +22 -18
- package/src/local/relay-connection.mjs +47 -35
- package/src/local/relay-diagnostics.mjs +65 -0
- package/src/local/remote-configuration.mjs +48 -0
- package/src/local/runtime-activation.mjs +141 -0
- package/src/local/runtime-diagnostics.mjs +21 -0
- package/src/local/runtime-relay.mjs +9 -8
- package/src/local/runtime-reporting.mjs +8 -3
- package/src/local/runtime-tool-handlers.mjs +8 -8
- package/src/local/runtime.mjs +73 -39
- package/src/local/secure-file.mjs +5 -1
- package/src/local/security-audit-log.mjs +204 -0
- package/src/local/service-convergence.mjs +17 -1
- package/src/local/service-ownership.mjs +17 -0
- package/src/local/service-restart-handoff.mjs +50 -0
- package/src/local/service-restart-scheduler.mjs +49 -0
- package/src/local/service-status.mjs +47 -0
- package/src/local/service.mjs +142 -43
- package/src/local/state-inventory.mjs +21 -3
- package/src/local/state.mjs +155 -17
- package/src/local/stdio.mjs +5 -0
- package/src/local/tool-executor.mjs +39 -6
- package/src/local/tool-result-boundary.mjs +51 -0
- package/src/local/tools.mjs +1 -0
- package/src/local/trusted-executable.mjs +53 -0
- package/src/local/trusted-git-executable.mjs +34 -0
- package/src/local/trusted-github-cli.mjs +24 -0
- package/src/local/windows-service.mjs +20 -0
- package/src/local/worker-deployment.mjs +3 -4
- package/src/local/worker-secret-file.mjs +2 -2
- package/src/local/workspace-file-service.mjs +34 -31
- package/src/shared/admin-auth.d.mts +2 -1
- package/src/shared/admin-auth.mjs +3 -3
- package/src/shared/device-session-auth.d.mts +20 -0
- package/src/shared/device-session-auth.mjs +51 -0
- package/src/shared/server-metadata.json +1 -1
- package/src/shared/tool-catalog.json +2 -2
- package/src/worker/account-admin.ts +23 -22
- package/src/worker/authority.ts +9 -2
- package/src/worker/daemon-auth.ts +45 -67
- package/src/worker/daemon-sockets.ts +12 -2
- package/src/worker/device-session-verifier.ts +129 -0
- package/src/worker/dpop.ts +151 -0
- package/src/worker/http.ts +47 -16
- package/src/worker/index.ts +53 -16
- package/src/worker/nonce-store.ts +1 -5
- package/src/worker/oauth-client-admin.ts +45 -0
- package/src/worker/oauth-controller.ts +55 -6
- package/src/worker/oauth-refresh-families.ts +21 -7
- package/src/worker/oauth-state.ts +6 -0
- package/src/worker/oauth-tokens.ts +28 -8
- package/src/worker/websocket-protocol.ts +8 -2
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { defaultStateRoot, expandHome } from "../src/local/state.mjs";
|
|
9
|
+
import { writePrereleaseActivation } from "./prerelease-activation.mjs";
|
|
10
|
+
import { computePromotionContentDigest } from "./promotion-digest.mjs";
|
|
11
|
+
import { readPublishedNpmPrerelease } from "./published-release.mjs";
|
|
12
|
+
import { verifyCurrentReleaseAcceptance } from "./release-acceptance.mjs";
|
|
13
|
+
import { assertSoakEligiblePrerelease } from "./release-channel.mjs";
|
|
14
|
+
|
|
15
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
16
|
+
const npmCli = process.env.npm_execpath;
|
|
17
|
+
let previousInstallation = null;
|
|
18
|
+
let installedPrerelease = "";
|
|
19
|
+
if (!npmCli) fail("published prerelease installation must run through npm");
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
if (!process.argv.includes("--allow-worker-deploy")) {
|
|
23
|
+
throw new Error("published prerelease activation updates the global package, same-name Worker, and login daemon; rerun with --allow-worker-deploy");
|
|
24
|
+
}
|
|
25
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
26
|
+
const requested = argumentValue("--version") || pkg.version;
|
|
27
|
+
const prerelease = assertSoakEligiblePrerelease(requested);
|
|
28
|
+
if (pkg.version !== prerelease.raw) {
|
|
29
|
+
throw new Error(`source checkout version ${pkg.version} does not match requested prerelease ${prerelease.raw}`);
|
|
30
|
+
}
|
|
31
|
+
const acceptance = verifyCurrentReleaseAcceptance(root);
|
|
32
|
+
if (!acceptance.required || acceptance.metadata.package_version !== prerelease.raw) {
|
|
33
|
+
throw new Error("published prerelease installation requires the locally accepted exact prerelease source");
|
|
34
|
+
}
|
|
35
|
+
const promotionDigest = computePromotionContentDigest(root);
|
|
36
|
+
if (acceptance.record.promotion_content_sha256 !== promotionDigest) {
|
|
37
|
+
throw new Error("current source promotion digest does not match local candidate acceptance");
|
|
38
|
+
}
|
|
39
|
+
const published = readPublishedNpmPrerelease(pkg.name, prerelease.raw, prerelease.npmTag);
|
|
40
|
+
if (published.integrity !== acceptance.metadata.integrity || published.shasum !== acceptance.metadata.shasum) {
|
|
41
|
+
throw new Error("npm prerelease bytes do not match the locally accepted candidate");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
previousInstallation = currentGlobalInstallation(pkg.name);
|
|
45
|
+
runNpm([
|
|
46
|
+
"install", "--global", "--omit=optional",
|
|
47
|
+
"--allow-scripts=esbuild,workerd,sharp,fsevents",
|
|
48
|
+
`${pkg.name}@${prerelease.raw}`,
|
|
49
|
+
]);
|
|
50
|
+
const installed = currentGlobalInstallation(pkg.name);
|
|
51
|
+
if (!installed || installed.version !== prerelease.raw) {
|
|
52
|
+
throw new Error(`global prerelease installation did not converge on ${prerelease.raw}`);
|
|
53
|
+
}
|
|
54
|
+
installedPrerelease = prerelease.raw;
|
|
55
|
+
|
|
56
|
+
const stateRoot = resolve(expandHome(argumentValue("--state-dir") || defaultStateRoot()));
|
|
57
|
+
const forwarded = forwardedActivationArgs();
|
|
58
|
+
const activation = runActivation(installed.entry, ["activate", ...forwarded, "--state-dir", stateRoot, "--json"]);
|
|
59
|
+
if (activation.version !== prerelease.raw || activation.daemon?.version !== prerelease.raw || activation.worker?.health?.version !== prerelease.raw) {
|
|
60
|
+
throw new Error("published prerelease activation did not converge on the exact registry version");
|
|
61
|
+
}
|
|
62
|
+
const recordPath = writePrereleaseActivation({
|
|
63
|
+
schema_version: 1,
|
|
64
|
+
package_name: pkg.name,
|
|
65
|
+
package_version: prerelease.raw,
|
|
66
|
+
source: "npm-prerelease",
|
|
67
|
+
shasum: published.shasum,
|
|
68
|
+
integrity: published.integrity,
|
|
69
|
+
promotion_content_sha256: promotionDigest,
|
|
70
|
+
activated_at: new Date().toISOString(),
|
|
71
|
+
published_at: published.publishedAt,
|
|
72
|
+
npm_dist_tag: prerelease.npmTag,
|
|
73
|
+
workspace_hash: workspaceHash(activation.workspace),
|
|
74
|
+
runtime_entry: installed.entry,
|
|
75
|
+
...(previousInstallation ? { previous: previousInstallation } : {}),
|
|
76
|
+
}, stateRoot);
|
|
77
|
+
|
|
78
|
+
console.log(`Published prerelease activated: ${prerelease.raw}`);
|
|
79
|
+
console.log(`Worker and login daemon version: ${activation.version}`);
|
|
80
|
+
console.log(`Soak activation record: ${recordPath}`);
|
|
81
|
+
console.log("Use this prerelease normally. Any blocking issue requires a new beta/rc version and restarts the soak clock.");
|
|
82
|
+
} catch (error) {
|
|
83
|
+
const message = boundedDiagnostic(error?.message || error);
|
|
84
|
+
if (installedPrerelease) {
|
|
85
|
+
const previousVersion = previousInstallation?.version || "unknown";
|
|
86
|
+
fail(`${message}. The global package is now ${installedPrerelease}, but Worker/service activation may not have converged. Previous global version: ${previousVersion}. Preserve state and logs; fix forward with the exact prerelease or restore package, Worker, service definition, browser extension, and state as one verified unit.`);
|
|
87
|
+
}
|
|
88
|
+
fail(message);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function currentGlobalInstallation(packageName) {
|
|
92
|
+
try {
|
|
93
|
+
const globalRoot = runNpm(["root", "--global"]).stdout.trim();
|
|
94
|
+
const packageRoot = join(globalRoot, packageName);
|
|
95
|
+
const packagePath = join(packageRoot, "package.json");
|
|
96
|
+
const entry = join(packageRoot, "bin", "machine-mcp.mjs");
|
|
97
|
+
if (!existsSync(packagePath) || !existsSync(entry)) return null;
|
|
98
|
+
const value = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
99
|
+
return { version: String(value.version || ""), entry };
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function runActivation(entry, args) {
|
|
106
|
+
const result = spawnSync(process.execPath, [entry, ...args], {
|
|
107
|
+
cwd: root,
|
|
108
|
+
env: process.env,
|
|
109
|
+
encoding: "utf8",
|
|
110
|
+
timeout: 300_000,
|
|
111
|
+
windowsHide: true,
|
|
112
|
+
});
|
|
113
|
+
if (result.error) throw result.error;
|
|
114
|
+
if (result.status !== 0) throw new Error(`prerelease runtime activation failed: ${boundedDiagnostic(result.stderr || result.stdout)}`);
|
|
115
|
+
try { return JSON.parse(result.stdout); } catch { throw new Error("prerelease runtime activation did not return valid JSON"); }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function runNpm(args) {
|
|
119
|
+
const result = spawnSync(process.execPath, [npmCli, ...args], {
|
|
120
|
+
cwd: root,
|
|
121
|
+
env: process.env,
|
|
122
|
+
encoding: "utf8",
|
|
123
|
+
timeout: 300_000,
|
|
124
|
+
windowsHide: true,
|
|
125
|
+
});
|
|
126
|
+
if (result.error) throw result.error;
|
|
127
|
+
if (result.status !== 0) throw new Error(`npm ${args[0]} failed: ${boundedDiagnostic(result.stderr || result.stdout)}`);
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function forwardedActivationArgs() {
|
|
132
|
+
const out = [];
|
|
133
|
+
const skipWithValue = new Set(["--version", "--state-dir"]);
|
|
134
|
+
for (let index = 2; index < process.argv.length; index += 1) {
|
|
135
|
+
const value = process.argv[index];
|
|
136
|
+
if (value === "--allow-worker-deploy") continue;
|
|
137
|
+
if (skipWithValue.has(value)) { index += 1; continue; }
|
|
138
|
+
if (["--version=", "--state-dir="].some((prefix) => value.startsWith(prefix))) continue;
|
|
139
|
+
if (value === "--json" || value.startsWith("--json=")) continue;
|
|
140
|
+
out.push(value);
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function argumentValue(name) {
|
|
146
|
+
const exact = process.argv.find((value) => value.startsWith(`${name}=`));
|
|
147
|
+
if (exact) return exact.slice(name.length + 1);
|
|
148
|
+
const index = process.argv.indexOf(name);
|
|
149
|
+
return index >= 0 ? process.argv[index + 1] : "";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function workspaceHash(workspace) {
|
|
153
|
+
const value = process.platform === "win32" ? String(workspace).toLowerCase() : String(workspace);
|
|
154
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 24);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function boundedDiagnostic(value) {
|
|
158
|
+
const home = String(process.env.HOME || process.env.USERPROFILE || "");
|
|
159
|
+
let text = String(value || "unknown error").replace(/[\r\n\t]+/g, " ").trim();
|
|
160
|
+
if (home) text = text.split(home).join("<home>");
|
|
161
|
+
return text.slice(0, 1600);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function fail(message) {
|
|
165
|
+
console.error(`published prerelease installation failed: ${message}`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
@@ -5,14 +5,13 @@ import {
|
|
|
5
5
|
mkdtempSync,
|
|
6
6
|
readFileSync,
|
|
7
7
|
rmSync,
|
|
8
|
-
writeFileSync,
|
|
9
8
|
} from "node:fs";
|
|
10
9
|
import { spawnSync } from "node:child_process";
|
|
11
10
|
import { tmpdir } from "node:os";
|
|
12
11
|
import { dirname, join, resolve } from "node:path";
|
|
13
12
|
import { fileURLToPath } from "node:url";
|
|
14
13
|
import {
|
|
15
|
-
|
|
14
|
+
acceptanceConfirmationForVersion,
|
|
16
15
|
ACCEPTANCE_SCHEMA_VERSION,
|
|
17
16
|
acceptancePath,
|
|
18
17
|
packProject,
|
|
@@ -21,6 +20,10 @@ import {
|
|
|
21
20
|
verifyCurrentReleaseAcceptance,
|
|
22
21
|
verifyTarball,
|
|
23
22
|
} from "./release-acceptance.mjs";
|
|
23
|
+
import { computePromotionContentDigest } from "./promotion-digest.mjs";
|
|
24
|
+
import { parseReleaseVersion } from "./release-channel.mjs";
|
|
25
|
+
import { validateCandidateManifest } from "./release-candidate-manifest.mjs";
|
|
26
|
+
import { replaceFileAtomicallySync } from "../src/local/exclusive-file.mjs";
|
|
24
27
|
|
|
25
28
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
26
29
|
const candidateDirectory = join(root, ".release-candidate");
|
|
@@ -48,14 +51,16 @@ function prepareCandidate() {
|
|
|
48
51
|
schema_version: ACCEPTANCE_SCHEMA_VERSION,
|
|
49
52
|
result: "pending",
|
|
50
53
|
...metadata,
|
|
54
|
+
promotion_content_sha256: computePromotionContentDigest(root),
|
|
51
55
|
prepared_at: new Date().toISOString(),
|
|
52
56
|
};
|
|
53
|
-
|
|
57
|
+
replaceFileAtomicallySync(candidateManifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
54
58
|
const phrase = confirmationPhrase(pkg.name, pkg.version);
|
|
55
59
|
console.log(`Release candidate created: ${join(candidateDirectory, metadata.filename)}`);
|
|
56
|
-
console.log("
|
|
57
|
-
|
|
58
|
-
console.log("
|
|
60
|
+
console.log("The repository owner activates this exact candidate with one persistent owner-side command:");
|
|
61
|
+
parseReleaseVersion(pkg.version);
|
|
62
|
+
console.log("npm run release:candidate:activate -- --allow-worker-deploy");
|
|
63
|
+
console.log("The owner runs this one command. It installs the exact candidate in the private state root, updates the same-name Worker, verifies candidate relay readiness, replaces the login daemon, verifies the background handoff, and exits while the service remains active.");
|
|
59
64
|
console.log("After that observed live verification succeeds, the coding agent records acceptance with:");
|
|
60
65
|
console.log(`npm run release:accept -- --confirm \"${phrase}\"`);
|
|
61
66
|
console.log("Automated tests alone do not authorize acceptance or the first GitHub push.");
|
|
@@ -68,10 +73,10 @@ function recordAcceptance() {
|
|
|
68
73
|
if (supplied !== expected) {
|
|
69
74
|
throw new Error(`interactive candidate verification confirmation must exactly match: ${expected}`);
|
|
70
75
|
}
|
|
71
|
-
const pending = readJson(candidateManifestPath, "release candidate manifest")
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
76
|
+
const pending = validateCandidateManifest(readJson(candidateManifestPath, "release candidate manifest"), {
|
|
77
|
+
packageName: pkg.name,
|
|
78
|
+
packageVersion: pkg.version,
|
|
79
|
+
});
|
|
75
80
|
verifyTarball(join(candidateDirectory, pending.filename), pending);
|
|
76
81
|
|
|
77
82
|
const verificationDirectory = join(candidateDirectory, "verification");
|
|
@@ -84,10 +89,15 @@ function recordAcceptance() {
|
|
|
84
89
|
}
|
|
85
90
|
}
|
|
86
91
|
|
|
92
|
+
const promotionDigest = computePromotionContentDigest(root);
|
|
93
|
+
if (pending.promotion_content_sha256 !== promotionDigest) {
|
|
94
|
+
throw new Error("source changed after candidate preparation: promotion content digest no longer matches");
|
|
95
|
+
}
|
|
96
|
+
|
|
87
97
|
const record = {
|
|
88
98
|
schema_version: ACCEPTANCE_SCHEMA_VERSION,
|
|
89
99
|
result: "passed",
|
|
90
|
-
confirmation:
|
|
100
|
+
confirmation: acceptanceConfirmationForVersion(pkg.version),
|
|
91
101
|
package_name: current.package_name,
|
|
92
102
|
package_version: current.package_version,
|
|
93
103
|
filename: current.filename,
|
|
@@ -95,11 +105,12 @@ function recordAcceptance() {
|
|
|
95
105
|
integrity: current.integrity,
|
|
96
106
|
accepted_at: new Date().toISOString(),
|
|
97
107
|
package_content_sha256: computePortablePackageDigest(),
|
|
108
|
+
promotion_content_sha256: promotionDigest,
|
|
98
109
|
};
|
|
99
110
|
verifyAcceptanceRecord(record, current);
|
|
100
111
|
const path = acceptancePath(root, pkg.version);
|
|
101
112
|
mkdirSync(dirname(path), { recursive: true });
|
|
102
|
-
|
|
113
|
+
replaceFileAtomicallySync(path, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o644 });
|
|
103
114
|
console.log(`Interactive local candidate acceptance recorded: ${path}`);
|
|
104
115
|
console.log("Commit this record with the candidate. Any packaged-file change invalidates it.");
|
|
105
116
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { assertPublishTag, parseReleaseVersion } from "./release-channel.mjs";
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const pkg = JSON.parse(readFileSync(resolve("package.json"), "utf8"));
|
|
9
|
+
const parsed = parseReleaseVersion(pkg.version);
|
|
10
|
+
const configured = String(process.env.npm_config_tag || "latest").trim().toLowerCase() || "latest";
|
|
11
|
+
assertPublishTag(parsed.raw, configured);
|
|
12
|
+
console.log(`npm publication channel verified: ${parsed.raw} -> ${parsed.npmTag}`);
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.error(`npm publication policy failed: ${error?.message || error}`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { defaultStateRoot, expandHome } from "../src/local/state.mjs";
|
|
4
|
+
import { replaceFileAtomicallySync } from "../src/local/exclusive-file.mjs";
|
|
5
|
+
import { ensureOwnerOnlyDirectorySync, readBoundedRegularFileSync } from "../src/local/secure-file.mjs";
|
|
6
|
+
import { assertSoakEligiblePrerelease } from "./release-channel.mjs";
|
|
7
|
+
|
|
8
|
+
export const ACTIVATION_SCHEMA_VERSION = 1;
|
|
9
|
+
const MAX_ACTIVATION_BYTES = 64 * 1024;
|
|
10
|
+
const SOURCES = new Set(["local-candidate", "npm-prerelease"]);
|
|
11
|
+
|
|
12
|
+
export function prereleaseActivationPath(version, stateRoot = defaultStateRoot()) {
|
|
13
|
+
const parsed = assertSoakEligiblePrerelease(version);
|
|
14
|
+
return join(resolve(expandHome(stateRoot)), "release-channels", "activations", `v${parsed.raw}.json`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function writePrereleaseActivation(record, stateRoot = defaultStateRoot()) {
|
|
18
|
+
const normalized = validatePrereleaseActivation(record);
|
|
19
|
+
const file = prereleaseActivationPath(normalized.package_version, stateRoot);
|
|
20
|
+
ensureOwnerOnlyDirectorySync(dirname(file));
|
|
21
|
+
replaceFileAtomicallySync(file, `${JSON.stringify(normalized, null, 2)}\n`, { mode: 0o600 });
|
|
22
|
+
return file;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function readPrereleaseActivation(version, stateRoot = defaultStateRoot()) {
|
|
26
|
+
const file = prereleaseActivationPath(version, stateRoot);
|
|
27
|
+
if (!existsSync(file)) throw new Error(`prerelease activation record is missing: ${file}`);
|
|
28
|
+
let value;
|
|
29
|
+
try { value = JSON.parse(readBoundedRegularFileSync(file, MAX_ACTIVATION_BYTES, "prerelease activation record", { verifyPathIdentity: true }).toString("utf8")); }
|
|
30
|
+
catch (error) { throw new Error(`prerelease activation record is unavailable or invalid: ${error.message}`); }
|
|
31
|
+
return validatePrereleaseActivation(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function validatePrereleaseActivation(value) {
|
|
35
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("prerelease activation record must be an object");
|
|
36
|
+
if (value.schema_version !== ACTIVATION_SCHEMA_VERSION) throw new Error("unsupported prerelease activation schema");
|
|
37
|
+
const parsed = assertSoakEligiblePrerelease(value.package_version);
|
|
38
|
+
if (value.package_name !== "machine-bridge-mcp") throw new Error("prerelease activation package name is invalid");
|
|
39
|
+
if (!SOURCES.has(value.source)) throw new Error("prerelease activation source is invalid");
|
|
40
|
+
if (!/^[0-9a-f]{40}$/.test(String(value.shasum || ""))) throw new Error("prerelease activation SHA-1 is invalid");
|
|
41
|
+
if (!/^sha512-[A-Za-z0-9+/]+={0,2}$/.test(String(value.integrity || ""))) throw new Error("prerelease activation integrity is invalid");
|
|
42
|
+
if (!/^[0-9a-f]{64}$/.test(String(value.promotion_content_sha256 || ""))) throw new Error("prerelease activation promotion digest is invalid");
|
|
43
|
+
const activatedAt = Date.parse(String(value.activated_at || ""));
|
|
44
|
+
if (!Number.isFinite(activatedAt)) throw new Error("prerelease activation timestamp is invalid");
|
|
45
|
+
if (value.source === "npm-prerelease") {
|
|
46
|
+
if (value.npm_dist_tag !== parsed.npmTag) throw new Error("prerelease activation npm dist-tag is invalid");
|
|
47
|
+
const publishedAt = Date.parse(String(value.published_at || ""));
|
|
48
|
+
if (!Number.isFinite(publishedAt) || publishedAt > activatedAt + 5 * 60 * 1000) throw new Error("prerelease activation publication timestamp is invalid");
|
|
49
|
+
}
|
|
50
|
+
const workspaceHash = String(value.workspace_hash || "");
|
|
51
|
+
if (workspaceHash && !/^[0-9a-f]{24}$/.test(workspaceHash)) throw new Error("prerelease activation workspace hash is invalid");
|
|
52
|
+
const runtimeEntry = String(value.runtime_entry || "");
|
|
53
|
+
if (runtimeEntry && !isAbsolute(runtimeEntry)) throw new Error("prerelease activation runtime entry must be absolute");
|
|
54
|
+
let previous;
|
|
55
|
+
if (value.previous !== undefined) {
|
|
56
|
+
if (!value.previous || typeof value.previous !== "object" || Array.isArray(value.previous)) throw new Error("prerelease activation previous runtime is invalid");
|
|
57
|
+
const previousVersion = String(value.previous.version || "");
|
|
58
|
+
const previousEntry = String(value.previous.entry || "");
|
|
59
|
+
if (!previousVersion || !previousEntry || !isAbsolute(previousEntry)) throw new Error("prerelease activation previous runtime is invalid");
|
|
60
|
+
previous = { version: previousVersion, entry: previousEntry };
|
|
61
|
+
}
|
|
62
|
+
return Object.freeze({
|
|
63
|
+
schema_version: ACTIVATION_SCHEMA_VERSION,
|
|
64
|
+
package_name: value.package_name,
|
|
65
|
+
package_version: parsed.raw,
|
|
66
|
+
source: value.source,
|
|
67
|
+
shasum: value.shasum,
|
|
68
|
+
integrity: value.integrity,
|
|
69
|
+
promotion_content_sha256: value.promotion_content_sha256,
|
|
70
|
+
activated_at: new Date(activatedAt).toISOString(),
|
|
71
|
+
...(value.source === "npm-prerelease" ? {
|
|
72
|
+
npm_dist_tag: value.npm_dist_tag,
|
|
73
|
+
published_at: new Date(Date.parse(value.published_at)).toISOString(),
|
|
74
|
+
} : {}),
|
|
75
|
+
...(workspaceHash ? { workspace_hash: workspaceHash } : {}),
|
|
76
|
+
...(runtimeEntry ? { runtime_entry: runtimeEntry } : {}),
|
|
77
|
+
...(previous ? { previous } : {}),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
@@ -3,9 +3,11 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { readBoundedRegularFileSync } from "../src/local/secure-file.mjs";
|
|
6
|
+
import { createTrustedGitResolver } from "../src/local/trusted-git-executable.mjs";
|
|
6
7
|
|
|
7
8
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
9
|
const selfPath = "scripts/privacy-check.mjs";
|
|
10
|
+
const gitExecutable = createTrustedGitResolver({ workspace: root });
|
|
9
11
|
const requestedOptions = new Set(process.argv.slice(2));
|
|
10
12
|
for (const option of requestedOptions) {
|
|
11
13
|
if (option !== "--history") throw new Error(`unknown privacy-check option: ${option}`);
|
|
@@ -68,7 +70,7 @@ process.stderr.write(`privacy check ok (${candidates.length} tracked/unignored f
|
|
|
68
70
|
|
|
69
71
|
function collectCandidateFiles(directory) {
|
|
70
72
|
try {
|
|
71
|
-
return execFileSync(
|
|
73
|
+
return execFileSync(gitExecutable(), ["-C", directory, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
72
74
|
encoding: "buffer",
|
|
73
75
|
maxBuffer: 32 * 1024 * 1024,
|
|
74
76
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -98,7 +100,7 @@ function collectCandidateFiles(directory) {
|
|
|
98
100
|
function scanReachableHistory(directory, entries, out) {
|
|
99
101
|
let listing;
|
|
100
102
|
try {
|
|
101
|
-
listing = execFileSync(
|
|
103
|
+
listing = execFileSync(gitExecutable(), ["-C", directory, "rev-list", "--objects", "--all", "-z"], {
|
|
102
104
|
encoding: "buffer",
|
|
103
105
|
maxBuffer: 64 * 1024 * 1024,
|
|
104
106
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -130,7 +132,7 @@ function scanReachableHistory(directory, entries, out) {
|
|
|
130
132
|
const hashes = [...objectPaths.keys()];
|
|
131
133
|
let metadata;
|
|
132
134
|
try {
|
|
133
|
-
metadata = execFileSync(
|
|
135
|
+
metadata = execFileSync(gitExecutable(), ["-C", directory, "cat-file", "--batch-check=%(objectname) %(objecttype) %(objectsize)"], {
|
|
134
136
|
input: `${hashes.join("\n")}\n`,
|
|
135
137
|
encoding: "utf8",
|
|
136
138
|
maxBuffer: 64 * 1024 * 1024,
|
|
@@ -162,7 +164,7 @@ function scanReachableHistory(directory, entries, out) {
|
|
|
162
164
|
}
|
|
163
165
|
let buffer;
|
|
164
166
|
try {
|
|
165
|
-
buffer = execFileSync(
|
|
167
|
+
buffer = execFileSync(gitExecutable(), ["-C", directory, "cat-file", "blob", hash], {
|
|
166
168
|
encoding: "buffer",
|
|
167
169
|
maxBuffer: 6 * 1024 * 1024,
|
|
168
170
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -194,7 +196,7 @@ function scanReachableHistory(directory, entries, out) {
|
|
|
194
196
|
function scanReachableCommitMessages(directory, entries, out) {
|
|
195
197
|
let output;
|
|
196
198
|
try {
|
|
197
|
-
output = execFileSync(
|
|
199
|
+
output = execFileSync(gitExecutable(), ["-C", directory, "log", "--all", "--format=%H%x00%B%x00"], {
|
|
198
200
|
encoding: "buffer",
|
|
199
201
|
maxBuffer: 64 * 1024 * 1024,
|
|
200
202
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { readBoundedRegularFileSync } from "../src/local/secure-file.mjs";
|
|
6
|
+
import { normalizePackRecord } from "./release-acceptance.mjs";
|
|
7
|
+
import { parseReleaseVersion } from "./release-channel.mjs";
|
|
8
|
+
|
|
9
|
+
const MAX_FILE_BYTES = 16 * 1024 * 1024;
|
|
10
|
+
const MAX_TOTAL_BYTES = 96 * 1024 * 1024;
|
|
11
|
+
const VERSION_PLACEHOLDER = "<machine-bridge-release-version>";
|
|
12
|
+
|
|
13
|
+
export function computePromotionContentDigest(root, options = {}) {
|
|
14
|
+
const pkg = options.packageJson || JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
15
|
+
const version = parseReleaseVersion(pkg.version).raw;
|
|
16
|
+
const record = options.packRecord || packageDryRun(root, pkg.name, options.npmCli);
|
|
17
|
+
if (!Array.isArray(record.files) || record.files.length < 1) throw new Error("npm pack dry-run omitted the package file inventory");
|
|
18
|
+
const entries = record.files.map((item) => ({
|
|
19
|
+
path: String(item?.path || ""),
|
|
20
|
+
mode: normalizePackMode(item?.mode),
|
|
21
|
+
})).sort((left, right) => left.path.localeCompare(right.path));
|
|
22
|
+
if (!entries.length || entries.some((entry) => entry.path.startsWith("/") || entry.path.includes("\\") || entry.path.split("/").includes(".."))) {
|
|
23
|
+
throw new Error("npm pack dry-run returned an invalid package path");
|
|
24
|
+
}
|
|
25
|
+
for (let index = 1; index < entries.length; index += 1) {
|
|
26
|
+
if (entries[index - 1].path === entries[index].path) throw new Error(`npm pack dry-run returned duplicate path: ${entries[index].path}`);
|
|
27
|
+
}
|
|
28
|
+
const hash = createHash("sha256");
|
|
29
|
+
let total = 0;
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
const relative = entry.path;
|
|
32
|
+
const bytes = readBoundedRegularFileSync(join(root, relative), MAX_FILE_BYTES, `promotion package file ${relative}`, { verifyPathIdentity: true });
|
|
33
|
+
total += bytes.length;
|
|
34
|
+
if (total > MAX_TOTAL_BYTES) throw new Error(`promotion package content exceeds ${MAX_TOTAL_BYTES} bytes`);
|
|
35
|
+
const normalized = normalizeReleaseMetadata(relative, bytes, version);
|
|
36
|
+
hash.update(`${relative}\0${entry.mode}\0${normalized.length}\0`, "utf8");
|
|
37
|
+
hash.update(normalized);
|
|
38
|
+
hash.update("\0", "utf8");
|
|
39
|
+
}
|
|
40
|
+
return hash.digest("hex");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
function normalizePackMode(value) {
|
|
45
|
+
const numeric = Number(value);
|
|
46
|
+
const mode = Number.isSafeInteger(numeric) ? numeric & 0o777 : NaN;
|
|
47
|
+
if (![0o644, 0o755].includes(mode)) throw new Error(`npm pack dry-run returned an invalid file mode: ${String(value)}`);
|
|
48
|
+
return mode.toString(8).padStart(4, "0");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function packageDryRun(root, packageName, npmCli = process.env.npm_execpath) {
|
|
52
|
+
if (!npmCli) throw new Error("promotion digest must run through npm so npm_execpath is available");
|
|
53
|
+
const result = spawnSync(process.execPath, [
|
|
54
|
+
npmCli,
|
|
55
|
+
"pack",
|
|
56
|
+
"--ignore-scripts",
|
|
57
|
+
"--silent",
|
|
58
|
+
"--dry-run",
|
|
59
|
+
"--json",
|
|
60
|
+
], {
|
|
61
|
+
cwd: root,
|
|
62
|
+
encoding: "utf8",
|
|
63
|
+
env: process.env,
|
|
64
|
+
windowsHide: true,
|
|
65
|
+
});
|
|
66
|
+
if (result.error) throw result.error;
|
|
67
|
+
if (result.status !== 0) throw new Error(`npm pack dry-run failed: ${String(result.stderr || result.stdout).trim()}`);
|
|
68
|
+
let parsed;
|
|
69
|
+
try { parsed = JSON.parse(result.stdout); } catch { throw new Error("npm pack dry-run did not return valid JSON"); }
|
|
70
|
+
const record = normalizePackRecord(parsed, packageName);
|
|
71
|
+
if (!record) throw new Error("npm pack dry-run did not return package metadata");
|
|
72
|
+
return record;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeReleaseMetadata(relative, bytes, version) {
|
|
76
|
+
if (relative === "package.json") {
|
|
77
|
+
const value = parseJson(bytes, relative);
|
|
78
|
+
value.version = VERSION_PLACEHOLDER;
|
|
79
|
+
return canonicalJson(value);
|
|
80
|
+
}
|
|
81
|
+
if (relative === "package-lock.json") {
|
|
82
|
+
const value = parseJson(bytes, relative);
|
|
83
|
+
value.version = VERSION_PLACEHOLDER;
|
|
84
|
+
if (value.packages?.[""] && typeof value.packages[""] === "object") value.packages[""].version = VERSION_PLACEHOLDER;
|
|
85
|
+
return canonicalJson(value);
|
|
86
|
+
}
|
|
87
|
+
if (relative === "browser-extension/manifest.json") {
|
|
88
|
+
const value = parseJson(bytes, relative);
|
|
89
|
+
value.version = "0.0.0";
|
|
90
|
+
value.version_name = VERSION_PLACEHOLDER;
|
|
91
|
+
return canonicalJson(value);
|
|
92
|
+
}
|
|
93
|
+
if (relative === "src/worker/index.ts") {
|
|
94
|
+
const text = bytes.toString("utf8");
|
|
95
|
+
const normalized = text.replace(/const SERVER_VERSION = "[^"]+";/, `const SERVER_VERSION = "${VERSION_PLACEHOLDER}";`);
|
|
96
|
+
if (normalized === text && !text.includes(`const SERVER_VERSION = "${VERSION_PLACEHOLDER}";`)) {
|
|
97
|
+
throw new Error("Worker source omitted the synchronized SERVER_VERSION constant");
|
|
98
|
+
}
|
|
99
|
+
return Buffer.from(normalized, "utf8");
|
|
100
|
+
}
|
|
101
|
+
if (relative === "CHANGELOG.md") {
|
|
102
|
+
return Buffer.from(bytes.toString("utf8").split(version).join(VERSION_PLACEHOLDER), "utf8");
|
|
103
|
+
}
|
|
104
|
+
return bytes;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseJson(bytes, label) {
|
|
108
|
+
try { return JSON.parse(bytes.toString("utf8")); } catch (error) {
|
|
109
|
+
throw new Error(`${label} is not valid JSON: ${error.message}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function canonicalJson(value) {
|
|
114
|
+
return Buffer.from(`${JSON.stringify(sortValue(value))}\n`, "utf8");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function sortValue(value) {
|
|
118
|
+
if (Array.isArray(value)) return value.map(sortValue);
|
|
119
|
+
if (!value || typeof value !== "object") return value;
|
|
120
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortValue(value[key])]));
|
|
121
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
import { parseReleaseVersion, requiresSoakForStable } from "./release-channel.mjs";
|
|
8
|
+
import { verifyCurrentStableSoak } from "./release-soak.mjs";
|
|
9
|
+
|
|
10
|
+
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
|
11
|
+
|
|
12
|
+
export function npmPublishArguments(version, mode) {
|
|
13
|
+
const parsed = parseReleaseVersion(version);
|
|
14
|
+
if (mode === "prerelease" && !parsed.prerelease) throw new Error("prerelease publication requires a dev, beta, or rc version");
|
|
15
|
+
if (mode === "stable" && parsed.prerelease) throw new Error("stable publication requires a version without a prerelease suffix");
|
|
16
|
+
if (!new Set(["prerelease", "stable"]).has(mode)) throw new Error("publication mode must be prerelease or stable");
|
|
17
|
+
return ["publish", "--tag", parsed.npmTag];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function publishCurrentNpmPackage(repositoryRoot, mode, options = {}) {
|
|
21
|
+
const pkg = JSON.parse(readFileSync(resolve(repositoryRoot, "package.json"), "utf8"));
|
|
22
|
+
const parsed = parseReleaseVersion(pkg.version);
|
|
23
|
+
const args = npmPublishArguments(parsed.raw, mode);
|
|
24
|
+
if (!parsed.prerelease && requiresSoakForStable(parsed.raw)) verifyCurrentStableSoak(repositoryRoot);
|
|
25
|
+
const npmCli = options.npmCli || process.env.npm_execpath;
|
|
26
|
+
if (!npmCli) throw new Error("npm publication must run through npm so npm_execpath is available");
|
|
27
|
+
const run = options.run || spawnSync;
|
|
28
|
+
const result = run(process.execPath, [npmCli, ...args], {
|
|
29
|
+
cwd: repositoryRoot,
|
|
30
|
+
encoding: "utf8",
|
|
31
|
+
env: process.env,
|
|
32
|
+
stdio: options.capture ? "pipe" : "inherit",
|
|
33
|
+
windowsHide: true,
|
|
34
|
+
});
|
|
35
|
+
if (result.error) throw result.error;
|
|
36
|
+
if (result.status !== 0) throw new Error(`npm publish failed with status ${result.status}`);
|
|
37
|
+
return { version: parsed.raw, tag: parsed.npmTag };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function main() {
|
|
41
|
+
const mode = process.argv[2] || "";
|
|
42
|
+
const result = publishCurrentNpmPackage(root, mode);
|
|
43
|
+
console.log(`npm publication completed: ${result.version} with dist-tag ${result.tag}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
|
|
47
|
+
main().catch((error) => {
|
|
48
|
+
console.error(`npm publication failed: ${error?.message || error}`);
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
});
|
|
51
|
+
}
|