@fitlab-ai/agent-infra 0.9.5 → 0.9.7
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/dist/bin/internal-cli.js +8 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +171 -20
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +68 -8
- package/dist/lib/agent-clients/adapters/codex.js +1 -1
- package/dist/lib/defaults.json +10 -2
- package/dist/lib/git/workflow.js +30 -4
- package/dist/lib/internal/codex-lifecycle.js +169 -3
- package/dist/lib/internal/codex-sandbox-controller.js +106 -0
- package/dist/lib/internal/release-workflow.js +108 -11
- package/dist/lib/internal/sandbox-control.js +43 -6
- package/dist/lib/internal/task-create.js +12 -4
- package/dist/lib/internal/task-orchestration.js +40 -7
- package/dist/lib/platform/context.js +6 -4
- package/dist/lib/platform/github-client.js +55 -2
- package/dist/lib/platform/issue-comments.js +2 -1
- package/dist/lib/process-data/checkpoint.js +319 -0
- package/dist/lib/process-data/index.js +128 -60
- package/dist/lib/process-data/materialize.js +84 -0
- package/dist/lib/process-data/normalize.js +34 -1
- package/dist/lib/process-data/reconcile.js +55 -1
- package/dist/lib/process-data/sources.js +422 -1
- package/dist/lib/process-data/store.js +341 -11
- package/dist/lib/release/workflow.js +29 -21
- package/dist/lib/sandbox/commands/create.js +111 -60
- package/dist/lib/sandbox/commands/prune.js +40 -16
- package/dist/lib/sandbox/commands/rm.js +372 -94
- package/dist/lib/sandbox/commands/start.js +4 -3
- package/dist/lib/sandbox/control/client.js +138 -22
- package/dist/lib/sandbox/control/container-identity.js +64 -0
- package/dist/lib/sandbox/control/executor.js +142 -0
- package/dist/lib/sandbox/control/lifecycle.js +557 -0
- package/dist/lib/sandbox/control/protocol.js +69 -26
- package/dist/lib/sandbox/control/server.js +395 -99
- package/dist/lib/sandbox/control/state.js +191 -0
- package/dist/lib/sandbox/control/timing.js +8 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/managed-fs.js +14 -2
- package/dist/lib/sandbox/mounts.js +22 -19
- package/dist/lib/sandbox/recovery.js +284 -50
- package/dist/lib/sandbox/workspace-view.js +90 -10
- package/dist/lib/sandbox/worktree-safety.js +196 -0
- package/dist/lib/server/daemon.js +1 -1
- package/dist/lib/server/process-control.js +17 -1
- package/dist/lib/server/process-state.js +57 -20
- package/dist/lib/task/artifact-lifecycle.js +87 -15
- package/dist/lib/task/artifact-receipts.js +128 -0
- package/dist/lib/task/codex-orchestration.js +236 -0
- package/dist/lib/task/commands/status.js +3 -0
- package/dist/lib/task/commands/validate.js +289 -0
- package/dist/lib/task/commit-finalization.js +36 -9
- package/dist/lib/task/create.js +10 -2
- package/dist/lib/task/delegation-receipts.js +109 -2
- package/dist/lib/task/events.js +149 -6
- package/dist/lib/task/finalization-artifacts.js +2 -1
- package/dist/lib/task/implementation-inputs.js +1 -7
- package/dist/lib/task/index.js +9 -0
- package/dist/lib/task/ledger.js +13 -1
- package/dist/lib/task/orchestration.js +365 -15
- package/dist/lib/task/sections.js +5 -1
- package/dist/lib/task/task-execution-lock.js +5 -4
- package/dist/lib/task/verification-engine.js +78 -14
- package/dist/lib/task/verification.js +1 -0
- package/dist/lifecycle-build-manifest.json +6 -0
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +202 -18
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +83 -6
- package/lib/agent-clients/adapters/codex.ts +1 -1
- package/lib/defaults.json +10 -2
- package/lib/git/workflow.ts +25 -3
- package/lib/internal/codex-lifecycle.ts +175 -4
- package/lib/internal/codex-sandbox-controller.ts +109 -0
- package/lib/internal/release-workflow.ts +107 -11
- package/lib/internal/sandbox-control.ts +38 -6
- package/lib/internal/task-create.ts +16 -4
- package/lib/internal/task-orchestration.ts +34 -7
- package/lib/platform/context.ts +8 -5
- package/lib/platform/github-client.ts +61 -3
- package/lib/platform/issue-comments.ts +2 -1
- package/lib/process-data/checkpoint.ts +335 -0
- package/lib/process-data/index.ts +112 -60
- package/lib/process-data/materialize.ts +83 -0
- package/lib/process-data/normalize.ts +29 -1
- package/lib/process-data/reconcile.ts +64 -2
- package/lib/process-data/sources.ts +449 -2
- package/lib/process-data/store.ts +304 -11
- package/lib/process-data/types.ts +124 -2
- package/lib/release/workflow.ts +69 -20
- package/lib/sandbox/commands/create.ts +70 -25
- package/lib/sandbox/commands/prune.ts +53 -15
- package/lib/sandbox/commands/rm.ts +423 -108
- package/lib/sandbox/commands/start.ts +4 -3
- package/lib/sandbox/control/client.ts +152 -28
- package/lib/sandbox/control/container-identity.ts +93 -0
- package/lib/sandbox/control/executor.ts +167 -0
- package/lib/sandbox/control/lifecycle.ts +573 -0
- package/lib/sandbox/control/protocol.ts +121 -68
- package/lib/sandbox/control/server.ts +398 -109
- package/lib/sandbox/control/state.ts +213 -0
- package/lib/sandbox/control/timing.ts +12 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/managed-fs.ts +17 -2
- package/lib/sandbox/mounts.ts +16 -8
- package/lib/sandbox/recovery.ts +335 -65
- package/lib/sandbox/workspace-view.ts +102 -11
- package/lib/sandbox/worktree-safety.ts +234 -0
- package/lib/server/daemon.ts +1 -1
- package/lib/server/process-control.ts +24 -3
- package/lib/server/process-state.ts +82 -27
- package/lib/task/artifact-lifecycle.ts +78 -14
- package/lib/task/artifact-receipts.ts +161 -0
- package/lib/task/codex-orchestration.ts +319 -0
- package/lib/task/commands/status.ts +2 -0
- package/lib/task/commands/validate.ts +282 -0
- package/lib/task/commit-finalization.ts +52 -9
- package/lib/task/create.ts +11 -2
- package/lib/task/delegation-receipts.ts +208 -7
- package/lib/task/events.ts +131 -5
- package/lib/task/finalization-artifacts.ts +2 -1
- package/lib/task/implementation-inputs.ts +1 -5
- package/lib/task/index.ts +8 -0
- package/lib/task/ledger.ts +16 -1
- package/lib/task/orchestration.ts +552 -18
- package/lib/task/sections.ts +6 -2
- package/lib/task/task-execution-lock.ts +11 -9
- package/lib/task/verification-engine.ts +93 -24
- package/lib/task/verification.ts +2 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +7 -0
- package/templates/.agents/README.zh-CN.md +5 -0
- package/templates/.agents/hooks/lifecycle-delegation.js +72 -12
- package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
- package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
- package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.en.md +3 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +25 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +25 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +69 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +69 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -3
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -3
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
- package/templates/.claude/commands/run-manual-validation.en.md +9 -0
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
- package/templates/.codex/hooks.json +8 -8
- package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
- package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
- package/templates/.agents/workspace/README.en.md +0 -26
- package/templates/.agents/workspace/README.zh-CN.md +0 -26
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { verifyLifecycleBuildIdentity } from './build-identity.js';
|
|
5
|
+
const DEFAULT_TTL_MS = 30_000;
|
|
6
|
+
const DEFAULT_TOMBSTONE_MS = 86_400_000;
|
|
7
|
+
const DEFAULT_MAX_RECORDS = 128;
|
|
8
|
+
const LOCK_RETRY_MS = 10;
|
|
9
|
+
const LOCK_TIMEOUT_MS = 1_000;
|
|
10
|
+
const LOCK_STALE_MS = 30_000;
|
|
11
|
+
function digest(value) {
|
|
12
|
+
return crypto.createHash('sha256').update(value).digest('hex');
|
|
13
|
+
}
|
|
14
|
+
function capabilityError(code, message) {
|
|
15
|
+
const error = new Error(`${code}: ${message}`);
|
|
16
|
+
error.name = code;
|
|
17
|
+
return error;
|
|
18
|
+
}
|
|
19
|
+
function sameController(left, right) {
|
|
20
|
+
return left?.instanceDigest === right?.instanceDigest
|
|
21
|
+
&& left?.controlGeneration === right?.controlGeneration;
|
|
22
|
+
}
|
|
23
|
+
function createCodexCapabilityStore(options = {}) {
|
|
24
|
+
const root = options.root
|
|
25
|
+
?? path.join(process.cwd(), '.agents', 'workspace', '.runtime', 'codex-capabilities');
|
|
26
|
+
const now = options.now ?? Date.now;
|
|
27
|
+
const createToken = options.token ?? (() => crypto.randomBytes(32).toString('base64url'));
|
|
28
|
+
const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
29
|
+
const tombstoneMs = options.tombstoneMs ?? DEFAULT_TOMBSTONE_MS;
|
|
30
|
+
const maxRecords = options.maxRecords ?? DEFAULT_MAX_RECORDS;
|
|
31
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
32
|
+
fs.chmodSync(root, 0o700);
|
|
33
|
+
function fileFor(token) {
|
|
34
|
+
return path.join(root, `${digest(token)}.json`);
|
|
35
|
+
}
|
|
36
|
+
function read(file) {
|
|
37
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
38
|
+
if (value.schemaVersion !== 1 || !Number.isSafeInteger(value.revision) || value.revision < 1) {
|
|
39
|
+
throw capabilityError('CODEX_CAPABILITY_STATE_INVALID', 'capability record schema is invalid');
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
function withWriteLock(operation) {
|
|
44
|
+
const lock = path.join(root, '.write.lock');
|
|
45
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
46
|
+
let descriptor = null;
|
|
47
|
+
while (descriptor === null) {
|
|
48
|
+
try {
|
|
49
|
+
descriptor = fs.openSync(lock, 'wx', 0o600);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error.code !== 'EEXIST')
|
|
53
|
+
throw error;
|
|
54
|
+
try {
|
|
55
|
+
if (Date.now() - fs.statSync(lock).mtimeMs > LOCK_STALE_MS) {
|
|
56
|
+
fs.unlinkSync(lock);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (lockError) {
|
|
61
|
+
if (lockError.code === 'ENOENT')
|
|
62
|
+
continue;
|
|
63
|
+
throw lockError;
|
|
64
|
+
}
|
|
65
|
+
if (Date.now() >= deadline) {
|
|
66
|
+
throw capabilityError('CODEX_CAPABILITY_STORE_BUSY', 'capability store is busy');
|
|
67
|
+
}
|
|
68
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, LOCK_RETRY_MS);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
return operation();
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
fs.closeSync(descriptor);
|
|
76
|
+
fs.unlinkSync(lock);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function write(file, value, expectedRevision) {
|
|
80
|
+
const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
81
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, {
|
|
82
|
+
encoding: 'utf8',
|
|
83
|
+
flag: 'wx',
|
|
84
|
+
mode: 0o600
|
|
85
|
+
});
|
|
86
|
+
try {
|
|
87
|
+
withWriteLock(() => {
|
|
88
|
+
options.beforeCompareAndSwap?.({ path: file, expectedRevision });
|
|
89
|
+
const actualRevision = fs.existsSync(file) ? read(file).revision : 0;
|
|
90
|
+
if (actualRevision !== expectedRevision) {
|
|
91
|
+
throw capabilityError('CODEX_CAPABILITY_REVISION_CONFLICT', `capability revision changed from ${expectedRevision} to ${actualRevision}`);
|
|
92
|
+
}
|
|
93
|
+
fs.renameSync(temporary, file);
|
|
94
|
+
fs.chmodSync(file, 0o600);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
if (fs.existsSync(temporary))
|
|
99
|
+
fs.unlinkSync(temporary);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function files() {
|
|
103
|
+
return fs.readdirSync(root)
|
|
104
|
+
.filter((name) => /^[a-f0-9]{64}\.json$/u.test(name))
|
|
105
|
+
.map((name) => path.join(root, name));
|
|
106
|
+
}
|
|
107
|
+
function expire(record, file, timestamp) {
|
|
108
|
+
if ((record.status === 'armed' || record.status === 'attested') && timestamp > record.expiresAt) {
|
|
109
|
+
const expired = Object.freeze({
|
|
110
|
+
...record,
|
|
111
|
+
revision: record.revision + 1,
|
|
112
|
+
status: 'expired'
|
|
113
|
+
});
|
|
114
|
+
write(file, expired, record.revision);
|
|
115
|
+
return expired;
|
|
116
|
+
}
|
|
117
|
+
return record;
|
|
118
|
+
}
|
|
119
|
+
function sweep() {
|
|
120
|
+
const timestamp = now();
|
|
121
|
+
const records = files().map((file) => ({ file, record: expire(read(file), file, timestamp) }));
|
|
122
|
+
for (const entry of records) {
|
|
123
|
+
const terminalAt = entry.record.consumedAt ?? entry.record.expiresAt;
|
|
124
|
+
if (['consumed', 'expired'].includes(entry.record.status)
|
|
125
|
+
&& timestamp - terminalAt > tombstoneMs) {
|
|
126
|
+
fs.unlinkSync(entry.file);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const remaining = files().map((file) => ({ file, record: read(file) }));
|
|
130
|
+
if (remaining.length <= maxRecords)
|
|
131
|
+
return;
|
|
132
|
+
const removable = remaining
|
|
133
|
+
.filter(({ record }) => ['consumed', 'expired'].includes(record.status))
|
|
134
|
+
.sort((left, right) => (left.record.consumedAt ?? left.record.expiresAt)
|
|
135
|
+
- (right.record.consumedAt ?? right.record.expiresAt));
|
|
136
|
+
while (files().length > maxRecords && removable.length > 0) {
|
|
137
|
+
fs.unlinkSync(removable.shift().file);
|
|
138
|
+
}
|
|
139
|
+
if (files().length > maxRecords) {
|
|
140
|
+
throw capabilityError('CODEX_CAPABILITY_CAPACITY_EXCEEDED', 'capability store has too many active records');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function arm(input) {
|
|
144
|
+
sweep();
|
|
145
|
+
const token = createToken();
|
|
146
|
+
const file = fileFor(token);
|
|
147
|
+
if (fs.existsSync(file))
|
|
148
|
+
throw capabilityError('CODEX_CAPABILITY_TOKEN_COLLISION', 'capability token already exists');
|
|
149
|
+
const timestamp = now();
|
|
150
|
+
const record = Object.freeze({
|
|
151
|
+
schemaVersion: 1,
|
|
152
|
+
revision: 1,
|
|
153
|
+
tokenDigest: digest(token),
|
|
154
|
+
taskId: input.taskId,
|
|
155
|
+
status: 'armed',
|
|
156
|
+
buildIdentity: input.buildIdentity,
|
|
157
|
+
controller: input.controller ?? null,
|
|
158
|
+
sessionId: null,
|
|
159
|
+
turnId: null,
|
|
160
|
+
toolUseId: null,
|
|
161
|
+
hookDefinitionHash: null,
|
|
162
|
+
armedAt: timestamp,
|
|
163
|
+
expiresAt: timestamp + ttlMs,
|
|
164
|
+
attestedAt: null,
|
|
165
|
+
consumedAt: null
|
|
166
|
+
});
|
|
167
|
+
write(file, record, 0);
|
|
168
|
+
return Object.freeze({
|
|
169
|
+
...record,
|
|
170
|
+
token,
|
|
171
|
+
path: file,
|
|
172
|
+
marker: `agent-infra-codex-capability:${token}`
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function loadActive(token) {
|
|
176
|
+
const file = fileFor(token);
|
|
177
|
+
if (!fs.existsSync(file)) {
|
|
178
|
+
throw capabilityError('CODEX_CAPABILITY_MISSING', 'capability token was not found');
|
|
179
|
+
}
|
|
180
|
+
const record = expire(read(file), file, now());
|
|
181
|
+
if (record.status === 'expired') {
|
|
182
|
+
throw capabilityError('CODEX_CAPABILITY_EXPIRED', 'capability token expired');
|
|
183
|
+
}
|
|
184
|
+
return { file, record };
|
|
185
|
+
}
|
|
186
|
+
function attest(input) {
|
|
187
|
+
const { file, record } = loadActive(input.token);
|
|
188
|
+
if (record.status !== 'armed') {
|
|
189
|
+
throw capabilityError(record.status === 'consumed' ? 'CODEX_CAPABILITY_REPLAY' : 'CODEX_CAPABILITY_STATE_INVALID', `capability is ${record.status}, expected armed`);
|
|
190
|
+
}
|
|
191
|
+
const identity = verifyLifecycleBuildIdentity(record.buildIdentity, input.buildIdentity);
|
|
192
|
+
if (!identity.ok || !sameController(record.controller, input.controller ?? null)) {
|
|
193
|
+
throw capabilityError('CODEX_CAPABILITY_PROVENANCE_MISMATCH', 'capability build or controller identity does not match');
|
|
194
|
+
}
|
|
195
|
+
if (![input.sessionId, input.turnId, input.toolUseId, input.hookDefinitionHash]
|
|
196
|
+
.every((value) => value.trim().length > 0)) {
|
|
197
|
+
throw capabilityError('CODEX_CAPABILITY_IDENTITY_INVALID', 'hook identity is incomplete');
|
|
198
|
+
}
|
|
199
|
+
const timestamp = now();
|
|
200
|
+
const next = Object.freeze({
|
|
201
|
+
...record,
|
|
202
|
+
revision: record.revision + 1,
|
|
203
|
+
status: 'attested',
|
|
204
|
+
sessionId: input.sessionId,
|
|
205
|
+
turnId: input.turnId,
|
|
206
|
+
toolUseId: input.toolUseId,
|
|
207
|
+
hookDefinitionHash: input.hookDefinitionHash,
|
|
208
|
+
attestedAt: timestamp
|
|
209
|
+
});
|
|
210
|
+
write(file, next, record.revision);
|
|
211
|
+
return next;
|
|
212
|
+
}
|
|
213
|
+
function consume(token, expected) {
|
|
214
|
+
sweep();
|
|
215
|
+
const { file, record } = loadActive(token);
|
|
216
|
+
if (record.status === 'consumed') {
|
|
217
|
+
throw capabilityError('CODEX_CAPABILITY_REPLAY', 'capability token was already consumed');
|
|
218
|
+
}
|
|
219
|
+
if (record.status !== 'attested') {
|
|
220
|
+
throw capabilityError('CODEX_CAPABILITY_NOT_ATTESTED', 'capability token has no current-session hook attestation');
|
|
221
|
+
}
|
|
222
|
+
const identity = verifyLifecycleBuildIdentity(record.buildIdentity, expected.buildIdentity);
|
|
223
|
+
if (!identity.ok
|
|
224
|
+
|| record.taskId !== expected.taskId
|
|
225
|
+
|| record.hookDefinitionHash !== expected.hookDefinitionHash
|
|
226
|
+
|| !sameController(record.controller, expected.controller ?? null)) {
|
|
227
|
+
throw capabilityError('CODEX_CAPABILITY_PROVENANCE_MISMATCH', 'capability consumption identity does not match');
|
|
228
|
+
}
|
|
229
|
+
const next = Object.freeze({
|
|
230
|
+
...record,
|
|
231
|
+
revision: record.revision + 1,
|
|
232
|
+
status: 'consumed',
|
|
233
|
+
consumedAt: now()
|
|
234
|
+
});
|
|
235
|
+
write(file, next, record.revision);
|
|
236
|
+
return next;
|
|
237
|
+
}
|
|
238
|
+
function inspect(token) {
|
|
239
|
+
return loadActive(token).record;
|
|
240
|
+
}
|
|
241
|
+
return Object.freeze({ arm, attest, consume, inspect, sweep });
|
|
242
|
+
}
|
|
243
|
+
export { createCodexCapabilityStore };
|
|
244
|
+
//# sourceMappingURL=capability-store.js.map
|
|
@@ -35,17 +35,29 @@ function requiredStrings(event) {
|
|
|
35
35
|
}
|
|
36
36
|
function derive(state) {
|
|
37
37
|
const { spawn, child, thread, settings, reroute, terminal, stop } = state;
|
|
38
|
-
if (spawn && child && (spawn.sessionId !== child.
|
|
38
|
+
if (spawn && child && (spawn.sessionId !== child.parentThreadId
|
|
39
39
|
|| spawn.nativeAgent !== child.nativeAgent)) {
|
|
40
40
|
return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'hook spawn and child identity do not match');
|
|
41
41
|
}
|
|
42
|
+
if (child && child.sessionId !== child.parentThreadId) {
|
|
43
|
+
return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'child event session does not match the parent thread');
|
|
44
|
+
}
|
|
42
45
|
if (child && thread && child.childThreadId !== thread.childThreadId) {
|
|
43
46
|
return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'hook and App Server child identity do not match');
|
|
44
47
|
}
|
|
48
|
+
if (child && thread && child.nativeAgent !== thread.nativeAgent) {
|
|
49
|
+
return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'hook and App Server agent roles do not match');
|
|
50
|
+
}
|
|
51
|
+
if (child && thread && child.parentThreadId !== thread.parentThreadId) {
|
|
52
|
+
return invalid(state, 'CODEX_EVIDENCE_PARENT_MISMATCH', 'hook and App Server parent identity do not match');
|
|
53
|
+
}
|
|
45
54
|
if (spawn && thread && (thread.parentThreadId !== spawn.sessionId
|
|
46
55
|
|| thread.sourceParentThreadId !== spawn.sessionId)) {
|
|
47
56
|
return invalid(state, 'CODEX_EVIDENCE_PARENT_MISMATCH', 'App Server parent identity does not match the spawning session');
|
|
48
57
|
}
|
|
58
|
+
if (spawn && thread && spawn.nativeAgent !== thread.nativeAgent) {
|
|
59
|
+
return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'spawn and App Server agent roles do not match');
|
|
60
|
+
}
|
|
49
61
|
if (thread?.forkedFromId !== null && thread?.forkedFromId !== undefined) {
|
|
50
62
|
return invalid(state, 'CODEX_EVIDENCE_FORK_FORBIDDEN', 'forked child threads are not fresh lifecycle evidence');
|
|
51
63
|
}
|
|
@@ -105,7 +117,7 @@ function derive(state) {
|
|
|
105
117
|
childThreadId: startEvidence.childThreadId,
|
|
106
118
|
turnId: terminal.turnId,
|
|
107
119
|
terminalStatus: 'completed',
|
|
108
|
-
hookStopObserved:
|
|
120
|
+
hookStopObserved: stop.source !== 'parent-rollout'
|
|
109
121
|
});
|
|
110
122
|
}
|
|
111
123
|
const status = stopEvidence
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import semver from 'semver';
|
|
7
|
+
import * as toml from 'smol-toml';
|
|
8
|
+
import { requestSandboxControl } from '../../../sandbox/control/client.js';
|
|
9
|
+
import { readSandboxControlStatus } from '../../../sandbox/control/state.js';
|
|
10
|
+
import { getProcessStartTime, isProcessAlive } from '../../../server/process-state.js';
|
|
11
|
+
import { computeLifecycleBuildIdentity } from './build-identity.js';
|
|
12
|
+
const CONTEXT_TTL_MS = 4 * 60 * 60 * 1_000;
|
|
13
|
+
function digestFiles(files) {
|
|
14
|
+
const hash = crypto.createHash('sha256');
|
|
15
|
+
for (const file of [...files].sort()) {
|
|
16
|
+
const stat = fs.lstatSync(file);
|
|
17
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
18
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
|
|
19
|
+
}
|
|
20
|
+
hash.update(path.basename(file));
|
|
21
|
+
hash.update('\0');
|
|
22
|
+
hash.update(fs.readFileSync(file));
|
|
23
|
+
hash.update('\0');
|
|
24
|
+
}
|
|
25
|
+
return hash.digest('hex');
|
|
26
|
+
}
|
|
27
|
+
function copyRegular(source, destination, mode) {
|
|
28
|
+
const stat = fs.lstatSync(source);
|
|
29
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
30
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_INPUT_INVALID');
|
|
31
|
+
}
|
|
32
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
|
|
33
|
+
fs.copyFileSync(source, destination);
|
|
34
|
+
fs.chmodSync(destination, mode);
|
|
35
|
+
}
|
|
36
|
+
function controlFromEnvironment() {
|
|
37
|
+
const token = process.env.AGENT_INFRA_CONTROL_TOKEN;
|
|
38
|
+
const generation = process.env.AGENT_INFRA_CONTROL_GENERATION;
|
|
39
|
+
const channelDir = process.env.AGENT_INFRA_CONTROL_DIR;
|
|
40
|
+
const statusDir = process.env.AGENT_INFRA_CONTROL_STATUS_DIR;
|
|
41
|
+
if (!token || !generation || !channelDir || !statusDir) {
|
|
42
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTROL_MISSING');
|
|
43
|
+
}
|
|
44
|
+
return { token, generation, channelDir, statusDir };
|
|
45
|
+
}
|
|
46
|
+
function verifyControl(taskId, control) {
|
|
47
|
+
let status = null;
|
|
48
|
+
let lastError = null;
|
|
49
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
50
|
+
try {
|
|
51
|
+
status = readSandboxControlStatus(control.statusDir);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
lastError = error;
|
|
56
|
+
if (attempt < 4)
|
|
57
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!status) {
|
|
61
|
+
throw new Error(`CODEX_SANDBOX_CONTROLLER_CONTROL_UNAVAILABLE: ${String(lastError)}`);
|
|
62
|
+
}
|
|
63
|
+
if (status.generation !== control.generation
|
|
64
|
+
|| status.state !== 'healthy'
|
|
65
|
+
|| Date.now() - status.updatedAt > 1_500) {
|
|
66
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTROL_UNAVAILABLE');
|
|
67
|
+
}
|
|
68
|
+
const response = requestSandboxControl({
|
|
69
|
+
family: 'task-orchestration',
|
|
70
|
+
args: [taskId, 'status'],
|
|
71
|
+
...control,
|
|
72
|
+
timeoutMs: 30_000
|
|
73
|
+
});
|
|
74
|
+
let payload = {};
|
|
75
|
+
try {
|
|
76
|
+
payload = JSON.parse(response.stdout);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_BINDING_INVALID');
|
|
80
|
+
}
|
|
81
|
+
if (payload.taskId !== taskId)
|
|
82
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_BINDING_INVALID');
|
|
83
|
+
}
|
|
84
|
+
function detectedCodexVersion() {
|
|
85
|
+
const result = spawnSync('codex', ['--version'], { encoding: 'utf8' });
|
|
86
|
+
const version = /codex-cli\s+(\d+\.\d+\.\d+)/u.exec(result.stdout ?? '')?.[1];
|
|
87
|
+
if (result.status !== 0 || !version)
|
|
88
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CODEX_UNAVAILABLE');
|
|
89
|
+
return version;
|
|
90
|
+
}
|
|
91
|
+
function sanitizedConfig(source, destination, sourceHome, targetHome) {
|
|
92
|
+
if (!fs.existsSync(source))
|
|
93
|
+
return [];
|
|
94
|
+
const parsed = toml.parse(fs.readFileSync(source, 'utf8'));
|
|
95
|
+
const allowed = new Set([
|
|
96
|
+
'model',
|
|
97
|
+
'model_provider',
|
|
98
|
+
'model_providers',
|
|
99
|
+
'model_reasoning_effort',
|
|
100
|
+
'model_auto_compact_token_limit',
|
|
101
|
+
'preferred_auth_method'
|
|
102
|
+
]);
|
|
103
|
+
const output = Object.fromEntries(Object.entries(parsed).filter(([key]) => allowed.has(key)));
|
|
104
|
+
const catalog = parsed.model_catalog_json;
|
|
105
|
+
if (typeof catalog === 'string' && catalog.trim()) {
|
|
106
|
+
const resolved = path.resolve(sourceHome, catalog.replace(/^~[/\\]/u, ''));
|
|
107
|
+
const relative = path.relative(sourceHome, resolved);
|
|
108
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
109
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONFIG_PATH_INVALID');
|
|
110
|
+
}
|
|
111
|
+
const target = path.join(targetHome, 'model-catalogs', path.basename(resolved));
|
|
112
|
+
copyRegular(resolved, target, 0o600);
|
|
113
|
+
output.model_catalog_json = target;
|
|
114
|
+
}
|
|
115
|
+
fs.writeFileSync(destination, `${toml.stringify(output)}\n`, { mode: 0o600 });
|
|
116
|
+
const providerEnvironment = new Set();
|
|
117
|
+
const providers = output.model_providers;
|
|
118
|
+
if (providers && typeof providers === 'object' && !Array.isArray(providers)) {
|
|
119
|
+
for (const provider of Object.values(providers)) {
|
|
120
|
+
if (!provider || typeof provider !== 'object' || Array.isArray(provider))
|
|
121
|
+
continue;
|
|
122
|
+
for (const key of ['env_key', 'envKey']) {
|
|
123
|
+
const name = provider[key];
|
|
124
|
+
if (typeof name === 'string' && /^[A-Z][A-Z0-9_]{1,127}$/u.test(name)) {
|
|
125
|
+
providerEnvironment.add(name);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return Object.freeze([...providerEnvironment]);
|
|
131
|
+
}
|
|
132
|
+
function isolatedEnvironment(home, shimDir, contextPath, taskId, control, providerEnvironment, sourceEnvironment) {
|
|
133
|
+
const inherited = new Set([
|
|
134
|
+
'LANG', 'LC_ALL', 'TERM', 'COLORTERM',
|
|
135
|
+
'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY',
|
|
136
|
+
'http_proxy', 'https_proxy', 'no_proxy',
|
|
137
|
+
'SSL_CERT_FILE', 'SSL_CERT_DIR', 'NODE_EXTRA_CA_CERTS',
|
|
138
|
+
'CODEX_CI',
|
|
139
|
+
...providerEnvironment
|
|
140
|
+
]);
|
|
141
|
+
const env = {};
|
|
142
|
+
for (const key of inherited) {
|
|
143
|
+
if (sourceEnvironment[key] !== undefined)
|
|
144
|
+
env[key] = sourceEnvironment[key];
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
...env,
|
|
148
|
+
HOME: home,
|
|
149
|
+
CODEX_HOME: home,
|
|
150
|
+
PATH: `${shimDir}${path.delimiter}${sourceEnvironment.PATH ?? ''}`,
|
|
151
|
+
AGENT_INFRA_TASK_ID: taskId,
|
|
152
|
+
AGENT_INFRA_CONTROL_TOKEN: control.token,
|
|
153
|
+
AGENT_INFRA_CONTROL_GENERATION: control.generation,
|
|
154
|
+
AGENT_INFRA_CONTROL_DIR: control.channelDir,
|
|
155
|
+
AGENT_INFRA_CONTROL_STATUS_DIR: control.statusDir,
|
|
156
|
+
AGENT_INFRA_CODEX_CONTROLLER_CONTEXT: contextPath
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function shellQuote(value) {
|
|
160
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
161
|
+
}
|
|
162
|
+
function prepareCodexSandboxController(input, options = {}) {
|
|
163
|
+
const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
|
|
164
|
+
const sourceHome = path.resolve(options.codexHome ?? process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex'));
|
|
165
|
+
const control = options.control ?? controlFromEnvironment();
|
|
166
|
+
(options.verifyTaskBinding ?? verifyControl)(input.taskId, control);
|
|
167
|
+
const version = (options.codexVersion ?? detectedCodexVersion)();
|
|
168
|
+
if (!semver.gte(version, '0.147.0'))
|
|
169
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CODEX_UNSUPPORTED');
|
|
170
|
+
const runtimeRoot = path.resolve(options.temporaryRoot
|
|
171
|
+
?? path.join(os.tmpdir(), 'agent-infra-codex-controllers'));
|
|
172
|
+
fs.mkdirSync(runtimeRoot, { recursive: true, mode: 0o700 });
|
|
173
|
+
fs.chmodSync(runtimeRoot, 0o700);
|
|
174
|
+
const key = crypto.createHash('sha256')
|
|
175
|
+
.update(`${input.taskId}\0${control.generation}`)
|
|
176
|
+
.digest('hex');
|
|
177
|
+
const leasePath = path.join(runtimeRoot, `${key}.lease.json`);
|
|
178
|
+
if (fs.existsSync(leasePath)) {
|
|
179
|
+
const lease = JSON.parse(fs.readFileSync(leasePath, 'utf8'));
|
|
180
|
+
if (Number.isSafeInteger(lease.pid) && typeof lease.startTime === 'number'
|
|
181
|
+
&& getProcessStartTime(lease.pid) === lease.startTime) {
|
|
182
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_BUSY');
|
|
183
|
+
}
|
|
184
|
+
fs.unlinkSync(leasePath);
|
|
185
|
+
}
|
|
186
|
+
const parentStartTime = getProcessStartTime(process.pid);
|
|
187
|
+
if (!parentStartTime)
|
|
188
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_PROCESS_IDENTITY_INVALID');
|
|
189
|
+
const instanceDigest = crypto.randomBytes(32).toString('hex');
|
|
190
|
+
fs.writeFileSync(leasePath, `${JSON.stringify({
|
|
191
|
+
version: 1,
|
|
192
|
+
pid: process.pid,
|
|
193
|
+
startTime: parentStartTime,
|
|
194
|
+
taskId: input.taskId,
|
|
195
|
+
generation: control.generation,
|
|
196
|
+
instanceDigest
|
|
197
|
+
})}\n`, { mode: 0o600, flag: 'wx' });
|
|
198
|
+
const home = fs.mkdtempSync(path.join(runtimeRoot, `${key}-`));
|
|
199
|
+
fs.chmodSync(home, 0o700);
|
|
200
|
+
let cleaned = false;
|
|
201
|
+
const cleanup = () => {
|
|
202
|
+
if (cleaned)
|
|
203
|
+
return;
|
|
204
|
+
cleaned = true;
|
|
205
|
+
const relative = path.relative(runtimeRoot, home);
|
|
206
|
+
if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
|
|
207
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const lease = JSON.parse(fs.readFileSync(leasePath, 'utf8'));
|
|
211
|
+
if (lease.instanceDigest === instanceDigest)
|
|
212
|
+
fs.unlinkSync(leasePath);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// A missing or replaced lease is owned by another cleanup path.
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
try {
|
|
219
|
+
const auth = path.join(sourceHome, 'auth.json');
|
|
220
|
+
if (fs.existsSync(auth))
|
|
221
|
+
copyRegular(auth, path.join(home, 'auth.json'), 0o600);
|
|
222
|
+
const providerEnvironment = sanitizedConfig(path.join(sourceHome, 'config.toml'), path.join(home, 'config.toml'), sourceHome, home);
|
|
223
|
+
const hooks = path.join(repoRoot, '.codex', 'hooks.json');
|
|
224
|
+
const executor = path.join(repoRoot, '.codex', 'agents', 'agent-infra-lifecycle-executor.toml');
|
|
225
|
+
const reviewer = path.join(repoRoot, '.codex', 'agents', 'agent-infra-lifecycle-reviewer.toml');
|
|
226
|
+
copyRegular(hooks, path.join(home, 'hooks.json'), 0o600);
|
|
227
|
+
copyRegular(executor, path.join(home, 'agents', path.basename(executor)), 0o600);
|
|
228
|
+
copyRegular(reviewer, path.join(home, 'agents', path.basename(reviewer)), 0o600);
|
|
229
|
+
if (crypto.createHash('sha256').update(fs.readFileSync(path.join(home, 'hooks.json'))).digest('hex')
|
|
230
|
+
!== crypto.createHash('sha256').update(fs.readFileSync(hooks)).digest('hex')
|
|
231
|
+
|| digestFiles([
|
|
232
|
+
path.join(home, 'agents', path.basename(executor)),
|
|
233
|
+
path.join(home, 'agents', path.basename(reviewer))
|
|
234
|
+
]) !== digestFiles([executor, reviewer])) {
|
|
235
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
|
|
236
|
+
}
|
|
237
|
+
const buildIdentity = computeLifecycleBuildIdentity(repoRoot);
|
|
238
|
+
const now = (options.now ?? Date.now)();
|
|
239
|
+
const context = Object.freeze({
|
|
240
|
+
version: 1,
|
|
241
|
+
taskId: input.taskId,
|
|
242
|
+
controlGeneration: control.generation,
|
|
243
|
+
controllerInstanceDigest: instanceDigest,
|
|
244
|
+
parentPid: process.pid,
|
|
245
|
+
parentStartTime,
|
|
246
|
+
issuedAt: now,
|
|
247
|
+
expiresAt: now + CONTEXT_TTL_MS,
|
|
248
|
+
buildIdentity,
|
|
249
|
+
hookDefinitionHash: crypto.createHash('sha256').update(fs.readFileSync(hooks)).digest('hex'),
|
|
250
|
+
lifecycleProfilesHash: digestFiles([executor, reviewer])
|
|
251
|
+
});
|
|
252
|
+
const contextPath = path.join(home, 'controller-context.json');
|
|
253
|
+
fs.writeFileSync(contextPath, `${JSON.stringify(context, null, 2)}\n`, { mode: 0o600 });
|
|
254
|
+
const shimDir = path.join(home, 'bin');
|
|
255
|
+
fs.mkdirSync(shimDir, { mode: 0o700 });
|
|
256
|
+
const internalCli = path.resolve(process.argv[1] ?? path.join(repoRoot, 'bin', 'internal-cli.ts'));
|
|
257
|
+
const source = internalCli.endsWith('.ts')
|
|
258
|
+
? `#!/bin/sh\nexec ${shellQuote(process.execPath)} --experimental-strip-types ${shellQuote(internalCli)} "$@"\n`
|
|
259
|
+
: `#!/bin/sh\nexec ${shellQuote(process.execPath)} ${shellQuote(internalCli)} "$@"\n`;
|
|
260
|
+
fs.writeFileSync(path.join(shimDir, 'agent-infra-internal'), source, { mode: 0o700 });
|
|
261
|
+
const policy = [
|
|
262
|
+
input.executorModel ? `--executor-model ${input.executorModel}` : '',
|
|
263
|
+
input.executorReasoningEffort ? `--executor-reasoning-effort ${input.executorReasoningEffort}` : '',
|
|
264
|
+
input.reviewerModel ? `--reviewer-model ${input.reviewerModel}` : '',
|
|
265
|
+
input.reviewerReasoningEffort ? `--reviewer-reasoning-effort ${input.reviewerReasoningEffort}` : ''
|
|
266
|
+
].filter(Boolean).join(' ');
|
|
267
|
+
const prompt = `$run-task ${input.taskRef}${policy ? ` ${policy}` : ''}`;
|
|
268
|
+
const args = Object.freeze([
|
|
269
|
+
'exec',
|
|
270
|
+
'--enable', 'hooks',
|
|
271
|
+
'--enable', 'multi_agent',
|
|
272
|
+
'--dangerously-bypass-hook-trust',
|
|
273
|
+
'--dangerously-bypass-approvals-and-sandbox',
|
|
274
|
+
'--json',
|
|
275
|
+
'-C', repoRoot,
|
|
276
|
+
prompt
|
|
277
|
+
]);
|
|
278
|
+
const env = isolatedEnvironment(home, shimDir, contextPath, input.taskId, control, providerEnvironment, options.environment ?? process.env);
|
|
279
|
+
return Object.freeze({
|
|
280
|
+
command: 'codex',
|
|
281
|
+
args,
|
|
282
|
+
env,
|
|
283
|
+
home,
|
|
284
|
+
contextPath,
|
|
285
|
+
context,
|
|
286
|
+
cleanup
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
cleanup();
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function verifyCodexSandboxControllerContext(contextPath, taskId, options = {}) {
|
|
295
|
+
const stat = fs.lstatSync(contextPath);
|
|
296
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0) {
|
|
297
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
|
|
298
|
+
}
|
|
299
|
+
const context = JSON.parse(fs.readFileSync(contextPath, 'utf8'));
|
|
300
|
+
const generation = options.generation ?? process.env.AGENT_INFRA_CONTROL_GENERATION;
|
|
301
|
+
if (context.version !== 1
|
|
302
|
+
|| context.taskId !== taskId
|
|
303
|
+
|| context.controlGeneration !== generation
|
|
304
|
+
|| context.expiresAt < (options.now ?? Date.now())
|
|
305
|
+
|| getProcessStartTime(context.parentPid) !== context.parentStartTime
|
|
306
|
+
|| !isProcessAlive(context.parentPid)) {
|
|
307
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
|
|
308
|
+
}
|
|
309
|
+
const current = computeLifecycleBuildIdentity(options.repoRoot ?? process.cwd());
|
|
310
|
+
if (JSON.stringify(current) !== JSON.stringify(context.buildIdentity)) {
|
|
311
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
|
|
312
|
+
}
|
|
313
|
+
return Object.freeze(context);
|
|
314
|
+
}
|
|
315
|
+
async function runCodexSandboxController(input, options = {}) {
|
|
316
|
+
const prepared = prepareCodexSandboxController(input, options);
|
|
317
|
+
try {
|
|
318
|
+
const child = spawn(prepared.command, [...prepared.args], {
|
|
319
|
+
cwd: options.repoRoot ?? process.cwd(),
|
|
320
|
+
env: prepared.env,
|
|
321
|
+
stdio: 'inherit',
|
|
322
|
+
detached: process.platform !== 'win32'
|
|
323
|
+
});
|
|
324
|
+
let forceKill;
|
|
325
|
+
const terminate = () => {
|
|
326
|
+
if (child.pid && process.platform !== 'win32') {
|
|
327
|
+
try {
|
|
328
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
child.kill('SIGTERM');
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
else
|
|
335
|
+
child.kill('SIGTERM');
|
|
336
|
+
forceKill = setTimeout(() => {
|
|
337
|
+
if (child.pid && process.platform !== 'win32') {
|
|
338
|
+
try {
|
|
339
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
child.kill('SIGKILL');
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
else
|
|
346
|
+
child.kill('SIGKILL');
|
|
347
|
+
}, 5_000);
|
|
348
|
+
forceKill.unref();
|
|
349
|
+
};
|
|
350
|
+
process.once('SIGINT', terminate);
|
|
351
|
+
process.once('SIGTERM', terminate);
|
|
352
|
+
try {
|
|
353
|
+
return await new Promise((resolve, reject) => {
|
|
354
|
+
child.once('error', reject);
|
|
355
|
+
child.once('exit', (code, signal) => resolve(code ?? (signal ? 1 : 0)));
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
finally {
|
|
359
|
+
if (forceKill)
|
|
360
|
+
clearTimeout(forceKill);
|
|
361
|
+
process.off('SIGINT', terminate);
|
|
362
|
+
process.off('SIGTERM', terminate);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
prepared.cleanup();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
export { prepareCodexSandboxController, runCodexSandboxController, verifyCodexSandboxControllerContext };
|
|
370
|
+
//# sourceMappingURL=sandbox-controller.js.map
|