machine-bridge-mcp 3.0.0-beta.21 → 3.0.0-beta.26
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 +134 -0
- package/CONTRIBUTING.md +3 -3
- package/GOVERNANCE.md +2 -2
- package/README.md +24 -6
- package/browser-extension/manifest.json +1 -1
- package/docs/AGENT_CONTEXT.md +10 -7
- package/docs/ARCHITECTURE.md +35 -22
- package/docs/AUDIT.md +85 -1
- package/docs/CLIENTS.md +6 -2
- package/docs/ENGINEERING.md +31 -9
- package/docs/LOCAL_AUTOMATION.md +4 -2
- package/docs/LOGGING.md +8 -8
- package/docs/OPERATIONS.md +43 -17
- package/docs/PRIVACY.md +18 -4
- package/docs/PROJECT_STANDARDS.md +2 -2
- package/docs/RELEASING.md +35 -11
- package/docs/TESTING.md +36 -16
- package/docs/THREAT_MODEL.md +20 -5
- package/docs/TOOL_REFERENCE.md +18 -12
- package/docs/UPGRADING.md +32 -0
- package/package.json +15 -6
- package/scripts/check-plan.mjs +8 -0
- package/scripts/coverage-check.mjs +30 -1
- package/scripts/foreground-daemon-recovery.mjs +88 -0
- package/scripts/github-release.mjs +22 -16
- package/scripts/install-published-prerelease.mjs +7 -7
- package/scripts/official-mcp-conformance.mjs +243 -0
- package/scripts/persistent-activation-process.mjs +36 -0
- package/scripts/release-candidate-manifest.mjs +12 -0
- package/scripts/release-publication-guard.mjs +65 -0
- package/scripts/release-state.mjs +1 -1
- package/scripts/sbom-check.mjs +99 -0
- package/scripts/start-release-candidate.mjs +39 -13
- package/src/local/agent-context-projection.mjs +26 -7
- package/src/local/agent-context.mjs +25 -4
- package/src/local/autostart-log-maintenance.mjs +36 -0
- package/src/local/capability-observer.mjs +5 -0
- package/src/local/child-process-settlement.mjs +103 -0
- package/src/local/cli-activate.mjs +42 -5
- package/src/local/cli-service.mjs +55 -5
- package/src/local/cli.mjs +59 -10
- package/src/local/daemon-process.mjs +24 -3
- package/src/local/delegated-process-sandbox.mjs +1 -0
- package/src/local/execution-routing.mjs +231 -0
- package/src/local/git-service.mjs +3 -1
- package/src/local/job-runner.mjs +55 -19
- package/src/local/macos-trust-broker.mjs +7 -0
- package/src/local/managed-job-runner-claim.mjs +54 -0
- package/src/local/managed-job-runner.mjs +13 -2
- package/src/local/process-execution.mjs +2 -2
- package/src/local/process-identity.mjs +11 -0
- package/src/local/process-tree-ownership-types.d.ts +37 -0
- package/src/local/process-tree-ownership.mjs +49 -41
- package/src/local/process-tree.mjs +1 -1
- package/src/local/relay-call-recovery.mjs +40 -21
- package/src/local/runtime-activation.mjs +357 -38
- package/src/local/runtime-capabilities.mjs +22 -6
- package/src/local/runtime-diagnostics.mjs +9 -2
- package/src/local/runtime.mjs +18 -4
- package/src/local/service-convergence.mjs +33 -0
- package/src/local/service-owner.mjs +147 -0
- package/src/local/service-restart-handoff.mjs +22 -8
- package/src/local/service-runtime.mjs +145 -0
- package/src/local/service.mjs +143 -25
- package/src/local/state.mjs +104 -7
- package/src/local/stdio.mjs +139 -45
- package/src/local/system-network-route.mjs +76 -0
- package/src/local/tool-executor.mjs +24 -6
- package/src/local/tools.mjs +6 -5
- package/src/local/windows-service-convergence.mjs +49 -0
- package/src/local/windows-service.mjs +30 -53
- package/src/shared/mcp-protocol.d.mts +27 -0
- package/src/shared/mcp-protocol.mjs +256 -0
- package/src/shared/mcp-subscriptions.d.mts +4 -0
- package/src/shared/mcp-subscriptions.mjs +59 -0
- package/src/shared/relay-contract.json +1 -0
- package/src/shared/result-projection.d.mts +2 -1
- package/src/shared/result-projection.mjs +13 -2
- package/src/shared/server-metadata.json +11 -4
- package/src/shared/tool-argument-validation.d.mts +17 -0
- package/src/shared/tool-argument-validation.mjs +325 -0
- package/src/shared/tool-catalog.json +18 -12
- package/src/worker/durable-stream-calls.ts +12 -24
- package/src/worker/http.ts +36 -2
- package/src/worker/index.ts +181 -165
- package/src/worker/mcp-http-contract.ts +276 -0
- package/src/worker/mcp-jsonrpc.ts +12 -6
- package/src/worker/mcp-legacy-dispatch.ts +104 -0
- package/src/worker/mcp-modern-controller.ts +199 -0
- package/src/worker/mcp-modern-proxy.ts +126 -0
- package/src/worker/mcp-modern-stream.ts +71 -0
- package/src/worker/mcp-session.ts +12 -3
- package/src/worker/mcp-stream-proxy-contract.ts +67 -0
- package/src/worker/mcp-stream-proxy.ts +17 -60
- package/src/worker/mcp-tool-call-input.ts +23 -0
- package/src/worker/tool-catalog.ts +29 -1
- package/src/worker/tool-timeout.ts +53 -12
- package/src/worker/worker-mcp-config.ts +23 -0
- package/src/worker/worker-metadata.ts +10 -1
- package/src/worker/worker-runtime-config.ts +19 -0
- package/src/worker/worker-static-routes.ts +7 -2
- package/tsconfig.local.json +7 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export async function activatePersistentRuntime(options = {}) {
|
|
2
2
|
const required = [
|
|
3
3
|
"acquireStartupLock",
|
|
4
|
+
"acquireServiceLock",
|
|
5
|
+
"inspectActivationOwnership",
|
|
4
6
|
"stopAutostart",
|
|
5
7
|
"acquireDaemonLock",
|
|
6
8
|
"prepareRemoteState",
|
|
@@ -13,20 +15,43 @@ export async function activatePersistentRuntime(options = {}) {
|
|
|
13
15
|
for (const name of required) {
|
|
14
16
|
if (typeof options[name] !== "function") throw new TypeError(`runtime activation requires ${name}`);
|
|
15
17
|
}
|
|
18
|
+
const expectedVersion = requiredExpectedVersion(options.expectedVersion);
|
|
19
|
+
const maximumAttempts = boundedAttempts(options.maximumAttempts ?? 240);
|
|
20
|
+
const candidateStartAttempts = boundedCandidateStartAttempts(options.candidateStartAttempts ?? 3);
|
|
21
|
+
const candidateRetryWait = optionalFunction(options.candidateRetryWait, "candidateRetryWait", defaultCandidateRetryWait);
|
|
22
|
+
const convergenceWait = optionalFunction(options.wait, "wait", defaultWait);
|
|
23
|
+
const restorePreviousAutostart = optionalFunction(options.restorePreviousAutostart, "restorePreviousAutostart", options.startAutostart);
|
|
24
|
+
if (options.repairRemoteState !== undefined && typeof options.repairRemoteState !== "function") {
|
|
25
|
+
throw new TypeError("runtime activation repairRemoteState must be a function");
|
|
26
|
+
}
|
|
16
27
|
|
|
17
|
-
|
|
28
|
+
let startupLock = null;
|
|
18
29
|
let startupReleased = false;
|
|
30
|
+
let serviceLock = null;
|
|
19
31
|
let daemonLock = null;
|
|
20
32
|
let candidateRuntime = null;
|
|
21
33
|
let candidateRelayVerified = false;
|
|
34
|
+
let remotePrepared = false;
|
|
35
|
+
let autostartInstalled = false;
|
|
22
36
|
let providerStopped = false;
|
|
37
|
+
let previousServiceRuntime = null;
|
|
23
38
|
let serviceStarted = false;
|
|
24
39
|
try {
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
27
|
-
|
|
40
|
+
const acquiredServiceLock = await options.acquireServiceLock();
|
|
41
|
+
if (!acquiredServiceLock?.acquired) {
|
|
42
|
+
const owner = acquiredServiceLock?.owner || {};
|
|
43
|
+
throw new Error(`another machine-service operation is active (pid ${owner.pid || "unknown"})`);
|
|
28
44
|
}
|
|
29
|
-
|
|
45
|
+
requireReleasableLock(acquiredServiceLock, "machine-service");
|
|
46
|
+
serviceLock = acquiredServiceLock;
|
|
47
|
+
const acquiredStartupLock = await options.acquireStartupLock();
|
|
48
|
+
requireReleasableLock(acquiredStartupLock, "startup");
|
|
49
|
+
startupLock = acquiredStartupLock;
|
|
50
|
+
const ownership = validateActivationOwnership(await options.inspectActivationOwnership());
|
|
51
|
+
providerStopped = ownership.previousServiceRuntimeActive;
|
|
52
|
+
previousServiceRuntime = ownership.previousServiceRuntime;
|
|
53
|
+
const providerStop = await options.stopAutostart();
|
|
54
|
+
providerStopped = validateProviderStop(providerStop) || providerStopped;
|
|
30
55
|
|
|
31
56
|
daemonLock = await options.acquireDaemonLock();
|
|
32
57
|
if (!daemonLock?.acquired) {
|
|
@@ -36,21 +61,40 @@ export async function activatePersistentRuntime(options = {}) {
|
|
|
36
61
|
}
|
|
37
62
|
throw new Error(`the existing daemon could not be taken over (pid ${owner.pid || "unknown"})`);
|
|
38
63
|
}
|
|
64
|
+
requireReleasableLock(daemonLock, "daemon");
|
|
39
65
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
66
|
+
let readiness;
|
|
67
|
+
try {
|
|
68
|
+
readiness = await options.prepareRemoteState({
|
|
69
|
+
onRemotePrepared() { remotePrepared = true; },
|
|
70
|
+
});
|
|
71
|
+
remotePrepared = true;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error?.deploymentSucceeded === true) remotePrepared = true;
|
|
74
|
+
throw error;
|
|
44
75
|
}
|
|
45
|
-
|
|
76
|
+
|
|
77
|
+
const candidate = await startCandidateWithRecovery({
|
|
78
|
+
attempts: candidateStartAttempts,
|
|
79
|
+
daemonLock,
|
|
80
|
+
readiness,
|
|
81
|
+
createRuntime: options.createRuntime,
|
|
82
|
+
repairRemoteState: options.repairRemoteState,
|
|
83
|
+
wait: candidateRetryWait,
|
|
84
|
+
onRuntimeCreated(runtime) { candidateRuntime = runtime; },
|
|
85
|
+
onRuntimeStopped() { candidateRuntime = null; },
|
|
86
|
+
onRemoteRepaired() { remotePrepared = true; },
|
|
87
|
+
});
|
|
88
|
+
candidateRuntime = candidate.runtime;
|
|
46
89
|
candidateRelayVerified = true;
|
|
47
90
|
|
|
48
91
|
const installed = await options.installAutostart();
|
|
49
92
|
const terminalError = candidateRuntime.terminalError?.();
|
|
50
93
|
if (terminalError) throw terminalError;
|
|
51
|
-
if (installed?.ok
|
|
52
|
-
throw new Error(`autostart installation failed (${installed
|
|
94
|
+
if (installed?.ok !== true) {
|
|
95
|
+
throw new Error(`autostart installation failed (${installed?.provider || "unknown"})`);
|
|
53
96
|
}
|
|
97
|
+
autostartInstalled = true;
|
|
54
98
|
|
|
55
99
|
candidateRuntime.stop();
|
|
56
100
|
candidateRuntime = null;
|
|
@@ -60,55 +104,295 @@ export async function activatePersistentRuntime(options = {}) {
|
|
|
60
104
|
startupReleased = true;
|
|
61
105
|
|
|
62
106
|
const serviceStart = await options.startAutostart();
|
|
63
|
-
|
|
64
|
-
throw new Error(`autostart start failed (${serviceStart.provider || "unknown"})`);
|
|
65
|
-
}
|
|
107
|
+
requireActiveServiceStart(serviceStart, "autostart start");
|
|
66
108
|
serviceStarted = true;
|
|
67
109
|
|
|
68
110
|
const convergence = await waitForActivatedRuntime({
|
|
69
|
-
expectedVersion
|
|
111
|
+
expectedVersion,
|
|
70
112
|
inspectDaemon: options.inspectDaemon,
|
|
71
113
|
checkWorker: options.checkWorker,
|
|
72
|
-
maximumAttempts
|
|
73
|
-
wait:
|
|
114
|
+
maximumAttempts,
|
|
115
|
+
wait: convergenceWait,
|
|
74
116
|
});
|
|
75
117
|
if (!convergence.ok) {
|
|
76
118
|
throw new Error(`persistent runtime activation did not converge (${convergence.reason})`);
|
|
77
119
|
}
|
|
120
|
+
serviceLock.release();
|
|
121
|
+
serviceLock = null;
|
|
78
122
|
return {
|
|
79
123
|
ok: true,
|
|
80
124
|
serviceStart,
|
|
81
125
|
convergence,
|
|
82
126
|
candidateRelayVerified,
|
|
127
|
+
candidateRecoveryRedeployed: candidate.repaired,
|
|
83
128
|
};
|
|
84
129
|
} catch (error) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
130
|
+
throw await failedActivationError({
|
|
131
|
+
error,
|
|
132
|
+
candidateRuntime,
|
|
133
|
+
daemonLock,
|
|
134
|
+
startupLock,
|
|
135
|
+
startupReleased,
|
|
136
|
+
serviceLock,
|
|
137
|
+
providerStopped,
|
|
138
|
+
serviceStarted,
|
|
139
|
+
remotePrepared,
|
|
140
|
+
autostartInstalled,
|
|
141
|
+
installAutostart: options.installAutostart,
|
|
142
|
+
startAutostart: options.startAutostart,
|
|
143
|
+
restorePreviousAutostart,
|
|
144
|
+
inspectPreviousAutostart: options.inspectPreviousAutostart,
|
|
145
|
+
previousServiceRuntime,
|
|
146
|
+
restoreWait: convergenceWait,
|
|
147
|
+
restoreMaximumAttempts: options.maximumAttempts,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function failedActivationError({
|
|
153
|
+
error,
|
|
154
|
+
candidateRuntime,
|
|
155
|
+
daemonLock,
|
|
156
|
+
startupLock,
|
|
157
|
+
startupReleased,
|
|
158
|
+
serviceLock,
|
|
159
|
+
providerStopped,
|
|
160
|
+
serviceStarted,
|
|
161
|
+
remotePrepared,
|
|
162
|
+
autostartInstalled,
|
|
163
|
+
installAutostart,
|
|
164
|
+
startAutostart,
|
|
165
|
+
restorePreviousAutostart,
|
|
166
|
+
inspectPreviousAutostart,
|
|
167
|
+
previousServiceRuntime,
|
|
168
|
+
restoreWait,
|
|
169
|
+
restoreMaximumAttempts,
|
|
170
|
+
}) {
|
|
171
|
+
const cleanupErrors = [];
|
|
172
|
+
try { candidateRuntime?.stop?.(); } catch (failure) { cleanupErrors.push(failure); }
|
|
173
|
+
try { daemonLock?.release?.(); } catch (failure) { cleanupErrors.push(failure); }
|
|
174
|
+
if (!startupReleased && startupLock) {
|
|
175
|
+
try { startupLock.release(); }
|
|
176
|
+
catch (failure) { cleanupErrors.push(failure); }
|
|
177
|
+
}
|
|
178
|
+
let recovery = null;
|
|
179
|
+
const cleanupIncomplete = error?.cleanupIncomplete === true || cleanupErrors.length > 0;
|
|
180
|
+
if (!cleanupIncomplete) {
|
|
92
181
|
try {
|
|
93
|
-
await
|
|
182
|
+
recovery = await restoreCompatibleProvider({
|
|
183
|
+
installAutostart,
|
|
184
|
+
startAutostart,
|
|
185
|
+
restorePreviousAutostart,
|
|
186
|
+
inspectPreviousAutostart,
|
|
187
|
+
previousServiceRuntime,
|
|
188
|
+
restoreWait,
|
|
189
|
+
restoreMaximumAttempts,
|
|
190
|
+
providerStopped,
|
|
191
|
+
serviceStarted,
|
|
192
|
+
candidateServiceRequired: remotePrepared,
|
|
193
|
+
candidateDefinitionRequired: remotePrepared && !autostartInstalled,
|
|
194
|
+
});
|
|
94
195
|
} catch (failure) { cleanupErrors.push(failure); }
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
196
|
+
}
|
|
197
|
+
if (serviceLock) {
|
|
198
|
+
try { serviceLock.release(); } catch (failure) { cleanupErrors.push(failure); }
|
|
199
|
+
}
|
|
200
|
+
if (cleanupErrors.length) {
|
|
201
|
+
return new AggregateError(
|
|
202
|
+
[error, ...cleanupErrors],
|
|
203
|
+
"persistent runtime activation failed and local cleanup was incomplete; inspect the daemon and service state before retrying",
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
if (recovery?.candidateServiceStarted) return activationFailureWithRecovery(error);
|
|
207
|
+
return error;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function startCandidateWithRecovery({
|
|
211
|
+
attempts = 3,
|
|
212
|
+
daemonLock,
|
|
213
|
+
readiness,
|
|
214
|
+
createRuntime,
|
|
215
|
+
repairRemoteState,
|
|
216
|
+
wait = defaultCandidateRetryWait,
|
|
217
|
+
onRuntimeCreated,
|
|
218
|
+
onRuntimeStopped,
|
|
219
|
+
onRemoteRepaired,
|
|
220
|
+
} = {}) {
|
|
221
|
+
const maximumAttempts = boundedCandidateStartAttempts(attempts);
|
|
222
|
+
let currentReadiness = readiness;
|
|
223
|
+
let repaired = false;
|
|
224
|
+
let lastError = null;
|
|
225
|
+
for (let attempt = 1; attempt <= maximumAttempts; attempt += 1) {
|
|
226
|
+
const runtime = createRuntime({ daemonLock, readiness: currentReadiness });
|
|
227
|
+
if (!runtime || typeof runtime.start !== "function" || typeof runtime.stop !== "function") {
|
|
228
|
+
throw new TypeError("runtime activation candidate must expose start and stop");
|
|
229
|
+
}
|
|
230
|
+
onRuntimeCreated(runtime);
|
|
231
|
+
try {
|
|
232
|
+
await runtime.start();
|
|
233
|
+
return { runtime, repaired };
|
|
234
|
+
} catch (error) {
|
|
235
|
+
lastError = error;
|
|
236
|
+
let stopFailure = null;
|
|
237
|
+
try { runtime.stop(); } catch (failure) { stopFailure = failure; }
|
|
238
|
+
finally { onRuntimeStopped(); }
|
|
239
|
+
if (stopFailure) {
|
|
240
|
+
const cleanupError = new AggregateError(
|
|
241
|
+
[error, stopFailure],
|
|
242
|
+
"candidate relay start failed and runtime cleanup was incomplete",
|
|
243
|
+
);
|
|
244
|
+
cleanupError.cleanupIncomplete = true;
|
|
245
|
+
throw cleanupError;
|
|
246
|
+
}
|
|
247
|
+
if (!recoverableCandidateStartError(error) || attempt >= maximumAttempts) throw error;
|
|
248
|
+
if (!repaired && typeof repairRemoteState === "function") {
|
|
249
|
+
let repairPrepared = false;
|
|
250
|
+
const markRemoteRepaired = () => {
|
|
251
|
+
if (repairPrepared) return;
|
|
252
|
+
repairPrepared = true;
|
|
253
|
+
onRemoteRepaired();
|
|
254
|
+
};
|
|
255
|
+
currentReadiness = await repairRemoteState({
|
|
256
|
+
attempt,
|
|
257
|
+
error,
|
|
258
|
+
readiness: currentReadiness,
|
|
259
|
+
onRemotePrepared: markRemoteRepaired,
|
|
260
|
+
});
|
|
261
|
+
repaired = true;
|
|
262
|
+
markRemoteRepaired();
|
|
263
|
+
}
|
|
264
|
+
await wait(attempt);
|
|
100
265
|
}
|
|
101
|
-
throw error;
|
|
102
266
|
}
|
|
267
|
+
throw lastError || new Error("candidate relay did not start");
|
|
103
268
|
}
|
|
104
269
|
|
|
270
|
+
function recoverableCandidateStartError(error) {
|
|
271
|
+
return error?.code === "relay_authentication_failed";
|
|
272
|
+
}
|
|
105
273
|
|
|
106
|
-
async function
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
274
|
+
async function restoreCompatibleProvider({
|
|
275
|
+
installAutostart,
|
|
276
|
+
startAutostart,
|
|
277
|
+
restorePreviousAutostart,
|
|
278
|
+
inspectPreviousAutostart,
|
|
279
|
+
previousServiceRuntime,
|
|
280
|
+
restoreWait,
|
|
281
|
+
restoreMaximumAttempts,
|
|
282
|
+
providerStopped,
|
|
283
|
+
serviceStarted,
|
|
284
|
+
candidateServiceRequired,
|
|
285
|
+
candidateDefinitionRequired,
|
|
286
|
+
}) {
|
|
287
|
+
if (serviceStarted || (!candidateServiceRequired && !providerStopped)) {
|
|
288
|
+
return { candidateServiceStarted: false };
|
|
289
|
+
}
|
|
290
|
+
if (candidateDefinitionRequired) {
|
|
291
|
+
const installed = await installAutostart();
|
|
292
|
+
if (installed?.ok !== true) {
|
|
293
|
+
throw new Error(`compatible candidate autostart could not be installed (${installed?.provider || "unknown"})`);
|
|
294
|
+
}
|
|
111
295
|
}
|
|
296
|
+
const restored = candidateServiceRequired
|
|
297
|
+
? await startAutostart()
|
|
298
|
+
: await restorePreviousAutostart();
|
|
299
|
+
const qualifier = candidateServiceRequired ? "compatible candidate" : "previous";
|
|
300
|
+
requireActiveServiceStart(restored, `${qualifier} autostart service recovery`);
|
|
301
|
+
if (!candidateServiceRequired) {
|
|
302
|
+
if (!previousServiceRuntime || typeof inspectPreviousAutostart !== "function") {
|
|
303
|
+
throw new Error("previous autostart service recovery lacks verified runtime identity evidence");
|
|
304
|
+
}
|
|
305
|
+
const convergence = await waitForRestoredServiceRuntime({
|
|
306
|
+
inspectDaemon: () => inspectPreviousAutostart(previousServiceRuntime),
|
|
307
|
+
expectedVersion: previousServiceRuntime.version,
|
|
308
|
+
maximumAttempts: restoreMaximumAttempts,
|
|
309
|
+
wait: restoreWait,
|
|
310
|
+
});
|
|
311
|
+
if (!convergence.ok) {
|
|
312
|
+
throw new Error(`previous autostart service recovery did not converge (${convergence.reason})`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return { candidateServiceStarted: candidateServiceRequired };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function validateActivationOwnership(value) {
|
|
319
|
+
const daemon = value?.daemon || {};
|
|
320
|
+
const provider = value?.provider || {};
|
|
321
|
+
if (typeof provider.active !== "boolean") {
|
|
322
|
+
throw new Error(`the machine autostart state could not be verified before activation (${provider.provider || "unknown"})`);
|
|
323
|
+
}
|
|
324
|
+
if (daemon.alive === true) {
|
|
325
|
+
if (daemon.mode === "foreground" || daemon.identity_reason === "foreground_daemon") {
|
|
326
|
+
throw new Error(`a foreground daemon is active (pid ${daemon.pid || "unknown"}); stop it explicitly before activation`);
|
|
327
|
+
}
|
|
328
|
+
if (daemon.verified_service_daemon !== true || daemon.mode !== "service") {
|
|
329
|
+
throw new Error(`the existing workspace daemon identity could not be verified before activation (${daemon.identity_reason || "unknown"})`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const previousServiceRuntimeActive = daemon.alive === true
|
|
333
|
+
&& daemon.verified_service_daemon === true
|
|
334
|
+
&& daemon.mode === "service";
|
|
335
|
+
if (provider.active === true && !previousServiceRuntimeActive) {
|
|
336
|
+
throw new Error("refusing to stop the active machine service because the selected workspace does not own its verified daemon");
|
|
337
|
+
}
|
|
338
|
+
let previousServiceRuntime = null;
|
|
339
|
+
if (previousServiceRuntimeActive) {
|
|
340
|
+
const evidence = value?.previousRuntime || {};
|
|
341
|
+
if (typeof evidence.version !== "string" || !evidence.version
|
|
342
|
+
|| typeof evidence.entryScript !== "string" || !evidence.entryScript) {
|
|
343
|
+
throw new Error("the previous service daemon identity evidence is incomplete before activation");
|
|
344
|
+
}
|
|
345
|
+
previousServiceRuntime = { version: evidence.version, entryScript: evidence.entryScript };
|
|
346
|
+
}
|
|
347
|
+
return { previousServiceRuntimeActive, previousServiceRuntime };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function validateProviderStop(result) {
|
|
351
|
+
if (result?.ok !== true) {
|
|
352
|
+
throw new Error(`the existing autostart service could not be stopped before activation (${result?.provider || "unknown"})`);
|
|
353
|
+
}
|
|
354
|
+
if (typeof result.active_before !== "boolean" || result.active !== false || typeof result.restore_required !== "boolean") {
|
|
355
|
+
throw new Error(`the existing autostart stop state could not be verified before activation (${result?.provider || "unknown"})`);
|
|
356
|
+
}
|
|
357
|
+
return result.restore_required;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function requireActiveServiceStart(result, operation) {
|
|
361
|
+
if (result?.ok !== true || result.active !== true) {
|
|
362
|
+
throw new Error(`${operation} did not reach an active persistent service (${result?.provider || "unknown"})`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function activationFailureWithRecovery(error) {
|
|
367
|
+
const message = error instanceof Error && error.message ? error.message : String(error || "activation failed");
|
|
368
|
+
const recoveredMessage = `${message}; a compatible candidate service was installed and started for automatic recovery`;
|
|
369
|
+
const recovered = error instanceof AggregateError
|
|
370
|
+
? new AggregateError(error.errors, recoveredMessage, { cause: error })
|
|
371
|
+
: new Error(recoveredMessage, { cause: error });
|
|
372
|
+
if (error?.code) recovered.code = error.code;
|
|
373
|
+
recovered.activationRecovery = "candidate_service_started";
|
|
374
|
+
return recovered;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export async function waitForRestoredServiceRuntime({
|
|
378
|
+
inspectDaemon, expectedVersion, maximumAttempts = 240, wait = defaultWait,
|
|
379
|
+
} = {}) {
|
|
380
|
+
if (typeof inspectDaemon !== "function") throw new TypeError("previous runtime recovery requires a daemon inspector");
|
|
381
|
+
const attempts = boundedAttempts(maximumAttempts);
|
|
382
|
+
let daemon = null;
|
|
383
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
384
|
+
daemon = await inspectDaemon();
|
|
385
|
+
if (daemon?.alive && daemon?.verified_service_daemon
|
|
386
|
+
&& daemon?.mode === "service" && daemon?.version === expectedVersion) {
|
|
387
|
+
return { ok: true, attempts: attempt, daemon };
|
|
388
|
+
}
|
|
389
|
+
if (attempt < attempts) await wait(attempt);
|
|
390
|
+
}
|
|
391
|
+
const reason = !daemon?.alive ? "daemon_not_running"
|
|
392
|
+
: daemon?.verified_service_daemon !== true ? `daemon_identity_${daemon?.identity_reason || "unverified"}`
|
|
393
|
+
: daemon?.mode !== "service" ? `daemon_mode_${daemon?.mode || "unknown"}`
|
|
394
|
+
: `daemon_version_${daemon?.version || "unknown"}`;
|
|
395
|
+
return { ok: false, attempts, daemon, reason };
|
|
112
396
|
}
|
|
113
397
|
|
|
114
398
|
export async function waitForActivatedRuntime({
|
|
@@ -126,7 +410,9 @@ export async function waitForActivatedRuntime({
|
|
|
126
410
|
let worker = null;
|
|
127
411
|
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
128
412
|
daemon = await inspectDaemon();
|
|
129
|
-
if (daemon?.alive && daemon?.verified_service_daemon
|
|
413
|
+
if (daemon?.alive && daemon?.verified_service_daemon
|
|
414
|
+
&& daemon?.startup_readiness_verified === true
|
|
415
|
+
&& daemon?.version === expectedVersion) {
|
|
130
416
|
worker = await checkWorker();
|
|
131
417
|
if (worker?.ok && worker?.version === expectedVersion) {
|
|
132
418
|
return { ok: true, attempts: attempt, daemon, worker };
|
|
@@ -137,12 +423,32 @@ export async function waitForActivatedRuntime({
|
|
|
137
423
|
const reasons = [];
|
|
138
424
|
if (!daemon?.alive) reasons.push("daemon_not_running");
|
|
139
425
|
else if (!daemon?.verified_service_daemon) reasons.push(`daemon_identity_${daemon?.identity_reason || "unverified"}`);
|
|
426
|
+
else if (daemon?.startup_readiness_verified !== true) reasons.push("daemon_startup_readiness_unverified");
|
|
140
427
|
else if (daemon?.version !== expectedVersion) reasons.push(`daemon_version_${daemon?.version || "unknown"}`);
|
|
141
428
|
if (!worker?.ok) reasons.push(`worker_${worker?.error || "not_ready"}`);
|
|
142
429
|
else if (worker?.version !== expectedVersion) reasons.push(`worker_version_${worker?.version || "unknown"}`);
|
|
143
430
|
return { ok: false, attempts, daemon, worker, reason: reasons.join(",") || "activation_not_converged" };
|
|
144
431
|
}
|
|
145
432
|
|
|
433
|
+
function requiredExpectedVersion(value) {
|
|
434
|
+
if (typeof value !== "string" || !/^[0-9A-Za-z.+-]{1,128}$/.test(value)) {
|
|
435
|
+
throw new TypeError("runtime activation expectedVersion must be a bounded package version string");
|
|
436
|
+
}
|
|
437
|
+
return value;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function optionalFunction(value, name, fallback) {
|
|
441
|
+
if (value === undefined) return fallback;
|
|
442
|
+
if (typeof value !== "function") throw new TypeError(`runtime activation ${name} must be a function`);
|
|
443
|
+
return value;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function requireReleasableLock(lock, name) {
|
|
447
|
+
if (!lock || typeof lock.release !== "function") {
|
|
448
|
+
throw new TypeError(`runtime activation ${name} lock must expose release`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
146
452
|
function boundedAttempts(value) {
|
|
147
453
|
const parsed = Number(value);
|
|
148
454
|
if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 1200) {
|
|
@@ -151,7 +457,20 @@ function boundedAttempts(value) {
|
|
|
151
457
|
return parsed;
|
|
152
458
|
}
|
|
153
459
|
|
|
460
|
+
function boundedCandidateStartAttempts(value) {
|
|
461
|
+
const parsed = Number(value);
|
|
462
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 10) {
|
|
463
|
+
throw new Error("candidate relay start attempts must be between 1 and 10");
|
|
464
|
+
}
|
|
465
|
+
return parsed;
|
|
466
|
+
}
|
|
467
|
+
|
|
154
468
|
function defaultWait(attempt) {
|
|
155
469
|
const delay = Math.min(1_000, 100 + attempt * 25);
|
|
156
470
|
return new Promise((resolvePromise) => { setTimeout(resolvePromise, delay); });
|
|
157
471
|
}
|
|
472
|
+
|
|
473
|
+
function defaultCandidateRetryWait(attempt) {
|
|
474
|
+
const delay = Math.min(2_000, 250 * (2 ** Math.max(0, attempt - 1)));
|
|
475
|
+
return new Promise((resolvePromise) => { setTimeout(resolvePromise, delay); });
|
|
476
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { classifyOperationalError } from "./log.mjs";
|
|
2
|
+
import { buildExecutionRouting } from "./execution-routing.mjs";
|
|
3
|
+
import { policyAllowsTool } from "./policy.mjs";
|
|
2
4
|
|
|
3
5
|
export async function sessionBootstrap({
|
|
4
6
|
agentContextManager,
|
|
@@ -7,9 +9,11 @@ export async function sessionBootstrap({
|
|
|
7
9
|
policy,
|
|
8
10
|
}, args = {}, context = {}) {
|
|
9
11
|
const bootstrap = await agentContextManager.sessionBootstrap(args, context);
|
|
12
|
+
const applicationAllowed = policyAllowsTool(policy, "list_local_applications");
|
|
13
|
+
const browserAllowed = policyAllowsTool(policy, "browser_status");
|
|
10
14
|
bootstrap.local_automation = {
|
|
11
|
-
applications: appAutomationManager.capabilities(),
|
|
12
|
-
browser:
|
|
15
|
+
applications: applicationAllowed ? appAutomationManager.capabilities() : null,
|
|
16
|
+
browser: browserAllowed ? {
|
|
13
17
|
existing_profile: true,
|
|
14
18
|
extension_bridge: true,
|
|
15
19
|
status_tool: "browser_status",
|
|
@@ -27,7 +31,9 @@ export async function resolveTaskCapabilities({
|
|
|
27
31
|
}, args = {}, context = {}) {
|
|
28
32
|
const result = await agentContextManager.resolveTaskCapabilities(args, context);
|
|
29
33
|
const task = String(args.task || "");
|
|
30
|
-
|
|
34
|
+
const applicationAllowed = policyAllowsTool(policy, "list_local_applications");
|
|
35
|
+
const browserAllowed = policyAllowsTool(policy, "browser_status");
|
|
36
|
+
if (applicationAllowed) {
|
|
31
37
|
let applications;
|
|
32
38
|
try {
|
|
33
39
|
applications = await appAutomationManager.listApplications({ query: "", max_results: 500 }, context);
|
|
@@ -49,7 +55,7 @@ export async function resolveTaskCapabilities({
|
|
|
49
55
|
.map(({ application, score }) => ({ ...application, score }));
|
|
50
56
|
} else {
|
|
51
57
|
result.application_matches = [];
|
|
52
|
-
result.application_discovery = { available: false, warning_count: 0, truncated: false, reason: "
|
|
58
|
+
result.application_discovery = { available: false, warning_count: 0, truncated: false, reason: "effective_policy" };
|
|
53
59
|
}
|
|
54
60
|
if (result.application_matches.length) {
|
|
55
61
|
result.recommended_tools = [...new Set([
|
|
@@ -60,10 +66,20 @@ export async function resolveTaskCapabilities({
|
|
|
60
66
|
"operate_local_application",
|
|
61
67
|
])];
|
|
62
68
|
}
|
|
63
|
-
result.browser_backend =
|
|
69
|
+
result.browser_backend = browserAllowed
|
|
64
70
|
? { tool: "browser_status", existing_profile: true, extension_bridge: true }
|
|
65
71
|
: null;
|
|
66
|
-
|
|
72
|
+
const routing = buildExecutionRouting(task, {
|
|
73
|
+
policy,
|
|
74
|
+
seedTools: result.recommended_tools,
|
|
75
|
+
commandRelevant: (result.command_matches?.[0]?.score || 0) >= 3,
|
|
76
|
+
skillRelevant: (result.skill_matches?.[0]?.score || 0) >= 3,
|
|
77
|
+
applicationMatches: result.application_matches,
|
|
78
|
+
browserAvailable: browserAllowed,
|
|
79
|
+
});
|
|
80
|
+
result.execution_routing = routing;
|
|
81
|
+
result.recommended_tools = routing.recommended_tools;
|
|
82
|
+
result.routing_observability = "Call server_info or project_overview to verify that bootstrap and task capability resolution reached the local runtime. Routing is advisory and does not restrict direct shell or any other tool allowed by the effective policy.";
|
|
67
83
|
capabilityObserver.recordResolution(task, result);
|
|
68
84
|
return result;
|
|
69
85
|
}
|
|
@@ -3,12 +3,16 @@ import { rm, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { classifyOperationalError } from "./log.mjs";
|
|
5
5
|
import { readBoundedFile } from "./workspace-file-service.mjs";
|
|
6
|
+
import { systemNetworkRouteCheck } from "./system-network-route.mjs";
|
|
7
|
+
|
|
8
|
+
export const RUNTIME_DIAGNOSTIC_PROCESS_TIMEOUT_MS = 30_000;
|
|
6
9
|
|
|
7
10
|
export async function diagnoseRuntime({
|
|
8
11
|
policy,
|
|
9
12
|
runtimeDir,
|
|
10
13
|
workspace,
|
|
11
14
|
runProcess,
|
|
15
|
+
runFixedInternal,
|
|
12
16
|
probeShell,
|
|
13
17
|
managedJobManager,
|
|
14
18
|
relayStatus = () => null,
|
|
@@ -44,6 +48,8 @@ export async function diagnoseRuntime({
|
|
|
44
48
|
layer: "remote-relay", ok: false, skipped: true, transport: "stdio-or-local",
|
|
45
49
|
});
|
|
46
50
|
|
|
51
|
+
checks.push(await systemNetworkRouteCheck({ runFixedInternal, classifyError: classifyOperationalError, context }));
|
|
52
|
+
|
|
47
53
|
const probe = join(runtimeDir, `.diagnostic-${process.pid}-${randomBytes(6).toString("hex")}`);
|
|
48
54
|
try {
|
|
49
55
|
await writeFile(probe, "ok\n", { mode: 0o600, flag: "wx" });
|
|
@@ -59,7 +65,7 @@ export async function diagnoseRuntime({
|
|
|
59
65
|
const direct = await runProcess(
|
|
60
66
|
process.execPath,
|
|
61
67
|
["-e", "process.stdout.write('ok')"],
|
|
62
|
-
|
|
68
|
+
RUNTIME_DIAGNOSTIC_PROCESS_TIMEOUT_MS,
|
|
63
69
|
true,
|
|
64
70
|
1024,
|
|
65
71
|
context,
|
|
@@ -75,7 +81,7 @@ export async function diagnoseRuntime({
|
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
if (policy.execMode === "shell") {
|
|
78
|
-
const result = await probeShell(context)
|
|
84
|
+
const result = await probeShell(context, RUNTIME_DIAGNOSTIC_PROCESS_TIMEOUT_MS)
|
|
79
85
|
.catch((error) => ({ code: 127, error_class: classifyOperationalError(error) }));
|
|
80
86
|
checks.push({
|
|
81
87
|
layer: "local-shell",
|
|
@@ -103,6 +109,7 @@ export async function diagnoseRuntime({
|
|
|
103
109
|
tool_call_blocked_before_response: "host/platform or connector gateway",
|
|
104
110
|
diagnostic_reached_daemon_but_spawn_failed: "local OS, endpoint security, shell configuration, or Machine Bridge policy",
|
|
105
111
|
system_network_stack_scope: "application proxy selection only; an operating-system VPN or TUN may still intercept the relay connection",
|
|
112
|
+
tunnel_default_route_detected: "the operating-system route is carried by a VPN/TUN; node selection and repair remain outside Machine Bridge",
|
|
106
113
|
managed_job_accepted_then_later_tools_blocked: "job continues independently; inspect with local CLI or a later read_job call",
|
|
107
114
|
},
|
|
108
115
|
policy,
|
package/src/local/runtime.mjs
CHANGED
|
@@ -334,7 +334,20 @@ export class LocalRuntime {
|
|
|
334
334
|
this.relayResumeSessionId = 0;
|
|
335
335
|
return true;
|
|
336
336
|
}
|
|
337
|
-
if (message.type === "pong")
|
|
337
|
+
if (message.type === "pong") {
|
|
338
|
+
this.relayCallRecovery.pulse();
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
if (message.type === "tool_result_ack") {
|
|
342
|
+
if (!isRelayReadyContext(relayContext, this.relay)
|
|
343
|
+
|| typeof message.id !== "string"
|
|
344
|
+
|| !/^call_[A-Za-z0-9_-]{8,240}$/.test(message.id)) {
|
|
345
|
+
this.handleRelayProtocolViolation("invalid_tool_result_ack");
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
this.relayCallRecovery.acknowledge(message.id);
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
338
351
|
if (message.type === "error") {
|
|
339
352
|
this.relay?.handleServerError(message);
|
|
340
353
|
return true;
|
|
@@ -516,7 +529,8 @@ export class LocalRuntime {
|
|
|
516
529
|
runtimeDir: this.runtimeDir,
|
|
517
530
|
workspace: this.workspace,
|
|
518
531
|
runProcess: (...args) => this.runProcess(...args),
|
|
519
|
-
|
|
532
|
+
runFixedInternal: (...args) => this.processExecutionService.runFixedInternal(...args),
|
|
533
|
+
probeShell: (callContext, timeoutMs) => this.processExecutionService.probeShell(callContext, timeoutMs),
|
|
520
534
|
managedJobManager: this.managedJobManager,
|
|
521
535
|
relayStatus: () => this.relay?.status?.() || null,
|
|
522
536
|
throwIfCancelled: (callContext) => this.throwIfCancelled(callContext),
|
|
@@ -533,7 +547,7 @@ export class LocalRuntime {
|
|
|
533
547
|
agentContextManager: this.agentContextManager,
|
|
534
548
|
appAutomationManager: this.appAutomationManager,
|
|
535
549
|
capabilityObserver: this.capabilityObserver,
|
|
536
|
-
policy: this.
|
|
550
|
+
policy: this.effectivePolicy(context),
|
|
537
551
|
}, args, context);
|
|
538
552
|
}
|
|
539
553
|
|
|
@@ -542,7 +556,7 @@ export class LocalRuntime {
|
|
|
542
556
|
agentContextManager: this.agentContextManager,
|
|
543
557
|
appAutomationManager: this.appAutomationManager,
|
|
544
558
|
capabilityObserver: this.capabilityObserver,
|
|
545
|
-
policy: this.
|
|
559
|
+
policy: this.effectivePolicy(context),
|
|
546
560
|
}, args, context);
|
|
547
561
|
}
|
|
548
562
|
|