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 +12 -0
- package/dist/src/adapters/codex/adapter.js +13 -4
- package/dist/src/adapters/codex/exec-launch-host.js +8 -2
- package/dist/src/adapters/codex/exec-runtime-options.js +11 -2
- package/dist/src/daemon/execution-coordinator.js +25 -4
- package/dist/src/daemon/manual-codex.js +1 -0
- package/dist/src/daemon/runtime.js +1 -0
- package/dist/src/safety/codex-manual-controller.js +14 -2
- package/dist/src/safety/execution-failure.js +32 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|