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.
Files changed (39) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/browser-extension/browser-operations.js +9 -3
  3. package/browser-extension/devtools-input.js +2 -2
  4. package/browser-extension/manifest.json +2 -2
  5. package/docs/ARCHITECTURE.md +6 -6
  6. package/docs/AUDIT.md +26 -0
  7. package/docs/ENGINEERING.md +3 -0
  8. package/docs/LOGGING.md +6 -2
  9. package/docs/OPERATIONS.md +5 -3
  10. package/docs/PROJECT_STANDARDS.md +1 -0
  11. package/docs/TESTING.md +6 -2
  12. package/docs/UPGRADING.md +6 -2
  13. package/package.json +4 -3
  14. package/scripts/coverage-check.mjs +3 -1
  15. package/src/local/account-access.mjs +7 -5
  16. package/src/local/call-registry.mjs +9 -3
  17. package/src/local/cli-local-admin.mjs +74 -38
  18. package/src/local/cli-options.mjs +18 -18
  19. package/src/local/cli-policy.mjs +1 -1
  20. package/src/local/cli-service.mjs +132 -0
  21. package/src/local/cli.mjs +24 -106
  22. package/src/local/log.mjs +10 -2
  23. package/src/local/managed-job-plan.mjs +3 -3
  24. package/src/local/policy.mjs +11 -6
  25. package/src/local/project-package.mjs +1 -1
  26. package/src/local/relay-connection.mjs +21 -0
  27. package/src/local/resource-operations.mjs +1 -1
  28. package/src/local/runtime-reporting.mjs +1 -1
  29. package/src/local/runtime.mjs +65 -27
  30. package/src/local/tool-executor.mjs +3 -3
  31. package/src/local/worker-deployment.mjs +15 -10
  32. package/src/local/worker-health.mjs +7 -3
  33. package/src/worker/access.ts +4 -3
  34. package/src/worker/http.ts +2 -2
  35. package/src/worker/index.ts +58 -17
  36. package/src/worker/oauth-controller.ts +11 -2
  37. package/src/worker/observability.ts +3 -1
  38. package/src/worker/pending-calls.ts +17 -0
  39. 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 = Object.freeze({
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 = Object.freeze({ add: 3, "generate-ssh-key": 3, remove: 2, check: 2 });
42
- const JOB_POSITIONAL_LIMITS = Object.freeze({ read: 2, inspect: 2, cancel: 2, approve: 2, submit: 2 });
43
- const ACCOUNT_POSITIONAL_LIMITS = Object.freeze({ list: 1, add: 3, role: 3, enable: 2, disable: 2, "rotate-password": 2, remove: 2 });
44
- const ACTION_POSITIONAL_RULES = Object.freeze({
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[action] ?? 1, tooMany: `resource ${action} received too many positional arguments` };
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[action] ?? 1, tooMany: `account ${action} received too many positional arguments` };
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[action] ?? 1, tooMany: `job ${action} received too many positional arguments` };
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[command];
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[command]) return STATIC_POSITIONAL_RULES[command];
148
- return ACTION_POSITIONAL_RULES[command]?.(args) || null;
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;
@@ -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[profile]) throw new Error(`--profile must be one of: ${Object.keys(POLICY_PROFILES).join(", ")}`);
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, inspectWorkspaceDaemon, stopWorkspaceServiceDaemon } from "./daemon-process.mjs";
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, serviceEnvironmentSummary } from "./service-environment.mjs";
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
- const COMMAND_HANDLERS = Object.freeze({
53
- start: startCommand,
54
- stdio: stdioCommand,
55
- "client-config": clientConfigCommand,
56
- status: statusCommand,
57
- doctor: doctorCommand,
58
- "full-test": fullTestCommand,
59
- workspace: workspaceCommand,
60
- service: serviceCommand,
61
- autostart: serviceCommand,
62
- "rotate-secrets": rotateSecretsCommand,
63
- resource: localAdminCommands.resourceCommand,
64
- account: accountCommand,
65
- browser: localAdminCommands.browserCommand,
66
- job: localAdminCommands.jobCommand,
67
- uninstall: uninstallCommand,
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[command];
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[profile]) throw new Error(`--profile must be one of: ${Object.keys(POLICY_PROFILES).join(", ")}`);
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, payload);
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);
@@ -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 requestedProfile = typeof policy.profile === "string" && policy.profile ? policy.profile : "custom";
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
- const canonical = POLICY_PROFILES[requestedProfile];
106
- if (canonical) Object.assign(normalized, canonical, { allowExec: canonical.execMode !== "off" });
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 requirements = POLICY_AVAILABILITY[String(availability || "")];
140
- if (!requirements) return false;
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