machine-bridge-mcp 2.0.0 → 3.0.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/CONTRIBUTING.md +20 -25
  3. package/README.md +21 -14
  4. package/SECURITY.md +147 -123
  5. package/browser-extension/manifest.json +3 -2
  6. package/browser-extension/service-worker.js +31 -12
  7. package/docs/ARCHITECTURE.md +28 -13
  8. package/docs/AUDIT.md +101 -2
  9. package/docs/CLIENTS.md +2 -2
  10. package/docs/ENGINEERING.md +9 -3
  11. package/docs/GETTING_STARTED.md +10 -9
  12. package/docs/LOCAL_AUTHORIZATION.md +115 -72
  13. package/docs/LOGGING.md +14 -3
  14. package/docs/MANAGED_JOBS.md +24 -29
  15. package/docs/MULTI_ACCOUNT.md +98 -37
  16. package/docs/OPERATIONS.md +34 -23
  17. package/docs/OVERVIEW.md +7 -7
  18. package/docs/PROJECT_STANDARDS.md +9 -5
  19. package/docs/RELEASING.md +171 -64
  20. package/docs/TESTING.md +21 -9
  21. package/docs/THREAT_MODEL.md +138 -67
  22. package/docs/TOOL_REFERENCE.md +2 -2
  23. package/docs/UPGRADING.md +102 -18
  24. package/native/macos/MachineBridgeTrustBroker.swift +233 -0
  25. package/package.json +37 -6
  26. package/scripts/candidate-runtime-store.mjs +38 -0
  27. package/scripts/check-plan.mjs +19 -0
  28. package/scripts/check-runner.mjs +4 -1
  29. package/scripts/commit-message-check.mjs +5 -2
  30. package/scripts/coverage-check.mjs +30 -2
  31. package/scripts/github-push.mjs +11 -0
  32. package/scripts/github-release.mjs +38 -7
  33. package/scripts/install-published-prerelease.mjs +167 -0
  34. package/scripts/local-release-acceptance.mjs +23 -12
  35. package/scripts/npm-publication-policy.mjs +16 -0
  36. package/scripts/prerelease-activation.mjs +79 -0
  37. package/scripts/privacy-check.mjs +7 -5
  38. package/scripts/promotion-digest.mjs +121 -0
  39. package/scripts/publish-npm.mjs +51 -0
  40. package/scripts/published-release.mjs +87 -0
  41. package/scripts/release-acceptance.mjs +30 -1
  42. package/scripts/release-candidate-manifest.mjs +40 -0
  43. package/scripts/release-channel.mjs +106 -0
  44. package/scripts/release-impact-check.mjs +12 -22
  45. package/scripts/release-soak.mjs +237 -0
  46. package/scripts/start-release-candidate.mjs +113 -7
  47. package/src/local/account-access.mjs +6 -0
  48. package/src/local/account-admin.mjs +34 -16
  49. package/src/local/agent-context.mjs +45 -42
  50. package/src/local/app-automation.mjs +9 -4
  51. package/src/local/authority-context.mjs +106 -0
  52. package/src/local/browser-bridge-http.mjs +4 -1
  53. package/src/local/browser-bridge.mjs +14 -9
  54. package/src/local/browser-broker-routes.mjs +4 -0
  55. package/src/local/browser-broker-server.mjs +5 -4
  56. package/src/local/browser-extension-identity.mjs +50 -0
  57. package/src/local/browser-extension-protocol.mjs +9 -4
  58. package/src/local/browser-operation-service.mjs +7 -0
  59. package/src/local/browser-pairing-http.mjs +36 -0
  60. package/src/local/browser-pairing-store.mjs +56 -47
  61. package/src/local/cli-account-admin.mjs +41 -7
  62. package/src/local/cli-activate.mjs +90 -0
  63. package/src/local/cli-approval.mjs +14 -58
  64. package/src/local/cli-local-admin.mjs +5 -15
  65. package/src/local/cli-options.mjs +11 -7
  66. package/src/local/cli-service.mjs +35 -5
  67. package/src/local/cli.mjs +119 -58
  68. package/src/local/daemon-process.mjs +6 -0
  69. package/src/local/delegated-process-sandbox.mjs +147 -0
  70. package/src/local/device-identity.mjs +157 -48
  71. package/src/local/device-root-provider.mjs +87 -0
  72. package/src/local/errors.mjs +25 -3
  73. package/src/local/git-service.mjs +16 -15
  74. package/src/local/job-runner.mjs +35 -27
  75. package/src/local/log.mjs +18 -44
  76. package/src/local/macos-trust-broker.mjs +320 -0
  77. package/src/local/managed-job-lock.mjs +18 -0
  78. package/src/local/managed-job-projection.mjs +9 -1
  79. package/src/local/managed-job-runner.mjs +13 -1
  80. package/src/local/managed-job-storage.mjs +2 -1
  81. package/src/local/managed-job-terminal.mjs +138 -0
  82. package/src/local/managed-jobs.mjs +88 -32
  83. package/src/local/operation-authorization.mjs +75 -239
  84. package/src/local/operation-risk.mjs +28 -0
  85. package/src/local/operation-state-lock.mjs +7 -86
  86. package/src/local/owner-state-lock.mjs +104 -0
  87. package/src/local/policy.mjs +19 -0
  88. package/src/local/process-execution.mjs +21 -9
  89. package/src/local/process-sessions.mjs +27 -17
  90. package/src/local/process-tracker.mjs +30 -6
  91. package/src/local/process-tree-ownership.mjs +59 -0
  92. package/src/local/process-tree.mjs +22 -18
  93. package/src/local/relay-connection.mjs +47 -35
  94. package/src/local/relay-diagnostics.mjs +65 -0
  95. package/src/local/remote-configuration.mjs +48 -0
  96. package/src/local/runtime-activation.mjs +141 -0
  97. package/src/local/runtime-diagnostics.mjs +21 -0
  98. package/src/local/runtime-relay.mjs +9 -8
  99. package/src/local/runtime-reporting.mjs +8 -3
  100. package/src/local/runtime-resource-service.mjs +77 -0
  101. package/src/local/runtime-tool-handlers.mjs +8 -8
  102. package/src/local/runtime.mjs +85 -95
  103. package/src/local/secure-file.mjs +5 -1
  104. package/src/local/security-audit-log.mjs +204 -0
  105. package/src/local/service-convergence.mjs +17 -1
  106. package/src/local/service-ownership.mjs +17 -0
  107. package/src/local/service-restart-handoff.mjs +50 -0
  108. package/src/local/service-restart-scheduler.mjs +49 -0
  109. package/src/local/service-status.mjs +47 -0
  110. package/src/local/service.mjs +142 -43
  111. package/src/local/state-inventory.mjs +21 -3
  112. package/src/local/state.mjs +155 -17
  113. package/src/local/stdio.mjs +5 -0
  114. package/src/local/tool-executor.mjs +39 -6
  115. package/src/local/tool-result-boundary.mjs +51 -0
  116. package/src/local/tools.mjs +1 -0
  117. package/src/local/trusted-executable.mjs +53 -0
  118. package/src/local/trusted-git-executable.mjs +34 -0
  119. package/src/local/trusted-github-cli.mjs +24 -0
  120. package/src/local/windows-service.mjs +20 -0
  121. package/src/local/worker-deployment.mjs +3 -4
  122. package/src/local/worker-secret-file.mjs +2 -2
  123. package/src/local/workspace-file-service.mjs +34 -31
  124. package/src/shared/admin-auth.d.mts +2 -1
  125. package/src/shared/admin-auth.mjs +3 -3
  126. package/src/shared/device-session-auth.d.mts +20 -0
  127. package/src/shared/device-session-auth.mjs +51 -0
  128. package/src/shared/log-redaction.d.mts +9 -0
  129. package/src/shared/log-redaction.mjs +51 -0
  130. package/src/shared/server-metadata.json +1 -1
  131. package/src/shared/tool-catalog.json +2 -2
  132. package/src/worker/account-admin.ts +23 -22
  133. package/src/worker/authority.ts +23 -4
  134. package/src/worker/daemon-auth.ts +45 -67
  135. package/src/worker/daemon-sockets.ts +12 -2
  136. package/src/worker/device-session-verifier.ts +129 -0
  137. package/src/worker/dpop.ts +151 -0
  138. package/src/worker/http.ts +47 -16
  139. package/src/worker/index.ts +53 -16
  140. package/src/worker/nonce-store.ts +1 -5
  141. package/src/worker/oauth-client-admin.ts +45 -0
  142. package/src/worker/oauth-controller.ts +55 -6
  143. package/src/worker/oauth-refresh-families.ts +21 -7
  144. package/src/worker/oauth-state.ts +6 -0
  145. package/src/worker/oauth-tokens.ts +28 -8
  146. package/src/worker/observability.ts +4 -2
  147. package/src/worker/websocket-protocol.ts +8 -2
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
7
+ import { readBoundedRegularFileSync } from "../src/local/secure-file.mjs";
8
+ import { replaceFileAtomicallySync } from "../src/local/exclusive-file.mjs";
9
+ import { readPrereleaseActivation } from "./prerelease-activation.mjs";
10
+ import { computePromotionContentDigest } from "./promotion-digest.mjs";
11
+ import {
12
+ SOAK_CONFIRMATION,
13
+ assertSoakEligiblePrerelease,
14
+ assertStablePromotion,
15
+ compareReleaseVersions,
16
+ formatSoakDuration,
17
+ minimumSoakSeconds,
18
+ parseReleaseVersion,
19
+ requiresSoakForStable,
20
+ } from "./release-channel.mjs";
21
+ import { verifyCurrentReleaseAcceptance } from "./release-acceptance.mjs";
22
+ import { readGithubPrerelease, readPublishedNpmPrerelease } from "./published-release.mjs";
23
+
24
+ export const SOAK_SCHEMA_VERSION = 1;
25
+ const MAX_SOAK_RECORD_BYTES = 64 * 1024;
26
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
27
+
28
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
29
+ const mode = process.argv[2] || "--verify";
30
+ try {
31
+ if (mode === "--record") recordCurrentPrereleaseSoak(root, { confirmation: argumentValue("--confirm"), stateRoot: argumentValue("--state-dir") || undefined });
32
+ else if (mode === "--verify") verifyCurrentStableSoak(root);
33
+ else if (mode === "--status") printStatus(root);
34
+ else throw new Error("usage: node scripts/release-soak.mjs [--record --confirm TEXT [--state-dir DIR]|--verify|--status]");
35
+ } catch (error) {
36
+ console.error(`release soak failed: ${error?.message || error}`);
37
+ process.exit(1);
38
+ }
39
+ }
40
+
41
+ export function soakRecordPath(repositoryRoot, stableVersion) {
42
+ const stable = parseReleaseVersion(stableVersion);
43
+ if (stable.prerelease) throw new Error("soak record path requires a stable version");
44
+ return join(repositoryRoot, "release-soak", `v${stable.raw}.json`);
45
+ }
46
+
47
+ export function recordCurrentPrereleaseSoak(repositoryRoot, options = {}) {
48
+ const pkg = readPackage(repositoryRoot);
49
+ const prerelease = assertSoakEligiblePrerelease(pkg.version);
50
+ const acceptance = verifyCurrentReleaseAcceptance(repositoryRoot);
51
+ if (!acceptance.required) throw new Error("prerelease package is missing local candidate acceptance policy");
52
+ const activation = readPrereleaseActivation(prerelease.raw, options.stateRoot);
53
+ if (activation.source !== "npm-prerelease") {
54
+ throw new Error("formal soak starts only after installing the published npm beta/rc; a local candidate activation is insufficient");
55
+ }
56
+ if (activation.integrity !== acceptance.metadata.integrity || activation.shasum !== acceptance.metadata.shasum) {
57
+ throw new Error("activated prerelease bytes do not match the locally accepted prerelease package");
58
+ }
59
+ const promotionDigest = computePromotionContentDigest(repositoryRoot);
60
+ if (activation.promotion_content_sha256 !== promotionDigest) {
61
+ throw new Error("activated prerelease promotion digest does not match the current source tree");
62
+ }
63
+ const previousStable = options.previousStableVersion || latestStableTag(repositoryRoot, prerelease.baseVersion);
64
+ const requiredSeconds = minimumSoakSeconds(prerelease.baseVersion, previousStable);
65
+ const activatedAt = Date.parse(activation.activated_at);
66
+ const acceptedAt = Number.isFinite(options.now) ? Number(options.now) : Date.now();
67
+ const observedSeconds = Math.floor((acceptedAt - activatedAt) / 1000);
68
+ if (observedSeconds < requiredSeconds) {
69
+ throw new Error(`prerelease soak is incomplete: observed ${formatSoakDuration(Math.max(0, observedSeconds))}, required ${formatSoakDuration(requiredSeconds)}`);
70
+ }
71
+ const expected = soakConfirmationPhrase(pkg.name, prerelease.raw, requiredSeconds);
72
+ if (options.confirmation !== expected) throw new Error(`soak confirmation must exactly match: ${expected}`);
73
+
74
+ const published = options.published || readPublishedNpmPrerelease(pkg.name, prerelease.raw, prerelease.npmTag);
75
+ if (
76
+ published.version !== prerelease.raw
77
+ || published.integrity !== acceptance.metadata.integrity
78
+ || published.shasum !== acceptance.metadata.shasum
79
+ || published.distTag !== prerelease.npmTag
80
+ || published.publishedAt !== activation.published_at
81
+ ) {
82
+ throw new Error("published npm prerelease metadata does not match the accepted and activated package");
83
+ }
84
+ const github = options.github || readGithubPrerelease(prerelease.raw);
85
+ if (github.tag !== `v${prerelease.raw}` || github.isPrerelease !== true) {
86
+ throw new Error("GitHub prerelease is missing or not marked as a prerelease");
87
+ }
88
+ const record = validateSoakRecord({
89
+ schema_version: SOAK_SCHEMA_VERSION,
90
+ result: "passed",
91
+ confirmation: SOAK_CONFIRMATION,
92
+ package_name: pkg.name,
93
+ stable_version: prerelease.baseVersion,
94
+ prerelease_version: prerelease.raw,
95
+ prerelease_channel: prerelease.channel,
96
+ prerelease_shasum: acceptance.metadata.shasum,
97
+ prerelease_integrity: acceptance.metadata.integrity,
98
+ promotion_content_sha256: promotionDigest,
99
+ activated_at: activation.activated_at,
100
+ published_at: published.publishedAt,
101
+ accepted_at: new Date(acceptedAt).toISOString(),
102
+ minimum_soak_seconds: requiredSeconds,
103
+ observed_soak_seconds: observedSeconds,
104
+ npm_dist_tag: prerelease.npmTag,
105
+ github_prerelease_tag: github.tag,
106
+ previous_stable_version: previousStable,
107
+ known_blocking_issues: 0,
108
+ });
109
+ const file = soakRecordPath(repositoryRoot, prerelease.baseVersion);
110
+ mkdirSync(dirname(file), { recursive: true });
111
+ replaceFileAtomicallySync(file, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o644 });
112
+ console.log(`Published prerelease soak accepted: ${file}`);
113
+ console.log("Stable promotion may change only synchronized version metadata; any functional package change requires a new prerelease and a new soak window.");
114
+ return record;
115
+ }
116
+
117
+ export function verifyCurrentStableSoak(repositoryRoot, options = {}) {
118
+ const pkg = readPackage(repositoryRoot);
119
+ if (!requiresSoakForStable(pkg.version)) return { required: false, version: pkg.version };
120
+ const file = soakRecordPath(repositoryRoot, pkg.version);
121
+ if (!existsSync(file)) throw new Error(`stable release soak record is missing: ${file}`);
122
+ let parsed;
123
+ try { parsed = JSON.parse(readBoundedRegularFileSync(file, MAX_SOAK_RECORD_BYTES, "release soak record", { verifyPathIdentity: true }).toString("utf8")); }
124
+ catch (error) { throw new Error(`release soak record is unavailable or invalid: ${error.message}`); }
125
+ const promotionDigest = computePromotionContentDigest(repositoryRoot, options);
126
+ const record = validateSoakRecord(parsed, { stableVersion: pkg.version, promotionDigest });
127
+ return { required: true, version: pkg.version, record, promotionDigest };
128
+ }
129
+
130
+ export function validateSoakRecord(value, options = {}) {
131
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("release soak record must be an object");
132
+ const allowed = new Set([
133
+ "schema_version", "result", "confirmation", "package_name", "stable_version", "prerelease_version",
134
+ "prerelease_channel", "prerelease_shasum", "prerelease_integrity", "promotion_content_sha256",
135
+ "activated_at", "published_at", "accepted_at", "minimum_soak_seconds", "observed_soak_seconds",
136
+ "npm_dist_tag", "github_prerelease_tag", "previous_stable_version", "known_blocking_issues",
137
+ ]);
138
+ const unknown = Object.keys(value).filter((key) => !allowed.has(key));
139
+ if (unknown.length) throw new Error(`release soak record contains unsupported fields: ${unknown.join(", ")}`);
140
+ if (value.schema_version !== SOAK_SCHEMA_VERSION) throw new Error("unsupported release soak schema");
141
+ if (value.result !== "passed" || value.confirmation !== SOAK_CONFIRMATION) throw new Error("release soak is not accepted");
142
+ if (value.package_name !== "machine-bridge-mcp") throw new Error("release soak package name is invalid");
143
+ const { stable, prerelease } = assertStablePromotion(value.stable_version, value.prerelease_version);
144
+ if (value.prerelease_channel !== prerelease.channel) throw new Error("release soak prerelease channel is inconsistent");
145
+ if (value.npm_dist_tag !== prerelease.npmTag) throw new Error("release soak npm dist-tag is inconsistent");
146
+ if (value.github_prerelease_tag !== `v${prerelease.raw}`) throw new Error("release soak GitHub prerelease tag is inconsistent");
147
+ if (!/^[0-9a-f]{40}$/.test(String(value.prerelease_shasum || ""))) throw new Error("release soak prerelease SHA-1 is invalid");
148
+ if (!/^sha512-[A-Za-z0-9+/]+={0,2}$/.test(String(value.prerelease_integrity || ""))) throw new Error("release soak prerelease integrity is invalid");
149
+ if (!/^[0-9a-f]{64}$/.test(String(value.promotion_content_sha256 || ""))) throw new Error("release soak promotion digest is invalid");
150
+ const activatedAt = Date.parse(String(value.activated_at || ""));
151
+ const publishedAt = Date.parse(String(value.published_at || ""));
152
+ const acceptedAt = Date.parse(String(value.accepted_at || ""));
153
+ if (![activatedAt, publishedAt, acceptedAt].every(Number.isFinite)) throw new Error("release soak timestamps are invalid");
154
+ if (publishedAt > activatedAt + 5 * 60 * 1000 || activatedAt > acceptedAt) throw new Error("release soak timestamp ordering is invalid");
155
+ if (!Number.isSafeInteger(value.minimum_soak_seconds) || value.minimum_soak_seconds < 60) throw new Error("release soak minimum duration is invalid");
156
+ if (!Number.isSafeInteger(value.observed_soak_seconds) || value.observed_soak_seconds < value.minimum_soak_seconds) throw new Error("release soak observed duration is insufficient");
157
+ const elapsed = Math.floor((acceptedAt - activatedAt) / 1000);
158
+ if (value.observed_soak_seconds > elapsed + 5 || value.observed_soak_seconds < elapsed - 5) throw new Error("release soak observed duration does not match its timestamps");
159
+ if (value.known_blocking_issues !== 0) throw new Error("stable release is blocked by known prerelease issues");
160
+ const previous = parseReleaseVersion(value.previous_stable_version);
161
+ if (previous.prerelease || compareReleaseVersions(stable, previous) <= 0) throw new Error("release soak previous stable version is invalid");
162
+ const expectedMinimum = minimumSoakSeconds(stable.raw, previous.raw);
163
+ if (value.minimum_soak_seconds < expectedMinimum) throw new Error("release soak duration is below the active policy minimum");
164
+ if (options.stableVersion && stable.raw !== parseReleaseVersion(options.stableVersion).raw) throw new Error("release soak record targets another stable version");
165
+ if (options.promotionDigest && value.promotion_content_sha256 !== options.promotionDigest) {
166
+ throw new Error("stable package content differs from the accepted prerelease; publish a new prerelease and restart soak");
167
+ }
168
+ return Object.freeze({
169
+ schema_version: SOAK_SCHEMA_VERSION,
170
+ result: "passed",
171
+ confirmation: SOAK_CONFIRMATION,
172
+ package_name: "machine-bridge-mcp",
173
+ stable_version: stable.raw,
174
+ prerelease_version: prerelease.raw,
175
+ prerelease_channel: prerelease.channel,
176
+ prerelease_shasum: String(value.prerelease_shasum),
177
+ prerelease_integrity: String(value.prerelease_integrity),
178
+ promotion_content_sha256: String(value.promotion_content_sha256),
179
+ activated_at: new Date(activatedAt).toISOString(),
180
+ published_at: new Date(publishedAt).toISOString(),
181
+ accepted_at: new Date(acceptedAt).toISOString(),
182
+ minimum_soak_seconds: Number(value.minimum_soak_seconds),
183
+ observed_soak_seconds: Number(value.observed_soak_seconds),
184
+ npm_dist_tag: prerelease.npmTag,
185
+ github_prerelease_tag: `v${prerelease.raw}`,
186
+ previous_stable_version: previous.raw,
187
+ known_blocking_issues: 0,
188
+ });
189
+ }
190
+
191
+ export function soakConfirmationPhrase(name, prereleaseVersion, requiredSeconds) {
192
+ return `I SOAK-TESTED ${name} ${prereleaseVersion} FOR AT LEAST ${formatSoakDuration(requiredSeconds)} WITH NO BLOCKING ISSUES`;
193
+ }
194
+
195
+ function latestStableTag(repositoryRoot, targetBaseVersion) {
196
+ const target = parseReleaseVersion(targetBaseVersion);
197
+ const result = run("git", ["tag", "--list", "v*"], { cwd: repositoryRoot });
198
+ const versions = result.stdout.split(/\r?\n/).map((tag) => tag.trim().replace(/^v/, "")).filter(Boolean)
199
+ .map((value) => { try { return parseReleaseVersion(value); } catch { return null; } })
200
+ .filter((value) => value && !value.prerelease && compareReleaseVersions(value, target) < 0)
201
+ .sort(compareReleaseVersions);
202
+ const previous = versions.at(-1);
203
+ if (!previous) throw new Error(`could not determine the previous stable version before ${target.raw}`);
204
+ return previous.raw;
205
+ }
206
+
207
+ function printStatus(repositoryRoot) {
208
+ const pkg = readPackage(repositoryRoot);
209
+ const parsed = parseReleaseVersion(pkg.version);
210
+ if (parsed.prerelease) {
211
+ console.log(JSON.stringify({ version: parsed.raw, channel: parsed.channel, stable_target: parsed.baseVersion, soak_record: soakRecordPath(repositoryRoot, parsed.baseVersion) }, null, 2));
212
+ return;
213
+ }
214
+ const result = verifyCurrentStableSoak(repositoryRoot);
215
+ console.log(JSON.stringify(result, null, 2));
216
+ }
217
+
218
+ function readPackage(repositoryRoot) {
219
+ const value = JSON.parse(readFileSync(join(repositoryRoot, "package.json"), "utf8"));
220
+ if (value.name !== "machine-bridge-mcp") throw new Error("unexpected package name");
221
+ parseReleaseVersion(value.version);
222
+ return value;
223
+ }
224
+
225
+ function run(file, args, options = {}) {
226
+ const result = spawnSync(file, args, { cwd: options.cwd || root, encoding: "utf8", env: process.env, windowsHide: true });
227
+ if (result.error) throw result.error;
228
+ if (result.status !== 0) throw new Error(`${file} ${args[0] || ""} failed: ${String(result.stderr || result.stdout).trim()}`);
229
+ return result;
230
+ }
231
+
232
+ function argumentValue(name) {
233
+ const exact = process.argv.find((value) => value.startsWith(`${name}=`));
234
+ if (exact) return exact.slice(name.length + 1);
235
+ const index = process.argv.indexOf(name);
236
+ return index >= 0 ? process.argv[index + 1] : "";
237
+ }
@@ -1,22 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawn, spawnSync } from "node:child_process";
4
- import { mkdirSync, readFileSync, rmSync } from "node:fs";
4
+ import { createHash } from "node:crypto";
5
+ import { existsSync, readFileSync, rmSync } from "node:fs";
5
6
  import { dirname, join, resolve } from "node:path";
6
7
  import { fileURLToPath } from "node:url";
8
+ import { defaultStateRoot, expandHome } from "../src/local/state.mjs";
9
+ import { ensureOwnerOnlyDirectorySync } from "../src/local/secure-file.mjs";
10
+ import { createCandidateRuntimePrefix, pruneInactiveCandidateRuntimes } from "./candidate-runtime-store.mjs";
11
+ import { writePrereleaseActivation } from "./prerelease-activation.mjs";
7
12
  import { verifyTarball } from "./release-acceptance.mjs";
13
+ import { parseReleaseVersion } from "./release-channel.mjs";
14
+ import { validateCandidateManifest } from "./release-candidate-manifest.mjs";
8
15
 
9
16
  const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
10
17
  const candidateDirectory = join(root, ".release-candidate");
11
18
  const manifestPath = join(candidateDirectory, "manifest.json");
12
- const installPrefix = join(candidateDirectory, "runtime");
19
+ const foregroundInstallPrefix = join(candidateDirectory, "runtime");
13
20
  const npmCli = process.env.npm_execpath;
14
21
 
15
22
  if (!npmCli) fail("candidate startup must run through npm so npm_execpath is available");
16
23
 
17
24
  try {
18
25
  const manifest = readJson(manifestPath, "release candidate manifest");
19
- if (manifest.result !== "pending") throw new Error("release candidate manifest is not pending");
26
+ validateCandidateManifest(manifest);
20
27
  const tarball = join(candidateDirectory, String(manifest.filename || ""));
21
28
  verifyTarball(tarball, manifest);
22
29
 
@@ -25,8 +32,13 @@ try {
25
32
  throw new Error(`candidate startup requires npm 12 or newer; current ${npmVersion}`);
26
33
  }
27
34
 
28
- rmSync(installPrefix, { recursive: true, force: true });
29
- mkdirSync(installPrefix, { recursive: true });
35
+ const activateService = process.argv.includes("--activate-service");
36
+ const stateRoot = resolve(expandHome(argumentValue("--state-dir") || defaultStateRoot()));
37
+ const installPrefix = activateService
38
+ ? createCandidateRuntimePrefix({ stateRoot, version: manifest.package_version, shasum: manifest.shasum })
39
+ : foregroundInstallPrefix;
40
+ if (!activateService) rmSync(installPrefix, { recursive: true, force: true });
41
+ ensureOwnerOnlyDirectorySync(installPrefix);
30
42
  runNpm([
31
43
  "install",
32
44
  "--global",
@@ -53,10 +65,17 @@ try {
53
65
  }
54
66
 
55
67
  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");
68
+ throw new Error("candidate activation may update the configured same-name Worker; rerun with --allow-worker-deploy to authorize that live candidate deployment");
69
+ }
70
+
71
+ const forwardedArgs = process.argv.slice(2).filter((value) => ![
72
+ "--install-only", "--allow-worker-deploy", "--activate-service",
73
+ ].includes(value));
74
+ if (activateService) {
75
+ activatePersistentCandidate({ manifest, installedPackage, installPrefix, stateRoot, forwardedArgs });
76
+ process.exit(0);
57
77
  }
58
78
 
59
- const forwardedArgs = process.argv.slice(2).filter((value) => value !== "--install-only" && value !== "--allow-worker-deploy");
60
79
  const cli = join(installedPackage, "bin", "machine-mcp.mjs");
61
80
  console.log("Authorized live candidate deployment: startup may update the configured same-name Worker when its version or deployment hash differs.");
62
81
  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.");
@@ -86,6 +105,93 @@ try {
86
105
  fail(error?.message || error);
87
106
  }
88
107
 
108
+ function activatePersistentCandidate({ manifest, installedPackage, installPrefix, stateRoot, forwardedArgs }) {
109
+ const releaseVersion = parseReleaseVersion(manifest.package_version);
110
+ const cli = join(installedPackage, "bin", "machine-mcp.mjs");
111
+ const args = ["activate", ...withoutManagedFlags(forwardedArgs), "--state-dir", stateRoot, "--json"];
112
+ const previous = currentGlobalInstallation(manifest.package_name);
113
+ console.log("Activating the exact prerelease as the persistent login daemon. Portable-root startup does not prompt; a separately provisioned Secure Enclave broker may request one user-presence operation.");
114
+ const result = spawnSync(process.execPath, [cli, ...args], {
115
+ cwd: root,
116
+ env: process.env,
117
+ encoding: "utf8",
118
+ timeout: 300_000,
119
+ windowsHide: true,
120
+ });
121
+ if (result.error) throw result.error;
122
+ if (result.status !== 0) throw new Error(`persistent candidate activation failed: ${result.stderr || result.stdout}`);
123
+ let activation;
124
+ try { activation = JSON.parse(result.stdout); } catch { throw new Error("persistent candidate activation did not return valid JSON"); }
125
+ if (
126
+ activation.ok !== true
127
+ || activation.version !== manifest.package_version
128
+ || activation.daemon?.version !== manifest.package_version
129
+ || activation.worker?.health?.version !== manifest.package_version
130
+ ) {
131
+ throw new Error("persistent candidate activation did not converge on the exact candidate version");
132
+ }
133
+
134
+ let recordPath = "";
135
+ if (releaseVersion.prerelease) {
136
+ recordPath = writePrereleaseActivation({
137
+ schema_version: 1,
138
+ package_name: manifest.package_name,
139
+ package_version: manifest.package_version,
140
+ source: "local-candidate",
141
+ shasum: manifest.shasum,
142
+ integrity: manifest.integrity,
143
+ promotion_content_sha256: manifest.promotion_content_sha256,
144
+ activated_at: new Date().toISOString(),
145
+ workspace_hash: workspaceHash(activation.workspace),
146
+ runtime_entry: cli,
147
+ ...(previous ? { previous } : {}),
148
+ }, stateRoot);
149
+ }
150
+ const removedRuntimes = pruneInactiveCandidateRuntimes({ stateRoot, activePrefix: installPrefix });
151
+ console.log(`Persistent release candidate activated: ${manifest.package_version}`);
152
+ if (recordPath) console.log(`Activation record: ${recordPath}`);
153
+ if (removedRuntimes.length) console.log(`Removed ${removedRuntimes.length} inactive candidate runtime(s).`);
154
+ console.log("The Worker and login daemon now run the exact candidate. The terminal may close; the coding agent should verify the live deployment through Machine Bridge.");
155
+ if (previous?.version) console.log(`Rollback baseline retained: globally installed ${previous.version}.`);
156
+ }
157
+
158
+ function currentGlobalInstallation(packageName) {
159
+ try {
160
+ const globalRoot = runNpm(["root", "--global"], root).stdout.trim();
161
+ const packageRoot = join(globalRoot, packageName);
162
+ const packagePath = join(packageRoot, "package.json");
163
+ if (!existsSync(packagePath)) return null;
164
+ const pkg = readJson(packagePath, "globally installed package");
165
+ return { version: String(pkg.version || ""), entry: join(packageRoot, "bin", "machine-mcp.mjs") };
166
+ } catch {
167
+ return null;
168
+ }
169
+ }
170
+
171
+ function withoutManagedFlags(args) {
172
+ const out = [];
173
+ for (let index = 0; index < args.length; index += 1) {
174
+ const value = args[index];
175
+ if (value === "--json" || value.startsWith("--json=")) continue;
176
+ if (value === "--state-dir") { index += 1; continue; }
177
+ if (value.startsWith("--state-dir=")) continue;
178
+ out.push(value);
179
+ }
180
+ return out;
181
+ }
182
+
183
+ function argumentValue(name) {
184
+ const exact = process.argv.find((value) => value.startsWith(`${name}=`));
185
+ if (exact) return exact.slice(name.length + 1);
186
+ const index = process.argv.indexOf(name);
187
+ return index >= 0 ? process.argv[index + 1] : "";
188
+ }
189
+
190
+ function workspaceHash(workspace) {
191
+ const value = process.platform === "win32" ? String(workspace).toLowerCase() : String(workspace);
192
+ return createHash("sha256").update(value).digest("hex").slice(0, 24);
193
+ }
194
+
89
195
  function runNpm(args, cwd) {
90
196
  const result = spawnSync(process.execPath, [npmCli, ...args], {
91
197
  cwd,
@@ -1,5 +1,6 @@
1
1
  import accessContract from "../shared/access-contract.json" with { type: "json" };
2
2
  import { BridgeError } from "./errors.mjs";
3
+ import { buildAuthorityContext } from "./authority-context.mjs";
3
4
  import { policyProfile, toolNamesForPolicy, assertToolAllowed } from "./policy.mjs";
4
5
 
5
6
  export const ACCOUNT_ACCESS_REVISION = Number(accessContract.revision);
@@ -32,6 +33,11 @@ export class AccountAccessGate {
32
33
  return normalized;
33
34
  }
34
35
 
36
+ authority(authorization, daemonPolicy, origin = "relay") {
37
+ const role = origin === "relay" ? normalizeAccountRole(authorization?.role) : "owner";
38
+ return buildAuthorityContext({ authorization: { ...authorization, role }, daemonPolicy, origin });
39
+ }
40
+
35
41
  names(role) {
36
42
  return accountRoleToolNames(role);
37
43
  }
@@ -1,6 +1,7 @@
1
- import { createHash, createHmac, randomBytes } from "node:crypto";
1
+ import { createHash, randomBytes } from "node:crypto";
2
2
  import { ADMIN_AUTH_SCHEME, adminAuthTranscript } from "../shared/admin-auth.mjs";
3
3
  import { ACCOUNT_ROLES, normalizeAccountRole } from "./account-access.mjs";
4
+ import { encodeDeviceSessionCertificate, signWithDeviceSessionIdentity, validateDeviceSessionIdentity } from "./device-identity.mjs";
4
5
  import { BridgeError } from "./errors.mjs";
5
6
 
6
7
  const REQUEST_TIMEOUT_MS = 15_000;
@@ -10,15 +11,18 @@ export function generateAccountPassword() {
10
11
  }
11
12
 
12
13
  export class AccountAdminClient {
13
- constructor({ workerUrl, adminSecret, fetchImpl = fetch }) {
14
+ constructor({ workerUrl, sessionIdentity, fetchImpl = fetch }) {
14
15
  this.workerUrl = normalizeWorkerUrl(workerUrl);
15
- if (typeof adminSecret !== "string" || adminSecret.length < 24) throw new BridgeError("invalid_request", "account administration secret is missing");
16
- this.adminSecret = adminSecret;
16
+ this.sessionIdentity = validateDeviceSessionIdentity(sessionIdentity);
17
17
  this.fetchImpl = fetchImpl;
18
18
  }
19
19
 
20
- list() {
21
- return this.request("GET", "/admin/accounts");
20
+ list() { return this.request("GET", "/admin/accounts"); }
21
+ listClients() { return this.request("GET", "/admin/clients"); }
22
+
23
+ removeClient({ clientId }) {
24
+ if (!/^mcp_client_[A-Za-z0-9_-]{43}$/.test(String(clientId || ""))) throw new BridgeError("invalid_request", "OAuth client id is invalid");
25
+ return this.request("DELETE", "/admin/clients", { client_id: clientId });
22
26
  }
23
27
 
24
28
  create({ name, role, password, displayName = "" }) {
@@ -61,7 +65,7 @@ export class AccountAdminClient {
61
65
  async request(method, pathname, body) {
62
66
  const serializedBody = body === undefined ? "" : JSON.stringify(body);
63
67
  const headers = accountAdminRequestHeaders({
64
- secret: this.adminSecret,
68
+ sessionIdentity: this.sessionIdentity,
65
69
  origin: this.workerUrl,
66
70
  method,
67
71
  pathname,
@@ -89,25 +93,39 @@ export class AccountAdminClient {
89
93
  }
90
94
  }
91
95
 
92
-
93
- export function accountAdminRequestHeaders({ secret, origin, method, pathname, body = "", now = Date.now(), nonce = randomBytes(24).toString("base64url") }) {
94
- if (typeof secret !== "string" || secret.length < 24) throw new BridgeError("invalid_request", "account administration secret is missing");
96
+ export function accountAdminRequestHeaders({
97
+ sessionIdentity,
98
+ origin,
99
+ method,
100
+ pathname,
101
+ body = "",
102
+ now = Date.now(),
103
+ nonce = randomBytes(24).toString("base64url"),
104
+ }) {
105
+ validateDeviceSessionIdentity(sessionIdentity, now);
95
106
  const issuedAt = Math.floor(Number(now) / 1000);
96
107
  const bodyHash = createHash("sha256").update(String(body)).digest("hex");
97
- const transcript = adminAuthTranscript({ origin, method: String(method).toUpperCase(), pathname, bodyHash, issuedAt, nonce });
98
- const signature = createHmac("sha256", secret).update(transcript).digest("base64url");
108
+ const transcript = adminAuthTranscript({
109
+ origin,
110
+ method: String(method).toUpperCase(),
111
+ pathname,
112
+ bodyHash,
113
+ keyId: sessionIdentity.keyId,
114
+ issuedAt,
115
+ nonce,
116
+ });
99
117
  return {
100
118
  "X-Bridge-Admin-Scheme": ADMIN_AUTH_SCHEME,
101
119
  "X-Bridge-Admin-Time": String(issuedAt),
102
120
  "X-Bridge-Admin-Nonce": nonce,
103
121
  "X-Bridge-Admin-Body-SHA256": bodyHash,
104
- "X-Bridge-Admin-Signature": signature,
122
+ "X-Bridge-Admin-Key": sessionIdentity.keyId,
123
+ "X-Bridge-Admin-Signature": signWithDeviceSessionIdentity(sessionIdentity, transcript),
124
+ "X-Bridge-Device-Certificate": encodeDeviceSessionCertificate(sessionIdentity),
105
125
  };
106
126
  }
107
127
 
108
- export function accountRoleNames() {
109
- return Object.keys(ACCOUNT_ROLES);
110
- }
128
+ export function accountRoleNames() { return Object.keys(ACCOUNT_ROLES); }
111
129
 
112
130
  function normalizeWorkerUrl(value) {
113
131
  const url = new URL(String(value || ""));