mtech-ai-local-runner 0.1.16 → 0.1.18

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.18: preparation deadline enforcement
14
+
15
+ The overall 30-second preparation budget also covers release inspection. A
16
+ synchronous metadata check can delay JavaScript timer callbacks. Local guards
17
+ now use a monotonic deadline after inspection and at spawn/input/admission
18
+ boundaries, refusing expired preparation even when its timer has not run. The
19
+ budget is not reset or extended; admitted employee work remains unbounded.
20
+ Fake callbacks reproduce the late-admission defect. Existing 0.1.17 receipts do
21
+ not contain phase timing, so this is not proof of the historical native wait's
22
+ cause or successful real-account connectivity. No new native stack sampler,
23
+ account operation, daemon change or actual CLI run is part of this correction.
24
+
25
+ ## 0.1.17: owned startup, pipe and transport observations
26
+
27
+ An owned-process collector marker distinguishes available diagnostics from a
28
+ missing collector. Fixed observations separate spawn, stdin write/end requests,
29
+ OS pipe finish, stdout/stderr activity/EOF, selected JSON event type names, exit,
30
+ and MCP TCP connection from authenticated HTTP discovery. No event payload is
31
+ copied into diagnostics. Pipe finish does **not** prove the CLI consumed stdin;
32
+ TCP activity does **not** prove the connection belongs to Codex or authorize tools.
33
+ Provider event type observations are not attestations of policy or model use.
34
+ The legacy `Reading prompt from stdin...` marker is not emitted for forced
35
+ `exec -` input in the inspected release source; its absence cannot locate a wait.
36
+ Failure snapshots are captured before owned cleanup. Existing preparation limits,
37
+ actual employee task limits, credentials, permissions and tool gates are unchanged.
38
+ This is local diagnostic coverage, not proof of the previous account failure's
39
+ cause or a successful real-account run. No automatic stack sampler is enabled.
40
+
13
41
  ## 0.1.16: bounded startup and MCP failure observations
14
42
 
15
43
  Failed Codex preparation now preserves fixed native error markers and separates
@@ -1,3 +1,4 @@
1
+ import { performance } from 'node:perf_hooks';
1
2
  import { localEndpointObservations } from '../../safety/local-tool-server.js';
2
3
  import { processStartupObservations } from '../../process/node-process-group-host.js';
3
4
  import { executionError } from '../../safety/execution-failure.js';
@@ -10,6 +11,7 @@ import { officialExecObservationLimits } from './exec-observer.js';
10
11
  import { EXEC_GATES, assessExecPreflight, requireExecHandshake } from './exec-preflight.js';
11
12
  import { EXEC_MODE, isReleaseExecMode, EXEC_TRANSPORT, assertOfficialExecPlanAvailable, claimOfficialExecPlan, requireOfficialExecPlan } from './exec-invocation.js';
12
13
  const fail = () => new Error('CODEX_EXEC_PREFLIGHT_REJECTED');
14
+ const PREPARATION_TIMEOUT_MS = 30_000;
13
15
  /** No default native inspector, handshake observer or OS process host is created.
14
16
  * Missing per-execution evidence blocks before spawn. Synthetic callbacks grant
15
17
  * only the fake host used by tests, never a compiled production release entry. */
@@ -69,17 +71,26 @@ export class CodexExecLaunchHost {
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
73
  const interrupt = (code) => { interruption ??= executionError(fail(), stage, code, mcpDetail(), observations()); };
74
+ // Timer callbacks cannot interrupt synchronous native metadata checks. Keep
75
+ // the same total budget and enforce it at effect boundaries with a monotonic
76
+ // clock, rather than spawning/submitting after an already expired inspection.
77
+ const deadline = performance.now() + PREPARATION_TIMEOUT_MS;
78
+ const assertPreparationBudget = () => { if (performance.now() >= deadline) {
79
+ interrupt('CODEX_EXEC_PREPARATION_TIMEOUT');
80
+ throw interruption;
81
+ } };
72
82
  let timer, external;
73
83
  try {
74
84
  this.assertContext(ctx, jobId);
75
85
  requireOfficialExecPlan(this.plan);
76
86
  if (typeof prompt !== 'string' || !prompt || prompt.length > 1_000_000 || signal?.aborted)
77
87
  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(); }, 30_000); });
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); });
79
89
  const preparation = (async () => {
80
90
  const supported = isReleaseExecMode(this.plan.mode);
81
91
  stage = 'codex_inspection';
82
92
  this.inspection = await (supported ? this.deps.inspectRelease?.(this.plan, this.abort.signal) : this.deps.inspect?.(this.plan, this.abort.signal));
93
+ assertPreparationBudget();
83
94
  const assessment = supported ? assessReleaseExec(this.inspection?.snapshot, this.plan) : assessExecPreflight(this.inspection?.snapshot, this.plan);
84
95
  if (!supported && !this.deps.inspect)
85
96
  assessment.reasonCodes.push(...officialExecObservationLimits());
@@ -92,7 +103,9 @@ export class CodexExecLaunchHost {
92
103
  const manual = ctx.approvedTaskPackage?.security_requirements?.mdm_verification === 'manual_prelaunch';
93
104
  if (manual && typeof this.inspection.beforeSpawn !== 'function')
94
105
  throw fail();
106
+ assertPreparationBudget();
95
107
  this.inspection.beforeSpawn?.();
108
+ assertPreparationBudget();
96
109
  stage = 'codex_spawn';
97
110
  const h = this.deps.processHost.spawn(invocation.binary, invocation.args, { cwd: this.plan.cwd, env: invocation.env });
98
111
  this.handle = h;
@@ -115,6 +128,7 @@ export class CodexExecLaunchHost {
115
128
  void this.cancel().catch(() => { });
116
129
  });
117
130
  this.current();
131
+ assertPreparationBudget();
118
132
  if (supported) {
119
133
  const gate = this.deps.execTools;
120
134
  gate.bind(h, () => { try {
@@ -126,35 +140,48 @@ export class CodexExecLaunchHost {
126
140
  } });
127
141
  // Vendor exec consumes stdin before it can initialize its required MCP.
128
142
  // Prompt submission may cause provider/account effects; do not call this no-model.
143
+ assertPreparationBudget();
129
144
  h.stdin.write(prompt);
130
145
  this.current();
146
+ assertPreparationBudget();
131
147
  h.stdin.end();
132
148
  this.current();
149
+ assertPreparationBudget();
133
150
  gate.promptSubmitted(h);
134
151
  stage = 'codex_mcp';
135
152
  const observed = await gate.wait(h, this.abort.signal);
136
153
  this.current();
154
+ assertPreparationBudget();
137
155
  requireExecHandshake(observed, h, this.plan);
156
+ assertPreparationBudget();
138
157
  gate.authorize(h);
139
158
  this.current();
159
+ assertPreparationBudget();
140
160
  requireExecHandshake(observed, h, this.plan);
161
+ assertPreparationBudget();
141
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 });
142
163
  return h;
143
164
  }
144
165
  const handshake = await this.deps.handshake?.(h, this.plan, this.abort.signal);
145
166
  this.current();
167
+ assertPreparationBudget();
146
168
  requireExecHandshake(handshake, h, this.plan);
147
169
  this.handshake = handshake;
148
170
  // An input hash is not a native effective-config certificate. Preserve the
149
171
  // evidence origin in the audit receipt and never reuse AppServer authority.
150
172
  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
173
  this.current();
174
+ assertPreparationBudget();
152
175
  requireExecHandshake(handshake, h, this.plan);
176
+ assertPreparationBudget();
153
177
  h.stdin.write(prompt);
154
178
  this.current();
179
+ assertPreparationBudget();
155
180
  h.stdin.end();
156
181
  this.current();
182
+ assertPreparationBudget();
157
183
  requireExecHandshake(handshake, h, this.plan);
184
+ assertPreparationBudget();
158
185
  this.receiptValue = receipt;
159
186
  return h;
160
187
  })();
@@ -1,5 +1,5 @@
1
1
  const startup = new WeakMap();
2
- /** Fixed text observations from this owned host only. Never raw stderr or authority. */
2
+ /** Fixed lifecycle/text observations from this owned host only. Never raw stderr or authority. */
3
3
  export function processStartupObservations(handle) { return [...(startup.get(handle) ?? [])]; }
4
4
  import { spawn } from "node:child_process";
5
5
  /** Each invocation owns one new process group; no name-based process sweeping. */
@@ -8,6 +8,9 @@ export class NodeProcessGroupHost {
8
8
  if (process.platform !== "darwin")
9
9
  throw new Error("Codex process isolation requires macOS");
10
10
  const child = spawn(command, args, { ...options, shell: false, detached: true, stdio: ["pipe", "pipe", "pipe"] });
11
+ const observations = new Set(['host_observer_v2']);
12
+ child.once('spawn', () => observations.add('host_process_spawned'));
13
+ child.stdin.once('finish', () => observations.add('host_stdin_finished'));
11
14
  let failure;
12
15
  let owned = true;
13
16
  let finished = false, consumed = false, queuedBytes = 0, wake;
@@ -44,8 +47,9 @@ export class NodeProcessGroupHost {
44
47
  if (signal === "SIGKILL")
45
48
  owned = false;
46
49
  };
47
- child.on("error", () => { failure ??= new Error("Provider process could not start"); terminated(null, null); notify(); });
50
+ child.on("error", () => { observations.add('host_process_spawn_failed'); failure ??= new Error("Provider process could not start"); terminated(null, null); notify(); });
48
51
  child.stdin.on("error", () => {
52
+ observations.add('host_stdin_failed');
49
53
  failure ??= new Error("Provider process input closed");
50
54
  try {
51
55
  kill();
@@ -58,8 +62,9 @@ export class NodeProcessGroupHost {
58
62
  });
59
63
  // Keep fixed observations and fixed marker prefixes only: no raw stderr,
60
64
  // account text, paths or URLs survive a data callback, even for split chunks.
61
- const observations = new Set();
62
65
  const markers = [
66
+ // Legacy wording appears only with implicit piped stdin, NOT forced `exec -`.
67
+ // Its absence cannot locate startup; pipe finish does not prove native consumption.
63
68
  { name: 'native_stdin_read', text: 'Reading prompt from stdin...', prefix: '' },
64
69
  { name: 'native_model_catalog_failed', text: 'codex_models_manager::manager: failed to refresh available models:', prefix: '' },
65
70
  { name: 'native_cloud_policy_failed', text: 'Failed to fetch cloud config bundle', prefix: '' },
@@ -71,6 +76,7 @@ export class NodeProcessGroupHost {
71
76
  { name: 'native_auth_refresh_unsupported', text: 'chatgpt auth token refresh is not supported in exec mode', prefix: '' },
72
77
  ];
73
78
  child.stderr.on('data', (chunk) => {
79
+ observations.add('host_stderr_data');
74
80
  const data = chunk.toString('utf8');
75
81
  for (const marker of markers) {
76
82
  if (observations.has(marker.name))
@@ -90,9 +96,10 @@ export class NodeProcessGroupHost {
90
96
  }
91
97
  }
92
98
  });
93
- child.stderr.once('end', () => { for (const marker of markers)
99
+ child.stderr.once('end', () => { observations.add('host_stderr_ended'); for (const marker of markers)
94
100
  marker.prefix = ''; });
95
101
  child.once("exit", (code, signal) => {
102
+ observations.add('host_process_exited');
96
103
  try {
97
104
  kill();
98
105
  }
@@ -111,12 +118,25 @@ export class NodeProcessGroupHost {
111
118
  throw new Error("Provider event exceeded the allowed size");
112
119
  if (queuedBytes + line.length + 1 > MAX_QUEUED_BYTES || lines.length >= MAX_QUEUED_LINES)
113
120
  throw new Error("Provider event buffer exceeded the allowed size");
121
+ observations.add('host_stdout_line');
122
+ // Diagnostic type names only; no event payload, IDs, text or provider error.
123
+ try {
124
+ const event = JSON.parse(line.toString('utf8'));
125
+ const types = {
126
+ 'thread.started': 'host_event_thread_started', 'turn.started': 'host_event_turn_started',
127
+ 'turn.completed': 'host_event_turn_completed', 'turn.failed': 'host_event_turn_failed', 'error': 'host_event_error',
128
+ };
129
+ if (event && typeof event === 'object' && 'type' in event && typeof event.type === 'string' && Object.hasOwn(types, event.type))
130
+ observations.add(types[event.type]);
131
+ }
132
+ catch { /* Malformed or unrelated output has no diagnostic authority. */ }
114
133
  lines.push({ text: line.toString('utf8').replace(/\r$/, ''), bytes: line.length + 1 });
115
134
  queuedBytes += line.length + 1;
116
135
  notify();
117
136
  };
118
137
  try {
119
138
  for await (const chunk of child.stdout) {
139
+ observations.add('host_stdout_data');
120
140
  pending = Buffer.concat([pending, Buffer.from(chunk)]);
121
141
  let end;
122
142
  while ((end = pending.indexOf(10)) !== -1) {
@@ -128,6 +148,7 @@ export class NodeProcessGroupHost {
128
148
  }
129
149
  if (pending.length)
130
150
  enqueue(pending);
151
+ observations.add('host_stdout_ended');
131
152
  }
132
153
  catch (error) {
133
154
  failure ??= error instanceof Error && ['Provider event exceeded the allowed size', 'Provider event buffer exceeded the allowed size'].includes(error.message) ? error : new Error('Provider event stream failed');
@@ -144,7 +165,7 @@ export class NodeProcessGroupHost {
144
165
  }
145
166
  })();
146
167
  const handle = {
147
- stdin: { write: (text) => { child.stdin.write(text); }, end: () => { child.stdin.end(); } },
168
+ stdin: { write: (text) => { observations.add('host_stdin_write_requested'); child.stdin.write(text); }, end: () => { observations.add('host_stdin_end_requested'); child.stdin.end(); } },
148
169
  stdout: { async *[Symbol.asyncIterator]() {
149
170
  if (consumed)
150
171
  throw new Error('Provider event stream already consumed');
@@ -1,6 +1,15 @@
1
1
  import { GatewayToolError } from '../gateway-client.js';
2
2
  /** Observations only: provider text cannot authorize a tool or certify a cause. */
3
- export const STARTUP_OBSERVATIONS = Object.freeze(['native_stdin_read', 'native_model_catalog_failed', 'native_cloud_policy_failed', 'native_cloud_policy_timeout', 'native_required_mcp_failed', 'native_config_failed', 'native_auth_refresh_unsupported', 'local_mcp_request_received', 'local_mcp_auth_rejected', 'local_mcp_origin_or_host_rejected', 'local_mcp_initialize_received']);
3
+ export const STARTUP_OBSERVATIONS = Object.freeze([
4
+ 'host_observer_v2', 'host_process_spawned', 'host_process_spawn_failed', 'host_process_exited',
5
+ 'host_stdin_write_requested', 'host_stdin_end_requested', 'host_stdin_finished', 'host_stdin_failed',
6
+ 'host_stdout_data', 'host_stdout_line', 'host_stdout_ended', 'host_stderr_data', 'host_stderr_ended',
7
+ 'host_event_thread_started', 'host_event_turn_started', 'host_event_turn_completed', 'host_event_turn_failed', 'host_event_error',
8
+ 'local_mcp_tcp_connected',
9
+ 'native_stdin_read', 'native_model_catalog_failed', 'native_cloud_policy_failed', 'native_cloud_policy_timeout',
10
+ 'native_required_mcp_failed', 'native_config_failed', 'native_auth_refresh_unsupported',
11
+ 'local_mcp_request_received', 'local_mcp_auth_rejected', 'local_mcp_origin_or_host_rejected', 'local_mcp_initialize_received',
12
+ ]);
4
13
  const issued = new WeakMap();
5
14
  export function executionFailure(error, stage, fallback = 'EXECUTION_FAILED') {
6
15
  if (error && typeof error === 'object') {
@@ -189,6 +189,8 @@ export class LocalToolServer {
189
189
  reply(400, { error: 'Invalid request' });
190
190
  }
191
191
  });
192
+ // Transport activity is not authenticated MCP discovery or tool authority.
193
+ this.server.on('connection', () => this.observations.add('local_mcp_tcp_connected'));
192
194
  this.server.requestTimeout = 15000;
193
195
  this.server.headersTimeout = 10000;
194
196
  this.starting = new Promise((resolve, reject) => { this.server.once('error', reject); this.server.listen(0, '127.0.0.1', () => resolve()); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtech-ai-local-runner",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "bin": {