machine-bridge-mcp 1.2.0 → 1.2.2
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 +19 -0
- package/browser-extension/browser-operations.js +9 -3
- package/browser-extension/devtools-input.js +2 -2
- package/browser-extension/manifest.json +2 -2
- package/docs/ARCHITECTURE.md +6 -6
- package/docs/AUDIT.md +26 -0
- package/docs/ENGINEERING.md +3 -0
- package/docs/LOGGING.md +6 -2
- package/docs/OPERATIONS.md +5 -3
- package/docs/PROJECT_STANDARDS.md +1 -0
- package/docs/TESTING.md +6 -2
- package/docs/UPGRADING.md +6 -2
- package/package.json +4 -3
- package/scripts/coverage-check.mjs +3 -1
- package/src/local/account-access.mjs +7 -5
- package/src/local/call-registry.mjs +9 -3
- package/src/local/cli-local-admin.mjs +74 -38
- package/src/local/cli-options.mjs +18 -18
- package/src/local/cli-policy.mjs +1 -1
- package/src/local/cli-service.mjs +132 -0
- package/src/local/cli.mjs +24 -106
- package/src/local/log.mjs +10 -2
- package/src/local/managed-job-plan.mjs +3 -3
- package/src/local/policy.mjs +11 -6
- package/src/local/project-package.mjs +1 -1
- package/src/local/relay-connection.mjs +21 -0
- package/src/local/resource-operations.mjs +1 -1
- package/src/local/runtime-reporting.mjs +1 -1
- package/src/local/runtime.mjs +65 -27
- package/src/local/tool-executor.mjs +3 -3
- package/src/local/worker-deployment.mjs +15 -10
- package/src/local/worker-health.mjs +7 -3
- package/src/worker/access.ts +4 -3
- package/src/worker/http.ts +2 -2
- package/src/worker/index.ts +58 -17
- package/src/worker/oauth-controller.ts +11 -2
- package/src/worker/observability.ts +3 -1
- package/src/worker/pending-calls.ts +17 -0
- package/wrangler.jsonc +1 -1
|
@@ -11,7 +11,7 @@ const VALUE_OPTIONS = new Set([
|
|
|
11
11
|
|
|
12
12
|
const LOG_FORMATS = new Set(["text", "json"]);
|
|
13
13
|
|
|
14
|
-
const COMMAND_OPTIONS = {
|
|
14
|
+
const COMMAND_OPTIONS = new Map(Object.entries({
|
|
15
15
|
start: new Set([
|
|
16
16
|
"workspace", "stateDir", "workerName", "quiet", "json", "verbose", "logLevel", "logFormat", "rotateSecrets", "forceWorker",
|
|
17
17
|
"daemonOnly", "noAutostart",
|
|
@@ -31,17 +31,17 @@ const COMMAND_OPTIONS = {
|
|
|
31
31
|
browser: new Set(["workspace", "stateDir", "json"]),
|
|
32
32
|
job: new Set(["workspace", "stateDir", "json", "yes"]),
|
|
33
33
|
uninstall: new Set(["stateDir", "keepWorker", "yes"]),
|
|
34
|
-
};
|
|
34
|
+
}));
|
|
35
35
|
|
|
36
36
|
const SINGLE_WORKSPACE_POSITIONAL_COMMANDS = new Set(["start", "stdio", "status", "doctor", "full-test", "rotate-secrets"]);
|
|
37
|
-
const STATIC_POSITIONAL_RULES =
|
|
38
|
-
"client-config"
|
|
39
|
-
uninstall
|
|
40
|
-
|
|
41
|
-
const RESOURCE_POSITIONAL_LIMITS = Object.
|
|
42
|
-
const JOB_POSITIONAL_LIMITS = Object.
|
|
43
|
-
const ACCOUNT_POSITIONAL_LIMITS = Object.
|
|
44
|
-
const ACTION_POSITIONAL_RULES = Object.
|
|
37
|
+
const STATIC_POSITIONAL_RULES = new Map([
|
|
38
|
+
["client-config", Object.freeze({ max: 1, tooMany: "client-config accepts at most one positional client name" })],
|
|
39
|
+
["uninstall", Object.freeze({ max: 0, tooMany: "uninstall does not accept positional arguments" })],
|
|
40
|
+
]);
|
|
41
|
+
const RESOURCE_POSITIONAL_LIMITS = new Map(Object.entries({ add: 3, "generate-ssh-key": 3, remove: 2, check: 2 }));
|
|
42
|
+
const JOB_POSITIONAL_LIMITS = new Map(Object.entries({ read: 2, inspect: 2, cancel: 2, approve: 2, submit: 2 }));
|
|
43
|
+
const ACCOUNT_POSITIONAL_LIMITS = new Map(Object.entries({ list: 1, add: 3, role: 3, enable: 2, disable: 2, "rotate-password": 2, remove: 2 }));
|
|
44
|
+
const ACTION_POSITIONAL_RULES = new Map(Object.entries({
|
|
45
45
|
workspace(args) {
|
|
46
46
|
const action = String(args._[0] || "show");
|
|
47
47
|
return { max: action === "set" || action === "select" ? 2 : 1, tooMany: `workspace ${action} received too many positional arguments`, workspaceConflictAfter: 1 };
|
|
@@ -50,14 +50,14 @@ const ACTION_POSITIONAL_RULES = Object.freeze({
|
|
|
50
50
|
const action = String(args._[0] || "status");
|
|
51
51
|
return { max: ["install", "status", "stop", "uninstall", "remove"].includes(action) ? 2 : 1, tooMany: `service ${action} received too many positional arguments`, workspaceConflictAfter: 1 };
|
|
52
52
|
},
|
|
53
|
-
autostart(args) { return ACTION_POSITIONAL_RULES.service(args); },
|
|
53
|
+
autostart(args) { return ACTION_POSITIONAL_RULES.get("service")(args); },
|
|
54
54
|
resource(args) {
|
|
55
55
|
const action = String(args._[0] || "list");
|
|
56
|
-
return { max: RESOURCE_POSITIONAL_LIMITS
|
|
56
|
+
return { max: RESOURCE_POSITIONAL_LIMITS.get(action) ?? 1, tooMany: `resource ${action} received too many positional arguments` };
|
|
57
57
|
},
|
|
58
58
|
account(args) {
|
|
59
59
|
const action = String(args._[0] || "list");
|
|
60
|
-
return { max: ACCOUNT_POSITIONAL_LIMITS
|
|
60
|
+
return { max: ACCOUNT_POSITIONAL_LIMITS.get(action) ?? 1, tooMany: `account ${action} received too many positional arguments` };
|
|
61
61
|
},
|
|
62
62
|
browser(args) {
|
|
63
63
|
const action = String(args._[0] || "status");
|
|
@@ -65,9 +65,9 @@ const ACTION_POSITIONAL_RULES = Object.freeze({
|
|
|
65
65
|
},
|
|
66
66
|
job(args) {
|
|
67
67
|
const action = String(args._[0] || "list");
|
|
68
|
-
return { max: JOB_POSITIONAL_LIMITS
|
|
68
|
+
return { max: JOB_POSITIONAL_LIMITS.get(action) ?? 1, tooMany: `job ${action} received too many positional arguments` };
|
|
69
69
|
},
|
|
70
|
-
});
|
|
70
|
+
}));
|
|
71
71
|
|
|
72
72
|
export function normalizeCommand(argv) {
|
|
73
73
|
if (!argv.length || argv[0].startsWith("--")) return ["start", argv];
|
|
@@ -105,7 +105,7 @@ export function parseArgs(argv) {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
export function validateCommandOptions(command, args) {
|
|
108
|
-
const allowed = COMMAND_OPTIONS
|
|
108
|
+
const allowed = COMMAND_OPTIONS.get(command);
|
|
109
109
|
if (!allowed) return;
|
|
110
110
|
for (const key of Object.keys(args)) {
|
|
111
111
|
if (key === "_" || key === "help" || key === "version") continue;
|
|
@@ -144,8 +144,8 @@ function positionalRule(command, args) {
|
|
|
144
144
|
if (SINGLE_WORKSPACE_POSITIONAL_COMMANDS.has(command)) {
|
|
145
145
|
return { max: 1, tooMany: `${command} accepts at most one positional workspace path`, workspaceConflictAfter: 0 };
|
|
146
146
|
}
|
|
147
|
-
if (STATIC_POSITIONAL_RULES
|
|
148
|
-
return ACTION_POSITIONAL_RULES
|
|
147
|
+
if (STATIC_POSITIONAL_RULES.has(command)) return STATIC_POSITIONAL_RULES.get(command);
|
|
148
|
+
return ACTION_POSITIONAL_RULES.get(command)?.(args) || null;
|
|
149
149
|
}
|
|
150
150
|
function parseBooleanOption(value, key) {
|
|
151
151
|
if (value === "true" || value === "1") return true;
|
package/src/local/cli-policy.mjs
CHANGED
|
@@ -37,7 +37,7 @@ function policyState(policy) {
|
|
|
37
37
|
function selectPolicyBase(args, stored, hasStored) {
|
|
38
38
|
if (args.profile !== undefined) {
|
|
39
39
|
const profile = String(args.profile).trim().toLowerCase();
|
|
40
|
-
if (!POLICY_PROFILES
|
|
40
|
+
if (!Object.hasOwn(POLICY_PROFILES, profile)) throw new Error(`--profile must be one of: ${Object.keys(POLICY_PROFILES).join(", ")}`);
|
|
41
41
|
return policyProfile(profile, "explicit");
|
|
42
42
|
}
|
|
43
43
|
if (!hasStored) return policyProfile(DEFAULT_POLICY_PROFILE, "default");
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import * as defaultService from "./service.mjs";
|
|
3
|
+
import { inspectWorkspaceDaemon, stopWorkspaceServiceDaemon } from "./daemon-process.mjs";
|
|
4
|
+
import { stopAndRemoveAutostart } from "./service-lifecycle.mjs";
|
|
5
|
+
import { serviceEnvironmentSummary } from "./service-environment.mjs";
|
|
6
|
+
import { loadState, resolveWorkspace, selectedWorkspace } from "./state.mjs";
|
|
7
|
+
|
|
8
|
+
const SERVICE_ACTION_HANDLERS = new Map([
|
|
9
|
+
["status", serviceStatusAction],
|
|
10
|
+
["install", serviceInstallAction],
|
|
11
|
+
["start", serviceStartAction],
|
|
12
|
+
["stop", serviceStopAction],
|
|
13
|
+
["uninstall", serviceUninstallAction],
|
|
14
|
+
["remove", serviceUninstallAction],
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
export function createServiceCommand(dependencies) {
|
|
18
|
+
const context = {
|
|
19
|
+
chooseWorkspace: requiredFunction(dependencies.chooseWorkspace, "chooseWorkspace"),
|
|
20
|
+
stateRootFromArgs: requiredFunction(dependencies.stateRootFromArgs, "stateRootFromArgs"),
|
|
21
|
+
structuredLogger: requiredFunction(dependencies.structuredLogger, "structuredLogger"),
|
|
22
|
+
service: dependencies.service || defaultService,
|
|
23
|
+
inspectWorkspaceDaemon: dependencies.inspectWorkspaceDaemon || inspectWorkspaceDaemon,
|
|
24
|
+
stopWorkspaceServiceDaemon: dependencies.stopWorkspaceServiceDaemon || stopWorkspaceServiceDaemon,
|
|
25
|
+
stopAndRemoveAutostart: dependencies.stopAndRemoveAutostart || stopAndRemoveAutostart,
|
|
26
|
+
serviceEnvironmentSummary: dependencies.serviceEnvironmentSummary || serviceEnvironmentSummary,
|
|
27
|
+
loadState: dependencies.loadState || loadState,
|
|
28
|
+
resolveWorkspace: dependencies.resolveWorkspace || resolveWorkspace,
|
|
29
|
+
selectedWorkspace: dependencies.selectedWorkspace || selectedWorkspace,
|
|
30
|
+
entryScript: dependencies.entryScript || process.argv[1],
|
|
31
|
+
setExitCode: dependencies.setExitCode || setProcessExitCode,
|
|
32
|
+
print: dependencies.print || printLine,
|
|
33
|
+
};
|
|
34
|
+
return (args) => serviceCommand(args, context);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function serviceCommand(args, context) {
|
|
38
|
+
const action = String(args._[0] || "status").toLowerCase();
|
|
39
|
+
const handler = SERVICE_ACTION_HANDLERS.get(action);
|
|
40
|
+
if (!handler) throw new Error(`Unknown service action: ${action}`);
|
|
41
|
+
const stateRoot = context.stateRootFromArgs(args);
|
|
42
|
+
return handler({ args, stateRoot, service: context.service, context });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function serviceStatusAction({ args, stateRoot, service, context }) {
|
|
46
|
+
const status = await service.autostartStatus();
|
|
47
|
+
const state = optionalServiceState(args, stateRoot, context);
|
|
48
|
+
const workspaceDaemon = state ? context.inspectWorkspaceDaemon(state) : null;
|
|
49
|
+
printServiceResult({
|
|
50
|
+
...status,
|
|
51
|
+
workspace: state?.workspace?.path || null,
|
|
52
|
+
workspace_daemon: workspaceDaemon,
|
|
53
|
+
service_environment: context.serviceEnvironmentSummary(stateRoot),
|
|
54
|
+
effective_active: Boolean(status.active || workspaceDaemon?.alive),
|
|
55
|
+
orphaned_workspace_daemon: Boolean(status.active === false && workspaceDaemon?.alive && workspaceDaemon?.verified_service_daemon),
|
|
56
|
+
}, context, false);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function serviceInstallAction({ args, stateRoot, service, context }) {
|
|
60
|
+
const workspaceArgs = { ...args, _: args._.slice(1) };
|
|
61
|
+
const workspace = await context.chooseWorkspace(workspaceArgs, { promptOnFirstRun: true, save: true, allowPositional: true });
|
|
62
|
+
const state = context.loadState(workspace, { stateDir: stateRoot });
|
|
63
|
+
if (!state.worker?.url) {
|
|
64
|
+
throw new Error("No deployed Worker is recorded for this workspace. Run `machine-mcp` once before `machine-mcp service install`.");
|
|
65
|
+
}
|
|
66
|
+
const result = await service.installAutostart({
|
|
67
|
+
workspace,
|
|
68
|
+
stateRoot,
|
|
69
|
+
entryScript: context.entryScript,
|
|
70
|
+
logger: context.structuredLogger(Boolean(args.quiet)),
|
|
71
|
+
});
|
|
72
|
+
printServiceResult(result, context);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function serviceStartAction({ args, service, context }) {
|
|
76
|
+
const result = await service.startAutostart({ logger: context.structuredLogger(Boolean(args.quiet)) });
|
|
77
|
+
printServiceResult(result, context);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function serviceStopAction({ args, stateRoot, service, context }) {
|
|
81
|
+
const logger = context.structuredLogger(Boolean(args.quiet));
|
|
82
|
+
const provider = await service.stopAutostart({ logger });
|
|
83
|
+
const state = optionalServiceState(args, stateRoot, context);
|
|
84
|
+
const workspaceDaemon = state
|
|
85
|
+
? await context.stopWorkspaceServiceDaemon(state, { logger, reason: "service stop" })
|
|
86
|
+
: { ok: true, found: false, stopped: false, verified_service_daemon: false, reason: "workspace_not_selected" };
|
|
87
|
+
printServiceResult({
|
|
88
|
+
...provider,
|
|
89
|
+
ok: provider?.ok !== false && workspaceDaemon.ok,
|
|
90
|
+
workspace: state?.workspace?.path || null,
|
|
91
|
+
workspace_daemon: workspaceDaemon,
|
|
92
|
+
}, context);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function serviceUninstallAction({ args, stateRoot, service, context }) {
|
|
96
|
+
const logger = context.structuredLogger(Boolean(args.quiet));
|
|
97
|
+
const state = optionalServiceState(args, stateRoot, context);
|
|
98
|
+
const lifecycle = await context.stopAndRemoveAutostart({
|
|
99
|
+
states: state ? [state] : [],
|
|
100
|
+
stateRoot,
|
|
101
|
+
logger,
|
|
102
|
+
reason: "service uninstall",
|
|
103
|
+
stopAutostart: service.stopAutostart,
|
|
104
|
+
uninstallAutostart: service.uninstallAutostart,
|
|
105
|
+
stopWorkspaceServiceDaemon: context.stopWorkspaceServiceDaemon,
|
|
106
|
+
});
|
|
107
|
+
printServiceResult({
|
|
108
|
+
...lifecycle,
|
|
109
|
+
workspace: state?.workspace?.path || null,
|
|
110
|
+
workspace_daemon: lifecycle.workspace_daemons[0] || null,
|
|
111
|
+
autostart_removed: lifecycle.removed,
|
|
112
|
+
}, context);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function optionalServiceState(args, stateRoot, context) {
|
|
116
|
+
const requested = args.workspace || args._[1] || context.selectedWorkspace(stateRoot);
|
|
117
|
+
if (!requested || requested === true) return null;
|
|
118
|
+
return context.loadState(context.resolveWorkspace(String(requested)), { stateDir: stateRoot });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function printServiceResult(result, context, updateExitCode = true) {
|
|
122
|
+
context.print(JSON.stringify(result, null, 2));
|
|
123
|
+
if (updateExitCode && result?.ok === false) context.setExitCode(1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function requiredFunction(value, name) {
|
|
127
|
+
if (typeof value !== "function") throw new TypeError(`service command requires ${name}`);
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function setProcessExitCode(value) { process.exitCode = value; }
|
|
132
|
+
function printLine(value) { console.log(value); }
|
package/src/local/cli.mjs
CHANGED
|
@@ -3,13 +3,14 @@ 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,
|
|
6
|
+
import { acquireDaemonLockWithTakeover, stopWorkspaceServiceDaemon } 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";
|
|
10
10
|
import { resolvePolicy } from "./cli-policy.mjs";
|
|
11
11
|
import { effectiveLogFormat, effectiveLogLevel, normalizeCommand, parseArgs, validateCommandOptions, validateLoggingOptions, validatePositionals } from "./cli-options.mjs";
|
|
12
12
|
import { createLocalAdminCommands } from "./cli-local-admin.mjs";
|
|
13
|
+
import { createServiceCommand } from "./cli-service.mjs";
|
|
13
14
|
import { generateAccountPassword } from "./account-admin.mjs";
|
|
14
15
|
import { accountAdminClient, createAccountCommand } from "./cli-account-admin.mjs";
|
|
15
16
|
export { resolvePolicy } from "./cli-policy.mjs";
|
|
@@ -18,7 +19,7 @@ import { classifyOperationalError, createLogger, sanitizeLogText } from "./log.m
|
|
|
18
19
|
import { runExecutable, runWrangler } from "./shell.mjs";
|
|
19
20
|
import { runFullAccessTest } from "./full-access-test.mjs";
|
|
20
21
|
import { stopAndRemoveAutostart } from "./service-lifecycle.mjs";
|
|
21
|
-
import { loadServiceEnvironment
|
|
22
|
+
import { loadServiceEnvironment } from "./service-environment.mjs";
|
|
22
23
|
import { ensureWorkerDeployment } from "./worker-deployment.mjs";
|
|
23
24
|
import { workerHealth } from "./worker-health.mjs";
|
|
24
25
|
export { workerHealthUserReason } from "./worker-health.mjs";
|
|
@@ -48,24 +49,25 @@ import {
|
|
|
48
49
|
|
|
49
50
|
const localAdminCommands = createLocalAdminCommands({ chooseWorkspace, confirm });
|
|
50
51
|
const accountCommand = createAccountCommand({ chooseWorkspace, confirm });
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
52
|
+
const serviceCommand = createServiceCommand({ chooseWorkspace, stateRootFromArgs, structuredLogger });
|
|
53
|
+
|
|
54
|
+
const COMMAND_HANDLERS = new Map([
|
|
55
|
+
["start", startCommand],
|
|
56
|
+
["stdio", stdioCommand],
|
|
57
|
+
["client-config", clientConfigCommand],
|
|
58
|
+
["status", statusCommand],
|
|
59
|
+
["doctor", doctorCommand],
|
|
60
|
+
["full-test", fullTestCommand],
|
|
61
|
+
["workspace", workspaceCommand],
|
|
62
|
+
["service", serviceCommand],
|
|
63
|
+
["autostart", serviceCommand],
|
|
64
|
+
["rotate-secrets", rotateSecretsCommand],
|
|
65
|
+
["resource", localAdminCommands.resourceCommand],
|
|
66
|
+
["account", accountCommand],
|
|
67
|
+
["browser", localAdminCommands.browserCommand],
|
|
68
|
+
["job", localAdminCommands.jobCommand],
|
|
69
|
+
["uninstall", uninstallCommand],
|
|
70
|
+
]);
|
|
69
71
|
|
|
70
72
|
export async function main(argv = process.argv.slice(2)) {
|
|
71
73
|
const [command, rest] = normalizeCommand(argv);
|
|
@@ -75,7 +77,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
75
77
|
validateCommandOptions(command, args);
|
|
76
78
|
validatePositionals(command, args);
|
|
77
79
|
validateLoggingOptions(args);
|
|
78
|
-
const handler = COMMAND_HANDLERS
|
|
80
|
+
const handler = COMMAND_HANDLERS.get(command);
|
|
79
81
|
if (handler) return handler(args);
|
|
80
82
|
console.error(`Unknown command: ${command}`);
|
|
81
83
|
usage();
|
|
@@ -352,7 +354,7 @@ async function clientConfigCommand(args) {
|
|
|
352
354
|
const workspace = await chooseWorkspace(workspaceArgs, { promptOnFirstRun: false, save: false, allowPositional: false });
|
|
353
355
|
const requested = String(args.client || args._[0] || "all").trim().toLowerCase();
|
|
354
356
|
const profile = String(args.profile || "full").trim().toLowerCase();
|
|
355
|
-
if (!POLICY_PROFILES
|
|
357
|
+
if (!Object.hasOwn(POLICY_PROFILES, profile)) throw new Error(`--profile must be one of: ${Object.keys(POLICY_PROFILES).join(", ")}`);
|
|
356
358
|
if (!["all", "claude", "cursor", "codex", "generic"].includes(requested)) throw new Error("client must be all, claude, cursor, codex, or generic");
|
|
357
359
|
const command = process.execPath;
|
|
358
360
|
const argsList = [resolve(process.argv[1]), "stdio", "--workspace", workspace, "--profile", profile];
|
|
@@ -537,90 +539,6 @@ async function rotateSecretsCommand(args) {
|
|
|
537
539
|
}
|
|
538
540
|
}
|
|
539
541
|
|
|
540
|
-
async function serviceCommand(args) {
|
|
541
|
-
const action = String(args._[0] || "status");
|
|
542
|
-
const stateRoot = stateRootFromArgs(args);
|
|
543
|
-
const { installAutostart, uninstallAutostart, autostartStatus, startAutostart, stopAutostart } = await import("./service.mjs");
|
|
544
|
-
if (action === "status") {
|
|
545
|
-
const status = await autostartStatus();
|
|
546
|
-
const state = optionalServiceState(args, stateRoot);
|
|
547
|
-
const workspaceDaemon = state ? inspectWorkspaceDaemon(state) : null;
|
|
548
|
-
console.log(JSON.stringify({
|
|
549
|
-
...status,
|
|
550
|
-
workspace: state?.workspace?.path || null,
|
|
551
|
-
workspace_daemon: workspaceDaemon,
|
|
552
|
-
service_environment: serviceEnvironmentSummary(stateRoot),
|
|
553
|
-
effective_active: Boolean(status.active || workspaceDaemon?.alive),
|
|
554
|
-
orphaned_workspace_daemon: Boolean(status.active === false && workspaceDaemon?.alive && workspaceDaemon?.verified_service_daemon),
|
|
555
|
-
}, null, 2));
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
if (action === "install") {
|
|
559
|
-
const workspaceArgs = { ...args, _: args._.slice(1) };
|
|
560
|
-
const workspace = await chooseWorkspace(workspaceArgs, { promptOnFirstRun: true, save: true, allowPositional: true });
|
|
561
|
-
const state = loadState(workspace, { stateDir: stateRoot });
|
|
562
|
-
if (!state.worker?.url) {
|
|
563
|
-
throw new Error("No deployed Worker is recorded for this workspace. Run `machine-mcp` once before `machine-mcp service install`.");
|
|
564
|
-
}
|
|
565
|
-
const result = await installAutostart({ workspace, stateRoot, entryScript: process.argv[1], logger: structuredLogger(Boolean(args.quiet)) });
|
|
566
|
-
console.log(JSON.stringify(result, null, 2));
|
|
567
|
-
if (result?.ok === false) process.exitCode = 1;
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
|
-
if (action === "start") {
|
|
571
|
-
const result = await startAutostart({ logger: structuredLogger(Boolean(args.quiet)) });
|
|
572
|
-
console.log(JSON.stringify(result, null, 2));
|
|
573
|
-
if (result?.ok === false) process.exitCode = 1;
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
if (action === "stop") {
|
|
577
|
-
const logger = structuredLogger(Boolean(args.quiet));
|
|
578
|
-
const provider = await stopAutostart({ logger });
|
|
579
|
-
const state = optionalServiceState(args, stateRoot);
|
|
580
|
-
const workspaceDaemon = state
|
|
581
|
-
? await stopWorkspaceServiceDaemon(state, { logger, reason: "service stop" })
|
|
582
|
-
: { ok: true, found: false, stopped: false, verified_service_daemon: false, reason: "workspace_not_selected" };
|
|
583
|
-
const result = {
|
|
584
|
-
...provider,
|
|
585
|
-
ok: provider?.ok !== false && workspaceDaemon.ok,
|
|
586
|
-
workspace: state?.workspace?.path || null,
|
|
587
|
-
workspace_daemon: workspaceDaemon,
|
|
588
|
-
};
|
|
589
|
-
console.log(JSON.stringify(result, null, 2));
|
|
590
|
-
if (!result.ok) process.exitCode = 1;
|
|
591
|
-
return;
|
|
592
|
-
}
|
|
593
|
-
if (action === "uninstall" || action === "remove") {
|
|
594
|
-
const logger = structuredLogger(Boolean(args.quiet));
|
|
595
|
-
const state = optionalServiceState(args, stateRoot);
|
|
596
|
-
const lifecycle = await stopAndRemoveAutostart({
|
|
597
|
-
states: state ? [state] : [],
|
|
598
|
-
stateRoot,
|
|
599
|
-
logger,
|
|
600
|
-
reason: "service uninstall",
|
|
601
|
-
stopAutostart,
|
|
602
|
-
uninstallAutostart,
|
|
603
|
-
stopWorkspaceServiceDaemon,
|
|
604
|
-
});
|
|
605
|
-
const output = {
|
|
606
|
-
...lifecycle,
|
|
607
|
-
workspace: state?.workspace?.path || null,
|
|
608
|
-
workspace_daemon: lifecycle.workspace_daemons[0] || null,
|
|
609
|
-
autostart_removed: lifecycle.removed,
|
|
610
|
-
};
|
|
611
|
-
console.log(JSON.stringify(output, null, 2));
|
|
612
|
-
if (!output.ok) process.exitCode = 1;
|
|
613
|
-
return;
|
|
614
|
-
}
|
|
615
|
-
throw new Error(`Unknown service action: ${action}`);
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
function optionalServiceState(args, stateRoot) {
|
|
619
|
-
const requested = args.workspace || args._[1] || selectedWorkspace(stateRoot);
|
|
620
|
-
if (!requested || requested === true) return null;
|
|
621
|
-
return loadState(resolveWorkspace(String(requested)), { stateDir: stateRoot });
|
|
622
|
-
}
|
|
623
|
-
|
|
624
542
|
async function installAutostartBestEffort({ workspace, stateRoot, entryScript, logger }) {
|
|
625
543
|
try {
|
|
626
544
|
const { installAutostart } = await import("./service.mjs");
|
package/src/local/log.mjs
CHANGED
|
@@ -55,7 +55,7 @@ export function createLogger(options = {}) {
|
|
|
55
55
|
const normalizedLevel = normalizeEventLevel(level);
|
|
56
56
|
const eventName = sanitizeEventName(name);
|
|
57
57
|
const payload = { event: eventName, ...fields };
|
|
58
|
-
const humanMessage = message || eventName;
|
|
58
|
+
const humanMessage = message || humanizeEventName(eventName);
|
|
59
59
|
if (options.format === "json") {
|
|
60
60
|
if (LEVEL_RANK[normalizedLevel] < LEVEL_RANK[minimumLevel]) return;
|
|
61
61
|
const entry = sanitizeLogValue({
|
|
@@ -77,7 +77,7 @@ export function createLogger(options = {}) {
|
|
|
77
77
|
error: [stderr, "[error]", COLORS.red],
|
|
78
78
|
};
|
|
79
79
|
const [stream, label, color] = methods[normalizedLevel];
|
|
80
|
-
write(stream, normalizedLevel, label, color, humanMessage,
|
|
80
|
+
write(stream, normalizedLevel, label, color, humanMessage, fields);
|
|
81
81
|
};
|
|
82
82
|
|
|
83
83
|
return {
|
|
@@ -189,6 +189,14 @@ function normalizeEventLevel(value) {
|
|
|
189
189
|
return Object.prototype.hasOwnProperty.call(LEVEL_RANK, level) ? level : "info";
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
function humanizeEventName(value) {
|
|
193
|
+
const words = String(value || "event")
|
|
194
|
+
.replace(/[._-]+/g, " ")
|
|
195
|
+
.replace(/\s+/g, " ")
|
|
196
|
+
.trim();
|
|
197
|
+
return words ? `${words[0].toUpperCase()}${words.slice(1)}` : "Event";
|
|
198
|
+
}
|
|
199
|
+
|
|
192
200
|
function sanitizeEventName(value) {
|
|
193
201
|
const name = String(value || "event").toLowerCase().replace(/[^a-z0-9._-]/g, "_").slice(0, 128);
|
|
194
202
|
return name || "event";
|
|
@@ -156,10 +156,10 @@ function referencedResources(steps, registry) {
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
if (names.size > MAX_RESOURCES) throw new Error(`job references more than ${MAX_RESOURCES} local resources`);
|
|
159
|
-
const out =
|
|
159
|
+
const out = Object.create(null);
|
|
160
160
|
let totalBytes = 0;
|
|
161
161
|
for (const name of names) {
|
|
162
|
-
const resource = registry[name];
|
|
162
|
+
const resource = Object.hasOwn(registry, name) ? registry[name] : null;
|
|
163
163
|
if (!resource) throw new Error(`unknown local resource: ${name}`);
|
|
164
164
|
const inspected = inspectResourceFile(resource.path, { allowInsecurePermissions: resource.allowInsecurePermissions === true, includeHash: true });
|
|
165
165
|
totalBytes += inspected.size;
|
|
@@ -174,7 +174,7 @@ function referencedResources(steps, registry) {
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
export function normalizeResourceRegistry(resources) {
|
|
177
|
-
const out =
|
|
177
|
+
const out = Object.create(null);
|
|
178
178
|
if (!resources || typeof resources !== "object" || Array.isArray(resources)) return out;
|
|
179
179
|
for (const [rawName, rawValue] of Object.entries(resources).slice(0, MAX_RESOURCES)) {
|
|
180
180
|
const name = validateResourceName(rawName);
|
package/src/local/policy.mjs
CHANGED
|
@@ -53,6 +53,7 @@ export const DEFAULT_POLICY_REVISION = Number(contract.revision);
|
|
|
53
53
|
export const POLICY_PROFILES = Object.freeze(Object.fromEntries(
|
|
54
54
|
Object.entries(contract.profiles).map(([name, value]) => [name, Object.freeze(/** @type {PolicyCapabilities} */ ({ ...value }))]),
|
|
55
55
|
));
|
|
56
|
+
const POLICY_PROFILE_NAMES = Object.freeze(new Set(Object.keys(POLICY_PROFILES)));
|
|
56
57
|
export const POLICY_ORIGINS = Object.freeze(new Set(contract.origins.map(String)));
|
|
57
58
|
/** @type {Readonly<Record<string, Readonly<AvailabilityRequirements>>>} */
|
|
58
59
|
export const POLICY_AVAILABILITY = Object.freeze(Object.fromEntries(
|
|
@@ -69,8 +70,8 @@ const TOOL_BY_NAME = new Map(TOOLS.map((tool) => [tool.name, tool]));
|
|
|
69
70
|
/** @param {unknown} name @param {unknown} [origin] */
|
|
70
71
|
export function policyProfile(name, origin = "explicit") {
|
|
71
72
|
const profile = String(name || "").trim().toLowerCase();
|
|
73
|
+
if (!POLICY_PROFILE_NAMES.has(profile)) throw new BridgeError("invalid_request", `unknown policy profile: ${profile}`);
|
|
72
74
|
const canonical = POLICY_PROFILES[profile];
|
|
73
|
-
if (!canonical) throw new BridgeError("invalid_request", `unknown policy profile: ${profile}`);
|
|
74
75
|
return normalizePolicy({ ...canonical, origin, revision: DEFAULT_POLICY_REVISION });
|
|
75
76
|
}
|
|
76
77
|
|
|
@@ -89,7 +90,8 @@ export function normalizePolicy(policy = {}) {
|
|
|
89
90
|
const revision = Number.isInteger(requestedRevision) && requestedRevision > 0
|
|
90
91
|
? requestedRevision
|
|
91
92
|
: DEFAULT_POLICY_REVISION;
|
|
92
|
-
const
|
|
93
|
+
const rawProfile = typeof policy.profile === "string" && policy.profile ? policy.profile : "custom";
|
|
94
|
+
const requestedProfile = POLICY_PROFILE_NAMES.has(rawProfile) ? rawProfile : "custom";
|
|
93
95
|
/** @type {NormalizedPolicy} */
|
|
94
96
|
const normalized = {
|
|
95
97
|
profile: requestedProfile,
|
|
@@ -102,8 +104,10 @@ export function normalizePolicy(policy = {}) {
|
|
|
102
104
|
minimalEnv: policy.minimalEnv !== false,
|
|
103
105
|
exposeAbsolutePaths: policy.exposeAbsolutePaths === true,
|
|
104
106
|
};
|
|
105
|
-
|
|
106
|
-
|
|
107
|
+
if (POLICY_PROFILE_NAMES.has(requestedProfile)) {
|
|
108
|
+
const canonical = POLICY_PROFILES[requestedProfile];
|
|
109
|
+
Object.assign(normalized, canonical, { allowExec: canonical.execMode !== "off" });
|
|
110
|
+
}
|
|
107
111
|
return Object.freeze(normalized);
|
|
108
112
|
}
|
|
109
113
|
|
|
@@ -136,8 +140,9 @@ export function assertCanonicalFullPolicy(policy = {}) {
|
|
|
136
140
|
/** @param {PolicyInput} policy @param {unknown} availability */
|
|
137
141
|
export function policyAllowsAvailability(policy, availability) {
|
|
138
142
|
const normalized = normalizePolicy(policy);
|
|
139
|
-
const
|
|
140
|
-
if (!
|
|
143
|
+
const availabilityName = String(availability || "");
|
|
144
|
+
if (!Object.hasOwn(POLICY_AVAILABILITY, availabilityName)) return false;
|
|
145
|
+
const requirements = POLICY_AVAILABILITY[availabilityName];
|
|
141
146
|
if (requirements.profile && normalized.profile !== requirements.profile) return false;
|
|
142
147
|
if (requirements.allowWrite === true && normalized.allowWrite !== true) return false;
|
|
143
148
|
if (Array.isArray(requirements.execModes) && !requirements.execModes.includes(normalized.execMode)) return false;
|
|
@@ -138,7 +138,7 @@ export function safeVersionValue(value) {
|
|
|
138
138
|
function packageScriptSearchTerms(script) {
|
|
139
139
|
const normalized = String(script).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
140
140
|
const root = normalized.split("-")[0];
|
|
141
|
-
return PACKAGE_SCRIPT_INTENTS[root]
|
|
141
|
+
return Object.hasOwn(PACKAGE_SCRIPT_INTENTS, root) ? PACKAGE_SCRIPT_INTENTS[root] : "";
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
function invalidPackageMetadata(packagePath, lockfiles, packageState) {
|
|
@@ -73,6 +73,8 @@ export class RelayConnection {
|
|
|
73
73
|
this.connectedOnce = null;
|
|
74
74
|
this.connectedOnceResolve = null;
|
|
75
75
|
this.connectedOnceReject = null;
|
|
76
|
+
this.sessionGeneration = 0;
|
|
77
|
+
this.activeSessionId = 0;
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
status() {
|
|
@@ -82,9 +84,14 @@ export class RelayConnection {
|
|
|
82
84
|
network_route: this.networkRoute,
|
|
83
85
|
reconnect_attempt: this.reconnectAttempt,
|
|
84
86
|
outage_active: this.outageStartedAt > 0,
|
|
87
|
+
session_generation: this.sessionGeneration,
|
|
85
88
|
};
|
|
86
89
|
}
|
|
87
90
|
|
|
91
|
+
currentSessionId() {
|
|
92
|
+
return this.ready ? this.activeSessionId : 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
88
95
|
start() {
|
|
89
96
|
if (!this.closed && this.connectedOnce) return this.connectedOnce;
|
|
90
97
|
this.closed = false;
|
|
@@ -99,6 +106,7 @@ export class RelayConnection {
|
|
|
99
106
|
stop() {
|
|
100
107
|
this.closed = true;
|
|
101
108
|
this.ready = false;
|
|
109
|
+
this.activeSessionId = 0;
|
|
102
110
|
this.clearTimer("heartbeatTimer", "clearInterval");
|
|
103
111
|
this.clearTimer("connectTimer", "clearTimeout");
|
|
104
112
|
this.clearTimer("handshakeTimer", "clearTimeout");
|
|
@@ -118,6 +126,15 @@ export class RelayConnection {
|
|
|
118
126
|
return this.sendOnSocket(this.socket, value);
|
|
119
127
|
}
|
|
120
128
|
|
|
129
|
+
sendForSession(value, expectedSessionId) {
|
|
130
|
+
const sessionId = Number(expectedSessionId) || 0;
|
|
131
|
+
if (!sessionId || sessionId !== this.activeSessionId) return { ok: false, reason: "session_ended" };
|
|
132
|
+
if (!this.ready || !this.isSocketOpen(this.socket)) return { ok: false, reason: "transport_unavailable" };
|
|
133
|
+
return this.sendOnSocket(this.socket, value)
|
|
134
|
+
? { ok: true, reason: "sent" }
|
|
135
|
+
: { ok: false, reason: "send_failed" };
|
|
136
|
+
}
|
|
137
|
+
|
|
121
138
|
interrupt(category = "relay_transport_error") {
|
|
122
139
|
if (this.closed || !this.socket) return false;
|
|
123
140
|
this.pendingCloseCategory = String(category || "relay_transport_error");
|
|
@@ -148,6 +165,8 @@ export class RelayConnection {
|
|
|
148
165
|
return false;
|
|
149
166
|
}
|
|
150
167
|
this.ready = true;
|
|
168
|
+
this.sessionGeneration += 1;
|
|
169
|
+
this.activeSessionId = this.sessionGeneration;
|
|
151
170
|
this.clearTimer("handshakeTimer", "clearTimeout");
|
|
152
171
|
this.connectedAt = this.now();
|
|
153
172
|
this.lastInboundAt = this.connectedAt;
|
|
@@ -268,6 +287,7 @@ export class RelayConnection {
|
|
|
268
287
|
const wasReady = this.ready;
|
|
269
288
|
this.socket = null;
|
|
270
289
|
this.ready = false;
|
|
290
|
+
this.activeSessionId = 0;
|
|
271
291
|
this.clearTimer("connectTimer", "clearTimeout");
|
|
272
292
|
this.clearTimer("heartbeatTimer", "clearInterval");
|
|
273
293
|
this.clearTimer("handshakeTimer", "clearTimeout");
|
|
@@ -329,6 +349,7 @@ export class RelayConnection {
|
|
|
329
349
|
const socket = this.socket;
|
|
330
350
|
this.closed = true;
|
|
331
351
|
this.ready = false;
|
|
352
|
+
this.activeSessionId = 0;
|
|
332
353
|
this.socket = null;
|
|
333
354
|
this.clearTimer("connectTimer", "clearTimeout");
|
|
334
355
|
this.clearTimer("heartbeatTimer", "clearInterval");
|
|
@@ -14,7 +14,7 @@ export async function generateRegisteredSshKey({ workspace, stateDir, name: rawN
|
|
|
14
14
|
let key = null;
|
|
15
15
|
try {
|
|
16
16
|
state.resources ||= {};
|
|
17
|
-
const existing = state.resources[name];
|
|
17
|
+
const existing = Object.hasOwn(state.resources, name) ? state.resources[name] : null;
|
|
18
18
|
if (existing?.path && !samePathIdentity(existing.path, target)) {
|
|
19
19
|
throw new Error(`local resource ${name} is already registered to a different file; remove it first`);
|
|
20
20
|
}
|
|
@@ -103,8 +103,8 @@ export async function buildProjectOverview({
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
function policyMatchesNamedProfile(policy) {
|
|
106
|
+
if (!Object.hasOwn(POLICY_PROFILES, policy.profile)) return false;
|
|
106
107
|
const named = POLICY_PROFILES[policy.profile];
|
|
107
|
-
if (!named) return false;
|
|
108
108
|
return policy.allowWrite === named.allowWrite
|
|
109
109
|
&& policy.execMode === named.execMode
|
|
110
110
|
&& policy.unrestrictedPaths === named.unrestrictedPaths
|