mtech-ai-local-runner 0.1.9 → 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 +23 -0
- package/dist/src/adapters/claude-code/adapter.js +10 -27
- package/dist/src/adapters/claude-code/runtime-options.js +1 -1
- package/dist/src/adapters/codex/account-sources.js +113 -0
- package/dist/src/adapters/codex/adapter.js +60 -4
- package/dist/src/adapters/codex/config-builder.js +3 -0
- package/dist/src/adapters/codex/exec-invocation.js +62 -0
- package/dist/src/adapters/codex/exec-launch-host.js +215 -0
- package/dist/src/adapters/codex/exec-observer.js +83 -0
- package/dist/src/adapters/codex/exec-preflight.js +41 -0
- package/dist/src/adapters/codex/exec-release-policy.js +44 -0
- package/dist/src/adapters/codex/exec-runtime-options.js +122 -0
- package/dist/src/adapters/codex/exec-tool-gate.js +81 -0
- package/dist/src/adapters/codex/global-instructions.js +32 -0
- package/dist/src/adapters/codex/manual-policy-scope.js +67 -0
- package/dist/src/adapters/codex/native-policy.js +20 -4
- package/dist/src/adapters/codex/native-session.js +4 -23
- package/dist/src/adapters/codex/unmanaged-scope.js +85 -0
- package/dist/src/auth/token-provider.js +2 -0
- package/dist/src/cli.js +54 -6
- package/dist/src/config.js +9 -2
- package/dist/src/daemon/execution-coordinator.js +25 -4
- package/dist/src/daemon/manual-codex.js +83 -0
- package/dist/src/daemon/runtime.js +17 -9
- package/dist/src/gateway-client.js +3 -2
- package/dist/src/safety/codex-exec-production-host.js +249 -0
- package/dist/src/safety/codex-exec-releases.js +3 -0
- package/dist/src/safety/codex-manual-controller.js +84 -0
- package/dist/src/safety/codex-manual-profile-query.js +30 -0
- package/dist/src/safety/codex-manual-registered.js +30 -0
- package/dist/src/safety/codex-native-observation.js +68 -0
- package/dist/src/safety/codex-native-process.js +59 -0
- package/dist/src/safety/codex-production-host.js +1 -1
- package/dist/src/safety/codex-reviewed-manual-release.js +37 -0
- package/dist/src/safety/codex-runtime-host.js +36 -0
- package/dist/src/safety/execution-failure.js +32 -0
- package/dist/src/safety/hook-cli.js +18 -2
- package/dist/src/safety/hook-command.js +156 -0
- package/dist/src/safety/hook-policy-builder.js +2 -0
- package/dist/src/safety/local-tool-server.js +41 -4
- package/dist/src/safety/security-requirements.js +11 -0
- package/dist/src/safety/stable-file-read.js +33 -0
- package/native/bin/codex-observe-darwin-arm64 +0 -0
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -10,6 +10,13 @@ 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.10: enforced PreToolUse hook
|
|
14
|
+
|
|
15
|
+
Earlier releases configured a hook command that the shell could not run, so Claude
|
|
16
|
+
Code ran tool calls without the hook policy. This release runs the hook through
|
|
17
|
+
Node. Unless the packaged hook denies a `git push origin HEAD` probe, `setup` and
|
|
18
|
+
the daemon stop and `doctor` reports `hook_not_enforcing`.
|
|
19
|
+
|
|
13
20
|
## 0.1.9: gated Codex foundation and shared CLI security
|
|
14
21
|
|
|
15
22
|
This package includes the production Codex host/probe and common authenticated
|
|
@@ -117,3 +124,19 @@ These preparation controls end at binding and do not impose time, turn, token
|
|
|
117
124
|
or cost termination limits on an approved unbounded employee-local Claude run.
|
|
118
125
|
Existing generation fencing, scope restrictions, cancellation and audit remain
|
|
119
126
|
in force.
|
|
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
|
+
|
|
140
|
+
### Manual Codex pilot (0.1.11)
|
|
141
|
+
|
|
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,20 +1,16 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { requireSecurityRequirements } from '../../safety/security-requirements.js';
|
|
3
3
|
import { createJobSecurityReceipt } from '../../safety/job-security-receipt.js';
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
5
4
|
import { isTerminalEventType } from "../../protocol.js";
|
|
6
5
|
import { parseClaudeLine } from "./stream-parser.js";
|
|
7
6
|
import { compilePrompt } from "./prompt-compiler.js";
|
|
8
7
|
import { completionEvent } from "./completion-report.js";
|
|
9
8
|
import { requireJobSecurity } from "../../safety/job-security-policy.js";
|
|
10
|
-
import { buildHookPolicy, encodeHookPolicy } from "../../safety/hook-policy-builder.js";
|
|
11
|
-
import {
|
|
9
|
+
import { HOOK_POLICY_ENV_VAR, buildHookPolicy, encodeHookPolicy } from "../../safety/hook-policy-builder.js";
|
|
10
|
+
import { formatHookCommand } from "../../safety/hook-command.js";
|
|
12
11
|
const ALLOWED_ENV_KEYS = ["PATH", "HOME", "USER", "LOGNAME", "LANG", "LC_ALL", "TMPDIR", "NODE_ENV", "CLAUDE_CONFIG_DIR"];
|
|
13
12
|
const ALLOWED_SUPERVISOR_COMMANDS = new Set(["continue", "answer_question", "request_revision", "request_checkpoint", "submit_for_review"]);
|
|
14
13
|
const LOCAL_NAMES = ["local_read", "local_search", "local_edit", "local_test"].map(name => `mcp__ai_local__${name}`);
|
|
15
|
-
function defaultPathExists(p) {
|
|
16
|
-
return existsSync(p);
|
|
17
|
-
}
|
|
18
14
|
function allowlistedEnv(env) {
|
|
19
15
|
const result = {};
|
|
20
16
|
for (const key of ALLOWED_ENV_KEYS) {
|
|
@@ -92,7 +88,7 @@ export class ClaudeCodeAdapter {
|
|
|
92
88
|
if (!ctx.sessionId) {
|
|
93
89
|
throw new Error("refusing to start: no runner-generated session ID was supplied");
|
|
94
90
|
}
|
|
95
|
-
const
|
|
91
|
+
const hookCommand = this.requireTrustedHookCommand();
|
|
96
92
|
const prompt = compilePrompt(ctx.taskPackage, { workItemLabel: this.options.workItemLabel });
|
|
97
93
|
try {
|
|
98
94
|
if (!this.options.createTools || !this.options.writeLocalMcpConfig)
|
|
@@ -115,7 +111,7 @@ export class ClaudeCodeAdapter {
|
|
|
115
111
|
});
|
|
116
112
|
const settings = {
|
|
117
113
|
hooks: {
|
|
118
|
-
PreToolUse: [{ matcher: "*", hooks: [{ type: "command", command:
|
|
114
|
+
PreToolUse: [{ matcher: "*", hooks: [{ type: "command", command: hookCommand }] }],
|
|
119
115
|
},
|
|
120
116
|
};
|
|
121
117
|
const args = [
|
|
@@ -247,25 +243,12 @@ export class ClaudeCodeAdapter {
|
|
|
247
243
|
}
|
|
248
244
|
}
|
|
249
245
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
throw new Error("refusing to start: PreToolUse hook must use a safe executable path without shell metacharacters");
|
|
257
|
-
}
|
|
258
|
-
const worktreeRoot = path.resolve(this.options.worktreeRoot);
|
|
259
|
-
const rel = path.relative(worktreeRoot, path.resolve(hookPath));
|
|
260
|
-
const isInsideWorktree = rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
261
|
-
if (isInsideWorktree) {
|
|
262
|
-
throw new Error("refusing to start: PreToolUse hook executable must live outside the assigned worktree");
|
|
263
|
-
}
|
|
264
|
-
const exists = this.options.hookExecutableExists ?? defaultPathExists;
|
|
265
|
-
if (!exists(hookPath)) {
|
|
266
|
-
throw new Error("refusing to start: PreToolUse hook executable was not found at the trusted path");
|
|
267
|
-
}
|
|
268
|
-
return hookPath;
|
|
246
|
+
/** Claude Code treats a hook it cannot run as no objection, so the hook runs through Node. */
|
|
247
|
+
requireTrustedHookCommand() {
|
|
248
|
+
return formatHookCommand({
|
|
249
|
+
nodeExecutablePath: this.options.hookNodeExecutablePath ?? process.execPath,
|
|
250
|
+
hookScriptPath: this.options.hookExecutablePath,
|
|
251
|
+
}, { worktreeRoot: this.options.worktreeRoot, exists: this.options.hookExecutableExists });
|
|
269
252
|
}
|
|
270
253
|
requireHandle() {
|
|
271
254
|
if (!this.handle) {
|
|
@@ -27,7 +27,7 @@ export function createClaudeCodeAdapterOptions(input) {
|
|
|
27
27
|
throw new Error('Claude account configuration path must be absolute');
|
|
28
28
|
return {
|
|
29
29
|
processHost: new NodeProcessGroupHost(), worktreeRoot: checkout, jobId: input.jobId, securityPolicy: input.securityPolicy,
|
|
30
|
-
cwd, env, binaryPath: input.binaryPath, hookExecutablePath: input.hookExecutablePath,
|
|
30
|
+
cwd, env, binaryPath: input.binaryPath, hookExecutablePath: input.hookExecutablePath, hookNodeExecutablePath: input.hookNodeExecutablePath,
|
|
31
31
|
maxBudgetUsd: input.maxBudgetUsd ?? 1, workItemLabel: input.workItemLabel ?? input.jobId,
|
|
32
32
|
registeredReadTools: [], writeMcpConfig: () => { throw new Error('Gateway relay is not a model tool endpoint'); },
|
|
33
33
|
createTools: async (ctx, pkg, approved) => {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { lstat, open, readdir, realpath } from 'node:fs/promises';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { parse } from 'smol-toml';
|
|
6
|
+
function denied() { throw new Error('CODEX_ACCOUNT_SOURCE_REJECTED'); }
|
|
7
|
+
const digest = (value) => createHash('sha256').update(value).digest('hex');
|
|
8
|
+
async function stat(file) { try {
|
|
9
|
+
return await lstat(file);
|
|
10
|
+
}
|
|
11
|
+
catch (e) {
|
|
12
|
+
if (e.code === 'ENOENT')
|
|
13
|
+
return null;
|
|
14
|
+
throw e;
|
|
15
|
+
} }
|
|
16
|
+
async function file(file) {
|
|
17
|
+
const s = await stat(file);
|
|
18
|
+
if (!s)
|
|
19
|
+
return null;
|
|
20
|
+
if (!s.isFile() || s.isSymbolicLink() || s.nlink !== 1 || s.size > 65_536)
|
|
21
|
+
denied();
|
|
22
|
+
const fd = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
23
|
+
try {
|
|
24
|
+
const actual = await fd.stat();
|
|
25
|
+
if (!actual.isFile() || actual.nlink !== 1 || actual.dev !== s.dev || actual.ino !== s.ino || actual.size > 65_536)
|
|
26
|
+
denied();
|
|
27
|
+
const bytes = Buffer.alloc(65_537);
|
|
28
|
+
let total = 0;
|
|
29
|
+
while (total < bytes.length) {
|
|
30
|
+
const { bytesRead } = await fd.read(bytes, total, bytes.length - total, total);
|
|
31
|
+
if (!bytesRead)
|
|
32
|
+
break;
|
|
33
|
+
total += bytesRead;
|
|
34
|
+
}
|
|
35
|
+
const after = await fd.stat(), named = await stat(file);
|
|
36
|
+
if (!named || !named.isFile() || named.isSymbolicLink() || named.nlink !== 1 || named.dev !== actual.dev || named.ino !== actual.ino
|
|
37
|
+
|| after.nlink !== 1 || total > 65_536 || total !== actual.size || after.size !== actual.size || after.mtimeMs !== actual.mtimeMs || after.ctimeMs !== actual.ctimeMs)
|
|
38
|
+
denied();
|
|
39
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes.subarray(0, total));
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
await fd.close();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function preferences(text) {
|
|
46
|
+
const v = text === null ? {} : parse(text), result = {};
|
|
47
|
+
for (const [key, value] of Object.entries(v)) {
|
|
48
|
+
// Every accepted leaf is replaced by the trusted generated -c overrides.
|
|
49
|
+
// Tables (hooks/features/MCP/providers/profiles), unknown fields and backend
|
|
50
|
+
// selection are deliberately not ambient inputs, even when disabled/empty.
|
|
51
|
+
if (key === 'project_doc_max_bytes') {
|
|
52
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
|
|
53
|
+
denied();
|
|
54
|
+
result[key] = value;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (!['model', 'model_provider', 'approval_policy', 'default_permissions', 'forced_login_method', 'web_search'].includes(key)
|
|
58
|
+
|| typeof value !== 'string' || !value || value.length > 256 || /[\r\n\0]/.test(value))
|
|
59
|
+
denied();
|
|
60
|
+
if (key === 'model_provider' && value !== 'openai' || key === 'forced_login_method' && value !== 'chatgpt')
|
|
61
|
+
denied();
|
|
62
|
+
if (key === 'approval_policy' && !['never', 'on-request', 'untrusted', 'on-failure'].includes(value))
|
|
63
|
+
denied();
|
|
64
|
+
if (key === 'web_search' && !['disabled', 'cached', 'live'].includes(value))
|
|
65
|
+
denied();
|
|
66
|
+
result[key] = value;
|
|
67
|
+
}
|
|
68
|
+
return Object.freeze(Object.fromEntries(Object.entries(result).sort(([a], [b]) => a.localeCompare(b))));
|
|
69
|
+
}
|
|
70
|
+
export async function inspectCodexAccountSources(home, cwd, account = path.join(home, '.codex')) {
|
|
71
|
+
try {
|
|
72
|
+
if (![home, cwd, account].every(p => path.isAbsolute(p) && path.resolve(p) === p && !/[\r\n\0]/.test(p)))
|
|
73
|
+
denied();
|
|
74
|
+
const root = await stat(account);
|
|
75
|
+
if (root && (!root.isDirectory() || root.isSymbolicLink() || await realpath(account) !== account))
|
|
76
|
+
denied();
|
|
77
|
+
const blocked = ['/etc/codex/rules'];
|
|
78
|
+
let parent = cwd;
|
|
79
|
+
while (true) {
|
|
80
|
+
const candidate = path.join(parent, '.codex');
|
|
81
|
+
if (candidate !== account)
|
|
82
|
+
blocked.push(candidate);
|
|
83
|
+
const next = path.dirname(parent);
|
|
84
|
+
if (next === parent)
|
|
85
|
+
break;
|
|
86
|
+
parent = next;
|
|
87
|
+
}
|
|
88
|
+
for (const entry of blocked)
|
|
89
|
+
if (await stat(entry))
|
|
90
|
+
denied();
|
|
91
|
+
const configPath = path.join(account, 'config.toml'), raw = await file(configPath), userConfig = preferences(raw);
|
|
92
|
+
const instructions = await file(path.join(account, 'AGENTS.md'));
|
|
93
|
+
if (instructions !== null && instructions.trim() !== '')
|
|
94
|
+
denied();
|
|
95
|
+
const rulesPath = path.join(account, 'rules'), rules = await stat(rulesPath);
|
|
96
|
+
if (rules && (!rules.isDirectory() || rules.isSymbolicLink() || (await readdir(rulesPath)).length !== 0))
|
|
97
|
+
denied();
|
|
98
|
+
return Object.freeze({ version: 1, configPath, userConfig, fingerprint: digest(JSON.stringify({ account, config: raw === null ? null : digest(raw), instructions: instructions === null ? null : digest(instructions), rules: rules ? 'empty-directory' : null })) });
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
denied();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** Retain one source snapshot; any later drift revokes preparation/turn startup. */
|
|
105
|
+
export function codexAccountSourceGuard(home, cwd, account) {
|
|
106
|
+
let snapshot;
|
|
107
|
+
return async () => {
|
|
108
|
+
const current = await inspectCodexAccountSources(home, cwd, account);
|
|
109
|
+
if (snapshot && snapshot.fingerprint !== current.fingerprint)
|
|
110
|
+
denied();
|
|
111
|
+
return snapshot ??= (current);
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { executionError } from '../../safety/execution-failure.js';
|
|
2
|
+
import { CodexExecLaunchHost } from './exec-launch-host.js';
|
|
1
3
|
import { requireSecurityRequirements } from '../../safety/security-requirements.js';
|
|
2
4
|
import { createJobSecurityReceipt } from '../../safety/job-security-receipt.js';
|
|
3
5
|
import { requireJobSecurity } from '../../safety/job-security-policy.js';
|
|
@@ -16,8 +18,11 @@ export class CodexAdapter {
|
|
|
16
18
|
tools;
|
|
17
19
|
started = false;
|
|
18
20
|
receipt;
|
|
19
|
-
|
|
21
|
+
terminalAudit;
|
|
22
|
+
securityReceipt() { return this.terminalAudit ? structuredClone(this.terminalAudit) : this.usingOfficialExec ? this.ownedExec?.receipt() : this.receipt ? structuredClone(this.receipt) : undefined; }
|
|
20
23
|
cancelled = false;
|
|
24
|
+
usingOfficialExec = false;
|
|
25
|
+
ownedExec;
|
|
21
26
|
parser = new CodexStreamParser();
|
|
22
27
|
startupAbort = new AbortController();
|
|
23
28
|
securityPolicy;
|
|
@@ -31,9 +36,43 @@ export class CodexAdapter {
|
|
|
31
36
|
if (!await this.options.verifyCompatibility())
|
|
32
37
|
throw new Error(CODEX_UNSUPPORTED_DIAGNOSTIC);
|
|
33
38
|
this.assertNotCancelled();
|
|
39
|
+
const approved = object(ctx.approvedTaskPackage);
|
|
40
|
+
const transport = approved.runtime_transport;
|
|
41
|
+
if (transport !== undefined && transport !== 'retained_app_server') {
|
|
42
|
+
if (transport !== 'official_exec_v1')
|
|
43
|
+
throw new Error('CODEX_EXEC_TRANSPORT_UNAVAILABLE');
|
|
44
|
+
const host = this.options.officialExec ?? await this.options.createOfficialExec?.(ctx, this.startupAbort.signal);
|
|
45
|
+
if (!(host instanceof CodexExecLaunchHost))
|
|
46
|
+
throw new Error('CODEX_EXEC_TRANSPORT_UNAVAILABLE');
|
|
47
|
+
if (this.cancelled) {
|
|
48
|
+
await this.options.closeOfficialExec?.();
|
|
49
|
+
throw new Error('CODEX_EXEC_PREFLIGHT_REJECTED');
|
|
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
|
+
}
|
|
57
|
+
this.ownedExec = host;
|
|
58
|
+
this.usingOfficialExec = true;
|
|
59
|
+
let stage = 'codex_prompt';
|
|
60
|
+
try {
|
|
61
|
+
const prompt = compileCodexPrompt(object(ctx.taskPackage), this.options.jobId);
|
|
62
|
+
stage = 'codex_launch';
|
|
63
|
+
this.handle = await this.ownedExec.start(ctx, this.options.jobId, prompt, this.startupAbort.signal);
|
|
64
|
+
this.receipt = this.ownedExec.receipt();
|
|
65
|
+
this.assertNotCancelled();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
const safe = executionError(error, stage, 'CODEX_EXEC_PREFLIGHT_REJECTED');
|
|
70
|
+
await this.cancel().catch(() => { });
|
|
71
|
+
throw safe;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
34
74
|
if (!this.options.startVerifiedSession)
|
|
35
75
|
throw new Error("CODEX_PRE_MODEL_UNAVAILABLE");
|
|
36
|
-
const approved = object(ctx.approvedTaskPackage);
|
|
37
76
|
const pkg = object(ctx.taskPackage);
|
|
38
77
|
const execution = object(pkg.execution);
|
|
39
78
|
const source = object(approved.repository);
|
|
@@ -122,6 +161,15 @@ export class CodexAdapter {
|
|
|
122
161
|
const events = this.parser.parse(line);
|
|
123
162
|
for (const event of events) {
|
|
124
163
|
if (["completed", "failed", "scope_conflict", "blocked"].includes(event.eventType)) {
|
|
164
|
+
if (event.eventType === 'completed' && this.usingOfficialExec) {
|
|
165
|
+
const audit = this.ownedExec?.receipt();
|
|
166
|
+
if (!audit) {
|
|
167
|
+
await this.cancel();
|
|
168
|
+
yield { eventType: 'failed', summary: 'Codex completion lacked its validated security audit' };
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
this.terminalAudit = structuredClone(audit);
|
|
172
|
+
}
|
|
125
173
|
await this.cancel();
|
|
126
174
|
yield event;
|
|
127
175
|
return;
|
|
@@ -139,6 +187,8 @@ export class CodexAdapter {
|
|
|
139
187
|
async cancel() {
|
|
140
188
|
this.cancelled = true;
|
|
141
189
|
try {
|
|
190
|
+
if (this.usingOfficialExec)
|
|
191
|
+
await this.ownedExec?.cancel();
|
|
142
192
|
this.options.revokeSecurity?.();
|
|
143
193
|
}
|
|
144
194
|
finally {
|
|
@@ -148,10 +198,16 @@ export class CodexAdapter {
|
|
|
148
198
|
finally {
|
|
149
199
|
this.startupAbort.abort();
|
|
150
200
|
try {
|
|
151
|
-
this.
|
|
201
|
+
if (!this.usingOfficialExec)
|
|
202
|
+
this.handle?.kill("SIGKILL");
|
|
152
203
|
}
|
|
153
204
|
finally {
|
|
154
|
-
|
|
205
|
+
try {
|
|
206
|
+
await this.tools?.close();
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
await this.options.closeOfficialExec?.();
|
|
210
|
+
}
|
|
155
211
|
}
|
|
156
212
|
}
|
|
157
213
|
}
|
|
@@ -32,7 +32,10 @@ export function buildCodexInvocation(input) {
|
|
|
32
32
|
if (input.scratch)
|
|
33
33
|
env.TMPDIR = input.scratch.tmpDir;
|
|
34
34
|
return {
|
|
35
|
+
// The owned control cwd is deliberately outside Git. Runner separately verifies
|
|
36
|
+
// the approved source SHA and scope; this flag does not change permissions.
|
|
35
37
|
args: ["--no-daemon", "exec", ...CODEX_ISOLATION_EXEC_FLAGS,
|
|
38
|
+
...(input.sourceRoot && input.sourceRoot !== input.worktreeRoot ? ["--skip-git-repo-check"] : []),
|
|
36
39
|
...overrides.flatMap((setting) => ["-c", setting]),
|
|
37
40
|
"--model", input.modelId, "-C", input.worktreeRoot, "--output-schema", input.schemaPath, "-"],
|
|
38
41
|
env,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { UNMANAGED_EXEC_MODE } from './unmanaged-scope.js';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { CODEX_GLOBAL_INSTRUCTION_WRAPPER, globalInstructionProfile } from './global-instructions.js';
|
|
4
|
+
import { randomBytes } from 'node:crypto';
|
|
5
|
+
import { buildCodexInvocation } from './config-builder.js';
|
|
6
|
+
import { securityRequirementsFingerprint as hash } from '../../safety/security-requirements.js';
|
|
7
|
+
export const EXEC_TRANSPORT = 'official_exec_v1';
|
|
8
|
+
export const EXEC_RELEASE_MODE = 'supported_cli_exec_release_policy_v2';
|
|
9
|
+
export const EXEC_MODE = 'supported_cli_exec_policy_v1';
|
|
10
|
+
export const isReleaseExecMode = (mode) => mode === EXEC_RELEASE_MODE || mode === UNMANAGED_EXEC_MODE;
|
|
11
|
+
const plans = new WeakMap();
|
|
12
|
+
const consumed = new WeakSet();
|
|
13
|
+
const fail = () => new Error('CODEX_EXEC_PLAN_REJECTED');
|
|
14
|
+
export function buildOfficialExecPlan(input) {
|
|
15
|
+
try {
|
|
16
|
+
const i = structuredClone(input.identity), inv = structuredClone(input.invocation);
|
|
17
|
+
if (input.policyMode !== undefined && !isReleaseExecMode(input.policyMode))
|
|
18
|
+
throw fail();
|
|
19
|
+
const keys = ['jobId', 'generation', 'actorId', 'runnerId', 'sourceSha', 'binarySha256', 'releaseManifestFingerprint', 'requirementsFingerprint', 'contractFingerprint', 'taskPackageFingerprint', 'launchFingerprint', 'commandsFingerprint', 'toolSchemaFingerprint', 'barrierFingerprint'];
|
|
20
|
+
if (JSON.stringify(Object.keys(i).sort()) !== JSON.stringify(keys.sort()) || !Number.isSafeInteger(i.generation) || i.generation < 1 || !/^[a-f0-9]{40}$/.test(i.sourceSha))
|
|
21
|
+
throw fail();
|
|
22
|
+
for (const [key, value] of Object.entries(i))
|
|
23
|
+
if (key !== 'generation' && (typeof value !== 'string' || !value || value.length > 256 || /[\r\n\0]/.test(value) || !['jobId', 'actorId', 'runnerId', 'sourceSha'].includes(key) && !/^[a-f0-9]{64}$/.test(value)))
|
|
24
|
+
throw fail();
|
|
25
|
+
if (!path.isAbsolute(input.binary) || /[\r\n\0]/.test(input.binary) || !['file', 'keyring', 'auto'].includes(input.credentialBackend) || !inv.scratch || !inv.sourceRoot)
|
|
26
|
+
throw fail();
|
|
27
|
+
const allowed = ['HOME', 'CODEX_HOME', 'PATH', 'USER', 'LOGNAME', 'LANG', 'LC_ALL', 'TMPDIR'];
|
|
28
|
+
if (Object.keys(inv.env).some(k => inv.env[k] !== undefined && (!allowed.includes(k) || typeof inv.env[k] !== 'string' || /[\r\n\0]/.test(inv.env[k]) || inv.env[k].length > 32768)) || inv.env.PATH?.split(path.delimiter).some(p => !path.isAbsolute(p)))
|
|
29
|
+
throw fail();
|
|
30
|
+
const cwd = path.resolve(inv.worktreeRoot), source = path.resolve(inv.sourceRoot);
|
|
31
|
+
if (cwd === source || cwd.startsWith(source + path.sep) || source.startsWith(cwd + path.sep))
|
|
32
|
+
throw fail();
|
|
33
|
+
const generated = buildCodexInvocation(inv), accountRoot = path.resolve(inv.env.CODEX_HOME ?? path.join(inv.env.HOME, '.codex'));
|
|
34
|
+
// Preserve the original backend explicitly; this is not proof of credentials.
|
|
35
|
+
generated.args.splice(generated.args.indexOf('--model'), 0, '-c', `cli_auth_credentials_store=${JSON.stringify(input.credentialBackend)}`);
|
|
36
|
+
const actual = isReleaseExecMode(input.policyMode) ? { binary: CODEX_GLOBAL_INSTRUCTION_WRAPPER, args: ['-p', globalInstructionProfile(accountRoot), input.binary, ...generated.args], env: generated.env } : { binary: input.binary, ...generated };
|
|
37
|
+
const executionNonce = randomBytes(32).toString('hex');
|
|
38
|
+
const plan = Object.freeze({ transport: EXEC_TRANSPORT, mode: input.policyMode ?? EXEC_MODE, executionNonce, identity: Object.freeze(i), modelId: inv.modelId, home: inv.env.HOME, accountRoot, credentialBackend: input.credentialBackend, binary: input.binary, cwd, endpoint: inv.endpoint.url, invocationFingerprint: hash({ ...actual }) });
|
|
39
|
+
plans.set(plan, structuredClone(actual));
|
|
40
|
+
return plan;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw fail();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Opaque process-local plan; serializing a plan cannot recreate invocation authority. */
|
|
47
|
+
export function requireOfficialExecPlan(plan) {
|
|
48
|
+
const p = plans.get(plan);
|
|
49
|
+
if (!p)
|
|
50
|
+
throw fail();
|
|
51
|
+
return structuredClone(p);
|
|
52
|
+
}
|
|
53
|
+
/** A plan authorizes one attempt across all host instances, including failed attempts. */
|
|
54
|
+
export function claimOfficialExecPlan(plan) {
|
|
55
|
+
assertOfficialExecPlanAvailable(plan);
|
|
56
|
+
consumed.add(plan);
|
|
57
|
+
}
|
|
58
|
+
export function assertOfficialExecPlanAvailable(plan) {
|
|
59
|
+
requireOfficialExecPlan(plan);
|
|
60
|
+
if (consumed.has(plan))
|
|
61
|
+
throw fail();
|
|
62
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { executionError } from '../../safety/execution-failure.js';
|
|
2
|
+
import { UNMANAGED_EXEC_MODE } from './unmanaged-scope.js';
|
|
3
|
+
import { fingerprint as taskFingerprint } from '../../daemon/execution-coordinator.js';
|
|
4
|
+
import { securityRequirementsFingerprint as hash } from '../../safety/security-requirements.js';
|
|
5
|
+
import { ExecToolGate } from './exec-tool-gate.js';
|
|
6
|
+
import { assessReleaseExec, requireReleaseDescriptor, releaseGates, EXEC_INPUT_GATES } from './exec-release-policy.js';
|
|
7
|
+
import { officialExecObservationLimits } from './exec-observer.js';
|
|
8
|
+
import { EXEC_GATES, assessExecPreflight, requireExecHandshake } from './exec-preflight.js';
|
|
9
|
+
import { EXEC_MODE, isReleaseExecMode, EXEC_TRANSPORT, assertOfficialExecPlanAvailable, claimOfficialExecPlan, requireOfficialExecPlan } from './exec-invocation.js';
|
|
10
|
+
const fail = () => new Error('CODEX_EXEC_PREFLIGHT_REJECTED');
|
|
11
|
+
/** No default native inspector, handshake observer or OS process host is created.
|
|
12
|
+
* Missing per-execution evidence blocks before spawn. Synthetic callbacks grant
|
|
13
|
+
* only the fake host used by tests, never a compiled production release entry. */
|
|
14
|
+
export class CodexExecLaunchHost {
|
|
15
|
+
plan;
|
|
16
|
+
deps;
|
|
17
|
+
used = false;
|
|
18
|
+
ownsAttempt = false;
|
|
19
|
+
stopped = false;
|
|
20
|
+
handle;
|
|
21
|
+
inspection;
|
|
22
|
+
exitAudit;
|
|
23
|
+
inspectionFingerprint;
|
|
24
|
+
handshake;
|
|
25
|
+
receiptValue;
|
|
26
|
+
abort = new AbortController();
|
|
27
|
+
closePromise;
|
|
28
|
+
constructor(plan, deps) {
|
|
29
|
+
this.plan = plan;
|
|
30
|
+
this.deps = deps;
|
|
31
|
+
requireOfficialExecPlan(plan);
|
|
32
|
+
this.deps = Object.freeze({ ...deps, processHost: Object.freeze({ spawn: deps.processHost.spawn.bind(deps.processHost) }) });
|
|
33
|
+
}
|
|
34
|
+
current() { if (this.stopped || this.abort.signal.aborted || this.inspection?.isCurrent() !== true || !(isReleaseExecMode(this.plan.mode) ? assessReleaseExec(this.inspection.snapshot, this.plan) : assessExecPreflight(this.inspection.snapshot, this.plan)).ready || hash(this.inspection.snapshot) !== this.inspectionFingerprint) {
|
|
35
|
+
if (this.plan.mode === UNMANAGED_EXEC_MODE)
|
|
36
|
+
void this.cancel().catch(() => { });
|
|
37
|
+
throw fail();
|
|
38
|
+
} }
|
|
39
|
+
assertContext(ctx, jobId) {
|
|
40
|
+
const a = ctx.approvedTaskPackage, r = a?.security_requirements, b = ctx.taskPackage, execution = b?.execution;
|
|
41
|
+
if (isReleaseExecMode(this.plan.mode)) {
|
|
42
|
+
if (!r)
|
|
43
|
+
throw fail();
|
|
44
|
+
requireReleaseDescriptor(r);
|
|
45
|
+
if (r.binary_sha256 !== this.plan.identity.binarySha256 || r.fixtures_sha256 !== this.plan.identity.barrierFingerprint || r.commands_sha256 !== this.plan.identity.commandsFingerprint || r.tools_sha256 !== this.plan.identity.toolSchemaFingerprint || !r.model_ids.includes(this.plan.modelId))
|
|
46
|
+
throw fail();
|
|
47
|
+
}
|
|
48
|
+
if (!a || !r || jobId !== this.plan.identity.jobId || ctx.generation !== this.plan.identity.generation || ctx.resumeSessionId || ctx.resolvedModelId !== this.plan.modelId || a.runtime_adapter !== 'codex_cli' || a.runtime_adapter_version !== '1' || a.runtime_transport !== EXEC_TRANSPORT || a.resolved_provider !== 'openai' || a.resolved_model_id !== this.plan.modelId || a.repository?.base_commit_sha !== this.plan.identity.sourceSha || a.contract_fingerprint !== this.plan.identity.contractFingerprint
|
|
49
|
+
|| taskFingerprint(a) !== this.plan.identity.taskPackageFingerprint || ctx.securityBinding?.taskPackageFingerprint !== this.plan.identity.taskPackageFingerprint || ctx.securityBinding.contractFingerprint !== this.plan.identity.contractFingerprint || ctx.securityBinding.launchFingerprint !== this.plan.identity.launchFingerprint
|
|
50
|
+
|| b?.repositories?.length !== 1 || b.repositories[0]?.base_commit_sha !== this.plan.identity.sourceSha
|
|
51
|
+
|| execution?.runtime_transport !== EXEC_TRANSPORT || execution?.mode !== 'employee_ai_work_session' || execution.runtime_adapter !== 'codex_cli' || execution.limits_mode !== 'unbounded'
|
|
52
|
+
|| r.version !== (this.plan.mode === UNMANAGED_EXEC_MODE ? 'cli-exec-security-requirements-v3' : isReleaseExecMode(this.plan.mode) ? 'cli-exec-security-requirements-v2' : 'cli-exec-security-requirements-v1') || r.security_mode !== this.plan.mode || r.transport !== EXEC_TRANSPORT || r.status !== 'verified' || r.adapter_id !== 'codex_cli' || r.adapter_version !== '1' || JSON.stringify(r.mandatory_gates) !== JSON.stringify(isReleaseExecMode(this.plan.mode) ? [...releaseGates(this.plan.mode), ...EXEC_INPUT_GATES, 'authenticated_mcp_discovery'] : EXEC_GATES) || r.release_manifest_sha256 !== this.plan.identity.releaseManifestFingerprint || r.fingerprint !== this.plan.identity.requirementsFingerprint || hash(r) !== r.fingerprint)
|
|
53
|
+
throw fail();
|
|
54
|
+
}
|
|
55
|
+
assertAvailableContext(ctx, jobId) { if (this.used || this.stopped)
|
|
56
|
+
throw fail(); this.assertContext(ctx, jobId); assertOfficialExecPlanAvailable(this.plan); if (isReleaseExecMode(this.plan.mode) && (!(this.deps.execTools instanceof ExecToolGate) || !this.deps.execTools.matches(this.plan)))
|
|
57
|
+
throw fail(); }
|
|
58
|
+
async start(ctx, jobId, prompt, signal) {
|
|
59
|
+
if (this.used || this.stopped)
|
|
60
|
+
throw fail();
|
|
61
|
+
this.assertAvailableContext(ctx, jobId);
|
|
62
|
+
claimOfficialExecPlan(this.plan);
|
|
63
|
+
this.ownsAttempt = true;
|
|
64
|
+
this.used = true;
|
|
65
|
+
let stage = 'codex_context';
|
|
66
|
+
let timer, external;
|
|
67
|
+
try {
|
|
68
|
+
this.assertContext(ctx, jobId);
|
|
69
|
+
requireOfficialExecPlan(this.plan);
|
|
70
|
+
if (typeof prompt !== 'string' || !prompt || prompt.length > 1_000_000 || signal?.aborted)
|
|
71
|
+
throw fail();
|
|
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); });
|
|
73
|
+
const preparation = (async () => {
|
|
74
|
+
const supported = isReleaseExecMode(this.plan.mode);
|
|
75
|
+
stage = 'codex_inspection';
|
|
76
|
+
this.inspection = await (supported ? this.deps.inspectRelease?.(this.plan, this.abort.signal) : this.deps.inspect?.(this.plan, this.abort.signal));
|
|
77
|
+
const assessment = supported ? assessReleaseExec(this.inspection?.snapshot, this.plan) : assessExecPreflight(this.inspection?.snapshot, this.plan);
|
|
78
|
+
if (!supported && !this.deps.inspect)
|
|
79
|
+
assessment.reasonCodes.push(...officialExecObservationLimits());
|
|
80
|
+
this.deps.onAssessment?.(structuredClone(assessment));
|
|
81
|
+
if (!assessment.ready || !supported && typeof this.deps.handshake !== 'function')
|
|
82
|
+
throw fail();
|
|
83
|
+
this.inspectionFingerprint = hash(this.inspection.snapshot);
|
|
84
|
+
this.current();
|
|
85
|
+
const invocation = requireOfficialExecPlan(this.plan);
|
|
86
|
+
const manual = ctx.approvedTaskPackage?.security_requirements?.mdm_verification === 'manual_prelaunch';
|
|
87
|
+
if (manual && typeof this.inspection.beforeSpawn !== 'function')
|
|
88
|
+
throw fail();
|
|
89
|
+
this.inspection.beforeSpawn?.();
|
|
90
|
+
stage = 'codex_spawn';
|
|
91
|
+
const h = this.deps.processHost.spawn(invocation.binary, invocation.args, { cwd: this.plan.cwd, env: invocation.env });
|
|
92
|
+
this.handle = h;
|
|
93
|
+
if (this.stopped) {
|
|
94
|
+
h.kill('SIGKILL');
|
|
95
|
+
throw fail();
|
|
96
|
+
}
|
|
97
|
+
h.once('exit', (code, signal) => {
|
|
98
|
+
// Preserve audit only for a clean owned exit that was still authorized.
|
|
99
|
+
// This immutable record never restores tool or spawn authority.
|
|
100
|
+
if (code === 0 && signal === null && !this.stopped && this.receiptValue) {
|
|
101
|
+
try {
|
|
102
|
+
this.current();
|
|
103
|
+
this.exitAudit = structuredClone(this.receiptValue);
|
|
104
|
+
}
|
|
105
|
+
catch { }
|
|
106
|
+
}
|
|
107
|
+
void this.cancel().catch(() => { });
|
|
108
|
+
});
|
|
109
|
+
this.current();
|
|
110
|
+
if (supported) {
|
|
111
|
+
const gate = this.deps.execTools;
|
|
112
|
+
gate.bind(h, () => { try {
|
|
113
|
+
this.current();
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return false;
|
|
118
|
+
} });
|
|
119
|
+
// Vendor exec consumes stdin before it can initialize its required MCP.
|
|
120
|
+
// Prompt submission may cause provider/account effects; do not call this no-model.
|
|
121
|
+
h.stdin.write(prompt);
|
|
122
|
+
this.current();
|
|
123
|
+
h.stdin.end();
|
|
124
|
+
this.current();
|
|
125
|
+
gate.promptSubmitted(h);
|
|
126
|
+
stage = 'codex_mcp';
|
|
127
|
+
const observed = await gate.wait(h, this.abort.signal);
|
|
128
|
+
this.current();
|
|
129
|
+
requireExecHandshake(observed, h, this.plan);
|
|
130
|
+
gate.authorize(h);
|
|
131
|
+
this.current();
|
|
132
|
+
requireExecHandshake(observed, h, this.plan);
|
|
133
|
+
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 });
|
|
134
|
+
return h;
|
|
135
|
+
}
|
|
136
|
+
const handshake = await this.deps.handshake?.(h, this.plan, this.abort.signal);
|
|
137
|
+
this.current();
|
|
138
|
+
requireExecHandshake(handshake, h, this.plan);
|
|
139
|
+
this.handshake = handshake;
|
|
140
|
+
// An input hash is not a native effective-config certificate. Preserve the
|
|
141
|
+
// evidence origin in the audit receipt and never reuse AppServer authority.
|
|
142
|
+
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 });
|
|
143
|
+
this.current();
|
|
144
|
+
requireExecHandshake(handshake, h, this.plan);
|
|
145
|
+
h.stdin.write(prompt);
|
|
146
|
+
this.current();
|
|
147
|
+
h.stdin.end();
|
|
148
|
+
this.current();
|
|
149
|
+
requireExecHandshake(handshake, h, this.plan);
|
|
150
|
+
this.receiptValue = receipt;
|
|
151
|
+
return h;
|
|
152
|
+
})();
|
|
153
|
+
return await Promise.race([preparation, stopped]);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
const safe = executionError(error, stage, 'CODEX_EXEC_PREFLIGHT_REJECTED');
|
|
157
|
+
await this.cancel().catch(() => { });
|
|
158
|
+
throw safe;
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
if (timer)
|
|
162
|
+
clearTimeout(timer);
|
|
163
|
+
if (external)
|
|
164
|
+
signal?.removeEventListener('abort', external);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
receipt() { if (this.exitAudit)
|
|
168
|
+
return structuredClone(this.exitAudit); if (!this.receiptValue)
|
|
169
|
+
return undefined; try {
|
|
170
|
+
if (this.plan.mode === UNMANAGED_EXEC_MODE)
|
|
171
|
+
this.current();
|
|
172
|
+
return this.receiptValue ? structuredClone(this.receiptValue) : undefined;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return undefined;
|
|
176
|
+
} }
|
|
177
|
+
cancel() {
|
|
178
|
+
if (this.closePromise)
|
|
179
|
+
return this.closePromise;
|
|
180
|
+
this.stopped = true;
|
|
181
|
+
this.receiptValue = undefined;
|
|
182
|
+
if (!this.ownsAttempt) {
|
|
183
|
+
this.closePromise = Promise.resolve();
|
|
184
|
+
return this.closePromise;
|
|
185
|
+
}
|
|
186
|
+
let finish, reject;
|
|
187
|
+
this.closePromise = new Promise((resolve, rejectPromise) => { finish = resolve; reject = rejectPromise; });
|
|
188
|
+
let cleanupError = false;
|
|
189
|
+
// Publish closePromise first: synchronous exit/cancel cannot recurse or skip cleanup.
|
|
190
|
+
try {
|
|
191
|
+
this.deps.execTools?.revoke();
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
cleanupError = true;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
this.deps.revokeTools();
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
cleanupError = true;
|
|
201
|
+
}
|
|
202
|
+
this.abort.abort();
|
|
203
|
+
try {
|
|
204
|
+
this.handle?.kill('SIGKILL');
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
cleanupError = true;
|
|
208
|
+
}
|
|
209
|
+
Promise.resolve().then(() => this.deps.closeTools()).then(() => { if (cleanupError)
|
|
210
|
+
reject(new Error('CODEX_EXEC_CLEANUP_UNCONFIRMED'));
|
|
211
|
+
else
|
|
212
|
+
finish(); }, () => reject(new Error('CODEX_EXEC_CLEANUP_UNCONFIRMED')));
|
|
213
|
+
return this.closePromise;
|
|
214
|
+
}
|
|
215
|
+
}
|