machine-bridge-mcp 2.0.0 → 3.0.0-beta.10
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 +109 -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 +87 -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,90 @@
|
|
|
1
|
+
import { acquireDaemonLockWithTakeover, inspectWorkspaceDaemon } from "./daemon-process.mjs";
|
|
2
|
+
import { effectiveLogFormat, effectiveLogLevel } from "./cli-options.mjs";
|
|
3
|
+
import { createLogger } from "./log.mjs";
|
|
4
|
+
import { activatePersistentRuntime } from "./runtime-activation.mjs";
|
|
5
|
+
import { installAutostart, startAutostart, stopAutostart } from "./service.mjs";
|
|
6
|
+
import { acquireStartupLockWithWait, loadState } from "./state.mjs";
|
|
7
|
+
import { workerHealth } from "./worker-health.mjs";
|
|
8
|
+
|
|
9
|
+
export function createActivateCommand({
|
|
10
|
+
chooseWorkspace,
|
|
11
|
+
prepareRemoteState,
|
|
12
|
+
createRemoteRuntime,
|
|
13
|
+
currentPackageVersion,
|
|
14
|
+
assertNodeVersion,
|
|
15
|
+
structuredLogger,
|
|
16
|
+
} = {}) {
|
|
17
|
+
for (const [name, value] of Object.entries({
|
|
18
|
+
chooseWorkspace,
|
|
19
|
+
prepareRemoteState,
|
|
20
|
+
createRemoteRuntime,
|
|
21
|
+
currentPackageVersion,
|
|
22
|
+
assertNodeVersion,
|
|
23
|
+
structuredLogger,
|
|
24
|
+
})) {
|
|
25
|
+
if (typeof value !== "function") throw new TypeError(`activate command requires ${name}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return async function activateCommand(args) {
|
|
29
|
+
assertNodeVersion();
|
|
30
|
+
const logger = createLogger({
|
|
31
|
+
level: args.json ? "error" : effectiveLogLevel(args),
|
|
32
|
+
format: effectiveLogFormat(args),
|
|
33
|
+
component: "activate",
|
|
34
|
+
});
|
|
35
|
+
const workspace = await chooseWorkspace(args, { promptOnFirstRun: false, save: true, allowPositional: true });
|
|
36
|
+
const state = loadState(workspace, { stateDir: args.stateDir });
|
|
37
|
+
if (!state.worker?.url) {
|
|
38
|
+
throw new Error("activate requires an existing deployment; run machine-mcp once interactively before persistent activation");
|
|
39
|
+
}
|
|
40
|
+
const activationArgs = { ...args, noAutostart: true, daemonOnly: false };
|
|
41
|
+
const expectedVersion = currentPackageVersion();
|
|
42
|
+
const result = await activatePersistentRuntime({
|
|
43
|
+
expectedVersion,
|
|
44
|
+
acquireStartupLock: () => acquireStartupLockWithWait(state, { operation: "activate", logger }),
|
|
45
|
+
stopAutostart: () => stopAutostart({ logger: structuredLogger(true) }),
|
|
46
|
+
acquireDaemonLock: () => acquireDaemonLockWithTakeover(state, {
|
|
47
|
+
takeOverServiceOwner: true,
|
|
48
|
+
ownerMetadata: { mode: "foreground", version: expectedVersion },
|
|
49
|
+
logger,
|
|
50
|
+
}),
|
|
51
|
+
prepareRemoteState: () => prepareRemoteState({ args: activationArgs, workspace, state, logger }),
|
|
52
|
+
createRuntime: ({ daemonLock, readiness }) => createRemoteRuntime({
|
|
53
|
+
args: activationArgs,
|
|
54
|
+
workspace,
|
|
55
|
+
state,
|
|
56
|
+
daemonLock,
|
|
57
|
+
deviceSessionIdentity: readiness.deviceSessionIdentity,
|
|
58
|
+
exitOnTerminal: false,
|
|
59
|
+
}),
|
|
60
|
+
installAutostart: () => installAutostart({
|
|
61
|
+
workspace,
|
|
62
|
+
stateRoot: state.paths.stateRoot,
|
|
63
|
+
entryScript: process.argv[1],
|
|
64
|
+
logger: structuredLogger(true),
|
|
65
|
+
}),
|
|
66
|
+
startAutostart: () => startAutostart({ logger: structuredLogger(true) }),
|
|
67
|
+
inspectDaemon: async () => inspectWorkspaceDaemon(state),
|
|
68
|
+
checkWorker: async () => workerHealth(state.worker.url, expectedVersion, { expectedWorkerName: state.worker.name }),
|
|
69
|
+
});
|
|
70
|
+
const convergence = result.convergence;
|
|
71
|
+
const payload = {
|
|
72
|
+
ok: true,
|
|
73
|
+
version: expectedVersion,
|
|
74
|
+
workspace: state.workspace.path,
|
|
75
|
+
worker: { name: state.worker.name, url: state.worker.url, health: convergence.worker },
|
|
76
|
+
daemon: convergence.daemon,
|
|
77
|
+
service: result.serviceStart,
|
|
78
|
+
candidate_relay_verified_before_handoff: result.candidateRelayVerified,
|
|
79
|
+
};
|
|
80
|
+
if (args.json) console.log(JSON.stringify(payload, null, 2));
|
|
81
|
+
else {
|
|
82
|
+
logger.success(`Persistent runtime activated: ${expectedVersion}`);
|
|
83
|
+
logger.safePlain(` Worker: ${state.worker.name}`);
|
|
84
|
+
logger.safePlain(` Daemon: pid ${convergence.daemon.pid}`);
|
|
85
|
+
logger.safePlain(" Candidate relay readiness was verified before the login service handoff.");
|
|
86
|
+
logger.safePlain(" The login service now owns the daemon; the activation terminal may close.");
|
|
87
|
+
}
|
|
88
|
+
return payload;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
-
OPERATION_APPROVAL_SCOPES,
|
|
3
|
-
approvePendingOperation,
|
|
4
2
|
clearOperationLeases,
|
|
5
|
-
grantOperationLease,
|
|
6
3
|
listOperationApprovals,
|
|
7
4
|
revokeOperationLease,
|
|
8
5
|
} from "./operation-authorization.mjs";
|
|
9
6
|
import { loadState } from "./state.mjs";
|
|
10
7
|
|
|
11
|
-
const ACTIONS = new Set(["list", "
|
|
8
|
+
const ACTIONS = new Set(["list", "revoke", "clear"]);
|
|
12
9
|
|
|
13
10
|
export function createApprovalCommand({ chooseWorkspace, confirm }) {
|
|
14
11
|
if (typeof chooseWorkspace !== "function" || typeof confirm !== "function") {
|
|
@@ -16,85 +13,44 @@ export function createApprovalCommand({ chooseWorkspace, confirm }) {
|
|
|
16
13
|
}
|
|
17
14
|
return async function approvalCommand(args) {
|
|
18
15
|
const action = String(args._[0] || "list").toLowerCase();
|
|
19
|
-
if (!ACTIONS.has(action))
|
|
16
|
+
if (!ACTIONS.has(action)) {
|
|
17
|
+
throw new Error("Terminal operation approval was removed. Remote calls run automatically within the account role ceiling; use account management to change trust and approval revoke/clear only to remove legacy leases.");
|
|
18
|
+
}
|
|
20
19
|
const workspace = await chooseWorkspace({ ...args, _: [] }, { promptOnFirstRun: false, save: false, allowPositional: false });
|
|
21
20
|
const state = loadState(workspace, { stateDir: args.stateDir });
|
|
22
21
|
const root = state.paths.profileDir;
|
|
23
22
|
if (action === "list") return renderList(listOperationApprovals(root), args.json === true);
|
|
24
|
-
if (action === "approve") {
|
|
25
|
-
const id = requiredPositional(args, 1, "approval approve requires APPROVAL_ID");
|
|
26
|
-
return renderLease(await approvePendingOperation(
|
|
27
|
-
root,
|
|
28
|
-
id,
|
|
29
|
-
args.duration || (args.full ? "8h" : "1h"),
|
|
30
|
-
Date.now(),
|
|
31
|
-
args.full ? "full" : "",
|
|
32
|
-
), args.json === true, "Approved");
|
|
33
|
-
}
|
|
34
|
-
if (action === "grant") {
|
|
35
|
-
const scope = requiredPositional(args, 1, "approval grant requires SCOPE");
|
|
36
|
-
if (!OPERATION_APPROVAL_SCOPES.includes(scope)) {
|
|
37
|
-
throw new Error(`approval scope must be one of: ${OPERATION_APPROVAL_SCOPES.join(", ")}`);
|
|
38
|
-
}
|
|
39
|
-
const clientId = String(args.client || "");
|
|
40
|
-
const accountId = String(args.account || "");
|
|
41
|
-
if (!clientId) throw new Error("approval grant requires --client CLIENT_ID; use * only for an intentional all-client lease");
|
|
42
|
-
if (!accountId) throw new Error("approval grant requires --account ACCOUNT_ID; use * only for an intentional all-account lease");
|
|
43
|
-
return renderLease(await grantOperationLease(root, {
|
|
44
|
-
accountId,
|
|
45
|
-
clientId,
|
|
46
|
-
scope,
|
|
47
|
-
duration: args.duration || "1h",
|
|
48
|
-
}), args.json === true, "Granted");
|
|
49
|
-
}
|
|
50
23
|
if (action === "revoke") {
|
|
51
24
|
const id = requiredPositional(args, 1, "approval revoke requires LEASE_ID");
|
|
52
25
|
const removed = await revokeOperationLease(root, id);
|
|
53
26
|
if (args.json) console.log(JSON.stringify({ lease_id: id, revoked: removed }, null, 2));
|
|
54
|
-
else console.log(removed ? `Revoked capability lease: ${id}` : `
|
|
27
|
+
else console.log(removed ? `Revoked legacy capability lease: ${id}` : `Legacy capability lease was not active: ${id}`);
|
|
55
28
|
return;
|
|
56
29
|
}
|
|
57
|
-
const approved = await confirm("Revoke all
|
|
30
|
+
const approved = await confirm("Revoke all legacy remote capability leases?", args.yes === true);
|
|
58
31
|
if (!approved) {
|
|
59
|
-
console.log("No capability leases were changed.");
|
|
32
|
+
console.log("No legacy capability leases were changed.");
|
|
60
33
|
return;
|
|
61
34
|
}
|
|
62
35
|
await clearOperationLeases(root);
|
|
63
36
|
if (args.json) console.log(JSON.stringify({ cleared: true }, null, 2));
|
|
64
|
-
else console.log("Revoked all
|
|
37
|
+
else console.log("Revoked all legacy remote capability leases.");
|
|
65
38
|
};
|
|
66
39
|
}
|
|
67
40
|
|
|
68
41
|
function renderList(result, json) {
|
|
69
42
|
if (json) {
|
|
70
|
-
console.log(JSON.stringify(result, null, 2));
|
|
43
|
+
console.log(JSON.stringify({ ...result, runtime_authorization: "automatic-within-role-ceiling" }, null, 2));
|
|
71
44
|
return;
|
|
72
45
|
}
|
|
73
|
-
if (!result.
|
|
74
|
-
console.log("No
|
|
46
|
+
if (!result.leases.length) {
|
|
47
|
+
console.log("No active legacy capability leases. Runtime authorization is automatic within each account role ceiling.");
|
|
75
48
|
return;
|
|
76
49
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
console.log(` ${item.id}\t${scopeText(item.scopes)}\t${item.category}\tclient ${shortId(item.client_id)}\texpires ${formatTime(item.expires_at)}`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
if (result.leases.length) {
|
|
84
|
-
console.log("Active capability leases:");
|
|
85
|
-
for (const item of result.leases) {
|
|
86
|
-
console.log(` ${item.id}\t${scopeText(item.scopes)}\tclient ${shortId(item.client_id)}\texpires ${formatTime(item.expires_at)}`);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function renderLease(lease, json, verb) {
|
|
92
|
-
if (json) {
|
|
93
|
-
console.log(JSON.stringify(lease, null, 2));
|
|
94
|
-
return;
|
|
50
|
+
console.log("Legacy capability leases (not consumed by the current runtime):");
|
|
51
|
+
for (const item of result.leases) {
|
|
52
|
+
console.log(` ${item.id}\t${scopeText(item.scopes)}\tclient ${shortId(item.client_id)}\texpires ${formatTime(item.expires_at)}`);
|
|
95
53
|
}
|
|
96
|
-
console.log(`${verb} ${scopeText(lease.scopes)} capability lease for client ${shortId(lease.client_id)} until ${formatTime(lease.expires_at)}.`);
|
|
97
|
-
console.log(`Lease ID: ${lease.id}`);
|
|
98
54
|
}
|
|
99
55
|
|
|
100
56
|
function requiredPositional(args, index, message) {
|
|
@@ -244,7 +244,10 @@ function readBrowserPairingState(pairingFile) {
|
|
|
244
244
|
throw new Error("browser bridge state is invalid; restart machine-mcp to repair it");
|
|
245
245
|
}
|
|
246
246
|
const port = Number(pairing.port);
|
|
247
|
-
|
|
247
|
+
const extensionToken = String(pairing.extensionToken || pairing.token || "");
|
|
248
|
+
if (!/^[A-Za-z0-9_-]{32,100}$/.test(extensionToken) || (pairing.runtimeToken !== undefined && !/^[A-Za-z0-9_-]{32,100}$/.test(String(pairing.runtimeToken)))) {
|
|
249
|
+
throw new Error("browser bridge state contains invalid bounded tokens");
|
|
250
|
+
}
|
|
248
251
|
if (!Number.isInteger(port) || port < 1024 || port > 65535) throw new Error("browser bridge state contains an invalid port");
|
|
249
252
|
return { port };
|
|
250
253
|
}
|
|
@@ -302,7 +305,7 @@ function openExternal(target) {
|
|
|
302
305
|
});
|
|
303
306
|
}
|
|
304
307
|
|
|
305
|
-
async function jobCommand(args, { chooseWorkspace
|
|
308
|
+
async function jobCommand(args, { chooseWorkspace }) {
|
|
306
309
|
const action = String(args._[0] || "list").toLowerCase();
|
|
307
310
|
const workspace = await chooseWorkspace({ ...args, _: [] }, { promptOnFirstRun: false, save: false, allowPositional: false });
|
|
308
311
|
const state = loadState(workspace, { stateDir: args.stateDir });
|
|
@@ -320,19 +323,6 @@ async function jobCommand(args, { chooseWorkspace, confirm }) {
|
|
|
320
323
|
else if (action === "read") result = manager.read({ job_id: args._[1] });
|
|
321
324
|
else if (action === "inspect") result = manager.inspectLocal({ job_id: args._[1] });
|
|
322
325
|
else if (action === "cancel") result = manager.cancel({ job_id: args._[1] });
|
|
323
|
-
else if (action === "approve") {
|
|
324
|
-
if (args.json && !args.yes) throw new Error("job approve --json requires --yes");
|
|
325
|
-
const inspection = manager.inspectLocal({ job_id: args._[1] });
|
|
326
|
-
if (!args.yes) {
|
|
327
|
-
console.log(JSON.stringify(inspection, null, 2));
|
|
328
|
-
const approved = await confirm(`Approve and execute managed job ${args._[1]}?`, false);
|
|
329
|
-
if (!approved) {
|
|
330
|
-
console.log("Managed job approval cancelled. Re-run with --yes after review to skip confirmation.");
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
result = manager.approve({ job_id: args._[1] }, { localOperator: true });
|
|
335
|
-
}
|
|
336
326
|
else if (action === "submit") {
|
|
337
327
|
const planPath = args._[1];
|
|
338
328
|
if (!planPath) throw new Error("job submit requires a JSON plan file");
|
|
@@ -3,10 +3,10 @@ import { normalizeLogLevel } from "./log.mjs";
|
|
|
3
3
|
const BOOLEAN_OPTIONS = new Set([
|
|
4
4
|
"help", "version", "quiet", "json", "verbose", "rotateSecrets", "forceWorker",
|
|
5
5
|
"daemonOnly", "noAutostart", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths",
|
|
6
|
-
"yes", "keepWorker", "allowInsecurePermissions", "showPaths",
|
|
6
|
+
"yes", "keepWorker", "allowInsecurePermissions", "showPaths",
|
|
7
7
|
]);
|
|
8
8
|
const VALUE_OPTIONS = new Set([
|
|
9
|
-
"workspace", "stateDir", "workerName", "profile", "execMode", "client", "
|
|
9
|
+
"workspace", "stateDir", "workerName", "profile", "execMode", "client", "logLevel", "logFormat",
|
|
10
10
|
]);
|
|
11
11
|
|
|
12
12
|
const LOG_FORMATS = new Set(["text", "json"]);
|
|
@@ -17,6 +17,10 @@ const COMMAND_OPTIONS = new Map(Object.entries({
|
|
|
17
17
|
"daemonOnly", "noAutostart",
|
|
18
18
|
"profile", "execMode", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths",
|
|
19
19
|
]),
|
|
20
|
+
activate: new Set([
|
|
21
|
+
"workspace", "stateDir", "workerName", "quiet", "json", "verbose", "logLevel", "logFormat", "rotateSecrets", "forceWorker",
|
|
22
|
+
"profile", "execMode", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths",
|
|
23
|
+
]),
|
|
20
24
|
stdio: new Set(["workspace", "stateDir", "profile", "execMode", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths", "verbose", "quiet", "logLevel", "logFormat"]),
|
|
21
25
|
"client-config": new Set(["workspace", "stateDir", "profile", "client", "json"]),
|
|
22
26
|
status: new Set(["workspace", "stateDir"]),
|
|
@@ -28,13 +32,13 @@ const COMMAND_OPTIONS = new Map(Object.entries({
|
|
|
28
32
|
autostart: new Set(["workspace", "stateDir", "quiet"]),
|
|
29
33
|
resource: new Set(["workspace", "stateDir", "allowInsecurePermissions", "showPaths", "json"]),
|
|
30
34
|
account: new Set(["workspace", "stateDir", "json", "yes"]),
|
|
31
|
-
approval: new Set(["workspace", "stateDir", "json", "yes"
|
|
35
|
+
approval: new Set(["workspace", "stateDir", "json", "yes"]),
|
|
32
36
|
browser: new Set(["workspace", "stateDir", "json"]),
|
|
33
37
|
job: new Set(["workspace", "stateDir", "json", "yes"]),
|
|
34
38
|
uninstall: new Set(["stateDir", "keepWorker", "yes"]),
|
|
35
39
|
}));
|
|
36
40
|
|
|
37
|
-
const SINGLE_WORKSPACE_POSITIONAL_COMMANDS = new Set(["start", "stdio", "status", "doctor", "full-test", "rotate-secrets"]);
|
|
41
|
+
const SINGLE_WORKSPACE_POSITIONAL_COMMANDS = new Set(["start", "activate", "stdio", "status", "doctor", "full-test", "rotate-secrets"]);
|
|
38
42
|
const STATIC_POSITIONAL_RULES = new Map([
|
|
39
43
|
["client-config", Object.freeze({ max: 1, tooMany: "client-config accepts at most one positional client name" })],
|
|
40
44
|
["uninstall", Object.freeze({ max: 0, tooMany: "uninstall does not accept positional arguments" })],
|
|
@@ -42,7 +46,7 @@ const STATIC_POSITIONAL_RULES = new Map([
|
|
|
42
46
|
const RESOURCE_POSITIONAL_LIMITS = new Map(Object.entries({ add: 3, "generate-ssh-key": 3, remove: 2, check: 2 }));
|
|
43
47
|
const JOB_POSITIONAL_LIMITS = new Map(Object.entries({ read: 2, inspect: 2, cancel: 2, approve: 2, submit: 2 }));
|
|
44
48
|
const ACCOUNT_POSITIONAL_LIMITS = new Map(Object.entries({ list: 1, add: 3, role: 3, enable: 2, disable: 2, "rotate-password": 2, remove: 2 }));
|
|
45
|
-
const APPROVAL_POSITIONAL_LIMITS = new Map(Object.entries({ list: 1,
|
|
49
|
+
const APPROVAL_POSITIONAL_LIMITS = new Map(Object.entries({ list: 1, revoke: 2, clear: 1 }));
|
|
46
50
|
const ACTION_POSITIONAL_RULES = new Map(Object.entries({
|
|
47
51
|
workspace(args) {
|
|
48
52
|
const action = String(args._[0] || "show");
|
|
@@ -50,7 +54,7 @@ const ACTION_POSITIONAL_RULES = new Map(Object.entries({
|
|
|
50
54
|
},
|
|
51
55
|
service(args) {
|
|
52
56
|
const action = String(args._[0] || "status");
|
|
53
|
-
return { max: ["install", "status", "stop"
|
|
57
|
+
return { max: ["install", "status", "stop"].includes(action) ? 2 : 1, tooMany: `service ${action} received too many positional arguments`, workspaceConflictAfter: 1 };
|
|
54
58
|
},
|
|
55
59
|
autostart(args) { return ACTION_POSITIONAL_RULES.get("service")(args); },
|
|
56
60
|
resource(args) {
|
|
@@ -63,7 +67,7 @@ const ACTION_POSITIONAL_RULES = new Map(Object.entries({
|
|
|
63
67
|
},
|
|
64
68
|
approval(args) {
|
|
65
69
|
const action = String(args._[0] || "list");
|
|
66
|
-
return { max: APPROVAL_POSITIONAL_LIMITS.get(action) ??
|
|
70
|
+
return { max: APPROVAL_POSITIONAL_LIMITS.get(action) ?? 2, tooMany: `approval ${action} received too many positional arguments` };
|
|
67
71
|
},
|
|
68
72
|
browser(args) {
|
|
69
73
|
const action = String(args._[0] || "status");
|
|
@@ -3,12 +3,14 @@ import * as defaultService from "./service.mjs";
|
|
|
3
3
|
import { inspectWorkspaceDaemon, stopWorkspaceServiceDaemon } from "./daemon-process.mjs";
|
|
4
4
|
import { stopAndRemoveAutostart } from "./service-lifecycle.mjs";
|
|
5
5
|
import { serviceEnvironmentSummary } from "./service-environment.mjs";
|
|
6
|
+
import { scheduleServiceRestart } from "./service-restart-scheduler.mjs";
|
|
6
7
|
import { loadState, resolveWorkspace, selectedWorkspace } from "./state.mjs";
|
|
7
8
|
|
|
8
9
|
const SERVICE_ACTION_HANDLERS = new Map([
|
|
9
10
|
["status", serviceStatusAction],
|
|
10
11
|
["install", serviceInstallAction],
|
|
11
12
|
["start", serviceStartAction],
|
|
13
|
+
["restart", serviceRestartAction],
|
|
12
14
|
["stop", serviceStopAction],
|
|
13
15
|
["uninstall", serviceUninstallAction],
|
|
14
16
|
["remove", serviceUninstallAction],
|
|
@@ -24,6 +26,7 @@ export function createServiceCommand(dependencies) {
|
|
|
24
26
|
stopWorkspaceServiceDaemon: dependencies.stopWorkspaceServiceDaemon || stopWorkspaceServiceDaemon,
|
|
25
27
|
stopAndRemoveAutostart: dependencies.stopAndRemoveAutostart || stopAndRemoveAutostart,
|
|
26
28
|
serviceEnvironmentSummary: dependencies.serviceEnvironmentSummary || serviceEnvironmentSummary,
|
|
29
|
+
scheduleServiceRestart: dependencies.scheduleServiceRestart || scheduleServiceRestart,
|
|
27
30
|
loadState: dependencies.loadState || loadState,
|
|
28
31
|
resolveWorkspace: dependencies.resolveWorkspace || resolveWorkspace,
|
|
29
32
|
selectedWorkspace: dependencies.selectedWorkspace || selectedWorkspace,
|
|
@@ -73,14 +76,24 @@ async function serviceInstallAction({ args, stateRoot, service, context }) {
|
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
async function serviceStartAction({ args, service, context }) {
|
|
79
|
+
assertGlobalServiceAction(args, "start");
|
|
76
80
|
const result = await service.startAutostart({ logger: context.structuredLogger(Boolean(args.quiet)) });
|
|
77
81
|
printServiceResult(result, context);
|
|
78
82
|
}
|
|
79
83
|
|
|
84
|
+
async function serviceRestartAction({ args, context }) {
|
|
85
|
+
assertGlobalServiceAction(args, "restart");
|
|
86
|
+
const result = await context.scheduleServiceRestart();
|
|
87
|
+
printServiceResult({ ...result, provider: "detached-handoff", reason: "restart_scheduled" }, context);
|
|
88
|
+
}
|
|
89
|
+
|
|
80
90
|
async function serviceStopAction({ args, stateRoot, service, context }) {
|
|
81
91
|
const logger = context.structuredLogger(Boolean(args.quiet));
|
|
82
|
-
const provider = await service.stopAutostart({ logger });
|
|
83
92
|
const state = optionalServiceState(args, stateRoot, context);
|
|
93
|
+
const status = await service.autostartStatus();
|
|
94
|
+
const before = state ? context.inspectWorkspaceDaemon(state) : null;
|
|
95
|
+
assertExplicitStopTarget(args, status, before);
|
|
96
|
+
const provider = await service.stopAutostart({ logger });
|
|
84
97
|
const workspaceDaemon = state
|
|
85
98
|
? await context.stopWorkspaceServiceDaemon(state, { logger, reason: "service stop" })
|
|
86
99
|
: { ok: true, found: false, stopped: false, verified_service_daemon: false, reason: "workspace_not_selected" };
|
|
@@ -92,16 +105,17 @@ async function serviceStopAction({ args, stateRoot, service, context }) {
|
|
|
92
105
|
}, context);
|
|
93
106
|
}
|
|
94
107
|
|
|
95
|
-
async function serviceUninstallAction({ args, stateRoot,
|
|
108
|
+
async function serviceUninstallAction({ args, stateRoot, context }) {
|
|
109
|
+
assertGlobalServiceAction(args, "uninstall");
|
|
96
110
|
const logger = context.structuredLogger(Boolean(args.quiet));
|
|
97
|
-
const state = optionalServiceState(args, stateRoot, context);
|
|
111
|
+
const state = optionalServiceState({ ...args, workspace: undefined, stateDir: undefined }, stateRoot, context);
|
|
98
112
|
const lifecycle = await context.stopAndRemoveAutostart({
|
|
99
113
|
states: state ? [state] : [],
|
|
100
114
|
stateRoot,
|
|
101
115
|
logger,
|
|
102
116
|
reason: "service uninstall",
|
|
103
|
-
stopAutostart: service.stopAutostart,
|
|
104
|
-
uninstallAutostart: service.uninstallAutostart,
|
|
117
|
+
stopAutostart: context.service.stopAutostart,
|
|
118
|
+
uninstallAutostart: context.service.uninstallAutostart,
|
|
105
119
|
stopWorkspaceServiceDaemon: context.stopWorkspaceServiceDaemon,
|
|
106
120
|
});
|
|
107
121
|
printServiceResult({
|
|
@@ -112,6 +126,22 @@ async function serviceUninstallAction({ args, stateRoot, service, context }) {
|
|
|
112
126
|
}, context);
|
|
113
127
|
}
|
|
114
128
|
|
|
129
|
+
function assertGlobalServiceAction(args, action) {
|
|
130
|
+
if (args.workspace || args.stateDir || args._[1]) {
|
|
131
|
+
throw new Error(`service ${action} acts on the single installed machine service and does not accept workspace or state overrides; use service install to change its owner`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function assertExplicitStopTarget(args, status, workspaceDaemon) {
|
|
136
|
+
if (!hasExplicitServiceTarget(args) || status?.active !== true) return;
|
|
137
|
+
if (workspaceDaemon?.alive === true && workspaceDaemon?.verified_service_daemon === true) return;
|
|
138
|
+
throw new Error("refusing to stop the active machine service because the requested workspace/state does not own its verified daemon; omit the target to stop the installed service globally");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function hasExplicitServiceTarget(args) {
|
|
142
|
+
return Boolean(args.workspace || args.stateDir || args._[1]);
|
|
143
|
+
}
|
|
144
|
+
|
|
115
145
|
function optionalServiceState(args, stateRoot, context) {
|
|
116
146
|
const requested = args.workspace || args._[1] || context.selectedWorkspace(stateRoot);
|
|
117
147
|
if (!requested || requested === true) return null;
|