mtech-ai-local-runner 0.1.29 → 0.1.31
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/README.md +23 -2
- package/dist/src/adapters/claude-code/adapter.js +13 -0
- package/dist/src/adapters/codex/adapter.js +16 -0
- package/dist/src/adapters/codex/exec-launch-host.js +37 -1
- package/dist/src/cli.js +23 -0
- package/dist/src/config.js +4 -4
- package/dist/src/daemon/execution-coordinator.js +74 -2
- package/dist/src/daemon/runtime.js +18 -2
- package/dist/src/gateway-client.js +67 -0
- package/dist/src/platform/macos/launchd.js +13 -2
- package/dist/src/process/node-process-group-host.js +36 -1
- package/dist/src/publication/candidate.js +95 -0
- package/dist/src/publication/execution-host.js +55 -0
- package/dist/src/publication/git-credential-broker.js +150 -0
- package/dist/src/publication/git-credential-helper.js +29 -0
- package/dist/src/publication/git-snapshot.js +321 -0
- package/dist/src/publication/git-transport.js +156 -0
- package/dist/src/publication/installed-host.js +90 -0
- package/dist/src/publication/isolated-verifier.js +67 -0
- package/dist/src/publication/journal.js +87 -0
- package/dist/src/publication/production-host.js +151 -0
- package/dist/src/publication/project-registration.js +49 -0
- package/dist/src/publication/publisher.js +166 -0
- package/dist/src/publication/recovery.js +53 -0
- package/dist/src/publication/runtime-release.js +91 -0
- package/dist/src/safety/command-policy-core.js +124 -0
- package/dist/src/safety/command-policy.js +1 -124
- package/dist/src/safety/job-tool-policy.js +9 -0
- package/dist/src/safety/local-read-observations.js +17 -0
- package/dist/src/safety/local-tool-policy.js +1 -1
- package/dist/src/safety/local-tool-server.js +36 -1
- package/dist/src/workspace/workspace-manager.js +14 -0
- package/package.json +3 -2
- package/verification/README.md +63 -0
- package/verification/runner-policy.cjs +24 -0
package/README.md
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
## 0.1.
|
|
1
|
+
## 0.1.30: governed local alert investigation evidence
|
|
2
|
+
|
|
3
|
+
The Gateway candidate offers explicit Local Codex Runner or Local Claude Runner
|
|
4
|
+
selection for read-only Sentry investigation. It binds the chosen adapter/model
|
|
5
|
+
and employee device to the exact approved launch; it never falls back to another
|
|
6
|
+
adapter. Each adapter retains authenticated host-owned bounded read observations
|
|
7
|
+
through completion and cleanup. These counts are employee-runner evidence, not
|
|
8
|
+
independent root-cause verification.
|
|
9
|
+
|
|
10
|
+
This release does not remove Claude's current unsupported host-admission gate.
|
|
11
|
+
An installed or authenticated CLI alone is not execution permission. Gateway
|
|
12
|
+
deployment, feature activation and actual execution approvals remain separate.
|
|
13
|
+
|
|
14
|
+
## 0.1.29: reviewed daemon Codex admission
|
|
2
15
|
|
|
3
16
|
This candidate connects the installed, hash-pinned root policy observer to the
|
|
4
17
|
existing employee-privileged Codex CLI adapter. The observer accepts only its
|
|
@@ -35,7 +48,15 @@ npx mtech-ai-local-runner setup
|
|
|
35
48
|
|
|
36
49
|
The package stores safe runner configuration locally and keeps OAuth refresh credentials in macOS Keychain when available.
|
|
37
50
|
|
|
38
|
-
Public npm installation requires no npm account or publishing OTP. Setup requires
|
|
51
|
+
Public npm installation requires no npm account or publishing OTP. Setup requires
|
|
52
|
+
the employee's own Gateway OAuth login and a supported, authenticated selected
|
|
53
|
+
CLI. Codex uses the employee's ChatGPT account. Claude requires its own login
|
|
54
|
+
and a separately verified host admission; it is currently not admitted.
|
|
55
|
+
|
|
56
|
+
For the current unattended Codex pilot, an administrator must separately install
|
|
57
|
+
the reviewed, employee-UID-bound policy observer on that Mac. The npm setup
|
|
58
|
+
command does not install that privileged service. A fresh employee computer is
|
|
59
|
+
not ready merely because npm installation succeeds.
|
|
39
60
|
|
|
40
61
|
## 0.1.28: completion receipt wire compatibility
|
|
41
62
|
|
|
@@ -8,6 +8,7 @@ import { completionEvent } from "./completion-report.js";
|
|
|
8
8
|
import { requireJobSecurity } from "../../safety/job-security-policy.js";
|
|
9
9
|
import { HOOK_POLICY_ENV_VAR, buildHookPolicy, encodeHookPolicy } from "../../safety/hook-policy-builder.js";
|
|
10
10
|
import { formatHookCommand } from "../../safety/hook-command.js";
|
|
11
|
+
import { localEndpointReadObservations } from '../../safety/local-tool-server.js';
|
|
11
12
|
const ALLOWED_ENV_KEYS = ["PATH", "HOME", "USER", "LOGNAME", "LANG", "LC_ALL", "TMPDIR", "NODE_ENV", "CLAUDE_CONFIG_DIR"];
|
|
12
13
|
const ALLOWED_SUPERVISOR_COMMANDS = new Set(["continue", "answer_question", "request_revision", "request_checkpoint", "submit_for_review"]);
|
|
13
14
|
const LOCAL_NAMES = ["local_read", "local_search", "local_edit", "local_test"].map(name => `mcp__ai_local__${name}`);
|
|
@@ -44,7 +45,16 @@ export class ClaudeCodeAdapter {
|
|
|
44
45
|
cancelled = false;
|
|
45
46
|
started = false;
|
|
46
47
|
receipt;
|
|
48
|
+
toolEndpoint;
|
|
49
|
+
readSnapshot;
|
|
47
50
|
securityReceipt() { return this.receipt ? structuredClone(this.receipt) : undefined; }
|
|
51
|
+
localReadObservations() {
|
|
52
|
+
const endpoint = this.toolEndpoint;
|
|
53
|
+
const observed = endpoint ? localEndpointReadObservations(endpoint.url, endpoint.token) : undefined;
|
|
54
|
+
if (observed)
|
|
55
|
+
this.readSnapshot = observed;
|
|
56
|
+
return this.readSnapshot ? structuredClone(this.readSnapshot) : undefined;
|
|
57
|
+
}
|
|
48
58
|
constructor(options) {
|
|
49
59
|
this.options = options;
|
|
50
60
|
}
|
|
@@ -97,6 +107,7 @@ export class ClaudeCodeAdapter {
|
|
|
97
107
|
if (this.cancelled)
|
|
98
108
|
throw new Error("adapter startup cancelled");
|
|
99
109
|
const endpoint = await this.tools.start();
|
|
110
|
+
this.toolEndpoint = endpoint;
|
|
100
111
|
if (this.cancelled)
|
|
101
112
|
throw new Error("adapter startup cancelled");
|
|
102
113
|
const mcpConfigPath = this.options.writeLocalMcpConfig(endpoint);
|
|
@@ -222,6 +233,8 @@ export class ClaudeCodeAdapter {
|
|
|
222
233
|
await this.cleanup();
|
|
223
234
|
}
|
|
224
235
|
async cleanup() {
|
|
236
|
+
this.localReadObservations();
|
|
237
|
+
this.toolEndpoint = undefined;
|
|
225
238
|
const tools = this.tools;
|
|
226
239
|
const handle = this.handle;
|
|
227
240
|
this.tools = undefined;
|
|
@@ -19,6 +19,7 @@ export class CodexAdapter {
|
|
|
19
19
|
started = false;
|
|
20
20
|
receipt;
|
|
21
21
|
terminalAudit;
|
|
22
|
+
localReadObservations() { return this.usingOfficialExec ? this.ownedExec?.localReadObservations() : undefined; }
|
|
22
23
|
securityReceipt() { return this.terminalAudit ? structuredClone(this.terminalAudit) : this.usingOfficialExec ? this.ownedExec?.receipt() : this.receipt ? structuredClone(this.receipt) : undefined; }
|
|
23
24
|
cancelled = false;
|
|
24
25
|
usingOfficialExec = false;
|
|
@@ -184,6 +185,21 @@ export class CodexAdapter {
|
|
|
184
185
|
await this.cancel();
|
|
185
186
|
}
|
|
186
187
|
}
|
|
188
|
+
async freezeForPublication() {
|
|
189
|
+
if (!this.usingOfficialExec || !this.ownedExec)
|
|
190
|
+
throw Error('CODEX_PUBLICATION_FREEZE_UNCONFIRMED');
|
|
191
|
+
let timer;
|
|
192
|
+
try {
|
|
193
|
+
await Promise.race([Promise.all([this.ownedExec.freezeForPublication(), this.cancel()]), new Promise((_, reject) => { timer = setTimeout(() => reject(Error('CODEX_PUBLICATION_FREEZE_UNCONFIRMED')), 10000); })]);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
throw Error('CODEX_PUBLICATION_FREEZE_UNCONFIRMED');
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
if (timer)
|
|
200
|
+
clearTimeout(timer);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
187
203
|
async cancel() {
|
|
188
204
|
this.cancelled = true;
|
|
189
205
|
try {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { performance } from 'node:perf_hooks';
|
|
2
2
|
import { PreparationClock, CODEX_PREPARATION_BUDGET_MS } from '../../safety/preparation-timing.js';
|
|
3
|
-
import { localEndpointObservations } from '../../safety/local-tool-server.js';
|
|
3
|
+
import { localEndpointObservations, localEndpointReadObservations } from '../../safety/local-tool-server.js';
|
|
4
4
|
import { processStartupObservations, freezeProcessLaunchObservation } from '../../process/node-process-group-host.js';
|
|
5
5
|
import { executionError } from '../../safety/execution-failure.js';
|
|
6
6
|
import { UNMANAGED_EXEC_MODE } from './unmanaged-scope.js';
|
|
@@ -24,6 +24,7 @@ export class CodexExecLaunchHost {
|
|
|
24
24
|
handle;
|
|
25
25
|
inspection;
|
|
26
26
|
exitAudit;
|
|
27
|
+
readAudit;
|
|
27
28
|
inspectionFingerprint;
|
|
28
29
|
handshake;
|
|
29
30
|
receiptValue;
|
|
@@ -233,9 +234,44 @@ export class CodexExecLaunchHost {
|
|
|
233
234
|
catch {
|
|
234
235
|
return undefined;
|
|
235
236
|
} }
|
|
237
|
+
localReadObservations() {
|
|
238
|
+
if (!this.stopped) {
|
|
239
|
+
try {
|
|
240
|
+
const token = requireOfficialExecPlan(this.plan).env.AI_GATEWAY_JOB_MCP_TOKEN;
|
|
241
|
+
const current = token ? localEndpointReadObservations(this.plan.endpoint, token) : undefined;
|
|
242
|
+
if (current)
|
|
243
|
+
this.readAudit = current;
|
|
244
|
+
}
|
|
245
|
+
catch { /* Absent observation is never interpreted as successful reads. */ }
|
|
246
|
+
}
|
|
247
|
+
return this.readAudit ? structuredClone(this.readAudit) : undefined;
|
|
248
|
+
}
|
|
249
|
+
/** A kill request is not an exit proof. Publication alone requires the trusted
|
|
250
|
+
* process-group host to observe real exit, closed pipes and group disappearance.
|
|
251
|
+
* This deadline bounds teardown only, never model execution duration. */
|
|
252
|
+
async freezeForPublication() {
|
|
253
|
+
let timer;
|
|
254
|
+
try {
|
|
255
|
+
if (!this.ownsAttempt || !this.handle?.waitForOwnedExit)
|
|
256
|
+
throw fail();
|
|
257
|
+
const handle = this.handle;
|
|
258
|
+
await Promise.race([
|
|
259
|
+
Promise.all([this.cancel(), handle.waitForOwnedExit(10000)]),
|
|
260
|
+
new Promise((_, reject) => { timer = setTimeout(() => reject(fail()), 10000); }),
|
|
261
|
+
]);
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
throw Error('CODEX_PUBLICATION_FREEZE_UNCONFIRMED');
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
if (timer)
|
|
268
|
+
clearTimeout(timer);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
236
271
|
cancel() {
|
|
237
272
|
if (this.closePromise)
|
|
238
273
|
return this.closePromise;
|
|
274
|
+
this.localReadObservations();
|
|
239
275
|
this.stopped = true;
|
|
240
276
|
this.receiptValue = undefined;
|
|
241
277
|
if (!this.ownsAttempt) {
|
package/dist/src/cli.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { recoverLocalFixPublication } from './publication/recovery.js';
|
|
3
|
+
import { loadInstalledLocalFixHost } from './publication/installed-host.js';
|
|
4
|
+
import { FileDaemonStateStore } from './state-store.js';
|
|
5
|
+
import { ProcessLock } from './daemon/process-lock.js';
|
|
2
6
|
import { runManualCodexJob } from './daemon/manual-codex.js';
|
|
3
7
|
import { unlinkSync, realpathSync } from "node:fs";
|
|
4
8
|
import { join } from "node:path";
|
|
@@ -228,6 +232,25 @@ export async function main(argv, env, io = {}) {
|
|
|
228
232
|
const credentialStore = new FileOAuthCredentialStore(config.stateRoot);
|
|
229
233
|
const oauthClient = new OAuthClient({ resource: local.gatewayMcpUrl, fetchFn });
|
|
230
234
|
const tokenProvider = new OAuthTokenProvider({ store: credentialStore, oauthClient, resource: local.gatewayMcpUrl, tokenOverride: env.AI_GATEWAY_LOCAL_RUNNER_TOKEN, allowRefresh: config.mode !== 'manual-codex', clock: () => Date.now() });
|
|
235
|
+
if (config.mode === 'publication-recover') {
|
|
236
|
+
const lock = new ProcessLock(join(config.stateRoot, 'daemon.lock'));
|
|
237
|
+
await lock.acquire();
|
|
238
|
+
try {
|
|
239
|
+
const gateway = new McpGatewayClient({ mcpUrl: local.gatewayMcpUrl, runnerId: local.runnerId, tokenProvider, fetchFn });
|
|
240
|
+
const host = await loadInstalledLocalFixHost({ stateRoot: config.stateRoot, runnerId: local.runnerId, gateway });
|
|
241
|
+
if (!host)
|
|
242
|
+
throw new Error('LOCAL_FIX_INSTALLATION_REQUIRED');
|
|
243
|
+
const state = new FileDaemonStateStore(config.stateRoot), stored = await state.load();
|
|
244
|
+
if (!stored)
|
|
245
|
+
throw new Error('LOCAL_PUBLICATION_RECOVERY_STATE_REQUIRED');
|
|
246
|
+
await recoverLocalFixPublication({ jobId: config.manualJobId, stored: stored, host, gateway, state });
|
|
247
|
+
stdout(JSON.stringify({ status: 'completed', jobId: config.manualJobId, modelStarted: false }));
|
|
248
|
+
return 0;
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
await lock.release();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
231
254
|
if (config.mode === "doctor") {
|
|
232
255
|
const adapters = doctorAdapters;
|
|
233
256
|
const localChecks = Promise.allSettled([
|
package/dist/src/config.js
CHANGED
|
@@ -6,11 +6,11 @@ export function isExecutionConfig(config) {
|
|
|
6
6
|
return config.mode === "shadow" || config.mode === "once";
|
|
7
7
|
}
|
|
8
8
|
export function isServiceConfig(config) {
|
|
9
|
-
return ["setup", "doctor", "run", "start", "stop", "status", "uninstall", "manual-codex"].includes(config.mode);
|
|
9
|
+
return ["setup", "doctor", "run", "start", "stop", "status", "uninstall", "manual-codex", "publication-recover"].includes(config.mode);
|
|
10
10
|
}
|
|
11
11
|
const EXECUTION_MODES = ["shadow", "once"];
|
|
12
12
|
const AUTH_MODES = ["login", "logout"];
|
|
13
|
-
const SERVICE_MODES = ["setup", "doctor", "run", "start", "stop", "status", "uninstall", "manual-codex"];
|
|
13
|
+
const SERVICE_MODES = ["setup", "doctor", "run", "start", "stop", "status", "uninstall", "manual-codex", "publication-recover"];
|
|
14
14
|
const ALL_MODES = [...EXECUTION_MODES, ...AUTH_MODES, ...SERVICE_MODES];
|
|
15
15
|
export const DEFAULT_GATEWAY_MCP_URL = "https://cloud.dev.service.ai-gateway.laravelvapor.org/api/v1/web/ai-gateway/mcp";
|
|
16
16
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -101,9 +101,9 @@ export function loadConfig(argv, env) {
|
|
|
101
101
|
const stateRoot = defaultStateRootFromEnv(env);
|
|
102
102
|
if (SERVICE_MODES.includes(mode)) {
|
|
103
103
|
let manualJobId;
|
|
104
|
-
if (mode === 'manual-codex') {
|
|
104
|
+
if (mode === 'manual-codex' || mode === 'publication-recover') {
|
|
105
105
|
if (rest.length !== 1 || !rest[0].startsWith('--job-id=') || !UUID_PATTERN.test(rest[0].slice(9)))
|
|
106
|
-
throw new Error(
|
|
106
|
+
throw new Error(`${mode} requires exactly --job-id=<uuid>`);
|
|
107
107
|
manualJobId = rest[0].slice(9).toLowerCase();
|
|
108
108
|
}
|
|
109
109
|
return {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PublicationOutcomeUnknownError } from '../publication/publisher.js';
|
|
1
2
|
import { executionError, executionFailure } from '../safety/execution-failure.js';
|
|
2
3
|
import { requireSecurityRequirements } from '../safety/security-requirements.js';
|
|
3
4
|
import { GatewayToolError } from "../gateway-client.js";
|
|
@@ -42,6 +43,8 @@ function immutable(value) {
|
|
|
42
43
|
}
|
|
43
44
|
class PreparationDeadlineError extends Error {
|
|
44
45
|
}
|
|
46
|
+
class LocalFixHostRequired extends Error {
|
|
47
|
+
}
|
|
45
48
|
export class ExecutionCoordinator {
|
|
46
49
|
deps;
|
|
47
50
|
constructor(deps) {
|
|
@@ -53,6 +56,10 @@ export class ExecutionCoordinator {
|
|
|
53
56
|
let workspace;
|
|
54
57
|
let adapter;
|
|
55
58
|
let binding;
|
|
59
|
+
let fixSession;
|
|
60
|
+
let localFixPublication;
|
|
61
|
+
const isLocalFix = job.taskPackage.local_fix_scope !== undefined;
|
|
62
|
+
const lifecycle = { frozen: false, current: true };
|
|
56
63
|
const deadline = Date.now() + (this.deps.preparationTimeoutMs ?? 900_000);
|
|
57
64
|
const step = async (operation) => {
|
|
58
65
|
const remaining = deadline - Date.now();
|
|
@@ -71,6 +78,8 @@ export class ExecutionCoordinator {
|
|
|
71
78
|
};
|
|
72
79
|
try {
|
|
73
80
|
const pkg = job.taskPackage;
|
|
81
|
+
if (isLocalFix && (!this.deps.localFixHost || pkg.repository?.access !== "write"))
|
|
82
|
+
throw new LocalFixHostRequired();
|
|
74
83
|
const repository = pkg.repository;
|
|
75
84
|
const adapterId = text(pkg.runtime_adapter, "runtime adapter");
|
|
76
85
|
const adapterVersion = text(pkg.runtime_adapter_version, "runtime adapter version");
|
|
@@ -85,6 +94,7 @@ export class ExecutionCoordinator {
|
|
|
85
94
|
repositoryKey: text(repository?.key, "repository key"),
|
|
86
95
|
providerUrl: text(repository?.provider_url, "repository provider URL"),
|
|
87
96
|
baseCommitSha: text(repository?.base_commit_sha, "repository SHA"),
|
|
97
|
+
...(isLocalFix ? { workingBranch: text(repository?.working_branch, "approved fix branch") } : {})
|
|
88
98
|
}));
|
|
89
99
|
stage = "adapter";
|
|
90
100
|
adapter = await step(() => this.deps.registry.select(adapterId, adapterVersion, { worktreePath: workspace.path, jobId: job.id }));
|
|
@@ -98,9 +108,13 @@ export class ExecutionCoordinator {
|
|
|
98
108
|
stage = "binding";
|
|
99
109
|
await this.deps.state.save({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, phase: "binding" });
|
|
100
110
|
binding = await step(() => this.deps.gateway.createBinding({ ...job, checkoutPath: workspace.path }));
|
|
111
|
+
if (isLocalFix)
|
|
112
|
+
fixSession = await step(() => this.deps.localFixHost.prepare({ job, checkoutPath: workspace.path, bindingId: binding.bindingId, lifecycle: { isFrozen: () => lifecycle.frozen, isCurrent: () => lifecycle.current, securityReceipt: () => adapter.securityReceipt?.() } }));
|
|
101
113
|
}
|
|
102
114
|
catch (error) {
|
|
103
115
|
const diagnostic = { stage, code: "PREPARATION_FAILED" };
|
|
116
|
+
if (error instanceof LocalFixHostRequired)
|
|
117
|
+
diagnostic.code = "LOCAL_FIX_HOST_REQUIRED";
|
|
104
118
|
if (error instanceof PreparationDeadlineError)
|
|
105
119
|
diagnostic.code = "PREPARATION_DEADLINE_EXCEEDED";
|
|
106
120
|
if (error instanceof GatewayToolError) {
|
|
@@ -141,6 +155,7 @@ export class ExecutionCoordinator {
|
|
|
141
155
|
if (controlsStopped)
|
|
142
156
|
return;
|
|
143
157
|
if (["cancel", "pause", "request_replan"].includes(command.command)) {
|
|
158
|
+
lifecycle.current = false;
|
|
144
159
|
requestedTerminal = { eventType: command.command === "cancel" ? "cancelled" : "blocked", summary: `supervisor ${command.command} acknowledged` };
|
|
145
160
|
await adapter.cancel();
|
|
146
161
|
}
|
|
@@ -167,6 +182,7 @@ export class ExecutionCoordinator {
|
|
|
167
182
|
};
|
|
168
183
|
const heartbeatTimer = this.deps.gateway.heartbeat || this.deps.gateway.readCommands ? setInterval(() => {
|
|
169
184
|
void (async () => { await this.deps.gateway.heartbeat?.(binding.bindingId, job); await readControls(); })().catch(async (error) => {
|
|
185
|
+
lifecycle.current = false;
|
|
170
186
|
heartbeatFailure = executionError(error, 'heartbeat', 'EXECUTION_FAILED');
|
|
171
187
|
await adapter.cancel().catch(() => undefined);
|
|
172
188
|
});
|
|
@@ -181,9 +197,22 @@ export class ExecutionCoordinator {
|
|
|
181
197
|
let payload = event.payload;
|
|
182
198
|
if (isTerminalEventType(event.eventType)) {
|
|
183
199
|
executionStage = 'terminal_evidence';
|
|
184
|
-
const evidence = this.deps.terminalEvidenceCollector.collect({ checkoutPath: workspace.path, baseSha: workspace.baseCommitSha });
|
|
200
|
+
const evidence = localFixPublication && typeof localFixPublication.diff_hash === 'string' && typeof localFixPublication.changed_file_manifest_hash === 'string' ? { diffHash: localFixPublication.diff_hash, changedFileManifestHash: localFixPublication.changed_file_manifest_hash } : this.deps.terminalEvidenceCollector.collect({ checkoutPath: workspace.path, baseSha: workspace.baseCommitSha });
|
|
185
201
|
payload = { ...payload, diff_hash: evidence.diffHash, changed_file_manifest_hash: evidence.changedFileManifestHash };
|
|
186
202
|
}
|
|
203
|
+
// A vendor report cannot supply host observations, even when its shape looks valid.
|
|
204
|
+
if (payload) {
|
|
205
|
+
payload = { ...payload };
|
|
206
|
+
delete payload.local_read_observations;
|
|
207
|
+
delete payload.local_fix_publication;
|
|
208
|
+
}
|
|
209
|
+
if (event.eventType === "completed" && localFixPublication)
|
|
210
|
+
payload = { ...payload, local_fix_publication: structuredClone(localFixPublication) };
|
|
211
|
+
if (isTerminalEventType(event.eventType) || event.eventType === 'blocked' || event.eventType === 'scope_conflict') {
|
|
212
|
+
const reads = safeLocalReadObservations(adapter.localReadObservations?.());
|
|
213
|
+
if (reads)
|
|
214
|
+
payload = { ...payload, local_read_observations: reads };
|
|
215
|
+
}
|
|
187
216
|
const normalized = {
|
|
188
217
|
protocolVersion: PROTOCOL_VERSION, bindingId: binding.bindingId,
|
|
189
218
|
sequence: sequence + 1, eventType: event.eventType,
|
|
@@ -226,6 +255,40 @@ export class ExecutionCoordinator {
|
|
|
226
255
|
await controlsInFlight;
|
|
227
256
|
if (requestedTerminal)
|
|
228
257
|
break;
|
|
258
|
+
if (event.eventType === "completed" && fixSession) {
|
|
259
|
+
// Stop only this owned model before running verified host publication.
|
|
260
|
+
// Controls/heartbeat remain active throughout the publication phase.
|
|
261
|
+
if (this.deps.localFixHost?.requiresOwnedFreeze) {
|
|
262
|
+
if (!adapter.freezeForPublication)
|
|
263
|
+
throw new Error('LOCAL_FIX_OWNED_FREEZE_UNAVAILABLE');
|
|
264
|
+
await adapter.freezeForPublication();
|
|
265
|
+
}
|
|
266
|
+
else
|
|
267
|
+
await adapter.cancel();
|
|
268
|
+
lifecycle.frozen = true;
|
|
269
|
+
await readControls();
|
|
270
|
+
if (requestedTerminal || heartbeatFailure)
|
|
271
|
+
break;
|
|
272
|
+
executionStage = 'local_fix_publication';
|
|
273
|
+
await this.deps.state.save({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, phase: 'publication_pending', bindingId: binding.bindingId, eventCursor: sequence, workspaceManifest: workspace.manifest, publicationRecovery: { version: 1, job, checkoutPath: workspace.path, bindingId: binding.bindingId, completedEvent: event, gatewayContext: this.deps.gateway.publicationRecoveryContext?.(binding.bindingId), securityReceipt: adapter.securityReceipt?.(), freezeConfirmed: lifecycle.frozen } });
|
|
274
|
+
localFixPublication = await fixSession.finalize();
|
|
275
|
+
if (this.deps.gateway.syncPublicationCursor) {
|
|
276
|
+
try {
|
|
277
|
+
const cursor = await this.deps.gateway.syncPublicationCursor(binding.bindingId, sequence);
|
|
278
|
+
if (!Number.isSafeInteger(cursor) || cursor < sequence)
|
|
279
|
+
throw new Error('Invalid event cursor');
|
|
280
|
+
sequence = cursor;
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
throw new PublicationOutcomeUnknownError();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (controlsInFlight)
|
|
287
|
+
await controlsInFlight;
|
|
288
|
+
if (requestedTerminal || heartbeatFailure)
|
|
289
|
+
break;
|
|
290
|
+
executionStage = 'event_stream';
|
|
291
|
+
}
|
|
229
292
|
controlsStopped = true;
|
|
230
293
|
}
|
|
231
294
|
await append(event);
|
|
@@ -251,6 +314,11 @@ export class ExecutionCoordinator {
|
|
|
251
314
|
}
|
|
252
315
|
}
|
|
253
316
|
catch (error) {
|
|
317
|
+
if (error instanceof PublicationOutcomeUnknownError) {
|
|
318
|
+
// The provider may already have committed/pushed/opened a PR. Do not
|
|
319
|
+
// release this run as failed or let restart begin another model session.
|
|
320
|
+
throw new Error('Local publication outcome requires reconciliation');
|
|
321
|
+
}
|
|
254
322
|
await adapter.cancel().catch(() => undefined);
|
|
255
323
|
// Do not complete a job whose terminal evidence was not acknowledged.
|
|
256
324
|
// A safe bounded reason prevents raw adapter/transport errors leaking.
|
|
@@ -268,13 +336,16 @@ export class ExecutionCoordinator {
|
|
|
268
336
|
controlsStopped = true;
|
|
269
337
|
if (heartbeatTimer)
|
|
270
338
|
clearInterval(heartbeatTimer);
|
|
339
|
+
lifecycle.current = false;
|
|
340
|
+
await fixSession?.close();
|
|
271
341
|
}
|
|
272
342
|
const status = terminal.eventType === "completed" ? "completed" : terminal.eventType === "cancelled" ? "cancelled" : terminal.eventType === "blocked" || terminal.eventType === "scope_conflict" ? "blocked" : "failed";
|
|
273
343
|
const diagnostic = status === "failed" || terminal.eventType === "blocked" ? completionReportDiagnostic(terminal.payload?.completion_report) : undefined;
|
|
274
344
|
const reportDiagnostic = diagnostic && (terminal.eventType === "blocked"
|
|
275
345
|
? diagnostic.code === "CODEX_COMPLETION_REPORT_BLOCKED" : diagnostic.code !== "CODEX_COMPLETION_REPORT_BLOCKED") ? diagnostic : undefined;
|
|
276
346
|
const securityReceipt = adapter.securityReceipt?.();
|
|
277
|
-
const
|
|
347
|
+
const reads = safeLocalReadObservations(adapter.localReadObservations?.());
|
|
348
|
+
const completionRequest = structuredClone({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, status, result: { binding_id: binding.bindingId, event_type: terminal.eventType, summary: terminal.summary, ...(localFixPublication ? { local_fix_publication: structuredClone(localFixPublication) } : {}), ...(reads ? { local_read_observations: reads } : {}), ...(failureDiagnostic ? { execution_failure: { ...failureDiagnostic } } : {}), ...(reportDiagnostic ? { completion_report: reportDiagnostic } : {}), ...(securityReceipt ? { security_receipt: securityReceipt } : {}) } });
|
|
278
349
|
await this.deps.state.save({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken,
|
|
279
350
|
phase: "terminal_pending", bindingId: binding.bindingId, terminalEventType: terminal.eventType,
|
|
280
351
|
completionRequest: structuredClone(completionRequest) });
|
|
@@ -282,3 +353,4 @@ export class ExecutionCoordinator {
|
|
|
282
353
|
await this.deps.state.clear();
|
|
283
354
|
}
|
|
284
355
|
}
|
|
356
|
+
import { safeLocalReadObservations } from '../safety/local-read-observations.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { loadInstalledLocalFixHost } from '../publication/installed-host.js';
|
|
1
2
|
import { createCodexDaemonHost } from '../safety/codex-daemon-host.js';
|
|
2
3
|
import { reconcileTerminalState } from './terminal-recovery.js';
|
|
3
4
|
import { packageVersion } from '../package-version.js';
|
|
@@ -26,6 +27,7 @@ export async function buildDaemon(input) {
|
|
|
26
27
|
const production = !input.codexSecurityHost && !input.codexSupportedProbe ? createCodexDaemonHost({ runnerId: input.runnerId, binary: input.env.AI_GATEWAY_LOCAL_RUNNER_CODEX_BINARY ?? 'codex', home: input.env.HOME ?? '', codexHome: input.env.CODEX_HOME, searchPath: input.env.PATH }) : undefined;
|
|
27
28
|
let pendingCodexClaim;
|
|
28
29
|
const gateway = new McpGatewayClient({ mcpUrl: input.gatewayMcpUrl, tokenProvider: input.tokenProvider, runnerId: input.runnerId, fetchFn: input.fetchFn });
|
|
30
|
+
const localFixHost = input.localFixHost ?? await loadInstalledLocalFixHost({ stateRoot: input.stateRoot, runnerId: input.runnerId, gateway });
|
|
29
31
|
// The same parts the CLI self-test ran before starting this daemon (verifyPackagedHook).
|
|
30
32
|
const hook = packagedHookCommandParts();
|
|
31
33
|
const plugin = new ClaudeCodePlugin({
|
|
@@ -45,6 +47,7 @@ export async function buildDaemon(input) {
|
|
|
45
47
|
const versions = Object.fromEntries(probes.filter((probe) => probe.available && probe.authenticated).map((probe) => [probe.id, probe.version]));
|
|
46
48
|
const state = new FileDaemonStateStore(input.stateRoot);
|
|
47
49
|
const coordinator = new ExecutionCoordinator({
|
|
50
|
+
localFixHost,
|
|
48
51
|
reportExecutionFailure: (diagnostic) => { process.stderr.write(`${JSON.stringify({ event: "execution_failed", ...diagnostic })}\n`); },
|
|
49
52
|
reportPreparationFailure: (diagnostic) => { process.stderr.write(`${JSON.stringify({ event: "preparation_failed", ...diagnostic })}\n`); },
|
|
50
53
|
terminalEvidenceCollector: buildTerminalEvidenceCollector(),
|
|
@@ -56,6 +59,8 @@ export async function buildDaemon(input) {
|
|
|
56
59
|
jobId: job.id, generation: job.generation, jobLeaseToken: job.leaseToken,
|
|
57
60
|
launchGrant: job.launchGrant, workLoopId: job.workLoopId, checkoutPath: job.checkoutPath,
|
|
58
61
|
}),
|
|
62
|
+
publicationRecoveryContext: bindingId => gateway.exportLocalPublicationRecoveryContext(bindingId),
|
|
63
|
+
syncPublicationCursor: (bindingId, cursor) => gateway.syncLocalPublicationCursor(bindingId, cursor),
|
|
59
64
|
readCommands: (bindingId, cursor) => gateway.readCommands(bindingId, cursor),
|
|
60
65
|
acknowledgeCommand: (bindingId, sequence) => gateway.acknowledgeCommand(bindingId, sequence),
|
|
61
66
|
appendEvent: (bindingId, event) => gateway.appendEvent(bindingId, event),
|
|
@@ -102,8 +107,7 @@ export async function buildDaemon(input) {
|
|
|
102
107
|
await gateway.heartbeatLocalRunner({
|
|
103
108
|
deviceFingerprint: createHash("sha256").update(`${process.getuid?.() ?? 0}|${runnerDisplayName()}|${input.gatewayMcpUrl}`).digest("hex"),
|
|
104
109
|
displayName: runnerDisplayName(), packageVersion: packageVersion(),
|
|
105
|
-
capabilities: probes
|
|
106
|
-
capabilities: probe.capabilities, authenticated: probe.available && probe.authenticated })),
|
|
110
|
+
capabilities: localRunnerCapabilities(probes, !!localFixHost),
|
|
107
111
|
});
|
|
108
112
|
},
|
|
109
113
|
wait: async () => {
|
|
@@ -146,6 +150,8 @@ export async function buildDaemon(input) {
|
|
|
146
150
|
export function runnerDisplayName() { return `${hostname()} local AI`; }
|
|
147
151
|
/** Unknown mutation outcomes halt polling; known failures replay only their terminal receipt. */
|
|
148
152
|
export async function reconcilePreparationState(previous, deps) {
|
|
153
|
+
if (["publication_pending", "publication_terminal_pending"].includes(previous.phase ?? ""))
|
|
154
|
+
throw new Error("Publication outcome requires reconciliation before runner polling");
|
|
149
155
|
if (previous.phase === "binding" || previous.phase === "binding_uncertain") {
|
|
150
156
|
throw new Error("Binding outcome requires reconciliation before runner polling");
|
|
151
157
|
}
|
|
@@ -159,3 +165,13 @@ export async function reconcilePreparationState(previous, deps) {
|
|
|
159
165
|
await deps.clear();
|
|
160
166
|
return true;
|
|
161
167
|
}
|
|
168
|
+
/** Vendor capability text cannot claim a host-owned publication facility. */
|
|
169
|
+
export function localRunnerCapabilities(probes, trustedLocalFixHost = false) {
|
|
170
|
+
return probes.map(probe => {
|
|
171
|
+
const authenticated = probe.available && probe.authenticated;
|
|
172
|
+
const capabilities = probe.capabilities.filter(value => value !== 'local_fix_publication_v1');
|
|
173
|
+
if (trustedLocalFixHost && authenticated && probe.id === 'codex_cli')
|
|
174
|
+
capabilities.push('local_fix_publication_v1');
|
|
175
|
+
return { adapter: probe.id, version: probe.version, authenticated, capabilities };
|
|
176
|
+
});
|
|
177
|
+
}
|
|
@@ -480,6 +480,73 @@ export class McpGatewayClient {
|
|
|
480
480
|
resolvedModelId: requiredString(result.binding.resolved_model_id, "resolved model ID"),
|
|
481
481
|
};
|
|
482
482
|
}
|
|
483
|
+
/** Account for the server-authored verification event before appending a
|
|
484
|
+
* terminal event. Never infer a cursor from model output or a local counter. */
|
|
485
|
+
async syncLocalPublicationCursor(bindingId, afterSequence) {
|
|
486
|
+
const page = await this.callTool('agent_runtime_events_read', {
|
|
487
|
+
binding_id: bindingId, after_sequence: afterSequence, limit: 100,
|
|
488
|
+
});
|
|
489
|
+
const events = page.events ?? [];
|
|
490
|
+
if (page.terminal || events.length === 0 || events.length >= 100)
|
|
491
|
+
throw new Error('LOCAL_PUBLICATION_EVENT_CURSOR_REJECTED');
|
|
492
|
+
let cursor = afterSequence, verified = false;
|
|
493
|
+
for (const event of events) {
|
|
494
|
+
if (!Number.isSafeInteger(event.sequence) || event.sequence !== cursor + 1 || isTerminalEventType(String(event.event_type)))
|
|
495
|
+
throw new Error('LOCAL_PUBLICATION_EVENT_CURSOR_REJECTED');
|
|
496
|
+
cursor = event.sequence;
|
|
497
|
+
const proof = event.payload?.local_publication;
|
|
498
|
+
if (event.event_type === 'test_result' && event.stage === 'local_publication_verification' && typeof proof?.gateway_mac === 'string' && /^[a-f0-9]{64}$/.test(proof.gateway_mac))
|
|
499
|
+
verified = true;
|
|
500
|
+
}
|
|
501
|
+
if (!verified)
|
|
502
|
+
throw new Error('LOCAL_PUBLICATION_EVENT_CURSOR_REJECTED');
|
|
503
|
+
return cursor;
|
|
504
|
+
}
|
|
505
|
+
/** Host-only publication envelope. This client is never a model MCP relay. */
|
|
506
|
+
localPublicationFence(input) {
|
|
507
|
+
return { job_id: input.jobId, generation: input.generation, lease_token: input.leaseToken,
|
|
508
|
+
launch_grant: input.launchGrant, binding_id: input.bindingId, runner_id: this.runnerId };
|
|
509
|
+
}
|
|
510
|
+
exportLocalPublicationRecoveryContext(bindingId) {
|
|
511
|
+
const c = this.requireContext(bindingId);
|
|
512
|
+
return structuredClone({ ...c, bundle: { contract: { content_fingerprint: requiredString(nested(c.bundle, 'contract.content_fingerprint'), 'contract fingerprint') } } });
|
|
513
|
+
}
|
|
514
|
+
async restoreLocalPublicationRecoveryContext(input, stored, terminalReplay = false) {
|
|
515
|
+
if (terminalReplay) {
|
|
516
|
+
if (stored.bindingId !== input.bindingId || typeof stored.leaseToken !== 'string' || !stored.leaseToken)
|
|
517
|
+
throw new Error('LOCAL_PUBLICATION_RECOVERY_CONTEXT_REJECTED');
|
|
518
|
+
this.context = structuredClone(stored);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const current = (await this.localPublicationContext(input)).context;
|
|
522
|
+
if (stored.bindingId !== input.bindingId || stored.workLoopId !== current.work_loop_id || nested(stored.bundle, 'contract.content_fingerprint') !== current.contract_fingerprint || typeof stored.leaseToken !== 'string' || !stored.leaseToken)
|
|
523
|
+
throw new Error('LOCAL_PUBLICATION_RECOVERY_CONTEXT_REJECTED');
|
|
524
|
+
this.context = structuredClone(stored);
|
|
525
|
+
}
|
|
526
|
+
async localPublicationContext(input) {
|
|
527
|
+
return this.callTool('local_agent_publication_context', this.localPublicationFence(input));
|
|
528
|
+
}
|
|
529
|
+
async executeLocalPublicationPr(input) {
|
|
530
|
+
return this.callTool('local_agent_publication_execute_pr', { ...this.localPublicationFence(input), publication_id: input.publicationId, signed_request: input.signedRequest, ...(input.canonicalApprovalId ? { canonical_approval_id: input.canonicalApprovalId } : {}) });
|
|
531
|
+
}
|
|
532
|
+
async authorizeLocalPublication(input) {
|
|
533
|
+
const receipt = { ...input.securityReceipt };
|
|
534
|
+
if (receipt.transport === 'official_exec_v1' && ['cli-exec-job-security-receipt-v2', 'cli-exec-job-security-receipt-v3'].includes(String(receipt.receipt_version))) {
|
|
535
|
+
delete receipt.preparation_timing;
|
|
536
|
+
delete receipt.launch_process;
|
|
537
|
+
}
|
|
538
|
+
return this.callTool('local_agent_publication_authorize', { ...this.localPublicationFence(input),
|
|
539
|
+
candidate: input.candidate, test_receipts: input.testReceipts, security_receipt: receipt, host_receipt: input.hostReceipt });
|
|
540
|
+
}
|
|
541
|
+
async issueLocalPublicationAction(input) {
|
|
542
|
+
return this.callTool('local_agent_publication_issue_action', { ...this.localPublicationFence(input),
|
|
543
|
+
publication_id: input.publicationId, action: input.action, commit_message: input.commitMessage,
|
|
544
|
+
pull_request_title: input.pullRequestTitle, pull_request_body: input.pullRequestBody });
|
|
545
|
+
}
|
|
546
|
+
async acceptLocalPublicationResult(input) {
|
|
547
|
+
return this.callTool('local_agent_publication_accept_result', { ...this.localPublicationFence(input),
|
|
548
|
+
publication_id: input.publicationId, action: input.action, result: input.result });
|
|
549
|
+
}
|
|
483
550
|
async completeLocalJob(input) {
|
|
484
551
|
let result = input.result;
|
|
485
552
|
const receipt = result.security_receipt;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chmod, mkdir, rename, writeFile } from "node:fs/promises";
|
|
1
|
+
import { access, chmod, mkdir, rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
4
|
import { promisify } from "node:util";
|
|
@@ -11,6 +11,7 @@ const defaultRun = async (file, args) => {
|
|
|
11
11
|
const result = await execFileAsync(file, args, { encoding: "utf8" });
|
|
12
12
|
return { stdout: result.stdout, stderr: result.stderr };
|
|
13
13
|
};
|
|
14
|
+
const defaultExists = async (path) => access(path).then(() => true, () => false);
|
|
14
15
|
const defaultWriteAtomic = async (path, contents, mode) => {
|
|
15
16
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
16
17
|
const temporary = `${path}.${process.pid}.tmp`;
|
|
@@ -47,8 +48,18 @@ export class LaunchdService {
|
|
|
47
48
|
await (this.deps.run ?? defaultRun)("/bin/launchctl", ["bootout", `gui/${this.deps.uid}/${LAUNCHD_LABEL}`])
|
|
48
49
|
.catch(() => undefined);
|
|
49
50
|
}
|
|
51
|
+
// `stop` boots the agent out of launchd, and kickstart cannot start a service launchd no
|
|
52
|
+
// longer knows, so start loads the installed plist again first. It never writes a plist:
|
|
53
|
+
// a missing one means setup has not run.
|
|
50
54
|
async start() {
|
|
51
|
-
|
|
55
|
+
const run = this.deps.run ?? defaultRun;
|
|
56
|
+
if (!(await this.status()).loaded) {
|
|
57
|
+
if (!(await (this.deps.exists ?? defaultExists)(this.plistPath))) {
|
|
58
|
+
throw new Error(`The runner service is not installed (${this.plistPath} is missing). Run npx mtech-ai-local-runner setup.`);
|
|
59
|
+
}
|
|
60
|
+
await run("/bin/launchctl", ["bootstrap", `gui/${this.deps.uid}`, this.plistPath]);
|
|
61
|
+
}
|
|
62
|
+
await run("/bin/launchctl", ["kickstart", "-k", `gui/${this.deps.uid}/${LAUNCHD_LABEL}`]);
|
|
52
63
|
}
|
|
53
64
|
async status() {
|
|
54
65
|
try {
|
|
@@ -31,6 +31,10 @@ export class NodeProcessGroupHost {
|
|
|
31
31
|
});
|
|
32
32
|
child.stdin.once('finish', () => observations.add('host_stdin_finished'));
|
|
33
33
|
let failure;
|
|
34
|
+
let realExit = false, closed = false;
|
|
35
|
+
let closedResolve;
|
|
36
|
+
const processClosed = new Promise(resolve => { closedResolve = resolve; });
|
|
37
|
+
child.once('close', () => { closed = true; closedResolve(); });
|
|
34
38
|
let owned = true;
|
|
35
39
|
let finished = false, consumed = false, queuedBytes = 0, wake;
|
|
36
40
|
const lines = [];
|
|
@@ -119,6 +123,7 @@ export class NodeProcessGroupHost {
|
|
|
119
123
|
child.stderr.once('end', () => { observations.add('host_stderr_ended'); for (const marker of markers)
|
|
120
124
|
marker.prefix = ''; });
|
|
121
125
|
child.once("exit", (code, signal) => {
|
|
126
|
+
realExit = true;
|
|
122
127
|
observations.add('host_process_exited');
|
|
123
128
|
try {
|
|
124
129
|
kill();
|
|
@@ -131,7 +136,7 @@ export class NodeProcessGroupHost {
|
|
|
131
136
|
// Drain from spawn, not from the first adapter event read: official exec can
|
|
132
137
|
// emit startup events before it initializes MCP, while the adapter awaits
|
|
133
138
|
// that initialization before consuming events. Bound the pre-consumer queue.
|
|
134
|
-
|
|
139
|
+
const stdoutDrained = (async () => {
|
|
135
140
|
let pending = Buffer.alloc(0);
|
|
136
141
|
const enqueue = (line) => {
|
|
137
142
|
if (line.length > 256_000)
|
|
@@ -212,6 +217,36 @@ export class NodeProcessGroupHost {
|
|
|
212
217
|
}
|
|
213
218
|
} },
|
|
214
219
|
kill: (signal) => kill((signal ?? "SIGKILL")),
|
|
220
|
+
waitForOwnedExit: async (timeoutMs) => {
|
|
221
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 10000)
|
|
222
|
+
throw Error('OWNED_PROCESS_EXIT_UNCONFIRMED');
|
|
223
|
+
const deadline = Date.now() + timeoutMs;
|
|
224
|
+
let timer;
|
|
225
|
+
try {
|
|
226
|
+
await Promise.race([Promise.all([processClosed, stdoutDrained]), new Promise((_, reject) => { timer = setTimeout(() => reject(Error('OWNED_PROCESS_EXIT_UNCONFIRMED')), timeoutMs); })]);
|
|
227
|
+
if (!realExit || !closed || !child.pid)
|
|
228
|
+
throw Error('OWNED_PROCESS_EXIT_UNCONFIRMED');
|
|
229
|
+
// Inspect only the exact process group created by this spawn. Never
|
|
230
|
+
// scan processes or signal an unrelated PID/group during verification.
|
|
231
|
+
for (;;) {
|
|
232
|
+
try {
|
|
233
|
+
process.kill(-child.pid, 0);
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
if (error.code === 'ESRCH')
|
|
237
|
+
return;
|
|
238
|
+
throw Error('OWNED_PROCESS_EXIT_UNCONFIRMED');
|
|
239
|
+
}
|
|
240
|
+
if (Date.now() >= deadline)
|
|
241
|
+
throw Error('OWNED_PROCESS_EXIT_UNCONFIRMED');
|
|
242
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
if (timer)
|
|
247
|
+
clearTimeout(timer);
|
|
248
|
+
}
|
|
249
|
+
},
|
|
215
250
|
once: (_event, callback) => { if (termination) {
|
|
216
251
|
const ended = termination;
|
|
217
252
|
queueMicrotask(() => callback(ended.code, ended.signal));
|