mtech-ai-local-runner 0.1.11 → 0.1.12

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
@@ -125,6 +125,18 @@ or cost termination limits on an approved unbounded employee-local Claude run.
125
125
  Existing generation fencing, scope restrictions, cancellation and audit remain
126
126
  in force.
127
127
 
128
+ ### Post-binding execution diagnostics (0.1.12)
129
+
130
+ The manual command and daemon log an `execution_failed` diagnostic (the manual
131
+ command uses `manual_execution_failed`) before attempting Gateway failure
132
+ reporting. Failed events and completion results carry `execution_failure` with
133
+ a fixed `stage` and `code`. Inner host-issued diagnostics survive outer catches;
134
+ unknown errors use a generic code. Credential values, raw exceptions, causes,
135
+ stacks, filesystem paths and provider stderr are not included. These diagnostics
136
+ do not prove whether provider inference occurred. A Gateway reporting outage
137
+ retains the local diagnostic and does not turn an unacknowledged terminal event
138
+ into a completed job. Existing reconciliation rules still apply.
139
+
128
140
  ### Manual Codex pilot (0.1.11)
129
141
 
130
142
  The reviewed manual Codex path preserves the existing CLI and ChatGPT login. It is separate from unattended daemon readiness. After the exact CLI/native reader and Gateway release are installed, an employee-authorized foreground pilot uses `mtec-ai-local-runner manual-codex --job-id=<approved Job UUID>`. It refuses a running daemon, unresolved recovery state, unknown release/account or a different Job. It does not stop services or alter personal settings. A direct administrator policy inventory occurs just before the approved execution; historical inventory is not continuous MDM monitoring. The Gateway OAuth client refuses refresh in this manual command. Actual vendor account startup may still refresh shared credentials and requires disclosure in the exact launch approval.
@@ -1,3 +1,4 @@
1
+ import { executionError } from '../../safety/execution-failure.js';
1
2
  import { CodexExecLaunchHost } from './exec-launch-host.js';
2
3
  import { requireSecurityRequirements } from '../../safety/security-requirements.js';
3
4
  import { createJobSecurityReceipt } from '../../safety/job-security-receipt.js';
@@ -47,19 +48,27 @@ export class CodexAdapter {
47
48
  await this.options.closeOfficialExec?.();
48
49
  throw new Error('CODEX_EXEC_PREFLIGHT_REJECTED');
49
50
  }
50
- host.assertAvailableContext(ctx, this.options.jobId);
51
+ try {
52
+ host.assertAvailableContext(ctx, this.options.jobId);
53
+ }
54
+ catch (error) {
55
+ throw executionError(error, 'codex_context', 'CODEX_EXEC_PREFLIGHT_REJECTED');
56
+ }
51
57
  this.ownedExec = host;
52
58
  this.usingOfficialExec = true;
59
+ let stage = 'codex_prompt';
53
60
  try {
54
61
  const prompt = compileCodexPrompt(object(ctx.taskPackage), this.options.jobId);
62
+ stage = 'codex_launch';
55
63
  this.handle = await this.ownedExec.start(ctx, this.options.jobId, prompt, this.startupAbort.signal);
56
64
  this.receipt = this.ownedExec.receipt();
57
65
  this.assertNotCancelled();
58
66
  return;
59
67
  }
60
- catch {
61
- await this.cancel();
62
- throw new Error('CODEX_EXEC_PREFLIGHT_REJECTED');
68
+ catch (error) {
69
+ const safe = executionError(error, stage, 'CODEX_EXEC_PREFLIGHT_REJECTED');
70
+ await this.cancel().catch(() => { });
71
+ throw safe;
63
72
  }
64
73
  }
65
74
  if (!this.options.startVerifiedSession)
@@ -1,3 +1,4 @@
1
+ import { executionError } from '../../safety/execution-failure.js';
1
2
  import { UNMANAGED_EXEC_MODE } from './unmanaged-scope.js';
2
3
  import { fingerprint as taskFingerprint } from '../../daemon/execution-coordinator.js';
3
4
  import { securityRequirementsFingerprint as hash } from '../../safety/security-requirements.js';
@@ -61,6 +62,7 @@ export class CodexExecLaunchHost {
61
62
  claimOfficialExecPlan(this.plan);
62
63
  this.ownsAttempt = true;
63
64
  this.used = true;
65
+ let stage = 'codex_context';
64
66
  let timer, external;
65
67
  try {
66
68
  this.assertContext(ctx, jobId);
@@ -70,6 +72,7 @@ export class CodexExecLaunchHost {
70
72
  const stopped = new Promise((_, reject) => { external = () => { void this.cancel().catch(() => { }); reject(fail()); }; this.abort.signal.addEventListener('abort', () => reject(fail()), { once: true }); signal?.addEventListener('abort', external, { once: true }); timer = setTimeout(external, 30_000); });
71
73
  const preparation = (async () => {
72
74
  const supported = isReleaseExecMode(this.plan.mode);
75
+ stage = 'codex_inspection';
73
76
  this.inspection = await (supported ? this.deps.inspectRelease?.(this.plan, this.abort.signal) : this.deps.inspect?.(this.plan, this.abort.signal));
74
77
  const assessment = supported ? assessReleaseExec(this.inspection?.snapshot, this.plan) : assessExecPreflight(this.inspection?.snapshot, this.plan);
75
78
  if (!supported && !this.deps.inspect)
@@ -84,6 +87,7 @@ export class CodexExecLaunchHost {
84
87
  if (manual && typeof this.inspection.beforeSpawn !== 'function')
85
88
  throw fail();
86
89
  this.inspection.beforeSpawn?.();
90
+ stage = 'codex_spawn';
87
91
  const h = this.deps.processHost.spawn(invocation.binary, invocation.args, { cwd: this.plan.cwd, env: invocation.env });
88
92
  this.handle = h;
89
93
  if (this.stopped) {
@@ -119,6 +123,7 @@ export class CodexExecLaunchHost {
119
123
  h.stdin.end();
120
124
  this.current();
121
125
  gate.promptSubmitted(h);
126
+ stage = 'codex_mcp';
122
127
  const observed = await gate.wait(h, this.abort.signal);
123
128
  this.current();
124
129
  requireExecHandshake(observed, h, this.plan);
@@ -147,9 +152,10 @@ export class CodexExecLaunchHost {
147
152
  })();
148
153
  return await Promise.race([preparation, stopped]);
149
154
  }
150
- catch {
155
+ catch (error) {
156
+ const safe = executionError(error, stage, 'CODEX_EXEC_PREFLIGHT_REJECTED');
151
157
  await this.cancel().catch(() => { });
152
- throw fail();
158
+ throw safe;
153
159
  }
154
160
  finally {
155
161
  if (timer)
@@ -1,3 +1,4 @@
1
+ import { executionError } from '../../safety/execution-failure.js';
1
2
  import path from 'node:path';
2
3
  import { mkdtemp, mkdir, realpath, writeFile, rm, lstat } from 'node:fs/promises';
3
4
  import { NodeProcessGroupHost } from '../../process/node-process-group-host.js';
@@ -46,12 +47,14 @@ export function buildCodexExecJobOptions(input) {
46
47
  used = true;
47
48
  const abort = () => { void close().catch(() => { }); };
48
49
  signal.addEventListener('abort', abort, { once: true });
50
+ let stage = 'codex_prepare';
49
51
  try {
50
52
  job = input.productionJob();
51
53
  job.attachCleanup(close);
52
54
  const admitted = await job.prepare(ctx);
53
55
  if (closed || signal.aborted)
54
56
  throw reject();
57
+ stage = 'codex_storage';
55
58
  if (!input.env.HOME || !path.isAbsolute(input.stateRoot))
56
59
  throw reject();
57
60
  const account = path.resolve(input.env.CODEX_HOME ?? path.join(input.env.HOME, '.codex'));
@@ -64,6 +67,7 @@ export function buildCodexExecJobOptions(input) {
64
67
  await Promise.all([cwd, ...Object.values(scratch)].map(p => mkdir(p, { mode: 0o700 })));
65
68
  if (closed || signal.aborted)
66
69
  throw reject();
70
+ stage = 'codex_config_ancestry';
67
71
  for (let dir = cwd;; dir = path.dirname(dir)) {
68
72
  for (const base of ['.codex/config.toml']) {
69
73
  const f = path.join(dir, base);
@@ -81,16 +85,20 @@ export function buildCodexExecJobOptions(input) {
81
85
  if (dir === path.dirname(dir))
82
86
  break;
83
87
  }
88
+ stage = 'codex_storage';
84
89
  const schema = path.join(control, 'report-schema.json');
85
90
  await writeFile(schema, JSON.stringify(CODEX_REPORT_SCHEMA), { mode: 0o600, flag: 'wx' });
86
91
  if (closed || signal.aborted)
87
92
  throw reject();
88
93
  const approved = ctx.approvedTaskPackage, requirements = approved.security_requirements;
94
+ stage = 'codex_tool_policy';
89
95
  policy = new LocalToolPolicy(jobToolPolicyOptions(source, ctx.taskPackage, approved, admitted.release.commands, { runtimeAdapter: 'codex_cli', runtimeAdapterVersion: '1' }));
96
+ stage = 'codex_tool_server';
90
97
  server = new LocalToolServer({ policy, authorizeCall: () => gate?.allowsCall() === true, onDiscovery: (event, scope) => gate?.discovery(event, scope) });
91
98
  const endpoint = await server.start();
92
99
  if (closed || signal.aborted)
93
100
  throw reject();
101
+ stage = 'codex_invocation';
94
102
  const plan = buildOfficialExecPlan({ policyMode: admitted.release.securityMode, binary: admitted.binary, credentialBackend: admitted.release.credentialBackend, identity: { jobId: input.jobId, generation: ctx.generation, actorId: admitted.actorId, runnerId: admitted.runnerId, sourceSha: String(approved.repository.base_commit_sha), binarySha256: admitted.release.binarySha256, releaseManifestFingerprint: String(requirements.release_manifest_sha256), requirementsFingerprint: String(requirements.fingerprint), contractFingerprint: ctx.securityBinding.contractFingerprint, taskPackageFingerprint: ctx.securityBinding.taskPackageFingerprint, launchFingerprint: ctx.securityBinding.launchFingerprint, commandsFingerprint: commandRegistryFingerprint(admitted.release.commands), toolSchemaFingerprint: fingerprint({ tools: localToolInventory() }), barrierFingerprint: admitted.release.fixturesFingerprint }, invocation: { modelId: ctx.resolvedModelId, worktreeRoot: cwd, sourceRoot: source, schemaPath: schema, endpoint, env, scratch } });
95
103
  gate = new ExecToolGate(plan);
96
104
  const host = new CodexExecLaunchHost(plan, { processHost, inspectRelease: p => job.inspect(p), execTools: gate, revokeTools: () => { gate?.revoke(); server?.revoke(); policy?.revoke(); job?.revoke(); }, closeTools: dispose });
@@ -99,11 +107,12 @@ export function buildCodexExecJobOptions(input) {
99
107
  throw reject();
100
108
  return host;
101
109
  }
102
- catch {
110
+ catch (error) {
111
+ const safe = executionError(error, stage, 'CODEX_EXEC_COMPOSITION_REJECTED');
103
112
  await close().catch(() => { });
104
113
  if (control)
105
114
  await rm(control, { recursive: true, force: true }).catch(() => { });
106
- throw reject();
115
+ throw safe;
107
116
  }
108
117
  finally {
109
118
  signal.removeEventListener('abort', abort);
@@ -1,3 +1,4 @@
1
+ import { executionError, executionFailure } from '../safety/execution-failure.js';
1
2
  import { requireSecurityRequirements } from '../safety/security-requirements.js';
2
3
  import { GatewayToolError } from "../gateway-client.js";
3
4
  import { createHash } from "node:crypto";
@@ -122,7 +123,8 @@ export class ExecutionCoordinator {
122
123
  return;
123
124
  }
124
125
  await this.deps.state.save({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, phase: "running", bindingId: binding.bindingId, workspaceManifest: workspace.manifest });
125
- let heartbeatFailure = null;
126
+ let heartbeatFailure;
127
+ let executionStage = 'binding_model';
126
128
  let requestedTerminal;
127
129
  let commandCursor = 0;
128
130
  let controlsStopped = false;
@@ -154,23 +156,29 @@ export class ExecutionCoordinator {
154
156
  try {
155
157
  await controlsInFlight;
156
158
  }
159
+ catch (error) {
160
+ throw executionError(error, 'commands', 'EXECUTION_FAILED');
161
+ }
157
162
  finally {
158
163
  controlsInFlight = undefined;
159
164
  }
160
165
  };
161
166
  const heartbeatTimer = this.deps.gateway.heartbeat || this.deps.gateway.readCommands ? setInterval(() => {
162
167
  void (async () => { await this.deps.gateway.heartbeat?.(binding.bindingId, job); await readControls(); })().catch(async (error) => {
163
- heartbeatFailure = error instanceof Error ? error : new Error(String(error));
168
+ heartbeatFailure = executionError(error, 'heartbeat', 'EXECUTION_FAILED');
164
169
  await adapter.cancel().catch(() => undefined);
165
170
  });
166
171
  }, 30_000) : undefined;
167
172
  heartbeatTimer?.unref();
168
173
  let terminal;
174
+ let failureDiagnostic;
169
175
  let sequence = 0;
170
176
  let checkpointSeen = false;
171
177
  const append = async (event) => {
178
+ const previousStage = executionStage;
172
179
  let payload = event.payload;
173
180
  if (isTerminalEventType(event.eventType)) {
181
+ executionStage = 'terminal_evidence';
174
182
  const evidence = this.deps.terminalEvidenceCollector.collect({ checkoutPath: workspace.path, baseSha: workspace.baseCommitSha });
175
183
  payload = { ...payload, diff_hash: evidence.diffHash, changed_file_manifest_hash: evidence.changedFileManifestHash };
176
184
  }
@@ -181,21 +189,27 @@ export class ExecutionCoordinator {
181
189
  ...(payload ? { payload } : {}),
182
190
  idempotencyKey: `${binding.bindingId}:${sequence + 1}`, occurredAt: new Date().toISOString(),
183
191
  };
192
+ executionStage = 'event_report';
184
193
  validateEvent(normalized);
185
194
  if (!this.deps.gateway.appendEvent)
186
195
  throw new Error("daemon event reporting is unavailable");
187
196
  await this.deps.gateway.appendEvent(binding.bindingId, normalized);
188
197
  sequence = normalized.sequence;
198
+ executionStage = 'state_save';
189
199
  if (event.eventType === "checkpoint_ready")
190
200
  checkpointSeen = true;
191
201
  await this.deps.state.save({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, phase: "running", bindingId: binding.bindingId, eventCursor: sequence, ...(isTerminalEventType(event.eventType) ? { terminalEventType: event.eventType } : {}), workspaceManifest: workspace.manifest });
202
+ executionStage = previousStage;
192
203
  };
193
204
  try {
194
205
  if (!job.taskPackage.resolved_model_id
195
206
  || binding.resolvedModelId !== job.taskPackage.resolved_model_id)
196
207
  throw new Error("Binding model differs from approved launch");
208
+ executionStage = 'adapter_start';
197
209
  await adapter.start({ securityBinding: { launchFingerprint: job.launchFingerprint, contractFingerprint: job.gatewayContractFingerprint, taskPackageFingerprint: job.gatewayTaskPackageFingerprint }, generation: job.generation, approvedTaskPackage: job.taskPackage, bindingId: binding.bindingId, sessionId: binding.bindingId, taskPackage: binding.taskPackage, resolvedModelId: binding.resolvedModelId });
210
+ executionStage = 'commands';
198
211
  await readControls();
212
+ executionStage = 'event_stream';
199
213
  if (adapter.events && !requestedTerminal) {
200
214
  for await (const event of adapter.events()) {
201
215
  if (heartbeatFailure)
@@ -238,7 +252,14 @@ export class ExecutionCoordinator {
238
252
  await adapter.cancel().catch(() => undefined);
239
253
  // Do not complete a job whose terminal evidence was not acknowledged.
240
254
  // A safe bounded reason prevents raw adapter/transport errors leaking.
241
- terminal = { eventType: "failed", summary: "daemon execution or event reporting failed" };
255
+ const diagnostic = executionFailure(error, executionStage);
256
+ failureDiagnostic = diagnostic;
257
+ // Log before reporting so an unavailable Gateway does not erase the cause.
258
+ try {
259
+ this.deps.reportExecutionFailure?.({ ...diagnostic });
260
+ }
261
+ catch { /* logging must not change terminal reconciliation */ }
262
+ terminal = { eventType: "failed", summary: "daemon execution or event reporting failed", payload: { execution_failure: { ...diagnostic } } };
242
263
  await append(terminal);
243
264
  }
244
265
  finally {
@@ -247,7 +268,7 @@ export class ExecutionCoordinator {
247
268
  clearInterval(heartbeatTimer);
248
269
  }
249
270
  const status = terminal.eventType === "completed" ? "completed" : terminal.eventType === "cancelled" ? "cancelled" : terminal.eventType === "blocked" || terminal.eventType === "scope_conflict" ? "blocked" : "failed";
250
- await this.deps.gateway.complete({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, status, result: { binding_id: binding.bindingId, event_type: terminal.eventType, summary: terminal.summary, ...(adapter.securityReceipt?.() ? { security_receipt: adapter.securityReceipt() } : {}) } });
271
+ await this.deps.gateway.complete({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, status, result: { binding_id: binding.bindingId, event_type: terminal.eventType, summary: terminal.summary, ...(failureDiagnostic ? { execution_failure: { ...failureDiagnostic } } : {}), ...(adapter.securityReceipt?.() ? { security_receipt: adapter.securityReceipt() } : {}) } });
251
272
  await this.deps.state.clear();
252
273
  }
253
274
  }
@@ -40,6 +40,7 @@ export async function runManualCodexJob(input, deps) {
40
40
  await state.save({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, phase: 'claimed' });
41
41
  let result = { jobId: job.id, status: 'unacknowledged' };
42
42
  const coordinator = new ExecutionCoordinator({ state, workspaces: deps?.workspaces ?? new WorkspaceManager({ root: input.workspaceRoot, git: new GitClient() }), terminalEvidenceCollector: deps?.terminalEvidenceCollector ?? buildTerminalEvidenceCollector(),
43
+ reportExecutionFailure: d => process.stderr.write(JSON.stringify({ event: 'manual_execution_failed', ...d }) + '\n'),
43
44
  reportPreparationFailure: d => process.stderr.write(JSON.stringify({ event: 'manual_preparation_failed', ...d }) + '\n'),
44
45
  registry: { async select(id, version, options) {
45
46
  if (id !== 'codex_cli' || version !== '1' || options.jobId !== job.id)
@@ -43,6 +43,7 @@ export async function buildDaemon(input) {
43
43
  const versions = Object.fromEntries(probes.filter((probe) => probe.available && probe.authenticated).map((probe) => [probe.id, probe.version]));
44
44
  const state = new FileDaemonStateStore(input.stateRoot);
45
45
  const coordinator = new ExecutionCoordinator({
46
+ reportExecutionFailure: (diagnostic) => { process.stderr.write(`${JSON.stringify({ event: "execution_failed", ...diagnostic })}\n`); },
46
47
  reportPreparationFailure: (diagnostic) => { process.stderr.write(`${JSON.stringify({ event: "preparation_failed", ...diagnostic })}\n`); },
47
48
  terminalEvidenceCollector: buildTerminalEvidenceCollector(),
48
49
  gateway: {
@@ -1,3 +1,4 @@
1
+ import { executionError } from './execution-failure.js';
1
2
  import { eligiblePersonalAccount } from '../adapters/codex/unmanaged-scope.js';
2
3
  import { securityRequirementsFingerprint as hash } from './security-requirements.js';
3
4
  import { commandRegistryFingerprint } from './job-security-session.js';
@@ -29,26 +30,33 @@ export function createManualPilotController(input, deps) {
29
30
  if (used || closed)
30
31
  throw rejected();
31
32
  used = true;
33
+ let stage = 'codex_manual_inputs';
32
34
  try {
33
35
  if (!readLocal || !path.isAbsolute(input.binary) || !path.isAbsolute(input.home) || await realpath(input.home) !== input.home || await realpath(account) !== account)
34
36
  throw rejected();
37
+ stage = 'codex_manual_release';
35
38
  const sha = await measureCommandFile(input.binary), matches = releases.filter(r => reviewedCodexExecRelease(r) && r.mdmVerification === 'manual_prelaunch' && r.binarySha256 === sha);
36
39
  if (matches.length !== 1)
37
40
  throw rejected();
38
41
  const release = matches[0], requirements = claim.task_package?.security_requirements;
42
+ stage = 'codex_manual_config';
39
43
  const config = parse((await readStableText(path.join(account, 'config.toml'))).text);
40
44
  if ((config.cli_auth_credentials_store ?? 'auto') !== release.credentialBackend || requirements?.release_manifest_sha256 !== execReleaseFingerprint(release) || requirements?.mdm_verification !== 'manual_prelaunch')
41
45
  throw rejected();
42
46
  host = createCodexExecProductionHost({ runnerId: input.runnerId, binary: input.binary, home: input.home, codexHome: account, releases: [release], readNativeAuth: deps?.readNativeAuth, policyScopeVerifier: { observe() { if (closed || !manual)
43
47
  throw rejected(); return manual.observe(); } } });
44
48
  // Full claim/requirements validation precedes any privileged query.
49
+ stage = 'codex_manual_claim';
45
50
  host.registerClaim(claim);
51
+ stage = 'codex_manual_auth';
46
52
  const credential = release.credentialBackend === 'auto' ? deps?.readNativeAuth?.(account, 'auto') : await readCodexCredentialStatus(account);
47
53
  if (!credential || credential.backend !== release.credentialBackend || !credential.available || !Number.isFinite(credential.expiresAt) || credential.expiresAt <= now())
48
54
  throw rejected();
55
+ stage = 'codex_manual_requirements';
49
56
  const job = claim.job, p = claim.task_package, r = requirements;
50
57
  if (r.security_mode !== release.securityMode || r.binary_sha256 !== release.binarySha256 || r.fixtures_sha256 !== release.fixturesFingerprint || r.commands_sha256 !== commandRegistryFingerprint(release.commands) || r.tools_sha256 !== fingerprint({ tools: localToolInventory() }) || !isDeepStrictEqual(r.model_ids, release.modelIds) || !release.modelIds.includes(String(p.resolved_model_id)))
51
58
  throw rejected();
59
+ stage = 'codex_manual_local_policy';
52
60
  const local = readLocal();
53
61
  if (!eligiblePersonalAccount(local.account) || local.account.backend !== release.credentialBackend || local.account.rootFingerprint !== hash({ accountRoot: account }) || !isDeepStrictEqual(Object.keys(local.sources).sort(), ['legacy_managed', 'system_defaults', 'system_requirements']) || Object.values(local.sources).some(s => !isDeepStrictEqual(Object.keys(s).sort(), ['fingerprint', 'status']) || !/^[a-f0-9]{64}$/.test(s.fingerprint)))
54
62
  throw rejected();
@@ -56,17 +64,21 @@ export function createManualPilotController(input, deps) {
56
64
  throw rejected();
57
65
  if (closed)
58
66
  throw rejected();
67
+ stage = 'codex_manual_inventory';
59
68
  const inventory = await (deps?.collectInventory ?? createManualProfileQuery().collectOnce)();
60
69
  if (closed)
61
70
  throw rejected();
71
+ stage = 'codex_manual_policy_scope';
62
72
  manual = createManualPolicyScopeVerifier({ binding: { jobId: job.id, generation: job.generation, actorId: claim.security_context.actor_id, runnerId: input.runnerId, sourceSha: p.repository.base_commit_sha, binarySha256: release.binarySha256, releaseManifestFingerprint: String(r.release_manifest_sha256), requirementsFingerprint: String(r.fingerprint), contractFingerprint: claim.contract_fingerprint, taskPackageFingerprint: claim.task_package_fingerprint, launchFingerprint: job.launch_fingerprint, commandsFingerprint: String(r.commands_sha256), toolSchemaFingerprint: String(r.tools_sha256), barrierFingerprint: String(r.fixtures_sha256) }, leaseDigest: createHash('sha256').update(claim.lease_token).digest('hex'), inventory, readLocal, now });
73
+ stage = 'codex_manual_host_probe';
63
74
  if (!(await host.probe()).available || closed)
64
75
  throw rejected();
65
76
  return host;
66
77
  }
67
- catch {
78
+ catch (error) {
79
+ const safe = executionError(error, stage, 'CODEX_MANUAL_PILOT_REJECTED');
68
80
  await close().catch(() => { });
69
- throw rejected();
81
+ throw safe;
70
82
  }
71
83
  } });
72
84
  }
@@ -0,0 +1,32 @@
1
+ import { GatewayToolError } from '../gateway-client.js';
2
+ const issued = new WeakMap();
3
+ export function executionFailure(error, stage, fallback = 'EXECUTION_FAILED') {
4
+ if (error && typeof error === 'object') {
5
+ const trusted = issued.get(error);
6
+ if (trusted)
7
+ return { ...trusted };
8
+ try {
9
+ if (error instanceof GatewayToolError && (error.code === 'GATEWAY_TOOL_REJECTED' || error.code === 'GATEWAY_TRANSPORT_ERROR' || error.code === 'WORK_LOOP_CANCELLED')) {
10
+ return { stage, code: error.code };
11
+ }
12
+ }
13
+ catch { /* revoked proxies and prototype traps have no diagnostic authority */ }
14
+ // Do not invoke error getters or accept arbitrary uppercase strings.
15
+ let code;
16
+ try {
17
+ code = Object.getOwnPropertyDescriptor(error, 'code')?.value;
18
+ }
19
+ catch { /* hostile proxy has no diagnostic authority */ }
20
+ const mapped = code === 'EACCES' || code === 'EPERM' ? 'LOCAL_ACCESS_DENIED'
21
+ : code === 'ENOENT' ? 'LOCAL_FILE_MISSING' : code === 'ENOSPC' ? 'LOCAL_STORAGE_FULL' : undefined;
22
+ if (mapped)
23
+ return { stage, code: mapped };
24
+ }
25
+ return { stage, code: fallback };
26
+ }
27
+ export function executionError(error, stage, fallback) {
28
+ const diagnostic = executionFailure(error, stage, fallback);
29
+ const safe = new Error(fallback);
30
+ issued.set(safe, Object.freeze(diagnostic));
31
+ return safe;
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtech-ai-local-runner",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "bin": {