mtech-ai-local-runner 0.1.12 → 0.1.14
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 +29 -0
- package/dist/src/adapters/codex/exec-launch-host.js +7 -2
- package/dist/src/adapters/codex/exec-tool-gate.js +7 -3
- package/dist/src/daemon/manual-codex.js +2 -2
- package/dist/src/safety/codex-manual-controller.js +5 -3
- package/dist/src/safety/codex-manual-profile-query.js +160 -15
- package/dist/src/safety/execution-failure.js +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,14 @@ 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.14: foreground administrator authentication wait
|
|
14
|
+
|
|
15
|
+
The manual Codex pilot waits for administrator authentication before starting
|
|
16
|
+
the 60-second profile inventory deadline. Cancelling the execution aborts its
|
|
17
|
+
owned pending query. Fixed error codes identify authentication and query failures
|
|
18
|
+
without exposing native stderr. This patch does not update a running daemon or
|
|
19
|
+
establish successful real-account Codex execution.
|
|
20
|
+
|
|
13
21
|
## 0.1.10: enforced PreToolUse hook
|
|
14
22
|
|
|
15
23
|
Earlier releases configured a hook command that the shell could not run, so Claude
|
|
@@ -125,6 +133,20 @@ or cost termination limits on an approved unbounded employee-local Claude run.
|
|
|
125
133
|
Existing generation fencing, scope restrictions, cancellation and audit remain
|
|
126
134
|
in force.
|
|
127
135
|
|
|
136
|
+
### MCP startup failure diagnostics (0.1.13)
|
|
137
|
+
|
|
138
|
+
Codex startup now distinguishes an owned process exiting before admission,
|
|
139
|
+
preparation timeout, explicit abort and rejected MCP discovery. A fixed `detail`
|
|
140
|
+
reports whether initialize, initialized notification or tool inventory is still
|
|
141
|
+
missing (or discovery completed). The first issued cause survives cleanup and
|
|
142
|
+
outer catches and reaches both the failed event and terminal Job result.
|
|
143
|
+
Raw provider stderr remains excluded. These diagnostics do not establish the
|
|
144
|
+
historical failure cause or prove whether provider inference occurred.
|
|
145
|
+
|
|
146
|
+
The preparation deadline and mandatory authenticated MCP checks are unchanged.
|
|
147
|
+
There is no automatic model retry or task duration/count cap. Installing this
|
|
148
|
+
patch does not authorize another actual Codex attempt or change login/settings.
|
|
149
|
+
|
|
128
150
|
### Post-binding execution diagnostics (0.1.12)
|
|
129
151
|
|
|
130
152
|
The manual command and daemon log an `execution_failed` diagnostic (the manual
|
|
@@ -139,4 +161,11 @@ into a completed job. Existing reconciliation rules still apply.
|
|
|
139
161
|
|
|
140
162
|
### Manual Codex pilot (0.1.11)
|
|
141
163
|
|
|
164
|
+
The foreground inventory separates human authentication wait from the bounded
|
|
165
|
+
query and evidence freshness. Its query deadline starts only after the fixed
|
|
166
|
+
root-identity check completes, not while the administrator prompt is waiting.
|
|
167
|
+
Cancellation revokes the pending query and rejects late results; authentication,
|
|
168
|
+
query timeout, command and validation failures use distinct safe codes. The
|
|
169
|
+
inventory must still be fresh at Codex spawn. No automatic retry is performed.
|
|
170
|
+
|
|
142
171
|
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.
|
|
@@ -63,13 +63,16 @@ export class CodexExecLaunchHost {
|
|
|
63
63
|
this.ownsAttempt = true;
|
|
64
64
|
this.used = true;
|
|
65
65
|
let stage = 'codex_context';
|
|
66
|
+
let interruption;
|
|
67
|
+
const mcpDetail = () => stage === 'codex_mcp' ? this.deps.execTools?.discoveryDetail() : undefined;
|
|
68
|
+
const interrupt = (code) => { interruption ??= executionError(fail(), stage, code, mcpDetail()); };
|
|
66
69
|
let timer, external;
|
|
67
70
|
try {
|
|
68
71
|
this.assertContext(ctx, jobId);
|
|
69
72
|
requireOfficialExecPlan(this.plan);
|
|
70
73
|
if (typeof prompt !== 'string' || !prompt || prompt.length > 1_000_000 || signal?.aborted)
|
|
71
74
|
throw fail();
|
|
72
|
-
const stopped = new Promise((_, reject) => { external = () => { void this.cancel().catch(() => { }); reject(
|
|
75
|
+
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); });
|
|
73
76
|
const preparation = (async () => {
|
|
74
77
|
const supported = isReleaseExecMode(this.plan.mode);
|
|
75
78
|
stage = 'codex_inspection';
|
|
@@ -95,6 +98,8 @@ export class CodexExecLaunchHost {
|
|
|
95
98
|
throw fail();
|
|
96
99
|
}
|
|
97
100
|
h.once('exit', (code, signal) => {
|
|
101
|
+
if (!this.stopped && !this.receiptValue)
|
|
102
|
+
interrupt('CODEX_EXEC_PROCESS_EXITED');
|
|
98
103
|
// Preserve audit only for a clean owned exit that was still authorized.
|
|
99
104
|
// This immutable record never restores tool or spawn authority.
|
|
100
105
|
if (code === 0 && signal === null && !this.stopped && this.receiptValue) {
|
|
@@ -153,7 +158,7 @@ export class CodexExecLaunchHost {
|
|
|
153
158
|
return await Promise.race([preparation, stopped]);
|
|
154
159
|
}
|
|
155
160
|
catch (error) {
|
|
156
|
-
const safe = executionError(error, stage, 'CODEX_EXEC_PREFLIGHT_REJECTED');
|
|
161
|
+
const safe = executionError(interruption ?? error, stage, 'CODEX_EXEC_PREFLIGHT_REJECTED', mcpDetail());
|
|
157
162
|
await this.cancel().catch(() => { });
|
|
158
163
|
throw safe;
|
|
159
164
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isReleaseExecMode, requireOfficialExecPlan } from './exec-invocation.js';
|
|
2
2
|
import { localDiscoveryScopeMatches, localToolInventory } from '../../safety/local-tool-server.js';
|
|
3
3
|
import { securityRequirementsFingerprint as hash } from '../../safety/security-requirements.js';
|
|
4
|
+
import { executionError } from '../../safety/execution-failure.js';
|
|
4
5
|
const denied = () => Error('EXEC_TOOL_GATE_REJECTED');
|
|
5
6
|
/** Host-owned capability, never populated from stdout/task data. Authenticated
|
|
6
7
|
* discovery is server-side evidence; it does not attest OS client identity. */
|
|
@@ -29,6 +30,9 @@ export class ExecToolGate {
|
|
|
29
30
|
catch {
|
|
30
31
|
return false;
|
|
31
32
|
} }
|
|
33
|
+
/** Metadata only: survives revocation; contains no endpoint or credential. */
|
|
34
|
+
discoveryDetail() { return ['mcp_initialize_missing', 'mcp_initialized_missing', 'mcp_inventory_missing', 'mcp_discovery_complete'][this.stage]; }
|
|
35
|
+
readinessError() { return executionError(denied(), 'codex_mcp', 'CODEX_EXEC_MCP_REJECTED', this.discoveryDetail()); }
|
|
32
36
|
promptSubmitted(process) { if (this.process !== process || this.submitted || !this.active())
|
|
33
37
|
throw denied(); this.submitted = true; }
|
|
34
38
|
discovery = (event, scope) => {
|
|
@@ -60,17 +64,17 @@ export class ExecToolGate {
|
|
|
60
64
|
};
|
|
61
65
|
async wait(process, signal) {
|
|
62
66
|
if (this.process !== process || !this.submitted || !this.active() || signal.aborted)
|
|
63
|
-
throw
|
|
67
|
+
throw this.readinessError();
|
|
64
68
|
if (this.stage !== 3)
|
|
65
69
|
await new Promise((resolve, reject) => {
|
|
66
|
-
const abort = () => { clear(); reject(
|
|
70
|
+
const abort = () => { clear(); reject(this.readinessError()); };
|
|
67
71
|
const clear = () => { signal.removeEventListener('abort', abort); this.settle = undefined; this.reject = undefined; };
|
|
68
72
|
this.reject = abort;
|
|
69
73
|
this.settle = () => { clear(); resolve(); };
|
|
70
74
|
signal.addEventListener('abort', abort, { once: true });
|
|
71
75
|
});
|
|
72
76
|
if (!this.active() || signal.aborted || this.stage !== 3)
|
|
73
|
-
throw
|
|
77
|
+
throw this.readinessError();
|
|
74
78
|
return { process, executionNonce: this.plan.executionNonce, endpoint: this.plan.endpoint, authenticated: true, ready: true, toolSchemaFingerprint: this.plan.identity.toolSchemaFingerprint, tools: localToolInventory().map(t => t.name), isCurrent: () => this.active() && this.stage === 3 };
|
|
75
79
|
}
|
|
76
80
|
authorize(process) { if (process !== this.process || !this.active() || this.stage !== 3)
|
|
@@ -55,8 +55,8 @@ export async function runManualCodexJob(input, deps) {
|
|
|
55
55
|
inner.attachCleanup(cleanup);
|
|
56
56
|
return inner.prepare(ctx);
|
|
57
57
|
}, async inspect(plan) { if (!inner)
|
|
58
|
-
throw Error('MANUAL_PILOT_NOT_PREPARED'); return inner.inspect(plan); }, revoke() { inner?.revoke(); }, attachCleanup(fn) { if (cleanup)
|
|
59
|
-
throw Error('MANUAL_PILOT_DUPLICATE_CLEANUP'); cleanup = fn; }, async close() { await inner?.close(); } };
|
|
58
|
+
throw Error('MANUAL_PILOT_NOT_PREPARED'); return inner.inspect(plan); }, revoke() { controller?.cancel(); inner?.revoke(); }, attachCleanup(fn) { if (cleanup)
|
|
59
|
+
throw Error('MANUAL_PILOT_DUPLICATE_CLEANUP'); cleanup = fn; }, async close() { controller?.cancel(); await inner?.close(); } };
|
|
60
60
|
const plugin = new CodexPlugin({ probe, options: () => buildCodexExecJobOptions({ jobId: job.id, worktreePath: options.worktreePath, stateRoot: input.stateRoot, env: input.env, productionJob: () => lazy, processHost: deps?.processHost }) });
|
|
61
61
|
adapter = await plugin.create(options);
|
|
62
62
|
return adapter;
|
|
@@ -25,8 +25,10 @@ export function createManualPilotController(input, deps) {
|
|
|
25
25
|
const claim = structuredClone(input.claim), releases = structuredClone(deps?.releases ?? CODEX_EXEC_RELEASES), readLocal = deps?.readLocal, now = deps?.now ?? Date.now;
|
|
26
26
|
const account = path.resolve(input.codexHome ?? path.join(input.home, '.codex'));
|
|
27
27
|
let used = false, closed = false, manual, host;
|
|
28
|
-
const
|
|
29
|
-
|
|
28
|
+
const queryAbort = new AbortController();
|
|
29
|
+
const cancel = () => { closed = true; queryAbort.abort(); manual?.revoke(); };
|
|
30
|
+
const close = async () => { cancel(); await host?.close(); };
|
|
31
|
+
return Object.freeze({ close, cancel, async prepare() {
|
|
30
32
|
if (used || closed)
|
|
31
33
|
throw rejected();
|
|
32
34
|
used = true;
|
|
@@ -65,7 +67,7 @@ export function createManualPilotController(input, deps) {
|
|
|
65
67
|
if (closed)
|
|
66
68
|
throw rejected();
|
|
67
69
|
stage = 'codex_manual_inventory';
|
|
68
|
-
const inventory = await (deps?.collectInventory ?? createManualProfileQuery().collectOnce)();
|
|
70
|
+
const inventory = await (deps?.collectInventory ?? createManualProfileQuery().collectOnce)(queryAbort.signal);
|
|
69
71
|
if (closed)
|
|
70
72
|
throw rejected();
|
|
71
73
|
stage = 'codex_manual_policy_scope';
|
|
@@ -1,30 +1,175 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
-
import { promisify } from 'node:util';
|
|
3
2
|
import { createHash } from 'node:crypto';
|
|
4
3
|
import { emptyProfileInventoryXml } from './codex-native-observation.js';
|
|
4
|
+
import { executionError, executionFailure } from './execution-failure.js';
|
|
5
5
|
const rejected = () => Error('CODEX_MANUAL_PROFILE_QUERY_REJECTED');
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
const AUTHENTICATED = 'MTEC_PROFILE_AUTHENTICATED_V1';
|
|
7
|
+
const QUERY_TIMEOUT_MS = 60_000;
|
|
8
|
+
const failureMarkers = Object.freeze({
|
|
9
|
+
MTEC_PROFILE_AUTH_CANCELLED: 'CODEX_MANUAL_AUTH_CANCELLED',
|
|
10
|
+
MTEC_PROFILE_AUTH_REJECTED: 'CODEX_MANUAL_AUTH_REJECTED',
|
|
11
|
+
MTEC_PROFILE_QUERY_TIMEOUT: 'CODEX_MANUAL_QUERY_TIMEOUT',
|
|
12
|
+
MTEC_PROFILE_QUERY_FAILED: 'CODEX_MANUAL_QUERY_COMMAND_FAILED',
|
|
13
|
+
MTEC_PROFILE_NONROOT: 'CODEX_MANUAL_QUERY_NONROOT',
|
|
14
|
+
});
|
|
15
|
+
const failure = (code) => executionError(rejected(), 'codex_manual_inventory', code);
|
|
16
|
+
/** No Runner deadline while the employee is authenticating. The fixed first
|
|
17
|
+
* command acquires the grant; only then does the bounded read-only query start.
|
|
18
|
+
* No user commands, credential writes or persistent privileged process. */
|
|
19
|
+
export const MANUAL_PROFILES_ARGUMENTS = Object.freeze(['-e', `with timeout of 2147483647 seconds
|
|
20
|
+
try
|
|
21
|
+
set uid to do shell script "/usr/bin/id -u" with administrator privileges
|
|
22
|
+
on error messageText number errorNumber
|
|
23
|
+
if errorNumber is -128 then
|
|
24
|
+
log "MTEC_PROFILE_AUTH_CANCELLED"
|
|
25
|
+
else
|
|
26
|
+
log "MTEC_PROFILE_AUTH_REJECTED"
|
|
27
|
+
end if
|
|
28
|
+
error "Administrator authentication failed" number 10001
|
|
29
|
+
end try
|
|
30
|
+
if uid is not "0" then
|
|
31
|
+
log "MTEC_PROFILE_NONROOT"
|
|
32
|
+
error "Unexpected query identity" number 10002
|
|
33
|
+
end if
|
|
34
|
+
log "MTEC_PROFILE_AUTHENTICATED_V1"
|
|
35
|
+
try
|
|
36
|
+
with timeout of 60 seconds
|
|
37
|
+
set inventory to do shell script "/usr/bin/profiles list -all -output stdout-xml" with administrator privileges
|
|
38
|
+
end timeout
|
|
39
|
+
on error messageText number errorNumber
|
|
40
|
+
if errorNumber is -1712 then
|
|
41
|
+
log "MTEC_PROFILE_QUERY_TIMEOUT"
|
|
42
|
+
else
|
|
43
|
+
log "MTEC_PROFILE_QUERY_FAILED"
|
|
44
|
+
end if
|
|
45
|
+
error "Profile inventory failed" number 10003
|
|
46
|
+
end try
|
|
47
|
+
return uid & linefeed & inventory
|
|
48
|
+
end timeout`]);
|
|
49
|
+
/** Injectable process seam, never a task-provided command. Protocol markers are
|
|
50
|
+
* fixed by our script; raw native stderr is discarded rather than published. */
|
|
51
|
+
export function createManualProfileProcessRunner(start = execFile) {
|
|
52
|
+
return (binary, args, options) => new Promise((resolve, reject) => {
|
|
53
|
+
let pending = '', marked = false, reason, settled = false;
|
|
54
|
+
let child;
|
|
55
|
+
const finishError = (error) => { if (settled)
|
|
56
|
+
return; settled = true; reject(error); };
|
|
57
|
+
try {
|
|
58
|
+
child = start(binary, [...args], { encoding: 'utf8', timeout: 0, maxBuffer: 1_048_576, signal: options.signal, env: { PATH: '/usr/bin:/bin', LANG: 'C', LC_ALL: 'C' } }, (error, stdout, stderr) => {
|
|
59
|
+
if (settled)
|
|
60
|
+
return;
|
|
61
|
+
settled = true;
|
|
62
|
+
if (error) {
|
|
63
|
+
reject(reason ? failure(reason) : executionError(error, 'codex_manual_inventory', 'CODEX_MANUAL_QUERY_COMMAND_FAILED'));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (reason) {
|
|
67
|
+
reject(failure(reason));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (!marked) {
|
|
71
|
+
reject(failure('CODEX_MANUAL_QUERY_PROTOCOL_INVALID'));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const rest = String(stderr).split(/\r?\n/).filter(line => line !== AUTHENTICATED && line !== '').join('\n');
|
|
75
|
+
resolve({ stdout: String(stdout), stderr: rest });
|
|
76
|
+
});
|
|
77
|
+
child.stderr?.on('data', (chunk) => {
|
|
78
|
+
if (settled)
|
|
79
|
+
return;
|
|
80
|
+
pending += chunk.toString();
|
|
81
|
+
if (pending.length > 1_048_576) {
|
|
82
|
+
finishError(failure('CODEX_MANUAL_QUERY_OUTPUT_INVALID'));
|
|
83
|
+
child?.kill();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
let end;
|
|
87
|
+
while ((end = pending.indexOf('\n')) !== -1) {
|
|
88
|
+
const line = pending.slice(0, end).replace(/\r$/, '');
|
|
89
|
+
pending = pending.slice(end + 1);
|
|
90
|
+
if (Object.hasOwn(failureMarkers, line))
|
|
91
|
+
reason = failureMarkers[line];
|
|
92
|
+
if (line === AUTHENTICATED) {
|
|
93
|
+
if (marked) {
|
|
94
|
+
finishError(failure('CODEX_MANUAL_QUERY_PROTOCOL_INVALID'));
|
|
95
|
+
child?.kill();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
marked = true;
|
|
99
|
+
try {
|
|
100
|
+
options.onAuthenticated();
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
finishError(failure('CODEX_MANUAL_QUERY_PROTOCOL_INVALID'));
|
|
104
|
+
child?.kill();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
finishError(executionError(error, 'codex_manual_inventory', 'CODEX_MANUAL_QUERY_COMMAND_FAILED'));
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const nativeRun = createManualProfileProcessRunner();
|
|
117
|
+
/** Foreground-only, single-use, cancellable. Human authentication wait is not
|
|
118
|
+
* query age: evidence is stamped when the bounded inventory actually finishes. */
|
|
13
119
|
export function createManualProfileQuery(input) {
|
|
14
120
|
const run = input?.run ?? nativeRun, now = input?.now ?? Date.now;
|
|
15
121
|
let used = false;
|
|
16
|
-
return Object.freeze({ async collectOnce() {
|
|
122
|
+
return Object.freeze({ async collectOnce(signal) {
|
|
17
123
|
if (used)
|
|
18
|
-
throw
|
|
124
|
+
throw failure('CODEX_MANUAL_QUERY_REUSED');
|
|
19
125
|
used = true;
|
|
126
|
+
const controller = new AbortController();
|
|
127
|
+
let timer;
|
|
128
|
+
let firstFailure, authenticatedAt;
|
|
129
|
+
const abort = (code) => { firstFailure ??= failure(code); controller.abort(); };
|
|
130
|
+
const externalAbort = () => abort('CODEX_MANUAL_QUERY_ABORTED');
|
|
131
|
+
signal?.addEventListener('abort', externalAbort, { once: true });
|
|
20
132
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
throw
|
|
133
|
+
if (signal?.aborted) {
|
|
134
|
+
externalAbort();
|
|
135
|
+
throw firstFailure;
|
|
136
|
+
}
|
|
137
|
+
const startedAt = now();
|
|
138
|
+
if (!Number.isSafeInteger(startedAt) || startedAt < 0)
|
|
139
|
+
throw failure('CODEX_MANUAL_QUERY_PROTOCOL_INVALID');
|
|
140
|
+
// Reject independently of whether an injected/native runner settles on abort.
|
|
141
|
+
const cancelled = new Promise((_, reject) => { controller.signal.addEventListener('abort', () => reject(firstFailure), { once: true }); });
|
|
142
|
+
const operation = run('/usr/bin/osascript', MANUAL_PROFILES_ARGUMENTS, { signal: controller.signal, onAuthenticated() {
|
|
143
|
+
if (controller.signal.aborted || authenticatedAt !== undefined)
|
|
144
|
+
throw failure('CODEX_MANUAL_QUERY_PROTOCOL_INVALID');
|
|
145
|
+
authenticatedAt = now();
|
|
146
|
+
if (!Number.isSafeInteger(authenticatedAt) || authenticatedAt < startedAt)
|
|
147
|
+
throw failure('CODEX_MANUAL_QUERY_PROTOCOL_INVALID');
|
|
148
|
+
timer = setTimeout(() => abort('CODEX_MANUAL_QUERY_TIMEOUT'), QUERY_TIMEOUT_MS);
|
|
149
|
+
} });
|
|
150
|
+
const result = await Promise.race([operation, cancelled]), observedAt = now();
|
|
151
|
+
if (firstFailure)
|
|
152
|
+
throw firstFailure;
|
|
153
|
+
if (authenticatedAt === undefined || !Number.isSafeInteger(observedAt) || observedAt < authenticatedAt)
|
|
154
|
+
throw failure('CODEX_MANUAL_QUERY_PROTOCOL_INVALID');
|
|
155
|
+
if (observedAt - authenticatedAt > QUERY_TIMEOUT_MS)
|
|
156
|
+
throw failure('CODEX_MANUAL_QUERY_TIMEOUT');
|
|
157
|
+
if (typeof result.stdout !== 'string' || result.stderr !== '' || !result.stdout.includes('\n'))
|
|
158
|
+
throw failure('CODEX_MANUAL_QUERY_OUTPUT_INVALID');
|
|
159
|
+
if (!result.stdout.startsWith('0\n'))
|
|
160
|
+
throw failure('CODEX_MANUAL_QUERY_NONROOT');
|
|
161
|
+
if (!emptyProfileInventoryXml(result.stdout.slice(2)))
|
|
162
|
+
throw failure('CODEX_MANUAL_QUERY_OUTPUT_INVALID');
|
|
24
163
|
return Object.freeze({ version: 1, exit: 0, euid: 0, scope: 'machine_and_current_user', complete: true, empty: true, observedAt, inventoryFingerprint: createHash('sha256').update(result.stdout.slice(2)).digest('hex') });
|
|
25
164
|
}
|
|
26
|
-
catch {
|
|
27
|
-
|
|
165
|
+
catch (error) {
|
|
166
|
+
const diagnostic = executionFailure(error, 'codex_manual_inventory', 'CODEX_MANUAL_QUERY_COMMAND_FAILED');
|
|
167
|
+
throw firstFailure ?? executionError(error, 'codex_manual_inventory', diagnostic.code);
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
if (timer)
|
|
171
|
+
clearTimeout(timer);
|
|
172
|
+
signal?.removeEventListener('abort', externalAbort);
|
|
28
173
|
}
|
|
29
174
|
} });
|
|
30
175
|
}
|
|
@@ -24,8 +24,12 @@ export function executionFailure(error, stage, fallback = 'EXECUTION_FAILED') {
|
|
|
24
24
|
}
|
|
25
25
|
return { stage, code: fallback };
|
|
26
26
|
}
|
|
27
|
-
export function executionError(error, stage, fallback) {
|
|
27
|
+
export function executionError(error, stage, fallback, detail) {
|
|
28
28
|
const diagnostic = executionFailure(error, stage, fallback);
|
|
29
|
+
// Only trusted callers supply this fixed vocabulary. Serialized provider
|
|
30
|
+
// errors, messages and stderr never become diagnostic authority.
|
|
31
|
+
if (!issued.has(error) && ['mcp_initialize_missing', 'mcp_initialized_missing', 'mcp_inventory_missing', 'mcp_discovery_complete'].includes(detail))
|
|
32
|
+
diagnostic.detail = detail;
|
|
29
33
|
const safe = new Error(fallback);
|
|
30
34
|
issued.set(safe, Object.freeze(diagnostic));
|
|
31
35
|
return safe;
|