mtech-ai-local-runner 0.1.18 → 0.1.20
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 +31 -0
- package/dist/src/adapters/codex/exec-launch-host.js +32 -8
- package/dist/src/daemon/execution-coordinator.js +1 -1
- package/dist/src/process/node-process-group-host.js +21 -1
- package/dist/src/process/owned-launch-observer.js +132 -0
- package/dist/src/safety/execution-failure.js +15 -2
- package/dist/src/safety/preparation-timing.js +37 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,37 @@ The package stores safe runner configuration locally and keeps OAuth refresh cre
|
|
|
10
10
|
|
|
11
11
|
Public npm installation requires no npm account or publishing OTP. Setup requires Gateway OAuth login, and local Claude Code must already be authenticated (`claude auth status`).
|
|
12
12
|
|
|
13
|
+
## 0.1.20: distinguish owned wrapper and target startup
|
|
14
|
+
|
|
15
|
+
Add bounded, read-only process-name/state/wait hints for the exact owned
|
|
16
|
+
wrapper PID. At most seven one-second queries run during preparation and stop
|
|
17
|
+
at admission, failure, cancellation or exit. Fixed categories exclude raw names,
|
|
18
|
+
paths, process IDs, arguments and environment. Cancellation hard-stops only its
|
|
19
|
+
owned query. Local logging receives a deep diagnostic copy so it cannot mutate
|
|
20
|
+
the evidence later reported to Gateway.
|
|
21
|
+
|
|
22
|
+
These are direct-child hints, not executable identity, native function or
|
|
23
|
+
historical cause proof; forked descendants are not inspected. Preparation
|
|
24
|
+
deadlines, authenticated MCP/tool gates and unlimited admitted employee work
|
|
25
|
+
remain unchanged. Installation does not update the daemon or run Codex.
|
|
26
|
+
See [owned launch observation](https://bitbucket.org/mtechbal/api-cloud-service-ai-gateway/src/development/docs/ai-ops-gateway/codex-owned-launch-observation.md).
|
|
27
|
+
|
|
28
|
+
## 0.1.19: bounded preparation phase timing
|
|
29
|
+
|
|
30
|
+
Record fixed host milestones with monotonic elapsed milliseconds and remaining
|
|
31
|
+
original 30-second budget: inspection, before-spawn checks, spawn request/return,
|
|
32
|
+
prompt write/end request/return, MCP wait/discovery and authorization/failure.
|
|
33
|
+
Safe timing survives cleanup into local diagnostics, failed events and terminal
|
|
34
|
+
results; successful admission also retains it in the security receipt. No prompts,
|
|
35
|
+
arguments, paths, URLs, credentials or raw provider text are included.
|
|
36
|
+
|
|
37
|
+
This separates pre-spawn budget consumption from post-spawn MCP waiting. Spawn
|
|
38
|
+
return and pipe-call return do not prove native start or stdin consumption, and
|
|
39
|
+
these timings do not establish the historical native wait's cause. The deadline,
|
|
40
|
+
tool/identity checks and unlimited admitted employee work are unchanged. No
|
|
41
|
+
sampler, extra permission, retry, real account run or daemon update is implied.
|
|
42
|
+
See [phase timing and remaining conditions](https://bitbucket.org/mtechbal/api-cloud-service-ai-gateway/src/development/docs/ai-ops-gateway/codex-preparation-phase-timing.md).
|
|
43
|
+
|
|
13
44
|
## 0.1.18: preparation deadline enforcement
|
|
14
45
|
|
|
15
46
|
The overall 30-second preparation budget also covers release inspection. A
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { performance } from 'node:perf_hooks';
|
|
2
|
+
import { PreparationClock, CODEX_PREPARATION_BUDGET_MS } from '../../safety/preparation-timing.js';
|
|
2
3
|
import { localEndpointObservations } from '../../safety/local-tool-server.js';
|
|
3
|
-
import { processStartupObservations } from '../../process/node-process-group-host.js';
|
|
4
|
+
import { processStartupObservations, freezeProcessLaunchObservation } from '../../process/node-process-group-host.js';
|
|
4
5
|
import { executionError } from '../../safety/execution-failure.js';
|
|
5
6
|
import { UNMANAGED_EXEC_MODE } from './unmanaged-scope.js';
|
|
6
7
|
import { fingerprint as taskFingerprint } from '../../daemon/execution-coordinator.js';
|
|
@@ -11,7 +12,6 @@ import { officialExecObservationLimits } from './exec-observer.js';
|
|
|
11
12
|
import { EXEC_GATES, assessExecPreflight, requireExecHandshake } from './exec-preflight.js';
|
|
12
13
|
import { EXEC_MODE, isReleaseExecMode, EXEC_TRANSPORT, assertOfficialExecPlanAvailable, claimOfficialExecPlan, requireOfficialExecPlan } from './exec-invocation.js';
|
|
13
14
|
const fail = () => new Error('CODEX_EXEC_PREFLIGHT_REJECTED');
|
|
14
|
-
const PREPARATION_TIMEOUT_MS = 30_000;
|
|
15
15
|
/** No default native inspector, handshake observer or OS process host is created.
|
|
16
16
|
* Missing per-execution evidence blocks before spawn. Synthetic callbacks grant
|
|
17
17
|
* only the fake host used by tests, never a compiled production release entry. */
|
|
@@ -70,11 +70,14 @@ export class CodexExecLaunchHost {
|
|
|
70
70
|
let interruption;
|
|
71
71
|
const mcpDetail = () => stage === 'codex_mcp' ? this.deps.execTools?.discoveryDetail() : undefined;
|
|
72
72
|
const observations = () => { const invocation = requireOfficialExecPlan(this.plan); return [...(this.handle ? processStartupObservations(this.handle) : []), ...localEndpointObservations(this.plan.endpoint, invocation.env.AI_GATEWAY_JOB_MCP_TOKEN)]; };
|
|
73
|
-
const
|
|
73
|
+
const launchObservation = () => this.handle ? freezeProcessLaunchObservation(this.handle) : undefined;
|
|
74
|
+
const timing = new PreparationClock();
|
|
75
|
+
timing.mark('preparation_start');
|
|
76
|
+
const interrupt = (code) => { timing.mark('preparation_failed'); interruption ??= executionError(fail(), stage, code, mcpDetail(), observations(), timing.snapshot(), launchObservation()); };
|
|
74
77
|
// Timer callbacks cannot interrupt synchronous native metadata checks. Keep
|
|
75
78
|
// the same total budget and enforce it at effect boundaries with a monotonic
|
|
76
79
|
// clock, rather than spawning/submitting after an already expired inspection.
|
|
77
|
-
const deadline =
|
|
80
|
+
const deadline = timing.deadline;
|
|
78
81
|
const assertPreparationBudget = () => { if (performance.now() >= deadline) {
|
|
79
82
|
interrupt('CODEX_EXEC_PREPARATION_TIMEOUT');
|
|
80
83
|
throw interruption;
|
|
@@ -85,11 +88,13 @@ export class CodexExecLaunchHost {
|
|
|
85
88
|
requireOfficialExecPlan(this.plan);
|
|
86
89
|
if (typeof prompt !== 'string' || !prompt || prompt.length > 1_000_000 || signal?.aborted)
|
|
87
90
|
throw fail();
|
|
88
|
-
const stopped = new Promise((_, reject) => { external = () => { interrupt('CODEX_EXEC_ABORTED'); void this.cancel().catch(() => { }); reject(interruption); }; this.abort.signal.addEventListener('abort', () => reject(interruption ?? fail()), { once: true }); signal?.addEventListener('abort', external, { once: true }); timer = setTimeout(() => { interrupt('CODEX_EXEC_PREPARATION_TIMEOUT'); external(); },
|
|
91
|
+
const stopped = new Promise((_, reject) => { external = () => { interrupt('CODEX_EXEC_ABORTED'); void this.cancel().catch(() => { }); reject(interruption); }; this.abort.signal.addEventListener('abort', () => reject(interruption ?? fail()), { once: true }); signal?.addEventListener('abort', external, { once: true }); timer = setTimeout(() => { interrupt('CODEX_EXEC_PREPARATION_TIMEOUT'); external(); }, CODEX_PREPARATION_BUDGET_MS); });
|
|
89
92
|
const preparation = (async () => {
|
|
90
93
|
const supported = isReleaseExecMode(this.plan.mode);
|
|
91
94
|
stage = 'codex_inspection';
|
|
95
|
+
timing.mark('inspection_start');
|
|
92
96
|
this.inspection = await (supported ? this.deps.inspectRelease?.(this.plan, this.abort.signal) : this.deps.inspect?.(this.plan, this.abort.signal));
|
|
97
|
+
timing.mark('inspection_complete');
|
|
93
98
|
assertPreparationBudget();
|
|
94
99
|
const assessment = supported ? assessReleaseExec(this.inspection?.snapshot, this.plan) : assessExecPreflight(this.inspection?.snapshot, this.plan);
|
|
95
100
|
if (!supported && !this.deps.inspect)
|
|
@@ -104,11 +109,15 @@ export class CodexExecLaunchHost {
|
|
|
104
109
|
if (manual && typeof this.inspection.beforeSpawn !== 'function')
|
|
105
110
|
throw fail();
|
|
106
111
|
assertPreparationBudget();
|
|
112
|
+
timing.mark('before_spawn_start');
|
|
107
113
|
this.inspection.beforeSpawn?.();
|
|
114
|
+
timing.mark('before_spawn_complete');
|
|
108
115
|
assertPreparationBudget();
|
|
109
116
|
stage = 'codex_spawn';
|
|
117
|
+
timing.mark('spawn_requested');
|
|
110
118
|
const h = this.deps.processHost.spawn(invocation.binary, invocation.args, { cwd: this.plan.cwd, env: invocation.env });
|
|
111
119
|
this.handle = h;
|
|
120
|
+
timing.mark('spawn_returned');
|
|
112
121
|
if (this.stopped) {
|
|
113
122
|
h.kill('SIGKILL');
|
|
114
123
|
throw fail();
|
|
@@ -141,15 +150,21 @@ export class CodexExecLaunchHost {
|
|
|
141
150
|
// Vendor exec consumes stdin before it can initialize its required MCP.
|
|
142
151
|
// Prompt submission may cause provider/account effects; do not call this no-model.
|
|
143
152
|
assertPreparationBudget();
|
|
153
|
+
timing.mark('prompt_write_requested');
|
|
144
154
|
h.stdin.write(prompt);
|
|
155
|
+
timing.mark('prompt_write_returned');
|
|
145
156
|
this.current();
|
|
146
157
|
assertPreparationBudget();
|
|
158
|
+
timing.mark('prompt_end_requested');
|
|
147
159
|
h.stdin.end();
|
|
160
|
+
timing.mark('prompt_end_returned');
|
|
148
161
|
this.current();
|
|
149
162
|
assertPreparationBudget();
|
|
150
163
|
gate.promptSubmitted(h);
|
|
151
164
|
stage = 'codex_mcp';
|
|
165
|
+
timing.mark('mcp_wait_started');
|
|
152
166
|
const observed = await gate.wait(h, this.abort.signal);
|
|
167
|
+
timing.mark('mcp_discovery_complete');
|
|
153
168
|
this.current();
|
|
154
169
|
assertPreparationBudget();
|
|
155
170
|
requireExecHandshake(observed, h, this.plan);
|
|
@@ -159,10 +174,13 @@ export class CodexExecLaunchHost {
|
|
|
159
174
|
assertPreparationBudget();
|
|
160
175
|
requireExecHandshake(observed, h, this.plan);
|
|
161
176
|
assertPreparationBudget();
|
|
162
|
-
|
|
177
|
+
timing.mark('preparation_authorized');
|
|
178
|
+
this.receiptValue = Object.freeze({ preparation_timing: timing.snapshot(), ...(launchObservation() ? { launch_process: launchObservation() } : {}), receipt_version: this.plan.mode === UNMANAGED_EXEC_MODE ? 'cli-exec-job-security-receipt-v3' : 'cli-exec-job-security-receipt-v2', evidence_origin: 'employee_runner', transport: EXEC_TRANSPORT, security_mode: this.plan.mode, ...(this.plan.mode === UNMANAGED_EXEC_MODE ? { policy_scope: this.inspection.snapshot.policyScope } : {}), phase: 'post_prompt_mcp_authorized', policy_evidence: 'reviewed_release_and_current_inputs', mcp_evidence: 'authenticated_server_discovery', native_policy_observed: false, first_inference_observed: false, execution_nonce: this.plan.executionNonce, invocation_fingerprint: this.plan.invocationFingerprint, ...this.plan.identity });
|
|
163
179
|
return h;
|
|
164
180
|
}
|
|
181
|
+
timing.mark('mcp_wait_started');
|
|
165
182
|
const handshake = await this.deps.handshake?.(h, this.plan, this.abort.signal);
|
|
183
|
+
timing.mark('mcp_discovery_complete');
|
|
166
184
|
this.current();
|
|
167
185
|
assertPreparationBudget();
|
|
168
186
|
requireExecHandshake(handshake, h, this.plan);
|
|
@@ -174,21 +192,27 @@ export class CodexExecLaunchHost {
|
|
|
174
192
|
assertPreparationBudget();
|
|
175
193
|
requireExecHandshake(handshake, h, this.plan);
|
|
176
194
|
assertPreparationBudget();
|
|
195
|
+
timing.mark('prompt_write_requested');
|
|
177
196
|
h.stdin.write(prompt);
|
|
197
|
+
timing.mark('prompt_write_returned');
|
|
178
198
|
this.current();
|
|
179
199
|
assertPreparationBudget();
|
|
200
|
+
timing.mark('prompt_end_requested');
|
|
180
201
|
h.stdin.end();
|
|
202
|
+
timing.mark('prompt_end_returned');
|
|
181
203
|
this.current();
|
|
182
204
|
assertPreparationBudget();
|
|
183
205
|
requireExecHandshake(handshake, h, this.plan);
|
|
184
206
|
assertPreparationBudget();
|
|
185
|
-
|
|
207
|
+
timing.mark('preparation_authorized');
|
|
208
|
+
this.receiptValue = Object.freeze({ ...receipt, preparation_timing: timing.snapshot(), ...(launchObservation() ? { launch_process: launchObservation() } : {}) });
|
|
186
209
|
return h;
|
|
187
210
|
})();
|
|
188
211
|
return await Promise.race([preparation, stopped]);
|
|
189
212
|
}
|
|
190
213
|
catch (error) {
|
|
191
|
-
|
|
214
|
+
timing.mark('preparation_failed');
|
|
215
|
+
const safe = executionError(interruption ?? error, stage, 'CODEX_EXEC_PREFLIGHT_REJECTED', mcpDetail(), observations(), timing.snapshot(), launchObservation());
|
|
192
216
|
await this.cancel().catch(() => { });
|
|
193
217
|
throw safe;
|
|
194
218
|
}
|
|
@@ -256,7 +256,7 @@ export class ExecutionCoordinator {
|
|
|
256
256
|
failureDiagnostic = diagnostic;
|
|
257
257
|
// Log before reporting so an unavailable Gateway does not erase the cause.
|
|
258
258
|
try {
|
|
259
|
-
this.deps.reportExecutionFailure?.(
|
|
259
|
+
this.deps.reportExecutionFailure?.(structuredClone(diagnostic));
|
|
260
260
|
}
|
|
261
261
|
catch { /* logging must not change terminal reconciliation */ }
|
|
262
262
|
terminal = { eventType: "failed", summary: "daemon execution or event reporting failed", payload: { execution_failure: { ...diagnostic } } };
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { OwnedLaunchObserver } from './owned-launch-observer.js';
|
|
1
3
|
const startup = new WeakMap();
|
|
4
|
+
const launchObservers = new WeakMap();
|
|
5
|
+
export function freezeProcessLaunchObservation(handle) { return launchObservers.get(handle)?.freeze(); }
|
|
2
6
|
/** Fixed lifecycle/text observations from this owned host only. Never raw stderr or authority. */
|
|
3
7
|
export function processStartupObservations(handle) { return [...(startup.get(handle) ?? [])]; }
|
|
4
8
|
import { spawn } from "node:child_process";
|
|
@@ -9,7 +13,22 @@ export class NodeProcessGroupHost {
|
|
|
9
13
|
throw new Error("Codex process isolation requires macOS");
|
|
10
14
|
const child = spawn(command, args, { ...options, shell: false, detached: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
11
15
|
const observations = new Set(['host_observer_v2']);
|
|
12
|
-
|
|
16
|
+
let launch;
|
|
17
|
+
child.once('spawn', () => {
|
|
18
|
+
observations.add('host_process_spawned');
|
|
19
|
+
if (command === '/usr/bin/sandbox-exec' && args[0] === '-p' && path.isAbsolute(args[2] ?? '')) {
|
|
20
|
+
try {
|
|
21
|
+
launch = new OwnedLaunchObserver({ pid: child.pid, parentPid: process.pid, wrapperName: path.basename(command), targetName: path.basename(args[2]), alive: () => owned && child.exitCode === null && child.signalCode === null, onSample: sample => {
|
|
22
|
+
observations.add(sample.status === 'observed' ? sample.image === 'wrapper_name_match' ? 'host_os_wrapper_name_seen' : sample.image === 'target_name_match' ? 'host_os_target_name_seen' : 'host_os_other_name_seen' : 'host_os_query_unavailable');
|
|
23
|
+
} });
|
|
24
|
+
launchObservers.set(handle, launch);
|
|
25
|
+
launch.start();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
observations.add('host_os_query_unavailable');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
13
32
|
child.stdin.once('finish', () => observations.add('host_stdin_finished'));
|
|
14
33
|
let failure;
|
|
15
34
|
let owned = true;
|
|
@@ -27,6 +46,7 @@ export class NodeProcessGroupHost {
|
|
|
27
46
|
callback(code, signal);
|
|
28
47
|
};
|
|
29
48
|
const kill = (signal = "SIGKILL") => {
|
|
49
|
+
launch?.stop();
|
|
30
50
|
if (!owned || !child.pid)
|
|
31
51
|
return;
|
|
32
52
|
try {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { performance } from 'node:perf_hooks';
|
|
4
|
+
const issued = new WeakMap();
|
|
5
|
+
export function trustedLaunchObservation(token) {
|
|
6
|
+
const value = token && typeof token === 'object' ? issued.get(token) : undefined;
|
|
7
|
+
return value ? structuredClone(value) : undefined;
|
|
8
|
+
}
|
|
9
|
+
/** Only one exact, directly-owned PID. No argv/environment/descendant scan,
|
|
10
|
+
* stack attachment, credentials, network, or privileged query. */
|
|
11
|
+
async function queryOwnedPid(pid, signal) {
|
|
12
|
+
return new Promise(resolve => {
|
|
13
|
+
let output = '', settled = false;
|
|
14
|
+
let timer;
|
|
15
|
+
let onAbort;
|
|
16
|
+
const finish = (value) => { if (settled)
|
|
17
|
+
return; settled = true; if (timer)
|
|
18
|
+
clearTimeout(timer); if (onAbort)
|
|
19
|
+
signal.removeEventListener('abort', onAbort); resolve(value); };
|
|
20
|
+
try {
|
|
21
|
+
const child = spawn('/bin/ps', ['-p', String(pid), '-o', 'pid=,ppid=,stat=,wchan=,comm='], { shell: false, env: { PATH: '/usr/bin:/bin', LANG: 'C', LC_ALL: 'C' }, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
22
|
+
child.stdout.on('data', (chunk) => { if (settled)
|
|
23
|
+
return; if (Buffer.byteLength(output) + chunk.length > 8192) {
|
|
24
|
+
finish({ status: 'output_invalid' });
|
|
25
|
+
try {
|
|
26
|
+
child.kill('SIGKILL');
|
|
27
|
+
}
|
|
28
|
+
catch { /* owned query already gone */ }
|
|
29
|
+
return;
|
|
30
|
+
} output += chunk.toString('utf8'); });
|
|
31
|
+
child.on('error', () => finish({ status: 'query_failed' }));
|
|
32
|
+
child.once('close', code => finish(code === 0 ? { status: 'ok', output } : { status: 'query_failed' }));
|
|
33
|
+
onAbort = () => { if (settled)
|
|
34
|
+
return; try {
|
|
35
|
+
child.kill('SIGKILL');
|
|
36
|
+
}
|
|
37
|
+
catch { /* owned query already gone */ } finish({ status: 'query_failed' }); };
|
|
38
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
39
|
+
if (signal.aborted) {
|
|
40
|
+
onAbort();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
timer = setTimeout(() => { finish({ status: 'query_timeout' }); try {
|
|
44
|
+
child.kill('SIGKILL');
|
|
45
|
+
}
|
|
46
|
+
catch { /* owned query already gone */ } }, 1000);
|
|
47
|
+
timer.unref();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
finish({ status: 'query_failed' });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** Names/wait symbols are observational hints, never executable identity or a
|
|
55
|
+
* native function attestation. Arbitrary process strings never leave this file. */
|
|
56
|
+
export class OwnedLaunchObserver {
|
|
57
|
+
input;
|
|
58
|
+
origin = performance.now();
|
|
59
|
+
active = false;
|
|
60
|
+
used = false;
|
|
61
|
+
pending = false;
|
|
62
|
+
discarded = false;
|
|
63
|
+
abort = new AbortController();
|
|
64
|
+
timer;
|
|
65
|
+
values = [];
|
|
66
|
+
frozen;
|
|
67
|
+
constructor(input) {
|
|
68
|
+
this.input = input;
|
|
69
|
+
if (!Number.isSafeInteger(input.pid) || input.pid < 1 || !Number.isSafeInteger(input.parentPid) || input.parentPid < 1)
|
|
70
|
+
throw Error('OWNED_LAUNCH_OBSERVER_REJECTED');
|
|
71
|
+
}
|
|
72
|
+
start() { if (this.used || this.frozen)
|
|
73
|
+
return; this.used = true; this.active = true; void this.sample(0); }
|
|
74
|
+
async sample(index) {
|
|
75
|
+
if (!this.active || !this.input.alive())
|
|
76
|
+
return;
|
|
77
|
+
this.pending = true;
|
|
78
|
+
let result;
|
|
79
|
+
try {
|
|
80
|
+
result = await (this.input.query ?? queryOwnedPid)(this.input.pid, this.abort.signal);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
result = { status: 'query_failed' };
|
|
84
|
+
}
|
|
85
|
+
this.pending = false;
|
|
86
|
+
if (!this.active || !this.input.alive())
|
|
87
|
+
return;
|
|
88
|
+
const previous = this.values.at(-1)?.elapsed_ms ?? 0, value = performance.now() - this.origin;
|
|
89
|
+
const elapsed_ms = Math.max(previous, Number.isFinite(value) ? Math.min(2147483647, Math.max(0, Math.floor(value))) : previous);
|
|
90
|
+
const sample = { elapsed_ms, status: result.status === 'ok' ? 'output_invalid' : result.status };
|
|
91
|
+
if (result.status === 'ok' && typeof result.output === 'string' && result.output.length <= 8192) {
|
|
92
|
+
const line = result.output.trim();
|
|
93
|
+
const match = /^(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+([^\r\n\0]+)$/.exec(line);
|
|
94
|
+
if (match) {
|
|
95
|
+
if (Number(match[1]) !== this.input.pid || Number(match[2]) !== this.input.parentPid)
|
|
96
|
+
sample.status = 'owner_mismatch';
|
|
97
|
+
else {
|
|
98
|
+
sample.status = 'observed';
|
|
99
|
+
const name = path.basename(match[5].trim());
|
|
100
|
+
sample.image = name === this.input.wrapperName ? 'wrapper_name_match' : name === this.input.targetName ? 'target_name_match' : 'other';
|
|
101
|
+
const states = { R: 'running', S: 'sleeping', I: 'sleeping', U: 'uninterruptible', D: 'uninterruptible', T: 'stopped', Z: 'zombie' };
|
|
102
|
+
sample.state = states[match[3][0]] ?? 'other';
|
|
103
|
+
const wait = match[4].toLowerCase();
|
|
104
|
+
sample.wait = ['-', '0', '0x0', '??'].includes(wait) ? 'not_reported' : /^(?:__)?psynch(?:_|$)/.test(wait) ? 'psynch' : /^mach_msg(?:_|$)/.test(wait) ? 'mach_message' : /^kevent/.test(wait) ? 'kevent' : /^(?:pipe_?read|piperd)$/.test(wait) ? 'pipe_read' : /^(?:wait4|wait)$/.test(wait) ? 'child_wait' : 'other';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
this.values.push(sample);
|
|
109
|
+
try {
|
|
110
|
+
this.input.onSample?.(structuredClone(sample));
|
|
111
|
+
}
|
|
112
|
+
catch { /* telemetry cannot alter execution */ }
|
|
113
|
+
const schedule = [0, 100, 500, 1500, 5000, 15000, 25000];
|
|
114
|
+
const next = schedule[index + 1];
|
|
115
|
+
if (next !== undefined && this.active) {
|
|
116
|
+
this.timer = setTimeout(() => { void this.sample(index + 1); }, Math.max(0, next - (performance.now() - this.origin)));
|
|
117
|
+
this.timer.unref();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
stop() { this.used = true; if (!this.active)
|
|
121
|
+
return; this.active = false; this.discarded ||= this.pending; if (this.timer)
|
|
122
|
+
clearTimeout(this.timer); this.abort.abort(); }
|
|
123
|
+
freeze() {
|
|
124
|
+
if (!this.frozen) {
|
|
125
|
+
this.stop();
|
|
126
|
+
this.frozen = { version: 'owned-launch-process-v1', collector: 'owned_pid_ps', samples: structuredClone(this.values), pending_query_discarded: this.discarded };
|
|
127
|
+
}
|
|
128
|
+
const token = structuredClone(this.frozen);
|
|
129
|
+
issued.set(token, structuredClone(this.frozen));
|
|
130
|
+
return token;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { trustedLaunchObservation } from '../process/owned-launch-observer.js';
|
|
2
|
+
import { trustedPreparationTiming } from './preparation-timing.js';
|
|
1
3
|
import { GatewayToolError } from '../gateway-client.js';
|
|
2
4
|
/** Observations only: provider text cannot authorize a tool or certify a cause. */
|
|
3
5
|
export const STARTUP_OBSERVATIONS = Object.freeze([
|
|
6
|
+
'host_os_wrapper_name_seen', 'host_os_target_name_seen', 'host_os_other_name_seen', 'host_os_query_unavailable',
|
|
4
7
|
'host_observer_v2', 'host_process_spawned', 'host_process_spawn_failed', 'host_process_exited',
|
|
5
8
|
'host_stdin_write_requested', 'host_stdin_end_requested', 'host_stdin_finished', 'host_stdin_failed',
|
|
6
9
|
'host_stdout_data', 'host_stdout_line', 'host_stdout_ended', 'host_stderr_data', 'host_stderr_ended',
|
|
@@ -15,7 +18,7 @@ export function executionFailure(error, stage, fallback = 'EXECUTION_FAILED') {
|
|
|
15
18
|
if (error && typeof error === 'object') {
|
|
16
19
|
const trusted = issued.get(error);
|
|
17
20
|
if (trusted)
|
|
18
|
-
return
|
|
21
|
+
return structuredClone(trusted);
|
|
19
22
|
try {
|
|
20
23
|
if (error instanceof GatewayToolError && (error.code === 'GATEWAY_TOOL_REJECTED' || error.code === 'GATEWAY_TRANSPORT_ERROR' || error.code === 'WORK_LOOP_CANCELLED')) {
|
|
21
24
|
return { stage, code: error.code };
|
|
@@ -35,7 +38,7 @@ export function executionFailure(error, stage, fallback = 'EXECUTION_FAILED') {
|
|
|
35
38
|
}
|
|
36
39
|
return { stage, code: fallback };
|
|
37
40
|
}
|
|
38
|
-
export function executionError(error, stage, fallback, detail, observations) {
|
|
41
|
+
export function executionError(error, stage, fallback, detail, observations, preparationTiming, launchObservation) {
|
|
39
42
|
const diagnostic = executionFailure(error, stage, fallback);
|
|
40
43
|
// Only trusted callers supply this fixed vocabulary. Serialized provider
|
|
41
44
|
// errors, messages and stderr never become diagnostic authority.
|
|
@@ -46,6 +49,16 @@ export function executionError(error, stage, fallback, detail, observations) {
|
|
|
46
49
|
if (fixed.length)
|
|
47
50
|
diagnostic.observations = fixed;
|
|
48
51
|
}
|
|
52
|
+
if (!diagnostic.preparation_timing) {
|
|
53
|
+
const timing = trustedPreparationTiming(preparationTiming);
|
|
54
|
+
if (timing)
|
|
55
|
+
diagnostic.preparation_timing = timing;
|
|
56
|
+
}
|
|
57
|
+
if (!diagnostic.launch_process) {
|
|
58
|
+
const observed = trustedLaunchObservation(launchObservation);
|
|
59
|
+
if (observed)
|
|
60
|
+
diagnostic.launch_process = observed;
|
|
61
|
+
}
|
|
49
62
|
const safe = new Error(fallback);
|
|
50
63
|
issued.set(safe, Object.freeze(diagnostic));
|
|
51
64
|
return safe;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
2
|
+
export const CODEX_PREPARATION_BUDGET_MS = 30_000;
|
|
3
|
+
const PHASES = [
|
|
4
|
+
'preparation_start', 'inspection_start', 'inspection_complete', 'before_spawn_start', 'before_spawn_complete',
|
|
5
|
+
'spawn_requested', 'spawn_returned', 'prompt_write_requested', 'prompt_write_returned',
|
|
6
|
+
'prompt_end_requested', 'prompt_end_returned', 'mcp_wait_started', 'mcp_discovery_complete',
|
|
7
|
+
'preparation_authorized', 'preparation_failed',
|
|
8
|
+
];
|
|
9
|
+
// Only snapshots issued here are accepted by the error-reporting boundary.
|
|
10
|
+
// JSON/provider/task objects, getters and copied snapshots carry no authority.
|
|
11
|
+
const issued = new WeakMap();
|
|
12
|
+
export function trustedPreparationTiming(value) {
|
|
13
|
+
const data = value && typeof value === 'object' ? issued.get(value) : undefined;
|
|
14
|
+
return data ? structuredClone(data) : undefined;
|
|
15
|
+
}
|
|
16
|
+
/** Fixed, one-shot milestone set: no payloads, wall-clock dates, polling or I/O.
|
|
17
|
+
* Times describe the host boundary, never native stdin consumption or policy. */
|
|
18
|
+
export class PreparationClock {
|
|
19
|
+
started = performance.now();
|
|
20
|
+
deadline = this.started + CODEX_PREPARATION_BUDGET_MS;
|
|
21
|
+
points = [];
|
|
22
|
+
mark(phase) {
|
|
23
|
+
if (!PHASES.includes(phase) || this.points.some(p => p.phase === phase))
|
|
24
|
+
return;
|
|
25
|
+
const previous = this.points.at(-1)?.elapsed_ms ?? 0, elapsed = performance.now() - this.started;
|
|
26
|
+
// Bound serialized numbers and keep telemetry monotonic even on an invalid
|
|
27
|
+
// clock reading. The operational deadline still uses the original clock.
|
|
28
|
+
const elapsed_ms = Math.max(previous, Number.isFinite(elapsed) ? Math.min(2_147_483_647, Math.max(0, Math.floor(elapsed))) : previous);
|
|
29
|
+
this.points.push({ phase, elapsed_ms, remaining_ms: Math.max(0, CODEX_PREPARATION_BUDGET_MS - elapsed_ms) });
|
|
30
|
+
}
|
|
31
|
+
snapshot() {
|
|
32
|
+
const data = { version: 'codex-preparation-timing-v1', budget_ms: CODEX_PREPARATION_BUDGET_MS, points: structuredClone(this.points) };
|
|
33
|
+
const token = structuredClone(data);
|
|
34
|
+
issued.set(token, data);
|
|
35
|
+
return token;
|
|
36
|
+
}
|
|
37
|
+
}
|