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
package/src/local/cli.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { join, resolve } from "node:path";
|
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import readline from "node:readline/promises";
|
|
5
5
|
import { LocalRuntime } from "./runtime.mjs";
|
|
6
|
-
import { acquireDaemonLockWithTakeover, stopWorkspaceServiceDaemon } from "./daemon-process.mjs";
|
|
6
|
+
import { acquireDaemonLockWithTakeover, inspectWorkspaceDaemon, stopWorkspaceServiceDaemon, workspaceDaemonOwnsPlatformAutostart } from "./daemon-process.mjs";
|
|
7
7
|
import { inspectProcessInstance } from "./process-identity.mjs";
|
|
8
8
|
import { runStdioServer } from "./stdio.mjs";
|
|
9
9
|
import { assertCanonicalFullPolicy, POLICY_PROFILES, toolsForPolicy } from "./tools.mjs";
|
|
@@ -12,6 +12,7 @@ import { effectiveLogFormat, effectiveLogLevel, normalizeCommand, parseArgs, val
|
|
|
12
12
|
import { createLocalAdminCommands } from "./cli-local-admin.mjs";
|
|
13
13
|
import { createApprovalCommand } from "./cli-approval.mjs";
|
|
14
14
|
import { createServiceCommand } from "./cli-service.mjs";
|
|
15
|
+
import { createActivateCommand } from "./cli-activate.mjs";
|
|
15
16
|
import { generateAccountPassword } from "./account-admin.mjs";
|
|
16
17
|
import { accountAdminClient, createAccountCommand } from "./cli-account-admin.mjs";
|
|
17
18
|
export { resolvePolicy } from "./cli-policy.mjs";
|
|
@@ -20,8 +21,10 @@ import { classifyOperationalError, createLogger, sanitizeLogText } from "./log.m
|
|
|
20
21
|
import { runExecutable, runWrangler } from "./shell.mjs";
|
|
21
22
|
import { runFullAccessTest } from "./full-access-test.mjs";
|
|
22
23
|
import { stopAndRemoveAutostart } from "./service-lifecycle.mjs";
|
|
24
|
+
import { stopOwnedPlatformService } from "./service-ownership.mjs";
|
|
23
25
|
import { loadServiceEnvironment } from "./service-environment.mjs";
|
|
24
|
-
import {
|
|
26
|
+
import { createDeviceSessionForRoot, deviceRootProviderStatus, ensurePreferredDeviceRoot } from "./device-root-provider.mjs";
|
|
27
|
+
import { convergeRemoteConfiguration } from "./remote-configuration.mjs";
|
|
25
28
|
import { workerHealth } from "./worker-health.mjs";
|
|
26
29
|
export { workerHealthUserReason } from "./worker-health.mjs";
|
|
27
30
|
import { activeStateJobs, activeStateLocks, knownProfileStates, knownWorkerNames } from "./state-inventory.mjs";
|
|
@@ -52,9 +55,18 @@ const localAdminCommands = createLocalAdminCommands({ chooseWorkspace, confirm }
|
|
|
52
55
|
const accountCommand = createAccountCommand({ chooseWorkspace, confirm });
|
|
53
56
|
const approvalCommand = createApprovalCommand({ chooseWorkspace, confirm });
|
|
54
57
|
const serviceCommand = createServiceCommand({ chooseWorkspace, stateRootFromArgs, structuredLogger });
|
|
58
|
+
const activateCommand = createActivateCommand({
|
|
59
|
+
chooseWorkspace,
|
|
60
|
+
prepareRemoteState,
|
|
61
|
+
createRemoteRuntime,
|
|
62
|
+
currentPackageVersion,
|
|
63
|
+
assertNodeVersion,
|
|
64
|
+
structuredLogger,
|
|
65
|
+
});
|
|
55
66
|
|
|
56
67
|
const COMMAND_HANDLERS = new Map([
|
|
57
68
|
["start", startCommand],
|
|
69
|
+
["activate", activateCommand],
|
|
58
70
|
["stdio", stdioCommand],
|
|
59
71
|
["client-config", clientConfigCommand],
|
|
60
72
|
["status", statusCommand],
|
|
@@ -206,11 +218,28 @@ async function prepareStartMode(args, state, logger) {
|
|
|
206
218
|
trimAutostartLogs(state.paths.stateRoot);
|
|
207
219
|
return { takeOverServiceOwner: false };
|
|
208
220
|
}
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
221
|
+
// The platform service name is machine-global, while state/workspace locks
|
|
222
|
+
// are scoped. Stop the platform service only when the daemon lock proves
|
|
223
|
+
// that the loaded service belongs to this exact state/workspace. An
|
|
224
|
+
// unrelated foreground start (including install smoke tests with isolated
|
|
225
|
+
// HOME/state) must never unload another Machine Bridge deployment.
|
|
226
|
+
const ownership = await stopOwnedPlatformService({
|
|
227
|
+
state,
|
|
228
|
+
inspectWorkspaceDaemon,
|
|
229
|
+
ownsPlatformAutostart: workspaceDaemonOwnsPlatformAutostart,
|
|
230
|
+
stopPlatformService: () => stopAutostartBestEffort(logger),
|
|
231
|
+
});
|
|
232
|
+
if (!ownership.owned) {
|
|
233
|
+
logger.debug?.("foreground startup left unrelated machine autostart untouched", {
|
|
234
|
+
workspace_daemon_present: ownership.daemon.present === true,
|
|
235
|
+
workspace_daemon_alive: ownership.daemon.alive === true,
|
|
236
|
+
identity_reason: ownership.daemon.identity_reason || "not_running",
|
|
237
|
+
});
|
|
238
|
+
return { takeOverServiceOwner: true, provider: null };
|
|
239
|
+
}
|
|
240
|
+
// Once ownership is proven, unload the provider first so KeepAlive cannot
|
|
241
|
+
// respawn the service while the verified process lock is reclaimed.
|
|
242
|
+
return ownership.provider;
|
|
214
243
|
}
|
|
215
244
|
|
|
216
245
|
function reportExistingDaemon(args, state, owner, logger) {
|
|
@@ -255,9 +284,9 @@ async function startRemoteRuntime({ args, workspace, state, daemonLock, logger }
|
|
|
255
284
|
let runtime = null;
|
|
256
285
|
try {
|
|
257
286
|
const readiness = await prepareRemoteState({ args, workspace, state, logger });
|
|
258
|
-
runtime = createRemoteRuntime({ args, workspace, state, daemonLock });
|
|
287
|
+
runtime = createRemoteRuntime({ args, workspace, state, daemonLock, deviceSessionIdentity: readiness.deviceSessionIdentity });
|
|
259
288
|
await runtime.start();
|
|
260
|
-
reportRemoteReady(args, state, readiness);
|
|
289
|
+
reportRemoteReady(args, state, readiness, logger);
|
|
261
290
|
keepProcessAlive({ daemon: runtime, lock: daemonLock, logger });
|
|
262
291
|
} catch (error) {
|
|
263
292
|
try { runtime?.stop?.(); } catch {}
|
|
@@ -267,32 +296,32 @@ async function startRemoteRuntime({ args, workspace, state, daemonLock, logger }
|
|
|
267
296
|
}
|
|
268
297
|
|
|
269
298
|
async function prepareRemoteState({ args, workspace, state, logger }) {
|
|
270
|
-
const workerName = validateWorkerName(args.workerName);
|
|
271
|
-
ensureWorkerSecrets(state, {
|
|
272
|
-
rotateSecrets: Boolean(args.rotateSecrets),
|
|
273
|
-
workerName,
|
|
274
|
-
allowWorkerRename: Boolean(args.forceWorker),
|
|
275
|
-
});
|
|
276
|
-
state.policy = resolvePolicy(args, state.policy);
|
|
277
|
-
state.policy.updatedAt = new Date().toISOString();
|
|
278
|
-
saveState(state);
|
|
279
|
-
|
|
280
|
-
let initialOwner = null;
|
|
281
299
|
if (!args.daemonOnly) {
|
|
282
|
-
await
|
|
283
|
-
initialOwner = await ensureInitialOwnerAccount(state);
|
|
300
|
+
await convergeRemoteConfiguration({ args, state });
|
|
284
301
|
} else if (!state.worker.url) {
|
|
285
302
|
throw new Error("--daemon-only requires an existing Worker URL; run start once without --daemon-only");
|
|
303
|
+
} else if (state.worker.pendingDeviceIdentity) {
|
|
304
|
+
throw new Error("--daemon-only cannot activate a pending device root; run a normal start once to deploy and promote it");
|
|
286
305
|
}
|
|
287
306
|
|
|
307
|
+
const deviceSessionIdentity = await createDeviceSessionForRoot(
|
|
308
|
+
state.worker.deviceIdentity,
|
|
309
|
+
state.worker.url,
|
|
310
|
+
"machine-bridge-mcp",
|
|
311
|
+
currentPackageVersion(),
|
|
312
|
+
{ profileDir: state.paths.profileDir, reason: "Authorize Machine Bridge startup" },
|
|
313
|
+
);
|
|
314
|
+
const initialOwner = args.daemonOnly ? null : await ensureInitialOwnerAccount(state, deviceSessionIdentity);
|
|
288
315
|
if (!args.daemonOnly && !args.noAutostart) {
|
|
289
316
|
await installAutostartBestEffort({ workspace, stateRoot: state.paths.stateRoot, entryScript: process.argv[1], logger });
|
|
290
317
|
}
|
|
291
|
-
return { initialOwner };
|
|
318
|
+
return { initialOwner, deviceSessionIdentity };
|
|
292
319
|
}
|
|
293
320
|
|
|
294
|
-
|
|
295
|
-
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
async function ensureInitialOwnerAccount(state, deviceSessionIdentity) {
|
|
324
|
+
const client = await accountAdminClient(state, deviceSessionIdentity);
|
|
296
325
|
const existing = await client.list();
|
|
297
326
|
if (existing.accounts.length > 0) return null;
|
|
298
327
|
const password = generateAccountPassword();
|
|
@@ -300,10 +329,11 @@ async function ensureInitialOwnerAccount(state) {
|
|
|
300
329
|
return { ...created.account, password };
|
|
301
330
|
}
|
|
302
331
|
|
|
303
|
-
function createRemoteRuntime({ args, workspace, state, daemonLock }) {
|
|
304
|
-
|
|
332
|
+
function createRemoteRuntime({ args, workspace, state, daemonLock, deviceSessionIdentity, exitOnTerminal = true }) {
|
|
333
|
+
const terminalState = { error: null };
|
|
334
|
+
const runtime = new LocalRuntime({
|
|
305
335
|
workerUrl: state.worker.url,
|
|
306
|
-
deviceIdentity:
|
|
336
|
+
deviceIdentity: deviceSessionIdentity,
|
|
307
337
|
expectedRelayVersion: currentPackageVersion(),
|
|
308
338
|
workspace,
|
|
309
339
|
policy: state.policy,
|
|
@@ -313,18 +343,29 @@ function createRemoteRuntime({ args, workspace, state, daemonLock }) {
|
|
|
313
343
|
resources: state.resources,
|
|
314
344
|
resourceStatePath: state.paths.statePath,
|
|
315
345
|
browserStateRoot: state.paths.stateRoot,
|
|
346
|
+
deviceRootStatus: deviceRootProviderStatus(state.worker.deviceIdentity),
|
|
316
347
|
onSuperseded: () => {
|
|
348
|
+
if (!exitOnTerminal) {
|
|
349
|
+
terminalState.error ??= new Error("candidate daemon was superseded before service handoff");
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
317
352
|
daemonLock.release();
|
|
318
353
|
process.exit(0);
|
|
319
354
|
},
|
|
320
|
-
onFatal: () => {
|
|
355
|
+
onFatal: (error) => {
|
|
356
|
+
if (!exitOnTerminal) {
|
|
357
|
+
terminalState.error ??= error instanceof Error ? error : new Error("candidate relay failed before service handoff");
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
321
360
|
daemonLock.release();
|
|
322
361
|
process.exit(1);
|
|
323
362
|
},
|
|
324
363
|
});
|
|
364
|
+
if (!exitOnTerminal) runtime.terminalError = () => terminalState.error;
|
|
365
|
+
return runtime;
|
|
325
366
|
}
|
|
326
367
|
|
|
327
|
-
function reportRemoteReady(args, state, readiness) {
|
|
368
|
+
function reportRemoteReady(args, state, readiness, logger) {
|
|
328
369
|
if (args.json) {
|
|
329
370
|
printStartJson(state, { initialOwner: readiness.initialOwner });
|
|
330
371
|
return;
|
|
@@ -333,6 +374,7 @@ function reportRemoteReady(args, state, readiness) {
|
|
|
333
374
|
quiet: Boolean(args.quiet),
|
|
334
375
|
verbose: Boolean(args.verbose),
|
|
335
376
|
initialOwner: readiness.initialOwner,
|
|
377
|
+
logger,
|
|
336
378
|
});
|
|
337
379
|
}
|
|
338
380
|
|
|
@@ -350,6 +392,7 @@ async function stdioCommand(args) {
|
|
|
350
392
|
resources: state.resources,
|
|
351
393
|
resourceStatePath: state.paths.statePath,
|
|
352
394
|
browserStateRoot: state.paths.stateRoot,
|
|
395
|
+
deviceRootStatus: state.worker?.deviceIdentity ? deviceRootProviderStatus(state.worker.deviceIdentity) : null,
|
|
353
396
|
});
|
|
354
397
|
}
|
|
355
398
|
|
|
@@ -379,10 +422,6 @@ async function clientConfigCommand(args) {
|
|
|
379
422
|
}
|
|
380
423
|
}
|
|
381
424
|
|
|
382
|
-
async function ensureWorker(state, args) {
|
|
383
|
-
const logger = createLogger({ level: args.json ? "error" : effectiveLogLevel(args), format: effectiveLogFormat(args), component: "worker" });
|
|
384
|
-
return ensureWorkerDeployment(state, args, { logger });
|
|
385
|
-
}
|
|
386
425
|
|
|
387
426
|
|
|
388
427
|
function printStartJson(state, { requestedChangesApplied = true, notice = "", initialOwner = null } = {}) {
|
|
@@ -401,8 +440,21 @@ function printStartJson(state, { requestedChangesApplied = true, notice = "", in
|
|
|
401
440
|
});
|
|
402
441
|
}
|
|
403
442
|
|
|
404
|
-
function printMcpConnection(state, { quiet = false, verbose = false, initialOwner = null } = {}) {
|
|
405
|
-
const
|
|
443
|
+
function printMcpConnection(state, { quiet = false, verbose = false, initialOwner = null, logger: readyLogger = null } = {}) {
|
|
444
|
+
const output = readyLogger || createLogger({ component: "ready", quiet, level: quiet ? "error" : verbose ? "debug" : "info" });
|
|
445
|
+
if (output.format === "json" && !initialOwner) {
|
|
446
|
+
output.event("success", "daemon.ready", {
|
|
447
|
+
mcp_server_url: state.worker.mcpServerUrl,
|
|
448
|
+
worker_name: state.worker.name,
|
|
449
|
+
workspace_path: state.workspace.path,
|
|
450
|
+
policy_profile: state.policy.profile,
|
|
451
|
+
policy_origin: state.policy.origin,
|
|
452
|
+
write_enabled: state.policy.allowWrite,
|
|
453
|
+
exec_mode: state.policy.execMode,
|
|
454
|
+
}, "Remote MCP bridge is ready");
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const logger = output;
|
|
406
458
|
logger.success("Remote MCP bridge is ready");
|
|
407
459
|
logger.plain(` MCP Server URL: ${state.worker.mcpServerUrl}`);
|
|
408
460
|
if (initialOwner) {
|
|
@@ -522,7 +574,12 @@ async function rotateSecretsCommand(args) {
|
|
|
522
574
|
const operationLogger = createLogger({ level: args.quiet ? "error" : "warn", component: "service" });
|
|
523
575
|
const startupLock = await acquireStartupLockWithWait(state, { operation: "rotate-secrets", logger: operationLogger });
|
|
524
576
|
try {
|
|
525
|
-
await
|
|
577
|
+
await stopOwnedPlatformService({
|
|
578
|
+
state,
|
|
579
|
+
inspectWorkspaceDaemon,
|
|
580
|
+
ownsPlatformAutostart: workspaceDaemonOwnsPlatformAutostart,
|
|
581
|
+
stopPlatformService: () => stopAutostartBestEffort(operationLogger),
|
|
582
|
+
});
|
|
526
583
|
const stopped = await stopWorkspaceServiceDaemon(state, { logger: operationLogger, reason: "secret rotation" });
|
|
527
584
|
if (stopped.found && !stopped.ok) {
|
|
528
585
|
const pid = stopped.pid ? `pid ${stopped.pid}` : "unknown pid";
|
|
@@ -534,10 +591,17 @@ async function rotateSecretsCommand(args) {
|
|
|
534
591
|
if (daemonIdentity?.current) {
|
|
535
592
|
throw new Error(`refusing to rotate secrets while the daemon is active (pid ${daemonOwner.pid}); stop the foreground daemon and retry`);
|
|
536
593
|
}
|
|
537
|
-
|
|
594
|
+
if (state.worker.pendingDeviceIdentity) throw new Error("a device-root rotation is already pending; run machine-mcp to deploy it before rotating again");
|
|
595
|
+
ensureWorkerSecrets(state, { rotateSecrets: true, deferDeviceRotation: true });
|
|
596
|
+
state.worker.pendingDeviceIdentity = await ensurePreferredDeviceRoot({
|
|
597
|
+
profileDir: state.paths.profileDir,
|
|
598
|
+
workspaceHash: state.workspace.hash,
|
|
599
|
+
existing: null,
|
|
600
|
+
rotate: true,
|
|
601
|
+
});
|
|
538
602
|
saveState(state);
|
|
539
|
-
console.log("
|
|
540
|
-
console.log("All account access tokens are invalid. Run machine-mcp to
|
|
603
|
+
console.log("Prepared a two-phase rotation for account administration, device root, and token-version secrets.");
|
|
604
|
+
console.log("All account access tokens are invalid. Run machine-mcp to deploy, verify, and atomically promote the pending device root.");
|
|
541
605
|
} finally {
|
|
542
606
|
startupLock.release();
|
|
543
607
|
}
|
|
@@ -694,14 +758,7 @@ function escapeRegExp(value) {
|
|
|
694
758
|
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
695
759
|
}
|
|
696
760
|
|
|
697
|
-
|
|
698
|
-
if (value === undefined || value === null || value === false) return undefined;
|
|
699
|
-
const name = String(value).trim();
|
|
700
|
-
if (!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(name)) {
|
|
701
|
-
throw new Error("--worker-name must be 1-63 lowercase letters, digits, or hyphens, and cannot start or end with a hyphen");
|
|
702
|
-
}
|
|
703
|
-
return name;
|
|
704
|
-
}
|
|
761
|
+
|
|
705
762
|
|
|
706
763
|
export function isSupportedNodeVersion(version = process.versions.node) {
|
|
707
764
|
const major = Number(String(version || "").replace(/^v/, "").split(".")[0]);
|
|
@@ -737,28 +794,25 @@ Usage:
|
|
|
737
794
|
|
|
738
795
|
Commands:
|
|
739
796
|
start Deploy/update Worker, take over autostart, run foreground daemon
|
|
797
|
+
activate Deploy/update Worker, replace autostart, start and verify background daemon
|
|
740
798
|
stdio Run a local MCP stdio server for Claude, Cursor, Codex, and compatible clients
|
|
741
799
|
client-config Print stdio client configuration snippets
|
|
742
800
|
workspace show Show remembered workspace
|
|
743
801
|
workspace set Re-select workspace; prompts with current/default path
|
|
744
802
|
service status Show autostart status
|
|
745
803
|
service install Install login autostart for remembered/current workspace
|
|
746
|
-
service start
|
|
804
|
+
service start Ensure the installed autostart service is running (idempotent)
|
|
805
|
+
service restart Schedule an explicit service-manager restart and return before handoff
|
|
747
806
|
service stop Stop the installed autostart service
|
|
748
807
|
service uninstall Remove only the autostart entry
|
|
749
808
|
status Print redacted local profile state and Worker health
|
|
750
809
|
doctor Check Node, Wrangler, Cloudflare login, Worker health
|
|
751
810
|
full-test Run real local full-profile capability tests in a temporary sandbox
|
|
752
811
|
rotate-secrets Rotate account-admin, device identity, and global token-version secrets
|
|
753
|
-
account list|add|role|enable|disable|rotate-password|remove
|
|
754
|
-
Manage
|
|
755
|
-
approval list
|
|
756
|
-
|
|
757
|
-
Approve its scope, or open an explicit full window (maximum 8h)
|
|
758
|
-
approval grant SCOPE --account ACCOUNT_ID --client CLIENT_ID [--duration 1h]
|
|
759
|
-
Pre-authorize a bounded scope; use * values only intentionally
|
|
760
|
-
approval revoke LEASE_ID | approval clear
|
|
761
|
-
Revoke one or all local capability leases
|
|
812
|
+
account list|clients|revoke-client|add|role|enable|disable|rotate-password|remove
|
|
813
|
+
Manage remote accounts, trusted clients, and targeted revocation
|
|
814
|
+
approval list|revoke|clear
|
|
815
|
+
Inspect or remove legacy leases; runtime calls are automatic within role ceilings
|
|
762
816
|
resource generate-ssh-key NAME [PATH]
|
|
763
817
|
Generate/reuse an Ed25519 key locally and register its private file by alias
|
|
764
818
|
browser status Show browser-extension bridge and connection status
|
|
@@ -165,6 +165,12 @@ export async function stopWorkspaceServiceDaemon(state, options = {}) {
|
|
|
165
165
|
};
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
export function workspaceDaemonOwnsPlatformAutostart(status = {}) {
|
|
169
|
+
return status.alive === true
|
|
170
|
+
&& status.verified_service_daemon === true
|
|
171
|
+
&& status.mode === "service";
|
|
172
|
+
}
|
|
173
|
+
|
|
168
174
|
export function inspectWorkspaceDaemon(state) {
|
|
169
175
|
const owner = readDaemonLockOwner(daemonLockPathForState(state));
|
|
170
176
|
if (!owner) return { present: false, alive: false, verified_service_daemon: false };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { BridgeError } from "./errors.mjs";
|
|
6
|
+
|
|
7
|
+
const MACOS_SANDBOX_EXEC = "/usr/bin/sandbox-exec";
|
|
8
|
+
let macosProbeResult;
|
|
9
|
+
|
|
10
|
+
export function delegatedProcessCommand({ command, args = [], workspace, runtimeDir, context = {}, platform = process.platform } = {}) {
|
|
11
|
+
const principal = context?.authority?.principal;
|
|
12
|
+
if (!principal || principal.kind !== "account" || principal.role === "owner") {
|
|
13
|
+
return { command, args, isolation: "owner-or-local-user" };
|
|
14
|
+
}
|
|
15
|
+
if (platform === "darwin" && macosSandboxAvailable({ platform })) {
|
|
16
|
+
const profile = macosProfile({ workspace, runtimeDir });
|
|
17
|
+
return {
|
|
18
|
+
command: MACOS_SANDBOX_EXEC,
|
|
19
|
+
args: ["-p", profile, String(command), ...args.map(String)],
|
|
20
|
+
isolation: "macos-sandbox-exec-workspace",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
throw new BridgeError(
|
|
24
|
+
"policy_denied",
|
|
25
|
+
"delegated process execution requires a behavior-verified OS workspace sandbox; use the owner account or an isolated worker account/VM",
|
|
26
|
+
{ details: { reason: "delegated_process_isolation_unavailable", platform } },
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function delegatedProcessIsolationStatus(platform = process.platform) {
|
|
31
|
+
if (platform === "darwin" && macosSandboxAvailable({ platform })) {
|
|
32
|
+
return {
|
|
33
|
+
available: true,
|
|
34
|
+
provider: "macos-sandbox-exec-deny-default",
|
|
35
|
+
network: "allowed",
|
|
36
|
+
filesystem: "workspace-and-runtime-write; system-runtime-read",
|
|
37
|
+
keychain: "common Keychain CLI access denied by the behavior probe; not a complete same-user tenancy boundary",
|
|
38
|
+
apple_events: "not reachable through the deny-default profile",
|
|
39
|
+
residual: "sandbox-exec is a compatibility boundary, not separate OS-user tenancy; choose a narrow workspace",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
available: false,
|
|
44
|
+
provider: null,
|
|
45
|
+
network: "unavailable",
|
|
46
|
+
filesystem: "unavailable",
|
|
47
|
+
keychain: "unavailable",
|
|
48
|
+
apple_events: "unavailable",
|
|
49
|
+
residual: platform === "darwin"
|
|
50
|
+
? "sandbox-exec exists but the workspace read/write, outside-path denial, or Keychain-denial behavior probe failed; delegated execution fails closed"
|
|
51
|
+
: "no verified delegated process sandbox provider is installed; delegated execution fails closed",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function macosDelegatedSandboxProfile({ workspace, runtimeDir } = {}) {
|
|
56
|
+
return macosProfile({ workspace, runtimeDir });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function macosSandboxAvailable(options = {}) {
|
|
60
|
+
if (options.refresh === true) macosProbeResult = undefined;
|
|
61
|
+
if (macosProbeResult !== undefined) return macosProbeResult;
|
|
62
|
+
const platform = String(options.platform || process.platform);
|
|
63
|
+
const exists = typeof options.exists === "function" ? options.exists : existsSync;
|
|
64
|
+
if (platform !== "darwin" || !exists(MACOS_SANDBOX_EXEC)) {
|
|
65
|
+
macosProbeResult = false;
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const probe = typeof options.behaviorProbe === "function" ? options.behaviorProbe : probeMacosDelegatedSandbox;
|
|
69
|
+
macosProbeResult = probe({ spawnSyncProcess: options.spawnSync || spawnSync }) === true;
|
|
70
|
+
return macosProbeResult;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function probeMacosDelegatedSandbox(options = {}) {
|
|
74
|
+
const run = typeof options.spawnSyncProcess === "function" ? options.spawnSyncProcess : spawnSync;
|
|
75
|
+
const root = mkdtempSync(path.join(tmpdir(), "mbm-delegated-sandbox-probe-"));
|
|
76
|
+
const workspace = path.join(root, "workspace");
|
|
77
|
+
const runtimeDir = path.join(root, "runtime");
|
|
78
|
+
const outside = path.join(root, "outside.txt");
|
|
79
|
+
const outsideWrite = path.join(root, "outside-write.txt");
|
|
80
|
+
const workspaceWrite = path.join(workspace, "allowed-write.txt");
|
|
81
|
+
try {
|
|
82
|
+
mkdirSync(workspace, { mode: 0o700 });
|
|
83
|
+
mkdirSync(runtimeDir, { mode: 0o700 });
|
|
84
|
+
writeFileSync(path.join(workspace, "allowed.txt"), "allowed\n", { mode: 0o600 });
|
|
85
|
+
writeFileSync(outside, "blocked\n", { mode: 0o600 });
|
|
86
|
+
const profile = macosProfile({ workspace, runtimeDir });
|
|
87
|
+
const execute = (argv) => run(MACOS_SANDBOX_EXEC, ["-p", profile, ...argv], {
|
|
88
|
+
encoding: "utf8",
|
|
89
|
+
timeout: 5_000,
|
|
90
|
+
maxBuffer: 64 * 1024,
|
|
91
|
+
windowsHide: true,
|
|
92
|
+
env: { PATH: "/usr/bin:/bin:/usr/sbin:/sbin", HOME: workspace, TMPDIR: runtimeDir, LANG: "C", LC_ALL: "C" },
|
|
93
|
+
});
|
|
94
|
+
const allowedRead = execute(["/bin/cat", path.join(workspace, "allowed.txt")]);
|
|
95
|
+
const allowedWrite = execute(["/bin/sh", "-c", `printf allowed > ${shellQuote(workspaceWrite)}`]);
|
|
96
|
+
const blockedRead = execute(["/bin/cat", outside]);
|
|
97
|
+
const blockedWrite = execute(["/bin/sh", "-c", `printf blocked > ${shellQuote(outsideWrite)}`]);
|
|
98
|
+
const keychain = execute(["/usr/bin/security", "list-keychains"]);
|
|
99
|
+
return allowedRead.status === 0 && allowedWrite.status === 0 && existsSync(workspaceWrite)
|
|
100
|
+
&& blockedRead.status !== 0 && blockedWrite.status !== 0 && !existsSync(outsideWrite)
|
|
101
|
+
&& keychain.status !== 0;
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
} finally {
|
|
105
|
+
rmSync(root, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function macosProfile({ workspace, runtimeDir }) {
|
|
110
|
+
const readable = [
|
|
111
|
+
"/System",
|
|
112
|
+
"/usr",
|
|
113
|
+
"/bin",
|
|
114
|
+
"/sbin",
|
|
115
|
+
"/Library",
|
|
116
|
+
"/Applications",
|
|
117
|
+
"/opt/homebrew",
|
|
118
|
+
"/nix/store",
|
|
119
|
+
"/private/etc",
|
|
120
|
+
"/private/var/db",
|
|
121
|
+
"/dev",
|
|
122
|
+
workspace,
|
|
123
|
+
runtimeDir,
|
|
124
|
+
].filter(Boolean);
|
|
125
|
+
const writable = [workspace, runtimeDir, "/private/tmp"].filter(Boolean);
|
|
126
|
+
return [
|
|
127
|
+
"(version 1)",
|
|
128
|
+
"(deny default)",
|
|
129
|
+
"(allow process*)",
|
|
130
|
+
"(allow signal (target self))",
|
|
131
|
+
"(allow sysctl-read)",
|
|
132
|
+
"(allow mach-lookup)",
|
|
133
|
+
"(allow ipc-posix-shm)",
|
|
134
|
+
"(allow network*)",
|
|
135
|
+
"(allow file-read-metadata)",
|
|
136
|
+
...readable.map((value) => `(allow file-read* (subpath ${sandboxString(path.resolve(value))}))`),
|
|
137
|
+
...writable.map((value) => `(allow file-write* (subpath ${sandboxString(path.resolve(value))}))`),
|
|
138
|
+
].join(" ");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function sandboxString(value) {
|
|
142
|
+
return `"${String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function shellQuote(value) {
|
|
146
|
+
return `'${String(value).replaceAll("'", `'\\''`)}'`;
|
|
147
|
+
}
|