mtech-ai-local-runner 0.1.17 → 0.1.19
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
CHANGED
|
@@ -10,6 +10,34 @@ 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.19: bounded preparation phase timing
|
|
14
|
+
|
|
15
|
+
Record fixed host milestones with monotonic elapsed milliseconds and remaining
|
|
16
|
+
original 30-second budget: inspection, before-spawn checks, spawn request/return,
|
|
17
|
+
prompt write/end request/return, MCP wait/discovery and authorization/failure.
|
|
18
|
+
Safe timing survives cleanup into local diagnostics, failed events and terminal
|
|
19
|
+
results; successful admission also retains it in the security receipt. No prompts,
|
|
20
|
+
arguments, paths, URLs, credentials or raw provider text are included.
|
|
21
|
+
|
|
22
|
+
This separates pre-spawn budget consumption from post-spawn MCP waiting. Spawn
|
|
23
|
+
return and pipe-call return do not prove native start or stdin consumption, and
|
|
24
|
+
these timings do not establish the historical native wait's cause. The deadline,
|
|
25
|
+
tool/identity checks and unlimited admitted employee work are unchanged. No
|
|
26
|
+
sampler, extra permission, retry, real account run or daemon update is implied.
|
|
27
|
+
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).
|
|
28
|
+
|
|
29
|
+
## 0.1.18: preparation deadline enforcement
|
|
30
|
+
|
|
31
|
+
The overall 30-second preparation budget also covers release inspection. A
|
|
32
|
+
synchronous metadata check can delay JavaScript timer callbacks. Local guards
|
|
33
|
+
now use a monotonic deadline after inspection and at spawn/input/admission
|
|
34
|
+
boundaries, refusing expired preparation even when its timer has not run. The
|
|
35
|
+
budget is not reset or extended; admitted employee work remains unbounded.
|
|
36
|
+
Fake callbacks reproduce the late-admission defect. Existing 0.1.17 receipts do
|
|
37
|
+
not contain phase timing, so this is not proof of the historical native wait's
|
|
38
|
+
cause or successful real-account connectivity. No new native stack sampler,
|
|
39
|
+
account operation, daemon change or actual CLI run is part of this correction.
|
|
40
|
+
|
|
13
41
|
## 0.1.17: owned startup, pipe and transport observations
|
|
14
42
|
|
|
15
43
|
An owned-process collector marker distinguishes available diagnostics from a
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
2
|
+
import { PreparationClock, CODEX_PREPARATION_BUDGET_MS } from '../../safety/preparation-timing.js';
|
|
1
3
|
import { localEndpointObservations } from '../../safety/local-tool-server.js';
|
|
2
4
|
import { processStartupObservations } from '../../process/node-process-group-host.js';
|
|
3
5
|
import { executionError } from '../../safety/execution-failure.js';
|
|
@@ -68,18 +70,31 @@ export class CodexExecLaunchHost {
|
|
|
68
70
|
let interruption;
|
|
69
71
|
const mcpDetail = () => stage === 'codex_mcp' ? this.deps.execTools?.discoveryDetail() : undefined;
|
|
70
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)]; };
|
|
71
|
-
const
|
|
73
|
+
const timing = new PreparationClock();
|
|
74
|
+
timing.mark('preparation_start');
|
|
75
|
+
const interrupt = (code) => { timing.mark('preparation_failed'); interruption ??= executionError(fail(), stage, code, mcpDetail(), observations(), timing.snapshot()); };
|
|
76
|
+
// Timer callbacks cannot interrupt synchronous native metadata checks. Keep
|
|
77
|
+
// the same total budget and enforce it at effect boundaries with a monotonic
|
|
78
|
+
// clock, rather than spawning/submitting after an already expired inspection.
|
|
79
|
+
const deadline = timing.deadline;
|
|
80
|
+
const assertPreparationBudget = () => { if (performance.now() >= deadline) {
|
|
81
|
+
interrupt('CODEX_EXEC_PREPARATION_TIMEOUT');
|
|
82
|
+
throw interruption;
|
|
83
|
+
} };
|
|
72
84
|
let timer, external;
|
|
73
85
|
try {
|
|
74
86
|
this.assertContext(ctx, jobId);
|
|
75
87
|
requireOfficialExecPlan(this.plan);
|
|
76
88
|
if (typeof prompt !== 'string' || !prompt || prompt.length > 1_000_000 || signal?.aborted)
|
|
77
89
|
throw fail();
|
|
78
|
-
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(); },
|
|
90
|
+
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); });
|
|
79
91
|
const preparation = (async () => {
|
|
80
92
|
const supported = isReleaseExecMode(this.plan.mode);
|
|
81
93
|
stage = 'codex_inspection';
|
|
94
|
+
timing.mark('inspection_start');
|
|
82
95
|
this.inspection = await (supported ? this.deps.inspectRelease?.(this.plan, this.abort.signal) : this.deps.inspect?.(this.plan, this.abort.signal));
|
|
96
|
+
timing.mark('inspection_complete');
|
|
97
|
+
assertPreparationBudget();
|
|
83
98
|
const assessment = supported ? assessReleaseExec(this.inspection?.snapshot, this.plan) : assessExecPreflight(this.inspection?.snapshot, this.plan);
|
|
84
99
|
if (!supported && !this.deps.inspect)
|
|
85
100
|
assessment.reasonCodes.push(...officialExecObservationLimits());
|
|
@@ -92,10 +107,16 @@ export class CodexExecLaunchHost {
|
|
|
92
107
|
const manual = ctx.approvedTaskPackage?.security_requirements?.mdm_verification === 'manual_prelaunch';
|
|
93
108
|
if (manual && typeof this.inspection.beforeSpawn !== 'function')
|
|
94
109
|
throw fail();
|
|
110
|
+
assertPreparationBudget();
|
|
111
|
+
timing.mark('before_spawn_start');
|
|
95
112
|
this.inspection.beforeSpawn?.();
|
|
113
|
+
timing.mark('before_spawn_complete');
|
|
114
|
+
assertPreparationBudget();
|
|
96
115
|
stage = 'codex_spawn';
|
|
116
|
+
timing.mark('spawn_requested');
|
|
97
117
|
const h = this.deps.processHost.spawn(invocation.binary, invocation.args, { cwd: this.plan.cwd, env: invocation.env });
|
|
98
118
|
this.handle = h;
|
|
119
|
+
timing.mark('spawn_returned');
|
|
99
120
|
if (this.stopped) {
|
|
100
121
|
h.kill('SIGKILL');
|
|
101
122
|
throw fail();
|
|
@@ -115,6 +136,7 @@ export class CodexExecLaunchHost {
|
|
|
115
136
|
void this.cancel().catch(() => { });
|
|
116
137
|
});
|
|
117
138
|
this.current();
|
|
139
|
+
assertPreparationBudget();
|
|
118
140
|
if (supported) {
|
|
119
141
|
const gate = this.deps.execTools;
|
|
120
142
|
gate.bind(h, () => { try {
|
|
@@ -126,42 +148,70 @@ export class CodexExecLaunchHost {
|
|
|
126
148
|
} });
|
|
127
149
|
// Vendor exec consumes stdin before it can initialize its required MCP.
|
|
128
150
|
// Prompt submission may cause provider/account effects; do not call this no-model.
|
|
151
|
+
assertPreparationBudget();
|
|
152
|
+
timing.mark('prompt_write_requested');
|
|
129
153
|
h.stdin.write(prompt);
|
|
154
|
+
timing.mark('prompt_write_returned');
|
|
130
155
|
this.current();
|
|
156
|
+
assertPreparationBudget();
|
|
157
|
+
timing.mark('prompt_end_requested');
|
|
131
158
|
h.stdin.end();
|
|
159
|
+
timing.mark('prompt_end_returned');
|
|
132
160
|
this.current();
|
|
161
|
+
assertPreparationBudget();
|
|
133
162
|
gate.promptSubmitted(h);
|
|
134
163
|
stage = 'codex_mcp';
|
|
164
|
+
timing.mark('mcp_wait_started');
|
|
135
165
|
const observed = await gate.wait(h, this.abort.signal);
|
|
166
|
+
timing.mark('mcp_discovery_complete');
|
|
136
167
|
this.current();
|
|
168
|
+
assertPreparationBudget();
|
|
137
169
|
requireExecHandshake(observed, h, this.plan);
|
|
170
|
+
assertPreparationBudget();
|
|
138
171
|
gate.authorize(h);
|
|
139
172
|
this.current();
|
|
173
|
+
assertPreparationBudget();
|
|
140
174
|
requireExecHandshake(observed, h, this.plan);
|
|
141
|
-
|
|
175
|
+
assertPreparationBudget();
|
|
176
|
+
timing.mark('preparation_authorized');
|
|
177
|
+
this.receiptValue = Object.freeze({ preparation_timing: timing.snapshot(), 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 });
|
|
142
178
|
return h;
|
|
143
179
|
}
|
|
180
|
+
timing.mark('mcp_wait_started');
|
|
144
181
|
const handshake = await this.deps.handshake?.(h, this.plan, this.abort.signal);
|
|
182
|
+
timing.mark('mcp_discovery_complete');
|
|
145
183
|
this.current();
|
|
184
|
+
assertPreparationBudget();
|
|
146
185
|
requireExecHandshake(handshake, h, this.plan);
|
|
147
186
|
this.handshake = handshake;
|
|
148
187
|
// An input hash is not a native effective-config certificate. Preserve the
|
|
149
188
|
// evidence origin in the audit receipt and never reuse AppServer authority.
|
|
150
189
|
const receipt = Object.freeze({ receipt_version: 'cli-exec-job-security-receipt-v1', evidence_origin: 'employee_runner', transport: EXEC_TRANSPORT, security_mode: EXEC_MODE, phase: 'exec_pre_prompt', execution_nonce: this.plan.executionNonce, invocation_fingerprint: this.plan.invocationFingerprint, ...this.plan.identity });
|
|
151
190
|
this.current();
|
|
191
|
+
assertPreparationBudget();
|
|
152
192
|
requireExecHandshake(handshake, h, this.plan);
|
|
193
|
+
assertPreparationBudget();
|
|
194
|
+
timing.mark('prompt_write_requested');
|
|
153
195
|
h.stdin.write(prompt);
|
|
196
|
+
timing.mark('prompt_write_returned');
|
|
154
197
|
this.current();
|
|
198
|
+
assertPreparationBudget();
|
|
199
|
+
timing.mark('prompt_end_requested');
|
|
155
200
|
h.stdin.end();
|
|
201
|
+
timing.mark('prompt_end_returned');
|
|
156
202
|
this.current();
|
|
203
|
+
assertPreparationBudget();
|
|
157
204
|
requireExecHandshake(handshake, h, this.plan);
|
|
158
|
-
|
|
205
|
+
assertPreparationBudget();
|
|
206
|
+
timing.mark('preparation_authorized');
|
|
207
|
+
this.receiptValue = Object.freeze({ ...receipt, preparation_timing: timing.snapshot() });
|
|
159
208
|
return h;
|
|
160
209
|
})();
|
|
161
210
|
return await Promise.race([preparation, stopped]);
|
|
162
211
|
}
|
|
163
212
|
catch (error) {
|
|
164
|
-
|
|
213
|
+
timing.mark('preparation_failed');
|
|
214
|
+
const safe = executionError(interruption ?? error, stage, 'CODEX_EXEC_PREFLIGHT_REJECTED', mcpDetail(), observations(), timing.snapshot());
|
|
165
215
|
await this.cancel().catch(() => { });
|
|
166
216
|
throw safe;
|
|
167
217
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { trustedPreparationTiming } from './preparation-timing.js';
|
|
1
2
|
import { GatewayToolError } from '../gateway-client.js';
|
|
2
3
|
/** Observations only: provider text cannot authorize a tool or certify a cause. */
|
|
3
4
|
export const STARTUP_OBSERVATIONS = Object.freeze([
|
|
@@ -15,7 +16,7 @@ export function executionFailure(error, stage, fallback = 'EXECUTION_FAILED') {
|
|
|
15
16
|
if (error && typeof error === 'object') {
|
|
16
17
|
const trusted = issued.get(error);
|
|
17
18
|
if (trusted)
|
|
18
|
-
return
|
|
19
|
+
return structuredClone(trusted);
|
|
19
20
|
try {
|
|
20
21
|
if (error instanceof GatewayToolError && (error.code === 'GATEWAY_TOOL_REJECTED' || error.code === 'GATEWAY_TRANSPORT_ERROR' || error.code === 'WORK_LOOP_CANCELLED')) {
|
|
21
22
|
return { stage, code: error.code };
|
|
@@ -35,7 +36,7 @@ export function executionFailure(error, stage, fallback = 'EXECUTION_FAILED') {
|
|
|
35
36
|
}
|
|
36
37
|
return { stage, code: fallback };
|
|
37
38
|
}
|
|
38
|
-
export function executionError(error, stage, fallback, detail, observations) {
|
|
39
|
+
export function executionError(error, stage, fallback, detail, observations, preparationTiming) {
|
|
39
40
|
const diagnostic = executionFailure(error, stage, fallback);
|
|
40
41
|
// Only trusted callers supply this fixed vocabulary. Serialized provider
|
|
41
42
|
// errors, messages and stderr never become diagnostic authority.
|
|
@@ -46,6 +47,11 @@ export function executionError(error, stage, fallback, detail, observations) {
|
|
|
46
47
|
if (fixed.length)
|
|
47
48
|
diagnostic.observations = fixed;
|
|
48
49
|
}
|
|
50
|
+
if (!diagnostic.preparation_timing) {
|
|
51
|
+
const timing = trustedPreparationTiming(preparationTiming);
|
|
52
|
+
if (timing)
|
|
53
|
+
diagnostic.preparation_timing = timing;
|
|
54
|
+
}
|
|
49
55
|
const safe = new Error(fallback);
|
|
50
56
|
issued.set(safe, Object.freeze(diagnostic));
|
|
51
57
|
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
|
+
}
|