@fitlab-ai/agent-infra 0.9.8 → 0.9.10
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/bin/internal-cli.js +11 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +58 -21
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +25 -11
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +173 -10
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +127 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +63 -92
- package/dist/lib/agent-clients/adapters/codex-sandbox.js +72 -11
- package/dist/lib/agent-clients/adapters/index.js +3 -1
- package/dist/lib/agent-clients/adapters/traecli.js +66 -0
- package/dist/lib/agent-clients/tokens.js +2 -2
- package/dist/lib/agent-clients/types.js +2 -1
- package/dist/lib/defaults.json +5 -0
- package/dist/lib/git/workflow.js +10 -1
- package/dist/lib/internal/codex-lifecycle.js +20 -6
- package/dist/lib/internal/codex-sandbox-controller.js +9 -15
- package/dist/lib/internal/git-workflow.js +5 -7
- package/dist/lib/internal/platform-pr.js +20 -7
- package/dist/lib/internal/release-workflow.js +2 -1
- package/dist/lib/internal/sandbox-control.js +79 -2
- package/dist/lib/internal/task-finalization.js +54 -0
- package/dist/lib/internal/task-orchestration.js +156 -291
- package/dist/lib/{task/commands/validate.js → internal/task-validate.js} +75 -42
- package/dist/lib/platform/pr-summary.js +48 -10
- package/dist/lib/platform/pull-requests.js +205 -41
- package/dist/lib/run/tui.js +5 -2
- package/dist/lib/sandbox/commands/rm.js +316 -55
- package/dist/lib/sandbox/control/client.js +190 -6
- package/dist/lib/sandbox/control/controller-registration.js +275 -0
- package/dist/lib/sandbox/control/executor.js +140 -5
- package/dist/lib/sandbox/control/lifecycle.js +3 -1
- package/dist/lib/sandbox/control/protocol.js +67 -6
- package/dist/lib/sandbox/index.js +5 -3
- package/dist/lib/sandbox/managed-fs.js +5 -0
- package/dist/lib/sandbox/task-resolver.js +50 -6
- package/dist/lib/sandbox/workspace-identity.js +27 -0
- package/dist/lib/sandbox/worktree-safety.js +215 -2
- package/dist/lib/task/codex-orchestration.js +161 -39
- package/dist/lib/task/commands/status.js +59 -1
- package/dist/lib/task/commit-operation.js +335 -0
- package/dist/lib/task/commit-policy.js +18 -0
- package/dist/lib/task/delegation-receipts.js +248 -6
- package/dist/lib/task/finalization.js +593 -0
- package/dist/lib/task/index.js +0 -9
- package/dist/lib/task/operation-outcome.js +33 -0
- package/dist/lib/task/orchestration.js +187 -761
- package/dist/lib/task/short-id.js +59 -1
- package/dist/lib/task/task-execution-lock.js +6 -1
- package/dist/lib/task/verification-engine.js +110 -60
- package/dist/lib/task/verification.js +2 -1
- package/dist/lib/task/workflow-warning-intents.js +50 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +61 -19
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +37 -10
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +276 -12
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +163 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +91 -118
- package/lib/agent-clients/adapters/codex-sandbox.ts +89 -14
- package/lib/agent-clients/adapters/index.ts +3 -1
- package/lib/agent-clients/adapters/traecli.ts +67 -0
- package/lib/agent-clients/tokens.ts +2 -2
- package/lib/agent-clients/types.ts +2 -1
- package/lib/defaults.json +5 -0
- package/lib/git/workflow.ts +11 -2
- package/lib/internal/codex-lifecycle.ts +19 -6
- package/lib/internal/codex-sandbox-controller.ts +9 -14
- package/lib/internal/git-workflow.ts +5 -6
- package/lib/internal/platform-pr.ts +14 -7
- package/lib/internal/release-workflow.ts +2 -1
- package/lib/internal/sandbox-control.ts +92 -2
- package/lib/internal/task-finalization.ts +68 -0
- package/lib/internal/task-orchestration.ts +20 -103
- package/lib/{task/commands/validate.ts → internal/task-validate.ts} +75 -40
- package/lib/platform/pr-summary.ts +55 -11
- package/lib/platform/pull-requests.ts +211 -41
- package/lib/run/tui.ts +5 -3
- package/lib/sandbox/commands/rm.ts +413 -61
- package/lib/sandbox/control/client.ts +255 -6
- package/lib/sandbox/control/controller-registration.ts +352 -0
- package/lib/sandbox/control/executor.ts +169 -5
- package/lib/sandbox/control/lifecycle.ts +2 -1
- package/lib/sandbox/control/protocol.ts +80 -7
- package/lib/sandbox/index.ts +5 -3
- package/lib/sandbox/managed-fs.ts +5 -0
- package/lib/sandbox/task-resolver.ts +58 -7
- package/lib/sandbox/workspace-identity.ts +50 -2
- package/lib/sandbox/workspace-view.ts +3 -3
- package/lib/sandbox/worktree-safety.ts +236 -2
- package/lib/task/codex-orchestration.ts +168 -40
- package/lib/task/commands/status.ts +78 -1
- package/lib/task/commit-operation.ts +430 -0
- package/lib/task/commit-policy.ts +30 -0
- package/lib/task/delegation-receipts.ts +263 -4
- package/lib/task/finalization.ts +761 -0
- package/lib/task/index.ts +0 -8
- package/lib/task/operation-outcome.ts +48 -0
- package/lib/task/orchestration.ts +218 -982
- package/lib/task/short-id.ts +65 -1
- package/lib/task/task-execution-lock.ts +10 -1
- package/lib/task/verification-engine.ts +115 -63
- package/lib/task/verification.ts +3 -2
- package/lib/task/workflow-warning-intents.ts +57 -2
- package/package.json +2 -2
- package/templates/.agents/QUICKSTART.en.md +3 -4
- package/templates/.agents/QUICKSTART.zh-CN.md +3 -4
- package/templates/.agents/hooks/lifecycle-delegation.js +1 -21
- package/templates/.agents/rules/README.en.md +1 -0
- package/templates/.agents/rules/README.zh-CN.md +1 -0
- package/templates/.agents/rules/compatibility-policy.en.md +39 -0
- package/templates/.agents/rules/compatibility-policy.zh-CN.md +39 -0
- package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -4
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -4
- package/templates/.agents/rules/pr-sync.en.md +1 -1
- package/templates/.agents/rules/pr-sync.github.en.md +2 -1
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +2 -0
- package/templates/.agents/rules/pr-sync.zh-CN.md +1 -0
- package/templates/.agents/rules/review-method.en.md +1 -0
- package/templates/.agents/rules/review-method.zh-CN.md +1 -0
- package/templates/.agents/rules/task-management.en.md +2 -2
- package/templates/.agents/rules/task-management.zh-CN.md +2 -2
- package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -0
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
- package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/code-task/SKILL.en.md +1 -0
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -0
- package/templates/.agents/skills/commit/SKILL.en.md +41 -125
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
- package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
- package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
- package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
- package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +17 -9
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +17 -9
- package/templates/.agents/skills/create-pr/reference/change-report.en.md +41 -0
- package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +41 -0
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
- package/templates/.agents/skills/plan-task/SKILL.en.md +2 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -1
- package/templates/.agents/skills/review-code/reference/migration-risks.en.md +8 -6
- package/templates/.agents/skills/review-code/reference/migration-risks.zh-CN.md +8 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +3 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +1 -1
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +1 -1
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +6 -6
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +24 -0
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
- package/templates/.codex/hooks.json +4 -4
- package/templates/.trae/skills/update-agent-infra.en.md +8 -0
- package/templates/.trae/skills/update-agent-infra.zh-CN.md +8 -0
- package/dist/lib/task/commit-finalization.js +0 -305
- package/dist/lib/task/commit-intent.js +0 -165
- package/lib/task/commit-finalization.ts +0 -419
- package/lib/task/commit-intent.ts +0 -230
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { SANDBOX_CONTROL_ADMISSION_WINDOW_MS, SANDBOX_CONTROL_MAX_BYTES, SANDBOX_CONTROL_STATUS_STALE_MS, isSandboxControlFamily } from './protocol.js';
|
|
5
|
+
import { normalizeAgentToken } from '../../agent-clients/tokens.js';
|
|
5
6
|
import { readSandboxControlStatus } from './state.js';
|
|
6
7
|
const SANDBOX_CONTROL_RESPONSE_SETTLE_MS = 250;
|
|
7
8
|
export class SandboxControlClientError extends Error {
|
|
@@ -150,24 +151,207 @@ function authority(params) {
|
|
|
150
151
|
export function requestSandboxControl(params) {
|
|
151
152
|
if (!isSandboxControlFamily(params.family))
|
|
152
153
|
clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' is not allowed`, false);
|
|
153
|
-
if (params.family === 'task-create')
|
|
154
|
-
clientError('SANDBOX_CONTROL_COMMAND_DENIED',
|
|
154
|
+
if (params.family === 'task-create' || params.family === 'codex-controller' || params.family === 'task-finalization') {
|
|
155
|
+
clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' requires a typed request`, false);
|
|
156
|
+
}
|
|
157
|
+
const auth = authority(params);
|
|
158
|
+
const issuedAt = Date.now();
|
|
159
|
+
const request = {
|
|
160
|
+
version: 3, id: randomUUID(), ...auth, issuedAt,
|
|
161
|
+
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
162
|
+
family: params.family, args: params.args,
|
|
163
|
+
controllerProcess: null,
|
|
164
|
+
controllerProof: null
|
|
165
|
+
};
|
|
166
|
+
return exchangeSandboxControl(request, params);
|
|
167
|
+
}
|
|
168
|
+
export function requestSandboxTaskControl(params) {
|
|
155
169
|
const auth = authority(params);
|
|
156
170
|
const issuedAt = Date.now();
|
|
157
171
|
const request = {
|
|
158
|
-
version:
|
|
172
|
+
version: 3,
|
|
173
|
+
id: randomUUID(),
|
|
174
|
+
...auth,
|
|
175
|
+
issuedAt,
|
|
159
176
|
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
160
|
-
family: params.family,
|
|
177
|
+
family: params.family,
|
|
178
|
+
args: params.args,
|
|
179
|
+
controllerProcess: null,
|
|
180
|
+
controllerProof: params.controllerProof
|
|
161
181
|
};
|
|
162
182
|
return exchangeSandboxControl(request, params);
|
|
163
183
|
}
|
|
184
|
+
export function requestSandboxTaskFinalization(params) {
|
|
185
|
+
const agent = normalizeAgentToken(params.agent);
|
|
186
|
+
if (!agent)
|
|
187
|
+
clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization agent is invalid', false);
|
|
188
|
+
const auth = authority(params);
|
|
189
|
+
const issuedAt = Date.now();
|
|
190
|
+
const request = {
|
|
191
|
+
version: 3,
|
|
192
|
+
id: randomUUID(),
|
|
193
|
+
...auth,
|
|
194
|
+
issuedAt,
|
|
195
|
+
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
196
|
+
family: 'task-finalization',
|
|
197
|
+
operation: 'complete',
|
|
198
|
+
agent,
|
|
199
|
+
args: [],
|
|
200
|
+
controllerProcess: null,
|
|
201
|
+
controllerProof: null
|
|
202
|
+
};
|
|
203
|
+
return exchangeSandboxControl(request, params);
|
|
204
|
+
}
|
|
205
|
+
function exactKeys(value, keys) {
|
|
206
|
+
return Object.keys(value).sort().join(',') === [...keys].sort().join(',');
|
|
207
|
+
}
|
|
208
|
+
function validProcess(value) {
|
|
209
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
210
|
+
return false;
|
|
211
|
+
const identity = value;
|
|
212
|
+
return exactKeys(identity, ['pid', 'startTime'])
|
|
213
|
+
&& Number.isSafeInteger(identity.pid) && identity.pid > 0
|
|
214
|
+
&& Number.isSafeInteger(identity.startTime) && identity.startTime >= 0;
|
|
215
|
+
}
|
|
216
|
+
function validBuild(value) {
|
|
217
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
218
|
+
return false;
|
|
219
|
+
const build = value;
|
|
220
|
+
return exactKeys(build, ['internalExecutableBuildHash', 'lifecycleContractHash', 'packageVersion', 'protocolVersion'])
|
|
221
|
+
&& build.protocolVersion === 3
|
|
222
|
+
&& typeof build.packageVersion === 'string' && build.packageVersion.length > 0
|
|
223
|
+
&& typeof build.internalExecutableBuildHash === 'string' && /^[a-f0-9]{64}$/u.test(build.internalExecutableBuildHash)
|
|
224
|
+
&& typeof build.lifecycleContractHash === 'string' && /^[a-f0-9]{64}$/u.test(build.lifecycleContractHash);
|
|
225
|
+
}
|
|
226
|
+
export function parseCodexControllerResult(response) {
|
|
227
|
+
if (response.phase !== 'completed' || response.error !== null || response.stderr !== '') {
|
|
228
|
+
clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller result outer response is invalid', false, true);
|
|
229
|
+
}
|
|
230
|
+
let value;
|
|
231
|
+
try {
|
|
232
|
+
if (!response.stdout.endsWith('\n') || response.stdout.slice(0, -1).includes('\n'))
|
|
233
|
+
throw new Error('not canonical');
|
|
234
|
+
value = JSON.parse(response.stdout);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller result payload is invalid', false, true);
|
|
238
|
+
}
|
|
239
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
240
|
+
clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller result payload is invalid', false, true);
|
|
241
|
+
}
|
|
242
|
+
const result = value;
|
|
243
|
+
const resultKeys = result.status === 'verified'
|
|
244
|
+
? ['binding', 'changed', 'error', 'lease', 'status', 'version']
|
|
245
|
+
: ['changed', 'error', 'lease', 'status', 'version'];
|
|
246
|
+
if (!exactKeys(result, resultKeys)
|
|
247
|
+
|| result.version !== 1 || !['opened', 'closed', 'failed', 'verified'].includes(result.status)) {
|
|
248
|
+
clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller result schema is invalid', false, true);
|
|
249
|
+
}
|
|
250
|
+
if (result.status === 'failed') {
|
|
251
|
+
const error = result.error;
|
|
252
|
+
if (response.exitCode !== 1 || result.changed !== false || result.lease !== null
|
|
253
|
+
|| !error || !exactKeys(error, ['code', 'message', 'retryable'])
|
|
254
|
+
|| typeof error.code !== 'string' || !/^[A-Z][A-Z0-9_]+$/u.test(error.code)
|
|
255
|
+
|| typeof error.message !== 'string' || error.retryable !== false) {
|
|
256
|
+
clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller failure result is invalid', false, true);
|
|
257
|
+
}
|
|
258
|
+
clientError(error.code, error.message, false, true);
|
|
259
|
+
}
|
|
260
|
+
if (response.exitCode !== 0 || result.error !== null) {
|
|
261
|
+
clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller success result is invalid', false, true);
|
|
262
|
+
}
|
|
263
|
+
if (result.status === 'closed') {
|
|
264
|
+
if (typeof result.changed !== 'boolean' || result.lease !== null) {
|
|
265
|
+
clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller close result is invalid', false, true);
|
|
266
|
+
}
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
if (result.status === 'verified') {
|
|
270
|
+
const binding = result.binding;
|
|
271
|
+
if (result.changed !== false || result.lease !== null || !binding
|
|
272
|
+
|| !exactKeys(binding, ['controllerInstanceDigest', 'controlGeneration', 'taskId'])
|
|
273
|
+
|| typeof binding.taskId !== 'string' || binding.taskId.length === 0
|
|
274
|
+
|| typeof binding.controlGeneration !== 'string' || binding.controlGeneration.length === 0
|
|
275
|
+
|| typeof binding.controllerInstanceDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(binding.controllerInstanceDigest)) {
|
|
276
|
+
clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller verify result is invalid', false, true);
|
|
277
|
+
}
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
const lease = result.lease;
|
|
281
|
+
if (result.changed !== true || !lease
|
|
282
|
+
|| !exactKeys(lease, [
|
|
283
|
+
'buildIdentity', 'controlGeneration', 'controllerInstanceDigest', 'controllerProcess',
|
|
284
|
+
'expiresAt', 'issuedAt', 'leaseId', 'leaseSecret', 'taskId', 'version'
|
|
285
|
+
])
|
|
286
|
+
|| lease.version !== 1
|
|
287
|
+
|| typeof lease.leaseId !== 'string' || !/^[a-f0-9]{64}$/u.test(lease.leaseId)
|
|
288
|
+
|| typeof lease.leaseSecret !== 'string' || !/^[a-f0-9]{64}$/u.test(lease.leaseSecret)
|
|
289
|
+
|| typeof lease.taskId !== 'string' || lease.taskId.length === 0
|
|
290
|
+
|| typeof lease.controlGeneration !== 'string' || lease.controlGeneration.length === 0
|
|
291
|
+
|| typeof lease.controllerInstanceDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(lease.controllerInstanceDigest)
|
|
292
|
+
|| !validProcess(lease.controllerProcess) || !validBuild(lease.buildIdentity)
|
|
293
|
+
|| !Number.isSafeInteger(lease.issuedAt) || !Number.isSafeInteger(lease.expiresAt)
|
|
294
|
+
|| lease.expiresAt <= lease.issuedAt) {
|
|
295
|
+
clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller open result is invalid', false, true);
|
|
296
|
+
}
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
function requestCodexController(params) {
|
|
300
|
+
const auth = authority(params);
|
|
301
|
+
const issuedAt = Date.now();
|
|
302
|
+
const request = {
|
|
303
|
+
version: 3,
|
|
304
|
+
id: randomUUID(),
|
|
305
|
+
...auth,
|
|
306
|
+
issuedAt,
|
|
307
|
+
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
308
|
+
family: 'codex-controller',
|
|
309
|
+
command: params.command,
|
|
310
|
+
args: [],
|
|
311
|
+
controllerProcess: params.controllerProcess,
|
|
312
|
+
controllerProof: params.controllerProof
|
|
313
|
+
};
|
|
314
|
+
const result = parseCodexControllerResult(exchangeSandboxControl(request, params));
|
|
315
|
+
if (result.status === 'opened'
|
|
316
|
+
&& (result.lease.controlGeneration !== auth.generation
|
|
317
|
+
|| result.lease.controllerProcess.pid !== params.controllerProcess.pid
|
|
318
|
+
|| result.lease.controllerProcess.startTime !== params.controllerProcess.startTime)) {
|
|
319
|
+
clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller result does not match the request', false, true);
|
|
320
|
+
}
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
export function requestCodexControllerOpen(params) {
|
|
324
|
+
const result = requestCodexController({ ...params, command: 'open', controllerProof: null });
|
|
325
|
+
if (result.status !== 'opened')
|
|
326
|
+
clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller open returned the wrong result', false, true);
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
export function requestCodexControllerClose(params) {
|
|
330
|
+
const result = requestCodexController({ ...params, command: 'close' });
|
|
331
|
+
if (result.status !== 'closed')
|
|
332
|
+
clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller close returned the wrong result', false, true);
|
|
333
|
+
return result;
|
|
334
|
+
}
|
|
335
|
+
export function requestCodexControllerVerify(params) {
|
|
336
|
+
const result = requestCodexController({
|
|
337
|
+
...params,
|
|
338
|
+
controllerProcess: params.controllerProof.controllerProcess,
|
|
339
|
+
command: 'verify'
|
|
340
|
+
});
|
|
341
|
+
if (result.status !== 'verified') {
|
|
342
|
+
clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller verify returned the wrong result', false, true);
|
|
343
|
+
}
|
|
344
|
+
return result;
|
|
345
|
+
}
|
|
164
346
|
export function requestSandboxTaskCreate(params) {
|
|
165
347
|
const auth = authority(params);
|
|
166
348
|
const issuedAt = Date.now();
|
|
167
349
|
const request = {
|
|
168
|
-
version:
|
|
350
|
+
version: 3, id: randomUUID(), ...auth, issuedAt,
|
|
169
351
|
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
170
|
-
family: 'task-create', candidate: params.candidate
|
|
352
|
+
family: 'task-create', candidate: params.candidate,
|
|
353
|
+
controllerProcess: null,
|
|
354
|
+
controllerProof: null
|
|
171
355
|
};
|
|
172
356
|
return exchangeSandboxControl(request, { ...params, timeoutMs: params.timeoutMs ?? 120_000 });
|
|
173
357
|
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { parseLinuxProcessStat } from '../../server/process-state.js';
|
|
5
|
+
import { commandForEngine, runProbe } from '../shell.js';
|
|
6
|
+
const CONTROLLER_TTL_MS = 4 * 60 * 60 * 1_000;
|
|
7
|
+
const HEX_256 = /^[a-f0-9]{64}$/u;
|
|
8
|
+
const REGISTRATION_KEYS = [
|
|
9
|
+
'buildIdentity', 'containerId', 'controlGeneration', 'controllerInstanceDigest',
|
|
10
|
+
'controllerProcess', 'expiresAt', 'issuedAt', 'leaseId', 'leaseSecretHash',
|
|
11
|
+
'taskId', 'version'
|
|
12
|
+
].sort().join(',');
|
|
13
|
+
export class CodexControllerRegistrationError extends Error {
|
|
14
|
+
code;
|
|
15
|
+
constructor(code, message) {
|
|
16
|
+
super(`${code}: ${message}`);
|
|
17
|
+
this.name = code;
|
|
18
|
+
this.code = code;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function fail(code, message) {
|
|
22
|
+
throw new CodexControllerRegistrationError(code, message);
|
|
23
|
+
}
|
|
24
|
+
function exactKeys(value, keys) {
|
|
25
|
+
return Object.keys(value).sort().join(',') === [...keys].sort().join(',');
|
|
26
|
+
}
|
|
27
|
+
function validProcess(value) {
|
|
28
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
29
|
+
return false;
|
|
30
|
+
const processValue = value;
|
|
31
|
+
return exactKeys(processValue, ['pid', 'startTime'])
|
|
32
|
+
&& Number.isSafeInteger(processValue.pid) && processValue.pid > 0
|
|
33
|
+
&& Number.isSafeInteger(processValue.startTime) && processValue.startTime >= 0;
|
|
34
|
+
}
|
|
35
|
+
function validBuild(value) {
|
|
36
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
37
|
+
return false;
|
|
38
|
+
const build = value;
|
|
39
|
+
return exactKeys(build, [
|
|
40
|
+
'internalExecutableBuildHash', 'lifecycleContractHash', 'packageVersion', 'protocolVersion'
|
|
41
|
+
])
|
|
42
|
+
&& build.protocolVersion === 3
|
|
43
|
+
&& typeof build.packageVersion === 'string' && build.packageVersion.length > 0
|
|
44
|
+
&& typeof build.internalExecutableBuildHash === 'string' && HEX_256.test(build.internalExecutableBuildHash)
|
|
45
|
+
&& typeof build.lifecycleContractHash === 'string' && HEX_256.test(build.lifecycleContractHash);
|
|
46
|
+
}
|
|
47
|
+
function parseRegistration(raw) {
|
|
48
|
+
let value;
|
|
49
|
+
try {
|
|
50
|
+
value = JSON.parse(raw);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
fail('CODEX_SANDBOX_CONTROLLER_REGISTRATION_INVALID', 'controller registration is malformed');
|
|
54
|
+
}
|
|
55
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
56
|
+
fail('CODEX_SANDBOX_CONTROLLER_REGISTRATION_INVALID', 'controller registration is invalid');
|
|
57
|
+
}
|
|
58
|
+
const registration = value;
|
|
59
|
+
if (Object.keys(registration).sort().join(',') !== REGISTRATION_KEYS
|
|
60
|
+
|| registration.version !== 1
|
|
61
|
+
|| typeof registration.taskId !== 'string'
|
|
62
|
+
|| typeof registration.controlGeneration !== 'string'
|
|
63
|
+
|| typeof registration.containerId !== 'string'
|
|
64
|
+
|| typeof registration.leaseId !== 'string' || !HEX_256.test(registration.leaseId)
|
|
65
|
+
|| typeof registration.leaseSecretHash !== 'string' || !HEX_256.test(registration.leaseSecretHash)
|
|
66
|
+
|| typeof registration.controllerInstanceDigest !== 'string' || !HEX_256.test(registration.controllerInstanceDigest)
|
|
67
|
+
|| !validProcess(registration.controllerProcess)
|
|
68
|
+
|| !validBuild(registration.buildIdentity)
|
|
69
|
+
|| !Number.isSafeInteger(registration.issuedAt)
|
|
70
|
+
|| !Number.isSafeInteger(registration.expiresAt)
|
|
71
|
+
|| registration.expiresAt <= registration.issuedAt) {
|
|
72
|
+
fail('CODEX_SANDBOX_CONTROLLER_REGISTRATION_INVALID', 'controller registration schema is invalid');
|
|
73
|
+
}
|
|
74
|
+
return registration;
|
|
75
|
+
}
|
|
76
|
+
function registrationPath(manifestPath) {
|
|
77
|
+
return path.join(path.dirname(path.resolve(manifestPath)), 'codex-controller.json');
|
|
78
|
+
}
|
|
79
|
+
function readRaw(file) {
|
|
80
|
+
try {
|
|
81
|
+
const stat = fs.lstatSync(file);
|
|
82
|
+
if (!stat.isFile() || stat.isSymbolicLink()
|
|
83
|
+
|| (process.platform !== 'win32' && (stat.mode & 0o777) !== 0o600)) {
|
|
84
|
+
fail('CODEX_SANDBOX_CONTROLLER_REGISTRATION_INVALID', 'controller registration file is unsafe');
|
|
85
|
+
}
|
|
86
|
+
return { raw: fs.readFileSync(file, 'utf8'), stat };
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (error.code === 'ENOENT')
|
|
90
|
+
return null;
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function sameFile(left, right) {
|
|
95
|
+
return left.dev === right.dev && left.ino === right.ino && left.mode === right.mode
|
|
96
|
+
&& left.size === right.size && left.mtimeMs === right.mtimeMs;
|
|
97
|
+
}
|
|
98
|
+
function secretHash(secret) {
|
|
99
|
+
return crypto.createHash('sha256')
|
|
100
|
+
.update('agent-infra/codex-controller-lease/v1\0')
|
|
101
|
+
.update(secret)
|
|
102
|
+
.digest('hex');
|
|
103
|
+
}
|
|
104
|
+
function safeEqual(left, right) {
|
|
105
|
+
if (!HEX_256.test(left) || !HEX_256.test(right))
|
|
106
|
+
return false;
|
|
107
|
+
return crypto.timingSafeEqual(Buffer.from(left, 'hex'), Buffer.from(right, 'hex'));
|
|
108
|
+
}
|
|
109
|
+
function defaultProbe(manifest, identity) {
|
|
110
|
+
const command = commandForEngine(manifest.engine, 'docker', [
|
|
111
|
+
'exec', manifest.containerIdentity.id, 'cat', `/proc/${identity.pid}/stat`
|
|
112
|
+
]);
|
|
113
|
+
const result = runProbe(command.cmd, command.args, { timeout: 2_000 });
|
|
114
|
+
if (result.status !== 0) {
|
|
115
|
+
const stderr = typeof result.stderr === 'string' ? result.stderr : result.stderr?.toString('utf8') ?? '';
|
|
116
|
+
return /no such (?:file|container)|not found|does not exist/iu.test(stderr) ? 'dead' : 'unknown';
|
|
117
|
+
}
|
|
118
|
+
const stdout = typeof result.stdout === 'string' ? result.stdout : result.stdout?.toString('utf8') ?? '';
|
|
119
|
+
const stat = parseLinuxProcessStat(stdout);
|
|
120
|
+
if (!stat)
|
|
121
|
+
return 'unknown';
|
|
122
|
+
if (stat.state === 'Z' || stat.startTime !== identity.startTime)
|
|
123
|
+
return 'dead';
|
|
124
|
+
return 'alive';
|
|
125
|
+
}
|
|
126
|
+
function assertRegistrationBinding(registration, manifest, buildIdentity) {
|
|
127
|
+
if (registration.taskId !== manifest.taskId
|
|
128
|
+
|| registration.controlGeneration !== manifest.generation
|
|
129
|
+
|| registration.containerId !== manifest.containerIdentity.id) {
|
|
130
|
+
fail('CODEX_SANDBOX_CONTROLLER_REGISTRATION_INVALID', 'controller registration binding is invalid');
|
|
131
|
+
}
|
|
132
|
+
if (registration.buildIdentity.protocolVersion !== buildIdentity.protocolVersion) {
|
|
133
|
+
fail('CODEX_LIFECYCLE_PROTOCOL_MISMATCH', 'Codex lifecycle protocol version does not match');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function atomicWrite(file, value) {
|
|
137
|
+
const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
138
|
+
try {
|
|
139
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600, flag: 'wx' });
|
|
140
|
+
fs.chmodSync(temporary, 0o600);
|
|
141
|
+
fs.renameSync(temporary, file);
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
fs.rmSync(temporary, { force: true });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export function readCodexControllerRegistration(manifestPath) {
|
|
148
|
+
const existing = readRaw(registrationPath(manifestPath));
|
|
149
|
+
if (!existing)
|
|
150
|
+
fail('CODEX_SANDBOX_CONTROLLER_REGISTRATION_MISSING', 'controller registration is missing');
|
|
151
|
+
return parseRegistration(existing.raw);
|
|
152
|
+
}
|
|
153
|
+
export function openCodexControllerRegistration(params, options = {}) {
|
|
154
|
+
if (params.manifest.mode !== 'task-bound' || !params.manifest.taskId) {
|
|
155
|
+
fail('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot register a Codex controller');
|
|
156
|
+
}
|
|
157
|
+
if (!validProcess(params.controllerProcess)) {
|
|
158
|
+
fail('CODEX_SANDBOX_CONTROLLER_PROCESS_INACTIVE', 'requested controller process is invalid');
|
|
159
|
+
}
|
|
160
|
+
const file = registrationPath(params.manifestPath);
|
|
161
|
+
const now = (options.now ?? Date.now)();
|
|
162
|
+
const initial = readRaw(file);
|
|
163
|
+
if (initial) {
|
|
164
|
+
const existing = parseRegistration(initial.raw);
|
|
165
|
+
assertRegistrationBinding(existing, params.manifest, params.buildIdentity);
|
|
166
|
+
if (existing.expiresAt > now) {
|
|
167
|
+
const oldState = (options.probeProcess ?? ((identity) => defaultProbe(params.manifest, identity)))(existing.controllerProcess);
|
|
168
|
+
if (oldState === 'alive')
|
|
169
|
+
fail('CODEX_SANDBOX_CONTROLLER_BUSY', 'an active controller registration already exists');
|
|
170
|
+
if (oldState === 'unknown')
|
|
171
|
+
fail('CODEX_SANDBOX_CONTROLLER_PROCESS_UNKNOWN', 'existing controller process state is unknown');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const requestedState = (options.probeProcess ?? ((identity) => defaultProbe(params.manifest, identity)))(params.controllerProcess);
|
|
175
|
+
if (requestedState === 'dead')
|
|
176
|
+
fail('CODEX_SANDBOX_CONTROLLER_PROCESS_INACTIVE', 'requested controller process is not alive');
|
|
177
|
+
if (requestedState === 'unknown')
|
|
178
|
+
fail('CODEX_SANDBOX_CONTROLLER_PROCESS_UNKNOWN', 'requested controller process state is unknown');
|
|
179
|
+
const randomHex = options.randomHex ?? (() => crypto.randomBytes(32).toString('hex'));
|
|
180
|
+
const leaseId = randomHex();
|
|
181
|
+
const leaseSecret = randomHex();
|
|
182
|
+
const controllerInstanceDigest = randomHex();
|
|
183
|
+
if (![leaseId, leaseSecret, controllerInstanceDigest].every((value) => HEX_256.test(value))) {
|
|
184
|
+
fail('CODEX_SANDBOX_CONTROLLER_CREDENTIAL_INVALID', 'generated controller credential is invalid');
|
|
185
|
+
}
|
|
186
|
+
const registration = Object.freeze({
|
|
187
|
+
version: 1,
|
|
188
|
+
taskId: params.manifest.taskId,
|
|
189
|
+
controlGeneration: params.manifest.generation,
|
|
190
|
+
containerId: params.manifest.containerIdentity.id,
|
|
191
|
+
leaseId,
|
|
192
|
+
leaseSecretHash: secretHash(leaseSecret),
|
|
193
|
+
controllerInstanceDigest,
|
|
194
|
+
controllerProcess: params.controllerProcess,
|
|
195
|
+
buildIdentity: params.buildIdentity,
|
|
196
|
+
issuedAt: now,
|
|
197
|
+
expiresAt: now + CONTROLLER_TTL_MS
|
|
198
|
+
});
|
|
199
|
+
options.beforeCommit?.();
|
|
200
|
+
const current = readRaw(file);
|
|
201
|
+
if ((!initial && current)
|
|
202
|
+
|| (initial && (!current || current.raw !== initial.raw || !sameFile(current.stat, initial.stat)))) {
|
|
203
|
+
fail('CODEX_SANDBOX_CONTROLLER_OWNERSHIP_LOST', 'controller registration changed before commit');
|
|
204
|
+
}
|
|
205
|
+
atomicWrite(file, registration);
|
|
206
|
+
return Object.freeze({
|
|
207
|
+
version: 1,
|
|
208
|
+
status: 'opened',
|
|
209
|
+
changed: true,
|
|
210
|
+
lease: Object.freeze({
|
|
211
|
+
version: 1,
|
|
212
|
+
leaseId,
|
|
213
|
+
leaseSecret,
|
|
214
|
+
taskId: registration.taskId,
|
|
215
|
+
controlGeneration: registration.controlGeneration,
|
|
216
|
+
controllerInstanceDigest,
|
|
217
|
+
controllerProcess: registration.controllerProcess,
|
|
218
|
+
buildIdentity: registration.buildIdentity,
|
|
219
|
+
issuedAt: registration.issuedAt,
|
|
220
|
+
expiresAt: registration.expiresAt
|
|
221
|
+
}),
|
|
222
|
+
error: null
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
export function closeCodexControllerRegistration(params) {
|
|
226
|
+
if (params.manifest.mode !== 'task-bound' || !params.manifest.taskId) {
|
|
227
|
+
fail('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot close a Codex controller registration');
|
|
228
|
+
}
|
|
229
|
+
const file = registrationPath(params.manifestPath);
|
|
230
|
+
const existingRaw = readRaw(file);
|
|
231
|
+
if (!existingRaw)
|
|
232
|
+
return Object.freeze({ version: 1, status: 'closed', changed: false, lease: null, error: null });
|
|
233
|
+
const existing = parseRegistration(existingRaw.raw);
|
|
234
|
+
if (existing.taskId !== params.manifest.taskId
|
|
235
|
+
|| existing.controlGeneration !== params.manifest.generation
|
|
236
|
+
|| existing.containerId !== params.manifest.containerIdentity.id
|
|
237
|
+
|| params.proof.version !== 1
|
|
238
|
+
|| existing.leaseId !== params.proof.leaseId
|
|
239
|
+
|| !safeEqual(existing.leaseSecretHash, secretHash(params.proof.leaseSecret))
|
|
240
|
+
|| JSON.stringify(existing.controllerProcess) !== JSON.stringify(params.proof.controllerProcess)) {
|
|
241
|
+
fail('CODEX_SANDBOX_CONTROLLER_PROOF_INVALID', 'controller close proof is invalid');
|
|
242
|
+
}
|
|
243
|
+
const current = readRaw(file);
|
|
244
|
+
if (!current || current.raw !== existingRaw.raw || !sameFile(current.stat, existingRaw.stat)) {
|
|
245
|
+
fail('CODEX_SANDBOX_CONTROLLER_OWNERSHIP_LOST', 'controller registration changed before close');
|
|
246
|
+
}
|
|
247
|
+
fs.unlinkSync(file);
|
|
248
|
+
return Object.freeze({ version: 1, status: 'closed', changed: true, lease: null, error: null });
|
|
249
|
+
}
|
|
250
|
+
export function resolveCodexControllerBinding(params) {
|
|
251
|
+
if (params.manifest.mode !== 'task-bound' || !params.manifest.taskId) {
|
|
252
|
+
fail('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot resolve a Codex controller registration');
|
|
253
|
+
}
|
|
254
|
+
const registration = readCodexControllerRegistration(params.manifestPath);
|
|
255
|
+
assertRegistrationBinding(registration, params.manifest, params.buildIdentity);
|
|
256
|
+
if (registration.expiresAt <= (params.now ?? Date.now())) {
|
|
257
|
+
fail('CODEX_SANDBOX_CONTROLLER_LEASE_EXPIRED', 'controller lease is expired');
|
|
258
|
+
}
|
|
259
|
+
if (params.proof.version !== 1
|
|
260
|
+
|| registration.leaseId !== params.proof.leaseId
|
|
261
|
+
|| !safeEqual(registration.leaseSecretHash, secretHash(params.proof.leaseSecret))
|
|
262
|
+
|| JSON.stringify(registration.controllerProcess) !== JSON.stringify(params.proof.controllerProcess)) {
|
|
263
|
+
fail('CODEX_SANDBOX_CONTROLLER_PROOF_INVALID', 'controller proof is invalid');
|
|
264
|
+
}
|
|
265
|
+
const state = (params.probeProcess ?? ((identity) => defaultProbe(params.manifest, identity)))(registration.controllerProcess);
|
|
266
|
+
if (state === 'dead')
|
|
267
|
+
fail('CODEX_SANDBOX_CONTROLLER_PROCESS_INACTIVE', 'controller process is not alive');
|
|
268
|
+
if (state === 'unknown')
|
|
269
|
+
fail('CODEX_SANDBOX_CONTROLLER_PROCESS_UNKNOWN', 'controller process state is unknown');
|
|
270
|
+
return Object.freeze({
|
|
271
|
+
instanceDigest: registration.controllerInstanceDigest,
|
|
272
|
+
controlGeneration: registration.controlGeneration
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
//# sourceMappingURL=controller-registration.js.map
|
|
@@ -4,8 +4,12 @@ import path from 'node:path';
|
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { getProcessStartTime } from '../../server/process-state.js';
|
|
6
6
|
import { createTask } from '../../task/create-service.js';
|
|
7
|
+
import { applyTaskFinalization } from '../../task/finalization.js';
|
|
8
|
+
import { verifyTaskEvent } from '../../task/verification.js';
|
|
7
9
|
import { bindSandboxControlTask } from './protocol.js';
|
|
8
10
|
import { atomicWriteJson, executionPath, terminateSandboxControlExecution } from './state.js';
|
|
11
|
+
import { computeLifecycleBuildIdentity } from '../../agent-clients/adapters/codex-lifecycle/build-identity.js';
|
|
12
|
+
import { closeCodexControllerRegistration, CodexControllerRegistrationError, openCodexControllerRegistration, resolveCodexControllerBinding } from './controller-registration.js';
|
|
9
13
|
function safeEnv(env) {
|
|
10
14
|
return Object.fromEntries(Object.entries(env).filter(([key]) => !key.toUpperCase().startsWith('AGENT_INFRA_CONTROL_')));
|
|
11
15
|
}
|
|
@@ -108,7 +112,61 @@ function waitForGate(nonce, timeoutMs = 2_000) {
|
|
|
108
112
|
});
|
|
109
113
|
});
|
|
110
114
|
}
|
|
111
|
-
function
|
|
115
|
+
function controllerFailure(error) {
|
|
116
|
+
const code = error instanceof CodexControllerRegistrationError
|
|
117
|
+
? error.code
|
|
118
|
+
: /^([A-Z][A-Z0-9_]+)/u.exec(error instanceof Error ? error.message : String(error))?.[1]
|
|
119
|
+
?? 'CODEX_SANDBOX_CONTROLLER_FAILED';
|
|
120
|
+
const message = error instanceof CodexControllerRegistrationError
|
|
121
|
+
? error.message
|
|
122
|
+
: `${code}: controller operation failed; inspect the sandbox controller and rebuild the sandbox if needed`;
|
|
123
|
+
const payload = {
|
|
124
|
+
version: 1,
|
|
125
|
+
status: 'failed',
|
|
126
|
+
changed: false,
|
|
127
|
+
lease: null,
|
|
128
|
+
error: { code, message, retryable: false }
|
|
129
|
+
};
|
|
130
|
+
return { exitCode: 1, stdout: `${JSON.stringify(payload)}\n`, stderr: '' };
|
|
131
|
+
}
|
|
132
|
+
function orchestrationFailure(code, message) {
|
|
133
|
+
return {
|
|
134
|
+
exitCode: 1,
|
|
135
|
+
stdout: `${JSON.stringify({
|
|
136
|
+
status: 'failed', changed: false, taskId: null, run: null, next: null,
|
|
137
|
+
error: { code, message }
|
|
138
|
+
})}\n`,
|
|
139
|
+
stderr: ''
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function finalizationResult(result) {
|
|
143
|
+
const payload = {
|
|
144
|
+
version: 1,
|
|
145
|
+
status: result.status,
|
|
146
|
+
changed: result.changed,
|
|
147
|
+
accepted: true,
|
|
148
|
+
result,
|
|
149
|
+
error: result.error
|
|
150
|
+
};
|
|
151
|
+
return {
|
|
152
|
+
exitCode: result.status === 'completed' ? 0 : result.status === 'blocked' ? 2 : 1,
|
|
153
|
+
stdout: `${JSON.stringify(payload)}\n`,
|
|
154
|
+
stderr: ''
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function isCodexPrepare(args) {
|
|
158
|
+
if (args[1] !== 'prepare')
|
|
159
|
+
return false;
|
|
160
|
+
const values = [];
|
|
161
|
+
for (let index = 2; index < args.length; index += 1) {
|
|
162
|
+
if (args[index] === '--client')
|
|
163
|
+
values.push(args[index + 1] ?? '');
|
|
164
|
+
else if (args[index]?.startsWith('--client='))
|
|
165
|
+
values.push(args[index].slice('--client='.length));
|
|
166
|
+
}
|
|
167
|
+
return values.length === 1 && values[0] === 'codex';
|
|
168
|
+
}
|
|
169
|
+
export function executeRequest(manifest, manifestPath, request, options = {}) {
|
|
112
170
|
if (request.family === 'task-create') {
|
|
113
171
|
const result = createTask(request.candidate, { repoRoot: manifest.repoRoot });
|
|
114
172
|
return {
|
|
@@ -117,13 +175,90 @@ function executeRequest(manifest, request) {
|
|
|
117
175
|
stderr: ''
|
|
118
176
|
};
|
|
119
177
|
}
|
|
178
|
+
if (request.family === 'codex-controller') {
|
|
179
|
+
try {
|
|
180
|
+
if (request.command === 'verify') {
|
|
181
|
+
const binding = (options.resolveControllerBinding ?? resolveCodexControllerBinding)({
|
|
182
|
+
manifest,
|
|
183
|
+
manifestPath,
|
|
184
|
+
proof: request.controllerProof,
|
|
185
|
+
buildIdentity: (options.buildIdentity ?? computeLifecycleBuildIdentity)(manifest.repoRoot)
|
|
186
|
+
});
|
|
187
|
+
return {
|
|
188
|
+
exitCode: 0,
|
|
189
|
+
stdout: `${JSON.stringify({
|
|
190
|
+
version: 1,
|
|
191
|
+
status: 'verified',
|
|
192
|
+
changed: false,
|
|
193
|
+
lease: null,
|
|
194
|
+
binding: {
|
|
195
|
+
taskId: manifest.taskId,
|
|
196
|
+
controlGeneration: binding.controlGeneration,
|
|
197
|
+
controllerInstanceDigest: binding.instanceDigest
|
|
198
|
+
},
|
|
199
|
+
error: null
|
|
200
|
+
})}\n`,
|
|
201
|
+
stderr: ''
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
const result = request.command === 'open'
|
|
205
|
+
? openCodexControllerRegistration({
|
|
206
|
+
manifest,
|
|
207
|
+
manifestPath,
|
|
208
|
+
controllerProcess: request.controllerProcess,
|
|
209
|
+
buildIdentity: (options.buildIdentity ?? computeLifecycleBuildIdentity)(manifest.repoRoot)
|
|
210
|
+
})
|
|
211
|
+
: closeCodexControllerRegistration({
|
|
212
|
+
manifest,
|
|
213
|
+
manifestPath,
|
|
214
|
+
proof: request.controllerProof
|
|
215
|
+
});
|
|
216
|
+
return { exitCode: 0, stdout: `${JSON.stringify(result)}\n`, stderr: '' };
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
return controllerFailure(error);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (request.family === 'task-finalization') {
|
|
223
|
+
return finalizationResult(applyTaskFinalization({ taskRef: manifest.taskId, intent: 'complete', agent: request.agent }, { repoRoot: manifest.repoRoot, preflight: (input, options) => verifyTaskEvent({ ...input, event: 'complete-task.hard-preflight' }, options) }));
|
|
224
|
+
}
|
|
120
225
|
const boundArgs = bindSandboxControlTask(request, manifest.taskId);
|
|
121
|
-
|
|
226
|
+
let controllerBinding = null;
|
|
227
|
+
if (request.family === 'task-orchestration') {
|
|
228
|
+
if (isCodexPrepare(request.args)) {
|
|
229
|
+
if (!request.controllerProof) {
|
|
230
|
+
return orchestrationFailure('CODEX_SANDBOX_CONTROLLER_PROOF_REQUIRED', 'Codex prepare requires a current controller lease proof');
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
controllerBinding = (options.resolveControllerBinding ?? resolveCodexControllerBinding)({
|
|
234
|
+
manifest,
|
|
235
|
+
manifestPath,
|
|
236
|
+
proof: request.controllerProof,
|
|
237
|
+
buildIdentity: (options.buildIdentity ?? computeLifecycleBuildIdentity)(manifest.repoRoot)
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
const code = error instanceof CodexControllerRegistrationError
|
|
242
|
+
? error.code
|
|
243
|
+
: 'CODEX_SANDBOX_CONTROLLER_PROOF_INVALID';
|
|
244
|
+
return orchestrationFailure(code, `${code}: Codex controller proof was rejected`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else if (request.controllerProof !== null) {
|
|
248
|
+
return orchestrationFailure('CODEX_SANDBOX_CONTROLLER_PROOF_INVALID', 'Controller proof is only accepted for canonical Codex prepare');
|
|
249
|
+
}
|
|
122
250
|
boundArgs.push('--git-worktree-root', manifest.worktreeRoot);
|
|
123
|
-
|
|
251
|
+
}
|
|
252
|
+
const result = (options.spawnDomain ?? spawnSync)(process.execPath, nodeEntryArgs(process.argv[1], [request.family, ...boundArgs]), {
|
|
124
253
|
cwd: manifest.repoRoot,
|
|
125
254
|
encoding: 'utf8',
|
|
126
|
-
env: {
|
|
255
|
+
env: {
|
|
256
|
+
...safeEnv(process.env),
|
|
257
|
+
AGENT_INFRA_RUNTIME_DIR: manifest.runtimeDir,
|
|
258
|
+
...(controllerBinding
|
|
259
|
+
? { AGENT_INFRA_CONTROL_CONTROLLER_BINDING: JSON.stringify(controllerBinding) }
|
|
260
|
+
: {})
|
|
261
|
+
}
|
|
127
262
|
});
|
|
128
263
|
return { exitCode: result.status ?? 1, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
|
|
129
264
|
}
|
|
@@ -138,7 +273,7 @@ export async function runSandboxControlExecutor(requestPath, nonce) {
|
|
|
138
273
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
139
274
|
if (fs.realpathSync.native(manifest.repoRoot) !== root)
|
|
140
275
|
throw new Error('SANDBOX_CONTROL_EXECUTOR_ROOT_INVALID');
|
|
141
|
-
const result = executeRequest(manifest, request);
|
|
276
|
+
const result = executeRequest(manifest, manifestPath, request);
|
|
142
277
|
process.stdout.write(result.stdout);
|
|
143
278
|
process.stderr.write(result.stderr);
|
|
144
279
|
process.exitCode = result.exitCode;
|