machine-bridge-mcp 2.0.0 → 3.0.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +120 -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 +28 -13
- package/docs/AUDIT.md +101 -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 +14 -3
- package/docs/MANAGED_JOBS.md +24 -29
- package/docs/MULTI_ACCOUNT.md +98 -37
- package/docs/OPERATIONS.md +34 -23
- package/docs/OVERVIEW.md +7 -7
- package/docs/PROJECT_STANDARDS.md +9 -5
- package/docs/RELEASING.md +171 -64
- package/docs/TESTING.md +21 -9
- 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 +37 -6
- 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 +119 -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 +18 -44
- 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-resource-service.mjs +77 -0
- package/src/local/runtime-tool-handlers.mjs +8 -8
- package/src/local/runtime.mjs +85 -95
- 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/log-redaction.d.mts +9 -0
- package/src/shared/log-redaction.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 +23 -4
- 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/observability.ts +4 -2
- package/src/worker/websocket-protocol.ts +8 -2
package/src/local/runtime.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { isRelayReadyContext } from "./relay-connection.mjs";
|
|
|
6
6
|
import { ProcessSessionManager } from "./process-sessions.mjs";
|
|
7
7
|
import { MAX_CONCURRENT_TOOL_CALLS } from "./execution-limits.mjs";
|
|
8
8
|
export { MAX_COMMAND_BYTES } from "./process-contract.mjs";
|
|
9
|
-
import { normalizePolicy, PolicyGate } from "./tools.mjs";
|
|
9
|
+
import { normalizePolicy, PolicyGate, toolNamesForPolicy } from "./tools.mjs";
|
|
10
10
|
import { publicError } from "./errors.mjs";
|
|
11
11
|
import { ProcessTracker } from "./process-tracker.mjs";
|
|
12
12
|
import { CallRegistry } from "./call-registry.mjs";
|
|
@@ -14,27 +14,29 @@ import { RuntimeObservability } from "./observability.mjs";
|
|
|
14
14
|
import { ToolExecutor } from "./tool-executor.mjs";
|
|
15
15
|
import { boundedErrorMessage, ProcessExecutionService } from "./process-execution.mjs";
|
|
16
16
|
import { GitService } from "./git-service.mjs";
|
|
17
|
+
import { createTrustedGitResolver } from "./trusted-git-executable.mjs";
|
|
17
18
|
import { LifecycleController } from "./lifecycle.mjs";
|
|
18
19
|
import { MAX_WRITE_BYTES, sha256, WorkspaceFileService } from "./workspace-file-service.mjs";
|
|
19
20
|
export { MAX_WRITE_BYTES, sha256 } from "./workspace-file-service.mjs";
|
|
20
21
|
import { classifyOperationalError } from "./log.mjs";
|
|
21
|
-
import {
|
|
22
|
-
import { generateRegisteredSshKey } from "./resource-operations.mjs";
|
|
23
|
-
import { expandHome } from "./state.mjs";
|
|
22
|
+
import { ManagedJobManager } from "./managed-jobs.mjs";
|
|
24
23
|
import { AgentContextManager } from "./agent-context.mjs";
|
|
25
24
|
import { AppAutomationManager } from "./app-automation.mjs";
|
|
26
25
|
import { BrowserBridgeManager } from "./browser-bridge.mjs";
|
|
27
26
|
import { CapabilityObserver } from "./capability-observer.mjs";
|
|
28
|
-
import { readBoundedRegularFileSync } from "./secure-file.mjs";
|
|
29
27
|
import { isPlainRecord } from "./records.mjs";
|
|
30
28
|
import { AccountAccessGate } from "./account-access.mjs";
|
|
31
29
|
import { buildProjectOverview, buildRuntimeInfo } from "./runtime-reporting.mjs";
|
|
32
30
|
import { diagnoseRuntime as runRuntimeDiagnostics } from "./runtime-diagnostics.mjs";
|
|
33
31
|
import { bindRuntimeToolHandlers, runtimeToolHandlerNames as registeredRuntimeToolHandlerNames } from "./runtime-tool-handlers.mjs";
|
|
34
32
|
import { OperationAuthorizer } from "./operation-authorization.mjs";
|
|
33
|
+
import { SecurityAuditLog } from "./security-audit-log.mjs";
|
|
34
|
+
import { delegatedProcessIsolationStatus } from "./delegated-process-sandbox.mjs";
|
|
35
|
+
import { policyForContext } from "./authority-context.mjs";
|
|
35
36
|
import { createRuntimeRelayConnection, normalizeRelayResumeCalls, normalizeRelayToolCall } from "./runtime-relay.mjs";
|
|
36
37
|
import { RelayCallRecovery } from "./relay-call-recovery.mjs";
|
|
37
|
-
import {
|
|
38
|
+
import { RuntimeResourceService } from "./runtime-resource-service.mjs";
|
|
39
|
+
import { assertContainedPath, createRuntimeDir, redactRuntimeErrorMessage } from "./runtime-paths.mjs";
|
|
38
40
|
import {
|
|
39
41
|
resolveTaskCapabilities as resolveRuntimeTaskCapabilities,
|
|
40
42
|
sessionBootstrap as buildRuntimeSessionBootstrap,
|
|
@@ -47,7 +49,7 @@ export function runtimeToolHandlerNames() {
|
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
export class LocalRuntime {
|
|
50
|
-
constructor({ workerUrl = "", deviceIdentity = null, expectedRelayVersion = "", workspace, policy, logger = console, onSuperseded = null, onFatal = null, jobRoot = "", approvalRoot = "", resources = {}, resourceStatePath = "", browserStateRoot = "", agentHome = process.env.HOME || process.env.USERPROFILE || "", codexHome = process.env.CODEX_HOME || "", recoverJobs = true, applicationAutomation = {} }) {
|
|
52
|
+
constructor({ workerUrl = "", deviceIdentity = null, expectedRelayVersion = "", workspace, policy, logger = console, onSuperseded = null, onFatal = null, jobRoot = "", approvalRoot = "", resources = {}, resourceStatePath = "", browserStateRoot = "", agentHome = process.env.HOME || process.env.USERPROFILE || "", codexHome = process.env.CODEX_HOME || "", recoverJobs = true, applicationAutomation = {}, deviceRootStatus = null, resolveGitExecutable = null }) {
|
|
51
53
|
const remoteWorkerUrl = workerUrl ? String(workerUrl) : "";
|
|
52
54
|
this.workspaceInput = resolve(workspace || process.cwd());
|
|
53
55
|
this.workspace = realpathSync.native ? realpathSync.native(this.workspaceInput) : realpathSync(this.workspaceInput);
|
|
@@ -58,6 +60,7 @@ export class LocalRuntime {
|
|
|
58
60
|
this.logger = logger;
|
|
59
61
|
this.onSuperseded = typeof onSuperseded === "function" ? onSuperseded : null;
|
|
60
62
|
this.resourceStatePath = resourceStatePath ? resolve(resourceStatePath) : "";
|
|
63
|
+
this.deviceRootStatus = deviceRootStatus && typeof deviceRootStatus === "object" ? Object.freeze({ ...deviceRootStatus }) : null;
|
|
61
64
|
this.processTracker = new ProcessTracker();
|
|
62
65
|
this.lifecycle = new LifecycleController("local runtime");
|
|
63
66
|
this.observability = new RuntimeObservability();
|
|
@@ -80,13 +83,15 @@ export class LocalRuntime {
|
|
|
80
83
|
this.mutationQueue = Promise.resolve();
|
|
81
84
|
this.capabilityObserver = new CapabilityObserver();
|
|
82
85
|
this.runtimeDir = createRuntimeDir();
|
|
86
|
+
this.resolveGitExecutable = createTrustedGitResolver({ resolve: resolveGitExecutable, workspace: this.workspace, stateRoot: browserStateRoot, runtimeDir: this.runtimeDir, home: agentHome });
|
|
83
87
|
this.workspaceFileService = new WorkspaceFileService({
|
|
84
88
|
workspace: this.workspace,
|
|
85
89
|
policy: this.policy,
|
|
86
90
|
policyGate: this.policyGate,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
policyForContext: (context) => this.effectivePolicy(context),
|
|
92
|
+
resolveExistingPath: (value, context) => this.resolveExistingPath(value, context),
|
|
93
|
+
resolveWritePath: (value, context) => this.resolveWritePath(value, context),
|
|
94
|
+
displayPath: (value, context) => this.displayPath(value, context),
|
|
90
95
|
throwIfCancelled: (context) => this.throwIfCancelled(context),
|
|
91
96
|
withMutationLock: (operation) => this.withMutationLock(operation),
|
|
92
97
|
});
|
|
@@ -96,6 +101,7 @@ export class LocalRuntime {
|
|
|
96
101
|
workspace: this.workspace,
|
|
97
102
|
policy: this.policy,
|
|
98
103
|
authorizeTool: (tool) => this.policyGate.assert(tool),
|
|
104
|
+
policyForContext: (context) => this.effectivePolicy(context),
|
|
99
105
|
resources,
|
|
100
106
|
resourceStatePath,
|
|
101
107
|
stateRoot: browserStateRoot,
|
|
@@ -106,21 +112,23 @@ export class LocalRuntime {
|
|
|
106
112
|
workspace: this.workspace,
|
|
107
113
|
policy: this.policy,
|
|
108
114
|
authorizeTool: (tool) => this.policyGate.assert(tool),
|
|
115
|
+
policyForContext: (context) => this.effectivePolicy(context),
|
|
109
116
|
runtimeDir: this.runtimeDir,
|
|
110
117
|
processTracker: this.processTracker,
|
|
111
|
-
resolveCwd: async (input) => {
|
|
112
|
-
const cwd = await this.resolveExistingPath(input);
|
|
118
|
+
resolveCwd: async (input, context) => {
|
|
119
|
+
const cwd = await this.resolveExistingPath(input, context);
|
|
113
120
|
if (!(await stat(cwd)).isDirectory()) throw new Error("cwd is not a directory");
|
|
114
121
|
return cwd;
|
|
115
122
|
},
|
|
116
|
-
displayPath: (value) => this.displayPath(value),
|
|
123
|
+
displayPath: (value, context) => this.displayPath(value, context),
|
|
117
124
|
throwIfCancelled: (context) => this.throwIfCancelled(context),
|
|
118
125
|
});
|
|
119
126
|
this.agentContextManager = new AgentContextManager({
|
|
120
127
|
workspace: this.workspace,
|
|
121
128
|
policy: this.policy,
|
|
122
|
-
|
|
123
|
-
|
|
129
|
+
policyForContext: (context) => this.effectivePolicy(context),
|
|
130
|
+
displayPath: (value, context) => this.displayPath(value, context),
|
|
131
|
+
resolveExistingPath: (value, context) => this.resolveExistingPath(value, context),
|
|
124
132
|
throwIfCancelled: (context) => this.throwIfCancelled(context),
|
|
125
133
|
home: agentHome,
|
|
126
134
|
codexHome,
|
|
@@ -129,37 +137,47 @@ export class LocalRuntime {
|
|
|
129
137
|
workspace: this.workspace,
|
|
130
138
|
policy: this.policy,
|
|
131
139
|
policyGate: this.policyGate,
|
|
140
|
+
policyForContext: (context) => this.effectivePolicy(context),
|
|
132
141
|
runtimeDir: this.runtimeDir,
|
|
133
142
|
processTracker: this.processTracker,
|
|
134
|
-
resolveExistingPath: (value) => this.resolveExistingPath(value),
|
|
143
|
+
resolveExistingPath: (value, context) => this.resolveExistingPath(value, context),
|
|
135
144
|
resolveLocalCommand: (args, context) => this.agentContextManager.resolveLocalCommand(args, context),
|
|
136
|
-
displayPath: (value) => this.displayPath(value),
|
|
145
|
+
displayPath: (value, context) => this.displayPath(value, context),
|
|
137
146
|
throwIfCancelled: (context) => this.throwIfCancelled(context),
|
|
138
|
-
retainCompletedOutput: (value) => this.processSessionManager.retainCompletedOutput(value),
|
|
147
|
+
retainCompletedOutput: (value, context) => this.processSessionManager.retainCompletedOutput(value, context),
|
|
139
148
|
});
|
|
140
149
|
this.gitService = new GitService({
|
|
141
|
-
resolveExistingPath: (value) => this.resolveExistingPath(value),
|
|
142
|
-
displayPath: (value) => this.displayPath(value),
|
|
143
|
-
|
|
150
|
+
resolveExistingPath: (value, context) => this.resolveExistingPath(value, context),
|
|
151
|
+
displayPath: (value, context) => this.displayPath(value, context),
|
|
152
|
+
runInternalProcess: (...args) => this.processExecutionService.runFixedInternal(...args),
|
|
153
|
+
gitExecutable: () => this.resolveGitExecutable(),
|
|
144
154
|
maximumBytes: MAX_WRITE_BYTES,
|
|
145
155
|
});
|
|
156
|
+
this.runtimeResourceService = new RuntimeResourceService({
|
|
157
|
+
workspace: this.workspace,
|
|
158
|
+
resourceStatePath: this.resourceStatePath,
|
|
159
|
+
currentResources: () => this.managedJobManager.currentResources(),
|
|
160
|
+
authorizeTool: (tool) => this.policyGate.assert(tool),
|
|
161
|
+
});
|
|
146
162
|
const runProcess = (cmd, argv, timeoutMs, allowFailure, maxOutputBytes, context, cwd, stdin) => this.runProcess(cmd, argv, timeoutMs, allowFailure, maxOutputBytes, context, cwd, stdin);
|
|
147
|
-
const readResourceText = (name) => this.
|
|
148
|
-
const readResourceBinary = (name) => this.
|
|
163
|
+
const readResourceText = (name) => this.runtimeResourceService.readText(name);
|
|
164
|
+
const readResourceBinary = (name) => this.runtimeResourceService.readBinary(name);
|
|
149
165
|
this.appAutomationManager = new AppAutomationManager({
|
|
150
166
|
...applicationAutomation,
|
|
151
167
|
policy: this.policy,
|
|
152
168
|
authorizeTool: (tool) => this.policyGate.assert(tool),
|
|
153
|
-
displayPath: (value) => this.displayPath(value),
|
|
169
|
+
displayPath: (value, context) => this.displayPath(value, context),
|
|
154
170
|
runProcess,
|
|
155
171
|
readResourceText,
|
|
156
172
|
throwIfCancelled: (context) => this.throwIfCancelled(context),
|
|
157
173
|
});
|
|
174
|
+
this.securityAudit = new SecurityAuditLog({ root: approvalRoot });
|
|
158
175
|
this.operationAuthorizer = new OperationAuthorizer({
|
|
159
176
|
workspace: this.workspace,
|
|
160
177
|
root: approvalRoot,
|
|
161
|
-
resolveExistingPath: (value) => this.resolveExistingPath(value),
|
|
162
|
-
resolveWritePath: (value) => this.resolveWritePath(value),
|
|
178
|
+
resolveExistingPath: (value, context) => this.resolveExistingPath(value, context),
|
|
179
|
+
resolveWritePath: (value, context) => this.resolveWritePath(value, context),
|
|
180
|
+
protectedRoots: [approvalRoot, browserStateRoot],
|
|
163
181
|
});
|
|
164
182
|
this.browserBridgeManager = new BrowserBridgeManager({
|
|
165
183
|
policy: this.policy,
|
|
@@ -178,8 +196,9 @@ export class LocalRuntime {
|
|
|
178
196
|
operationAuthorizer: this.operationAuthorizer,
|
|
179
197
|
callRegistry: this.callRegistry,
|
|
180
198
|
observability: this.observability,
|
|
199
|
+
securityAudit: this.securityAudit,
|
|
181
200
|
logger: this.logger,
|
|
182
|
-
safeMessage: (error, args) => this.safeErrorMessage(error, args),
|
|
201
|
+
safeMessage: (error, args, context) => this.safeErrorMessage(error, args, context),
|
|
183
202
|
slowMs: SLOW_TOOL_CALL_MS,
|
|
184
203
|
});
|
|
185
204
|
this.relay = createRuntimeRelayConnection(this, {
|
|
@@ -198,11 +217,11 @@ export class LocalRuntime {
|
|
|
198
217
|
|
|
199
218
|
tools() { return this.policyGate.names().filter((name) => name !== "server_info"); }
|
|
200
219
|
|
|
201
|
-
runtimeInfo() {
|
|
202
|
-
|
|
220
|
+
runtimeInfo(context = {}) {
|
|
221
|
+
const info = buildRuntimeInfo({
|
|
203
222
|
workspace: this.workspace,
|
|
204
|
-
displayPath: (value) => this.displayPath(value),
|
|
205
|
-
policy: this.
|
|
223
|
+
displayPath: (value) => this.displayPath(value, context),
|
|
224
|
+
policy: this.effectivePolicy(context),
|
|
206
225
|
toolNames: this.tools(),
|
|
207
226
|
capabilityObserver: this.capabilityObserver,
|
|
208
227
|
observability: this.observability,
|
|
@@ -214,6 +233,16 @@ export class LocalRuntime {
|
|
|
214
233
|
processSessionManager: this.processSessionManager,
|
|
215
234
|
managedJobManager: this.managedJobManager,
|
|
216
235
|
});
|
|
236
|
+
return {
|
|
237
|
+
...info,
|
|
238
|
+
security_audit: this.securityAudit.snapshot(),
|
|
239
|
+
trust: {
|
|
240
|
+
device_root: this.deviceRootStatus,
|
|
241
|
+
daemon_session: { ephemeral: true, certificate_lifetime_seconds: 86400, reconnect_prompts: false },
|
|
242
|
+
delegated_process_isolation: delegatedProcessIsolationStatus(),
|
|
243
|
+
routine_operation_prompts: false,
|
|
244
|
+
},
|
|
245
|
+
};
|
|
217
246
|
}
|
|
218
247
|
|
|
219
248
|
async start() {
|
|
@@ -427,20 +456,24 @@ export class LocalRuntime {
|
|
|
427
456
|
}
|
|
428
457
|
|
|
429
458
|
async projectOverview(context = {}) {
|
|
459
|
+
const effectivePolicy = this.effectivePolicy(context);
|
|
430
460
|
return buildProjectOverview({
|
|
431
461
|
workspace: this.workspace,
|
|
432
|
-
displayPath: (value) => this.displayPath(value),
|
|
433
|
-
policy:
|
|
434
|
-
toolNames:
|
|
462
|
+
displayPath: (value) => this.displayPath(value, context),
|
|
463
|
+
policy: effectivePolicy,
|
|
464
|
+
toolNames: toolNamesForPolicy(effectivePolicy).filter((name) => name !== "server_info"),
|
|
465
|
+
daemonPolicy: this.policy,
|
|
466
|
+
daemonToolNames: this.tools(),
|
|
435
467
|
capabilityObserver: this.capabilityObserver,
|
|
436
468
|
listTopLevel: (callContext) => this.listDir(".", callContext),
|
|
437
|
-
|
|
438
|
-
|
|
469
|
+
runInternalProcess: (...args) => this.processExecutionService.runFixedInternal(...args),
|
|
470
|
+
gitExecutable: () => this.resolveGitExecutable(),
|
|
471
|
+
safeErrorMessage: (error) => this.safeErrorMessage(error, {}, context),
|
|
439
472
|
throwIfCancelled: (callContext) => this.throwIfCancelled(callContext),
|
|
440
473
|
}, context);
|
|
441
474
|
}
|
|
442
475
|
|
|
443
|
-
listRoots() { return this.workspaceFileService.listRoots(); }
|
|
476
|
+
listRoots(context = {}) { return this.workspaceFileService.listRoots(context); }
|
|
444
477
|
|
|
445
478
|
listDir(pathValue, context = {}) { return this.workspaceFileService.listDir(pathValue, context); }
|
|
446
479
|
|
|
@@ -482,43 +515,14 @@ export class LocalRuntime {
|
|
|
482
515
|
runProcess: (...args) => this.runProcess(...args),
|
|
483
516
|
probeShell: (callContext) => this.processExecutionService.probeShell(callContext),
|
|
484
517
|
managedJobManager: this.managedJobManager,
|
|
518
|
+
relayStatus: () => this.relay?.status?.() || null,
|
|
485
519
|
throwIfCancelled: (callContext) => this.throwIfCancelled(callContext),
|
|
486
520
|
}, context);
|
|
487
521
|
}
|
|
488
522
|
|
|
489
523
|
async generateSshKeyResource(args = {}, context = {}) {
|
|
490
524
|
this.throwIfCancelled(context);
|
|
491
|
-
this.
|
|
492
|
-
if (!this.resourceStatePath) throw new Error("local resource state is unavailable in this runtime");
|
|
493
|
-
const home = process.env.HOME || process.env.USERPROFILE;
|
|
494
|
-
if (!home) throw new Error("HOME or USERPROFILE is required to choose a default SSH key path");
|
|
495
|
-
const target = args.path
|
|
496
|
-
? resolve(expandHome(String(args.path)))
|
|
497
|
-
: resolve(home, ".ssh", `machine-mcp-${args.name}-ed25519`);
|
|
498
|
-
const key = await generateRegisteredSshKey({
|
|
499
|
-
workspace: this.workspace,
|
|
500
|
-
stateDir: stateRootFromProfileStatePath(this.resourceStatePath),
|
|
501
|
-
name: args.name,
|
|
502
|
-
targetPath: target,
|
|
503
|
-
comment: args.comment || `machine-mcp:${args.name}`,
|
|
504
|
-
});
|
|
505
|
-
const exposePaths = args.expose_paths === true;
|
|
506
|
-
return {
|
|
507
|
-
name: key.name,
|
|
508
|
-
created: key.created,
|
|
509
|
-
registered: key.registered,
|
|
510
|
-
fingerprint: key.fingerprint,
|
|
511
|
-
key_type: key.keyType,
|
|
512
|
-
private_mode: key.privateMode,
|
|
513
|
-
public_mode: key.publicMode,
|
|
514
|
-
private_key_content_exposed: key.privateKeyContentExposed,
|
|
515
|
-
available_to_new_jobs_immediately: key.availableToNewJobsImmediately,
|
|
516
|
-
paths_exposed: exposePaths,
|
|
517
|
-
...(exposePaths ? {
|
|
518
|
-
private_key_path: resolve(key.privateKeyPath),
|
|
519
|
-
public_key_path: resolve(key.publicKeyPath),
|
|
520
|
-
} : {}),
|
|
521
|
-
};
|
|
525
|
+
return this.runtimeResourceService.generateSshKey(args);
|
|
522
526
|
}
|
|
523
527
|
|
|
524
528
|
async sessionBootstrap(args = {}, context = {}) {
|
|
@@ -539,24 +543,6 @@ export class LocalRuntime {
|
|
|
539
543
|
}, args, context);
|
|
540
544
|
}
|
|
541
545
|
|
|
542
|
-
readLocalResourceBinary(name) {
|
|
543
|
-
const registry = this.managedJobManager.currentResources();
|
|
544
|
-
const resource = Object.hasOwn(registry, name) ? registry[name] : null;
|
|
545
|
-
if (!resource) throw new Error(`unknown local resource: ${name}`);
|
|
546
|
-
const inspected = inspectResourceFile(resource.path, { allowInsecurePermissions: resource.allowInsecurePermissions === true });
|
|
547
|
-
if (inspected.size > 1024 * 1024) throw new Error("local resource exceeds 1 MiB browser injection limit");
|
|
548
|
-
return { buffer: readBoundedRegularFileSync(resource.path, 1024 * 1024), path: resource.path, size: inspected.size };
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
readLocalResourceText(name) {
|
|
552
|
-
const { buffer } = this.readLocalResourceBinary(name);
|
|
553
|
-
try {
|
|
554
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(buffer);
|
|
555
|
-
} catch {
|
|
556
|
-
throw new Error(`local resource is not valid UTF-8 text: ${name}`);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
546
|
runDirectProcess(args, context = {}) {
|
|
561
547
|
return this.processExecutionService.runDirect(args, context);
|
|
562
548
|
}
|
|
@@ -577,6 +563,10 @@ export class LocalRuntime {
|
|
|
577
563
|
return this.processExecutionService.run(cmd, args, timeoutMs, allowFailure, maxOutputBytes, context, cwd, stdin);
|
|
578
564
|
}
|
|
579
565
|
|
|
566
|
+
effectivePolicy(context = {}) {
|
|
567
|
+
return policyForContext(context, this.policy);
|
|
568
|
+
}
|
|
569
|
+
|
|
580
570
|
resolvePath(inputPath = ".") {
|
|
581
571
|
const raw = String(inputPath || ".");
|
|
582
572
|
if (raw.includes("\0")) throw new Error("path contains a NUL byte");
|
|
@@ -599,14 +589,14 @@ export class LocalRuntime {
|
|
|
599
589
|
return this.workspaceCanonicalPromise;
|
|
600
590
|
}
|
|
601
591
|
|
|
602
|
-
async resolveExistingPath(inputPath = ".") {
|
|
592
|
+
async resolveExistingPath(inputPath = ".", context = {}) {
|
|
603
593
|
const candidate = this.resolvePath(inputPath);
|
|
604
594
|
const [workspace, canonical] = await Promise.all([this.canonicalWorkspace(), realpath(candidate)]);
|
|
605
|
-
if (!this.
|
|
595
|
+
if (!this.effectivePolicy(context).unrestrictedPaths) assertContainedPath(workspace, canonical);
|
|
606
596
|
return canonical;
|
|
607
597
|
}
|
|
608
598
|
|
|
609
|
-
async resolveWritePath(inputPath = ".") {
|
|
599
|
+
async resolveWritePath(inputPath = ".", context = {}) {
|
|
610
600
|
const candidate = this.resolvePath(inputPath);
|
|
611
601
|
const candidateInfo = await lstat(candidate).catch(() => null);
|
|
612
602
|
if (candidateInfo?.isSymbolicLink()) throw new Error("refusing to overwrite a symbolic link");
|
|
@@ -617,23 +607,23 @@ export class LocalRuntime {
|
|
|
617
607
|
ancestor = parent;
|
|
618
608
|
}
|
|
619
609
|
const [workspace, canonicalAncestor] = await Promise.all([this.canonicalWorkspace(), realpath(ancestor)]);
|
|
620
|
-
if (!this.
|
|
610
|
+
if (!this.effectivePolicy(context).unrestrictedPaths) assertContainedPath(workspace, canonicalAncestor);
|
|
621
611
|
const suffix = relative(ancestor, candidate);
|
|
622
612
|
return suffix ? resolve(canonicalAncestor, suffix) : canonicalAncestor;
|
|
623
613
|
}
|
|
624
614
|
|
|
625
|
-
displayPath(fullPath) {
|
|
615
|
+
displayPath(fullPath, context = {}) {
|
|
626
616
|
const absolute = resolve(fullPath);
|
|
627
|
-
if (this.
|
|
617
|
+
if (this.effectivePolicy(context).exposeAbsolutePaths) return absolute;
|
|
628
618
|
const shown = relative(this.workspace, absolute);
|
|
629
619
|
const insideWorkspace = shown === "" || (!shown.startsWith(`..${sep}`) && shown !== ".." && !isAbsolute(shown));
|
|
630
620
|
if (insideWorkspace) return shown ? shown.split(sep).join("/") : ".";
|
|
631
621
|
return `<external-path:${sha256(absolute).slice(0, 12)}>`;
|
|
632
622
|
}
|
|
633
623
|
|
|
634
|
-
safeErrorMessage(error, toolArgs = {}) {
|
|
624
|
+
safeErrorMessage(error, toolArgs = {}, context = {}) {
|
|
635
625
|
const message = boundedErrorMessage(error);
|
|
636
|
-
if (this.
|
|
626
|
+
if (this.effectivePolicy(context).exposeAbsolutePaths) return message;
|
|
637
627
|
return redactRuntimeErrorMessage(message, {
|
|
638
628
|
error,
|
|
639
629
|
toolArgs,
|
|
@@ -641,7 +631,7 @@ export class LocalRuntime {
|
|
|
641
631
|
workspaceInput: this.workspaceInput,
|
|
642
632
|
runtimeDir: this.runtimeDir,
|
|
643
633
|
home: process.env.HOME || process.env.USERPROFILE || "",
|
|
644
|
-
displayPath: (value) => this.displayPath(value),
|
|
634
|
+
displayPath: (value) => this.displayPath(value, context),
|
|
645
635
|
});
|
|
646
636
|
}
|
|
647
637
|
|
|
@@ -16,6 +16,9 @@ export function openRegularFileSync(file, flags, options = {}) {
|
|
|
16
16
|
try {
|
|
17
17
|
const info = fstatSync(fd);
|
|
18
18
|
if (!info.isFile()) throw new Error(`${label} is not a regular file`);
|
|
19
|
+
if (options.rejectMultipleLinks === true && Number(info.nlink) > 1) {
|
|
20
|
+
throw new Error(`${label} must not have multiple hard links`);
|
|
21
|
+
}
|
|
19
22
|
if (options.verifyPathIdentity === true) {
|
|
20
23
|
const pathInfo = lstatSync(file);
|
|
21
24
|
if (pathInfo.isSymbolicLink() || !pathInfo.isFile()) throw new Error(`${label} must be a regular file and not a symbolic link`);
|
|
@@ -39,7 +42,7 @@ function withRegularFileSync(file, flags, options, callback) {
|
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
export function chmodRegularFileSync(file, mode, label = "path") {
|
|
42
|
-
return withRegularFileSync(file, fsConstants.O_RDONLY, { label, chmod: mode }, () => undefined);
|
|
45
|
+
return withRegularFileSync(file, fsConstants.O_RDONLY, { label, chmod: mode, rejectMultipleLinks: true }, () => undefined);
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
export function readBoundedRegularFileSync(file, maxBytes, label = "path", options = {}) {
|
|
@@ -52,6 +55,7 @@ export function readBoundedRegularFileWithInfoSync(file, maxBytes, label = "path
|
|
|
52
55
|
return withRegularFileSync(file, fsConstants.O_RDONLY, {
|
|
53
56
|
label,
|
|
54
57
|
verifyPathIdentity: options.verifyPathIdentity === true,
|
|
58
|
+
rejectMultipleLinks: options.rejectMultipleLinks === true,
|
|
55
59
|
}, (fd, info) => {
|
|
56
60
|
if (info.size > limit) throw new Error(`file exceeds ${limit} bytes`);
|
|
57
61
|
options.afterOpen?.({ fd, info });
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { replaceFileAtomicallySync } from "./exclusive-file.mjs";
|
|
5
|
+
import { ensureOwnerOnlyDirectorySync, readBoundedRegularFileSync } from "./secure-file.mjs";
|
|
6
|
+
import { withOwnerStateLock } from "./owner-state-lock.mjs";
|
|
7
|
+
|
|
8
|
+
const SCHEMA_VERSION = 1;
|
|
9
|
+
const MAX_EVENTS = 4096;
|
|
10
|
+
const MAX_BYTES = 4 * 1024 * 1024;
|
|
11
|
+
const HASH_PATTERN = /^[a-f0-9]{64}$/;
|
|
12
|
+
|
|
13
|
+
export class SecurityAuditLog {
|
|
14
|
+
constructor({ root = "", now = Date.now } = {}) {
|
|
15
|
+
this.root = root ? path.resolve(root) : "";
|
|
16
|
+
this.file = this.root ? path.join(this.root, "security-audit.json") : "";
|
|
17
|
+
this.now = typeof now === "function" ? now : Date.now;
|
|
18
|
+
this.queue = Promise.resolve();
|
|
19
|
+
this.lastError = "";
|
|
20
|
+
if (this.file && existsSync(this.file)) {
|
|
21
|
+
try { this.readVerifiedState(); } catch (error) { this.lastError = errorClass(error); }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
record(input = {}) {
|
|
26
|
+
if (!this.file) return Promise.resolve(false);
|
|
27
|
+
const operation = async () => {
|
|
28
|
+
try {
|
|
29
|
+
return await withOwnerStateLock(this.root, async () => {
|
|
30
|
+
const state = this.readVerifiedState();
|
|
31
|
+
const event = buildEvent(input, state, this.now());
|
|
32
|
+
state.events.push(event);
|
|
33
|
+
while (state.events.length > MAX_EVENTS) {
|
|
34
|
+
const removed = state.events.shift();
|
|
35
|
+
state.anchor = removed.hash;
|
|
36
|
+
}
|
|
37
|
+
writeState(this.file, state);
|
|
38
|
+
this.lastError = "";
|
|
39
|
+
return true;
|
|
40
|
+
}, {
|
|
41
|
+
purpose: "security-audit", fileName: "security-audit.lock", label: "security audit",
|
|
42
|
+
});
|
|
43
|
+
} catch (error) {
|
|
44
|
+
this.lastError = errorClass(error);
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const result = this.queue.then(operation, operation);
|
|
49
|
+
this.queue = result.then(() => undefined, () => undefined);
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
snapshot() {
|
|
54
|
+
if (!this.file) return { enabled: false, healthy: true, retained: 0, maximum: MAX_EVENTS };
|
|
55
|
+
try {
|
|
56
|
+
const state = this.readVerifiedState();
|
|
57
|
+
return {
|
|
58
|
+
enabled: true,
|
|
59
|
+
healthy: !this.lastError,
|
|
60
|
+
retained: state.events.length,
|
|
61
|
+
maximum: MAX_EVENTS,
|
|
62
|
+
last_event_at: state.events.at(-1)?.timestamp || null,
|
|
63
|
+
last_error_class: this.lastError || null,
|
|
64
|
+
content_logged: false,
|
|
65
|
+
chain_verified: true,
|
|
66
|
+
};
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return {
|
|
69
|
+
enabled: true,
|
|
70
|
+
healthy: false,
|
|
71
|
+
retained: 0,
|
|
72
|
+
maximum: MAX_EVENTS,
|
|
73
|
+
last_event_at: null,
|
|
74
|
+
last_error_class: errorClass(error),
|
|
75
|
+
content_logged: false,
|
|
76
|
+
chain_verified: false,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
readVerifiedState() {
|
|
82
|
+
if (!existsSync(this.file)) return emptyState();
|
|
83
|
+
const raw = readBoundedRegularFileSync(this.file, MAX_BYTES, "security audit state");
|
|
84
|
+
let state;
|
|
85
|
+
try { state = JSON.parse(raw.toString("utf8")); } catch (error) {
|
|
86
|
+
throw new Error("security audit state is not valid JSON", { cause: error });
|
|
87
|
+
}
|
|
88
|
+
validateState(state);
|
|
89
|
+
let previous = state.anchor;
|
|
90
|
+
for (const event of state.events) {
|
|
91
|
+
if (event.previous_hash !== previous || event.hash !== eventHash(event)) {
|
|
92
|
+
throw new Error("security audit hash chain verification failed");
|
|
93
|
+
}
|
|
94
|
+
previous = event.hash;
|
|
95
|
+
}
|
|
96
|
+
return state;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function buildEvent(input, state, nowMs) {
|
|
101
|
+
const timestamp = new Date(Number(nowMs)).toISOString();
|
|
102
|
+
if (!Number.isFinite(Date.parse(timestamp))) throw new Error("security audit timestamp is invalid");
|
|
103
|
+
const principal = input.principal && typeof input.principal === "object" ? input.principal : {};
|
|
104
|
+
const event = {
|
|
105
|
+
sequence: state.next_sequence,
|
|
106
|
+
timestamp,
|
|
107
|
+
outcome: boundedToken(input.outcome, "unknown"),
|
|
108
|
+
tool: boundedToken(input.tool, "unknown"),
|
|
109
|
+
risk_category: boundedText(input.riskCategory, "ordinary operation", 160),
|
|
110
|
+
target_hash: HASH_PATTERN.test(String(input.targetHash || "")) ? String(input.targetHash) : null,
|
|
111
|
+
account_ref: principal.accountId ? privateReference(state.identity_salt, principal.accountId) : null,
|
|
112
|
+
client_ref: principal.clientId ? privateReference(state.identity_salt, principal.clientId) : null,
|
|
113
|
+
family_ref: principal.familyId ? privateReference(state.identity_salt, principal.familyId) : null,
|
|
114
|
+
account_version: Number.isSafeInteger(principal.accountVersion) ? principal.accountVersion : null,
|
|
115
|
+
role: boundedToken(principal.role, principal.kind === "local" ? "local" : "unknown"),
|
|
116
|
+
duration_ms: boundedNumber(input.durationMs),
|
|
117
|
+
input_bytes: boundedNumber(input.inputBytes),
|
|
118
|
+
output_bytes: boundedNumber(input.outputBytes),
|
|
119
|
+
error_code: input.errorCode ? boundedToken(input.errorCode, "unknown") : null,
|
|
120
|
+
previous_hash: state.events.at(-1)?.hash || state.anchor,
|
|
121
|
+
};
|
|
122
|
+
const completed = { ...event, hash: eventHash(event) };
|
|
123
|
+
state.next_sequence += 1;
|
|
124
|
+
return completed;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function emptyState() {
|
|
128
|
+
return {
|
|
129
|
+
schemaVersion: SCHEMA_VERSION,
|
|
130
|
+
identity_salt: randomBytes(32).toString("hex"),
|
|
131
|
+
anchor: randomBytes(32).toString("hex"),
|
|
132
|
+
next_sequence: 1,
|
|
133
|
+
events: [],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function validateState(state) {
|
|
138
|
+
if (!plainRecord(state) || state.schemaVersion !== SCHEMA_VERSION) throw new Error("security audit state schema is invalid");
|
|
139
|
+
if (!HASH_PATTERN.test(state.identity_salt) || !HASH_PATTERN.test(state.anchor)) throw new Error("security audit state identity is invalid");
|
|
140
|
+
if (!Number.isSafeInteger(state.next_sequence) || state.next_sequence < 1) throw new Error("security audit sequence is invalid");
|
|
141
|
+
if (!Array.isArray(state.events) || state.events.length > MAX_EVENTS || !state.events.every(validEvent)) {
|
|
142
|
+
throw new Error("security audit events are invalid");
|
|
143
|
+
}
|
|
144
|
+
if (state.events.length && state.next_sequence <= state.events.at(-1).sequence) throw new Error("security audit sequence did not advance");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function validEvent(event) {
|
|
148
|
+
return plainRecord(event)
|
|
149
|
+
&& Number.isSafeInteger(event.sequence)
|
|
150
|
+
&& event.sequence > 0
|
|
151
|
+
&& Number.isFinite(Date.parse(String(event.timestamp || "")))
|
|
152
|
+
&& typeof event.outcome === "string"
|
|
153
|
+
&& typeof event.tool === "string"
|
|
154
|
+
&& typeof event.risk_category === "string"
|
|
155
|
+
&& (event.target_hash === null || HASH_PATTERN.test(event.target_hash))
|
|
156
|
+
&& ["account_ref", "client_ref", "family_ref"].every((key) => event[key] === null || HASH_PATTERN.test(event[key]))
|
|
157
|
+
&& (event.account_version === null || Number.isSafeInteger(event.account_version))
|
|
158
|
+
&& typeof event.role === "string"
|
|
159
|
+
&& Number.isFinite(event.duration_ms)
|
|
160
|
+
&& Number.isFinite(event.input_bytes)
|
|
161
|
+
&& Number.isFinite(event.output_bytes)
|
|
162
|
+
&& (event.error_code === null || typeof event.error_code === "string")
|
|
163
|
+
&& HASH_PATTERN.test(event.previous_hash)
|
|
164
|
+
&& HASH_PATTERN.test(event.hash);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function eventHash(event) {
|
|
168
|
+
const value = { ...event };
|
|
169
|
+
delete value.hash;
|
|
170
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function privateReference(salt, value) {
|
|
174
|
+
return createHash("sha256").update(salt).update("\0").update(String(value)).digest("hex");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function writeState(file, state) {
|
|
178
|
+
ensureOwnerOnlyDirectorySync(path.dirname(file));
|
|
179
|
+
const content = `${JSON.stringify(state)}\n`;
|
|
180
|
+
if (Buffer.byteLength(content) > MAX_BYTES) throw new Error("security audit state exceeds its size limit");
|
|
181
|
+
replaceFileAtomicallySync(file, content, { mode: 0o600 });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function boundedToken(value, fallback) {
|
|
185
|
+
const text = String(value || fallback).replace(/[^A-Za-z0-9._:-]/g, "_").slice(0, 128);
|
|
186
|
+
return text || fallback;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function boundedText(value, fallback, maximum) {
|
|
190
|
+
return String(value || fallback).replace(/[\r\n\t\u0000-\u001f\u007f]/g, " ").trim().slice(0, maximum) || fallback;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function boundedNumber(value) {
|
|
194
|
+
const number = Number(value);
|
|
195
|
+
return Number.isFinite(number) && number >= 0 ? Math.min(Math.floor(number), Number.MAX_SAFE_INTEGER) : 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function errorClass(error) {
|
|
199
|
+
return String(error?.code || error?.name || "audit_error").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 80);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function plainRecord(value) {
|
|
203
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
204
|
+
}
|
|
@@ -1,14 +1,30 @@
|
|
|
1
1
|
const DEFAULT_ATTEMPTS = 20;
|
|
2
2
|
const DEFAULT_DELAY_MS = 100;
|
|
3
3
|
|
|
4
|
+
export async function waitForActiveStatus(
|
|
5
|
+
readStatus,
|
|
6
|
+
{ attempts = DEFAULT_ATTEMPTS, delayMs = DEFAULT_DELAY_MS, sleep = delay } = {},
|
|
7
|
+
) {
|
|
8
|
+
return waitForStatus(readStatus, (status) => status?.active === true, { attempts, delayMs, sleep });
|
|
9
|
+
}
|
|
10
|
+
|
|
4
11
|
export async function waitForInactiveStatus(
|
|
5
12
|
readStatus,
|
|
6
13
|
{ attempts = DEFAULT_ATTEMPTS, delayMs = DEFAULT_DELAY_MS, sleep = delay } = {},
|
|
14
|
+
) {
|
|
15
|
+
return waitForStatus(readStatus, (status) => status?.active !== true, { attempts, delayMs, sleep });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function waitForStatus(
|
|
19
|
+
readStatus,
|
|
20
|
+
predicate,
|
|
21
|
+
{ attempts = DEFAULT_ATTEMPTS, delayMs = DEFAULT_DELAY_MS, sleep = delay } = {},
|
|
7
22
|
) {
|
|
8
23
|
if (typeof readStatus !== "function") throw new TypeError("readStatus must be a function");
|
|
24
|
+
if (typeof predicate !== "function") throw new TypeError("predicate must be a function");
|
|
9
25
|
const maximum = Number.isInteger(attempts) && attempts > 0 ? attempts : DEFAULT_ATTEMPTS;
|
|
10
26
|
let status = await readStatus();
|
|
11
|
-
for (let attempt = 1; status
|
|
27
|
+
for (let attempt = 1; !predicate(status) && attempt < maximum; attempt += 1) {
|
|
12
28
|
await sleep(delayMs);
|
|
13
29
|
status = await readStatus();
|
|
14
30
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export async function stopOwnedPlatformService({
|
|
2
|
+
state,
|
|
3
|
+
inspectWorkspaceDaemon,
|
|
4
|
+
ownsPlatformAutostart,
|
|
5
|
+
stopPlatformService,
|
|
6
|
+
} = {}) {
|
|
7
|
+
if (!state) throw new TypeError("service ownership check requires state");
|
|
8
|
+
if (typeof inspectWorkspaceDaemon !== "function") throw new TypeError("service ownership check requires inspectWorkspaceDaemon");
|
|
9
|
+
if (typeof ownsPlatformAutostart !== "function") throw new TypeError("service ownership check requires ownsPlatformAutostart");
|
|
10
|
+
if (typeof stopPlatformService !== "function") throw new TypeError("service ownership check requires stopPlatformService");
|
|
11
|
+
const daemon = inspectWorkspaceDaemon(state);
|
|
12
|
+
if (!ownsPlatformAutostart(daemon)) {
|
|
13
|
+
return { owned: false, stopped: false, daemon, provider: null };
|
|
14
|
+
}
|
|
15
|
+
const provider = await stopPlatformService();
|
|
16
|
+
return { owned: true, stopped: provider?.ok !== false, daemon, provider };
|
|
17
|
+
}
|