mtech-ai-local-runner 0.1.18 → 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,22 @@ 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
+
13
29
  ## 0.1.18: preparation deadline enforcement
14
30
 
15
31
  The overall 30-second preparation budget also covers release inspection. A
@@ -1,4 +1,5 @@
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
4
  import { processStartupObservations } from '../../process/node-process-group-host.js';
4
5
  import { executionError } from '../../safety/execution-failure.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,13 @@ 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 interrupt = (code) => { interruption ??= executionError(fail(), stage, code, mcpDetail(), observations()); };
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()); };
74
76
  // Timer callbacks cannot interrupt synchronous native metadata checks. Keep
75
77
  // the same total budget and enforce it at effect boundaries with a monotonic
76
78
  // clock, rather than spawning/submitting after an already expired inspection.
77
- const deadline = performance.now() + PREPARATION_TIMEOUT_MS;
79
+ const deadline = timing.deadline;
78
80
  const assertPreparationBudget = () => { if (performance.now() >= deadline) {
79
81
  interrupt('CODEX_EXEC_PREPARATION_TIMEOUT');
80
82
  throw interruption;
@@ -85,11 +87,13 @@ export class CodexExecLaunchHost {
85
87
  requireOfficialExecPlan(this.plan);
86
88
  if (typeof prompt !== 'string' || !prompt || prompt.length > 1_000_000 || signal?.aborted)
87
89
  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(); }, PREPARATION_TIMEOUT_MS); });
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); });
89
91
  const preparation = (async () => {
90
92
  const supported = isReleaseExecMode(this.plan.mode);
91
93
  stage = 'codex_inspection';
94
+ timing.mark('inspection_start');
92
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');
93
97
  assertPreparationBudget();
94
98
  const assessment = supported ? assessReleaseExec(this.inspection?.snapshot, this.plan) : assessExecPreflight(this.inspection?.snapshot, this.plan);
95
99
  if (!supported && !this.deps.inspect)
@@ -104,11 +108,15 @@ export class CodexExecLaunchHost {
104
108
  if (manual && typeof this.inspection.beforeSpawn !== 'function')
105
109
  throw fail();
106
110
  assertPreparationBudget();
111
+ timing.mark('before_spawn_start');
107
112
  this.inspection.beforeSpawn?.();
113
+ timing.mark('before_spawn_complete');
108
114
  assertPreparationBudget();
109
115
  stage = 'codex_spawn';
116
+ timing.mark('spawn_requested');
110
117
  const h = this.deps.processHost.spawn(invocation.binary, invocation.args, { cwd: this.plan.cwd, env: invocation.env });
111
118
  this.handle = h;
119
+ timing.mark('spawn_returned');
112
120
  if (this.stopped) {
113
121
  h.kill('SIGKILL');
114
122
  throw fail();
@@ -141,15 +149,21 @@ export class CodexExecLaunchHost {
141
149
  // Vendor exec consumes stdin before it can initialize its required MCP.
142
150
  // Prompt submission may cause provider/account effects; do not call this no-model.
143
151
  assertPreparationBudget();
152
+ timing.mark('prompt_write_requested');
144
153
  h.stdin.write(prompt);
154
+ timing.mark('prompt_write_returned');
145
155
  this.current();
146
156
  assertPreparationBudget();
157
+ timing.mark('prompt_end_requested');
147
158
  h.stdin.end();
159
+ timing.mark('prompt_end_returned');
148
160
  this.current();
149
161
  assertPreparationBudget();
150
162
  gate.promptSubmitted(h);
151
163
  stage = 'codex_mcp';
164
+ timing.mark('mcp_wait_started');
152
165
  const observed = await gate.wait(h, this.abort.signal);
166
+ timing.mark('mcp_discovery_complete');
153
167
  this.current();
154
168
  assertPreparationBudget();
155
169
  requireExecHandshake(observed, h, this.plan);
@@ -159,10 +173,13 @@ export class CodexExecLaunchHost {
159
173
  assertPreparationBudget();
160
174
  requireExecHandshake(observed, h, this.plan);
161
175
  assertPreparationBudget();
162
- this.receiptValue = Object.freeze({ 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 });
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 });
163
178
  return h;
164
179
  }
180
+ timing.mark('mcp_wait_started');
165
181
  const handshake = await this.deps.handshake?.(h, this.plan, this.abort.signal);
182
+ timing.mark('mcp_discovery_complete');
166
183
  this.current();
167
184
  assertPreparationBudget();
168
185
  requireExecHandshake(handshake, h, this.plan);
@@ -174,21 +191,27 @@ export class CodexExecLaunchHost {
174
191
  assertPreparationBudget();
175
192
  requireExecHandshake(handshake, h, this.plan);
176
193
  assertPreparationBudget();
194
+ timing.mark('prompt_write_requested');
177
195
  h.stdin.write(prompt);
196
+ timing.mark('prompt_write_returned');
178
197
  this.current();
179
198
  assertPreparationBudget();
199
+ timing.mark('prompt_end_requested');
180
200
  h.stdin.end();
201
+ timing.mark('prompt_end_returned');
181
202
  this.current();
182
203
  assertPreparationBudget();
183
204
  requireExecHandshake(handshake, h, this.plan);
184
205
  assertPreparationBudget();
185
- this.receiptValue = receipt;
206
+ timing.mark('preparation_authorized');
207
+ this.receiptValue = Object.freeze({ ...receipt, preparation_timing: timing.snapshot() });
186
208
  return h;
187
209
  })();
188
210
  return await Promise.race([preparation, stopped]);
189
211
  }
190
212
  catch (error) {
191
- const safe = executionError(interruption ?? error, stage, 'CODEX_EXEC_PREFLIGHT_REJECTED', mcpDetail(), observations());
213
+ timing.mark('preparation_failed');
214
+ const safe = executionError(interruption ?? error, stage, 'CODEX_EXEC_PREFLIGHT_REJECTED', mcpDetail(), observations(), timing.snapshot());
192
215
  await this.cancel().catch(() => { });
193
216
  throw safe;
194
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 { ...trusted, ...(trusted.observations ? { observations: [...trusted.observations] } : {}) };
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtech-ai-local-runner",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "bin": {