@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.
Files changed (182) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +11 -1
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +58 -21
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +25 -11
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +173 -10
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +127 -0
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
  9. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +63 -92
  10. package/dist/lib/agent-clients/adapters/codex-sandbox.js +72 -11
  11. package/dist/lib/agent-clients/adapters/index.js +3 -1
  12. package/dist/lib/agent-clients/adapters/traecli.js +66 -0
  13. package/dist/lib/agent-clients/tokens.js +2 -2
  14. package/dist/lib/agent-clients/types.js +2 -1
  15. package/dist/lib/defaults.json +5 -0
  16. package/dist/lib/git/workflow.js +10 -1
  17. package/dist/lib/internal/codex-lifecycle.js +20 -6
  18. package/dist/lib/internal/codex-sandbox-controller.js +9 -15
  19. package/dist/lib/internal/git-workflow.js +5 -7
  20. package/dist/lib/internal/platform-pr.js +20 -7
  21. package/dist/lib/internal/release-workflow.js +2 -1
  22. package/dist/lib/internal/sandbox-control.js +79 -2
  23. package/dist/lib/internal/task-finalization.js +54 -0
  24. package/dist/lib/internal/task-orchestration.js +156 -291
  25. package/dist/lib/{task/commands/validate.js → internal/task-validate.js} +75 -42
  26. package/dist/lib/platform/pr-summary.js +48 -10
  27. package/dist/lib/platform/pull-requests.js +205 -41
  28. package/dist/lib/run/tui.js +5 -2
  29. package/dist/lib/sandbox/commands/rm.js +316 -55
  30. package/dist/lib/sandbox/control/client.js +190 -6
  31. package/dist/lib/sandbox/control/controller-registration.js +275 -0
  32. package/dist/lib/sandbox/control/executor.js +140 -5
  33. package/dist/lib/sandbox/control/lifecycle.js +3 -1
  34. package/dist/lib/sandbox/control/protocol.js +67 -6
  35. package/dist/lib/sandbox/index.js +5 -3
  36. package/dist/lib/sandbox/managed-fs.js +5 -0
  37. package/dist/lib/sandbox/task-resolver.js +50 -6
  38. package/dist/lib/sandbox/workspace-identity.js +27 -0
  39. package/dist/lib/sandbox/worktree-safety.js +215 -2
  40. package/dist/lib/task/codex-orchestration.js +161 -39
  41. package/dist/lib/task/commands/status.js +59 -1
  42. package/dist/lib/task/commit-operation.js +335 -0
  43. package/dist/lib/task/commit-policy.js +18 -0
  44. package/dist/lib/task/delegation-receipts.js +248 -6
  45. package/dist/lib/task/finalization.js +593 -0
  46. package/dist/lib/task/index.js +0 -9
  47. package/dist/lib/task/operation-outcome.js +33 -0
  48. package/dist/lib/task/orchestration.js +187 -761
  49. package/dist/lib/task/short-id.js +59 -1
  50. package/dist/lib/task/task-execution-lock.js +6 -1
  51. package/dist/lib/task/verification-engine.js +110 -60
  52. package/dist/lib/task/verification.js +2 -1
  53. package/dist/lib/task/workflow-warning-intents.js +50 -1
  54. package/dist/lifecycle-build-manifest.json +3 -3
  55. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +61 -19
  56. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +37 -10
  57. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +276 -12
  58. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +163 -0
  59. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
  60. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
  61. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +91 -118
  62. package/lib/agent-clients/adapters/codex-sandbox.ts +89 -14
  63. package/lib/agent-clients/adapters/index.ts +3 -1
  64. package/lib/agent-clients/adapters/traecli.ts +67 -0
  65. package/lib/agent-clients/tokens.ts +2 -2
  66. package/lib/agent-clients/types.ts +2 -1
  67. package/lib/defaults.json +5 -0
  68. package/lib/git/workflow.ts +11 -2
  69. package/lib/internal/codex-lifecycle.ts +19 -6
  70. package/lib/internal/codex-sandbox-controller.ts +9 -14
  71. package/lib/internal/git-workflow.ts +5 -6
  72. package/lib/internal/platform-pr.ts +14 -7
  73. package/lib/internal/release-workflow.ts +2 -1
  74. package/lib/internal/sandbox-control.ts +92 -2
  75. package/lib/internal/task-finalization.ts +68 -0
  76. package/lib/internal/task-orchestration.ts +20 -103
  77. package/lib/{task/commands/validate.ts → internal/task-validate.ts} +75 -40
  78. package/lib/platform/pr-summary.ts +55 -11
  79. package/lib/platform/pull-requests.ts +211 -41
  80. package/lib/run/tui.ts +5 -3
  81. package/lib/sandbox/commands/rm.ts +413 -61
  82. package/lib/sandbox/control/client.ts +255 -6
  83. package/lib/sandbox/control/controller-registration.ts +352 -0
  84. package/lib/sandbox/control/executor.ts +169 -5
  85. package/lib/sandbox/control/lifecycle.ts +2 -1
  86. package/lib/sandbox/control/protocol.ts +80 -7
  87. package/lib/sandbox/index.ts +5 -3
  88. package/lib/sandbox/managed-fs.ts +5 -0
  89. package/lib/sandbox/task-resolver.ts +58 -7
  90. package/lib/sandbox/workspace-identity.ts +50 -2
  91. package/lib/sandbox/workspace-view.ts +3 -3
  92. package/lib/sandbox/worktree-safety.ts +236 -2
  93. package/lib/task/codex-orchestration.ts +168 -40
  94. package/lib/task/commands/status.ts +78 -1
  95. package/lib/task/commit-operation.ts +430 -0
  96. package/lib/task/commit-policy.ts +30 -0
  97. package/lib/task/delegation-receipts.ts +263 -4
  98. package/lib/task/finalization.ts +761 -0
  99. package/lib/task/index.ts +0 -8
  100. package/lib/task/operation-outcome.ts +48 -0
  101. package/lib/task/orchestration.ts +218 -982
  102. package/lib/task/short-id.ts +65 -1
  103. package/lib/task/task-execution-lock.ts +10 -1
  104. package/lib/task/verification-engine.ts +115 -63
  105. package/lib/task/verification.ts +3 -2
  106. package/lib/task/workflow-warning-intents.ts +57 -2
  107. package/package.json +2 -2
  108. package/templates/.agents/QUICKSTART.en.md +3 -4
  109. package/templates/.agents/QUICKSTART.zh-CN.md +3 -4
  110. package/templates/.agents/hooks/lifecycle-delegation.js +1 -21
  111. package/templates/.agents/rules/README.en.md +1 -0
  112. package/templates/.agents/rules/README.zh-CN.md +1 -0
  113. package/templates/.agents/rules/compatibility-policy.en.md +39 -0
  114. package/templates/.agents/rules/compatibility-policy.zh-CN.md +39 -0
  115. package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -4
  116. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -4
  117. package/templates/.agents/rules/pr-sync.en.md +1 -1
  118. package/templates/.agents/rules/pr-sync.github.en.md +2 -1
  119. package/templates/.agents/rules/pr-sync.github.zh-CN.md +2 -0
  120. package/templates/.agents/rules/pr-sync.zh-CN.md +1 -0
  121. package/templates/.agents/rules/review-method.en.md +1 -0
  122. package/templates/.agents/rules/review-method.zh-CN.md +1 -0
  123. package/templates/.agents/rules/task-management.en.md +2 -2
  124. package/templates/.agents/rules/task-management.zh-CN.md +2 -2
  125. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -0
  126. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -0
  127. package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
  128. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
  129. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  130. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  131. package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
  132. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
  133. package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
  134. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
  135. package/templates/.agents/skills/code-task/SKILL.en.md +1 -0
  136. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -0
  137. package/templates/.agents/skills/commit/SKILL.en.md +41 -125
  138. package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
  139. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  140. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  141. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
  142. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
  143. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  144. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  145. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
  146. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
  147. package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
  148. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
  149. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  150. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  151. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  152. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  153. package/templates/.agents/skills/create-pr/SKILL.en.md +17 -9
  154. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +17 -9
  155. package/templates/.agents/skills/create-pr/reference/change-report.en.md +41 -0
  156. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +41 -0
  157. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  158. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  159. package/templates/.agents/skills/plan-task/SKILL.en.md +2 -1
  160. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -1
  161. package/templates/.agents/skills/review-code/reference/migration-risks.en.md +8 -6
  162. package/templates/.agents/skills/review-code/reference/migration-risks.zh-CN.md +8 -6
  163. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +3 -3
  164. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +3 -3
  165. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +1 -1
  166. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +1 -1
  167. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
  168. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
  169. package/templates/.agents/skills/run-task/SKILL.en.md +6 -6
  170. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -6
  171. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
  172. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
  173. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +24 -0
  174. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  175. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  176. package/templates/.codex/hooks.json +4 -4
  177. package/templates/.trae/skills/update-agent-infra.en.md +8 -0
  178. package/templates/.trae/skills/update-agent-infra.zh-CN.md +8 -0
  179. package/dist/lib/task/commit-finalization.js +0 -305
  180. package/dist/lib/task/commit-intent.js +0 -165
  181. package/lib/task/commit-finalization.ts +0 -419
  182. package/lib/task/commit-intent.ts +0 -230
@@ -9,8 +9,17 @@ import {
9
9
  type SandboxControlError,
10
10
  type SandboxControlRequest,
11
11
  type SandboxControlResponse,
12
- type SandboxTaskCreateRequest
12
+ type SandboxTaskCreateRequest,
13
+ type SandboxTaskCommandRequest,
14
+ type SandboxTaskFinalizationRequest,
15
+ type SandboxCodexControllerRequest
13
16
  } from './protocol.ts';
17
+ import type {
18
+ CodexControllerLeaseProofV1,
19
+ CodexControllerOpened
20
+ } from './controller-registration.ts';
21
+ import type { ProcessIdentity } from '../../server/process-state.ts';
22
+ import { normalizeAgentToken } from '../../agent-clients/tokens.ts';
14
23
  import { readSandboxControlStatus } from './state.ts';
15
24
  import type { TaskCreateCandidateV1 } from '../../task/create.ts';
16
25
 
@@ -177,17 +186,255 @@ export function requestSandboxControl(params: Readonly<{
177
186
  token?: string; generation?: string; timeoutMs?: number;
178
187
  }>): SandboxControlResponse {
179
188
  if (!isSandboxControlFamily(params.family)) clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' is not allowed`, false);
180
- if (params.family === 'task-create') clientError('SANDBOX_CONTROL_COMMAND_DENIED', "'task-create' requires a typed candidate", false);
189
+ if (params.family === 'task-create' || params.family === 'codex-controller' || params.family === 'task-finalization') {
190
+ clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' requires a typed request`, false);
191
+ }
181
192
  const auth = authority(params);
182
193
  const issuedAt = Date.now();
183
194
  const request: SandboxControlRequest = {
184
- version: 2, id: randomUUID(), ...auth, issuedAt,
195
+ version: 3, id: randomUUID(), ...auth, issuedAt,
196
+ expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
197
+ family: params.family as 'task-lifecycle' | 'task-orchestration', args: params.args,
198
+ controllerProcess: null,
199
+ controllerProof: null
200
+ };
201
+ return exchangeSandboxControl(request, params);
202
+ }
203
+
204
+ export function requestSandboxTaskControl(params: Readonly<{
205
+ family: 'task-lifecycle' | 'task-orchestration';
206
+ args: string[];
207
+ controllerProof: CodexControllerLeaseProofV1 | null;
208
+ channelDir?: string;
209
+ statusDir?: string;
210
+ token?: string;
211
+ generation?: string;
212
+ timeoutMs?: number;
213
+ }>): SandboxControlResponse {
214
+ const auth = authority(params);
215
+ const issuedAt = Date.now();
216
+ const request: SandboxTaskCommandRequest = {
217
+ version: 3,
218
+ id: randomUUID(),
219
+ ...auth,
220
+ issuedAt,
221
+ expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
222
+ family: params.family,
223
+ args: params.args,
224
+ controllerProcess: null,
225
+ controllerProof: params.controllerProof
226
+ };
227
+ return exchangeSandboxControl(request, params);
228
+ }
229
+
230
+ export function requestSandboxTaskFinalization(params: Readonly<{
231
+ agent: string;
232
+ channelDir?: string;
233
+ statusDir?: string;
234
+ token?: string;
235
+ generation?: string;
236
+ timeoutMs?: number;
237
+ }>): SandboxControlResponse {
238
+ const agent = normalizeAgentToken(params.agent);
239
+ if (!agent) clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization agent is invalid', false);
240
+ const auth = authority(params);
241
+ const issuedAt = Date.now();
242
+ const request: SandboxTaskFinalizationRequest = {
243
+ version: 3,
244
+ id: randomUUID(),
245
+ ...auth,
246
+ issuedAt,
185
247
  expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
186
- family: params.family, args: params.args
248
+ family: 'task-finalization',
249
+ operation: 'complete',
250
+ agent,
251
+ args: [],
252
+ controllerProcess: null,
253
+ controllerProof: null
187
254
  };
188
255
  return exchangeSandboxControl(request, params);
189
256
  }
190
257
 
258
+ type CodexControllerClosed = Readonly<{
259
+ version: 1;
260
+ status: 'closed';
261
+ changed: boolean;
262
+ lease: null;
263
+ error: null;
264
+ }>;
265
+
266
+ type CodexControllerVerified = Readonly<{
267
+ version: 1;
268
+ status: 'verified';
269
+ changed: false;
270
+ lease: null;
271
+ binding: Readonly<{
272
+ taskId: string;
273
+ controlGeneration: string;
274
+ controllerInstanceDigest: string;
275
+ }>;
276
+ error: null;
277
+ }>;
278
+
279
+ function exactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
280
+ return Object.keys(value).sort().join(',') === [...keys].sort().join(',');
281
+ }
282
+
283
+ function validProcess(value: unknown): value is ProcessIdentity {
284
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
285
+ const identity = value as Record<string, unknown>;
286
+ return exactKeys(identity, ['pid', 'startTime'])
287
+ && Number.isSafeInteger(identity.pid) && (identity.pid as number) > 0
288
+ && Number.isSafeInteger(identity.startTime) && (identity.startTime as number) >= 0;
289
+ }
290
+
291
+ function validBuild(value: unknown): boolean {
292
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
293
+ const build = value as Record<string, unknown>;
294
+ return exactKeys(build, ['internalExecutableBuildHash', 'lifecycleContractHash', 'packageVersion', 'protocolVersion'])
295
+ && build.protocolVersion === 3
296
+ && typeof build.packageVersion === 'string' && build.packageVersion.length > 0
297
+ && typeof build.internalExecutableBuildHash === 'string' && /^[a-f0-9]{64}$/u.test(build.internalExecutableBuildHash)
298
+ && typeof build.lifecycleContractHash === 'string' && /^[a-f0-9]{64}$/u.test(build.lifecycleContractHash);
299
+ }
300
+
301
+ export function parseCodexControllerResult(response: SandboxControlResponse): CodexControllerOpened | CodexControllerClosed | CodexControllerVerified {
302
+ if (response.phase !== 'completed' || response.error !== null || response.stderr !== '') {
303
+ clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller result outer response is invalid', false, true);
304
+ }
305
+ let value: unknown;
306
+ try {
307
+ if (!response.stdout.endsWith('\n') || response.stdout.slice(0, -1).includes('\n')) throw new Error('not canonical');
308
+ value = JSON.parse(response.stdout);
309
+ } catch {
310
+ clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller result payload is invalid', false, true);
311
+ }
312
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
313
+ clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller result payload is invalid', false, true);
314
+ }
315
+ const result = value as Record<string, unknown>;
316
+ const resultKeys = result.status === 'verified'
317
+ ? ['binding', 'changed', 'error', 'lease', 'status', 'version']
318
+ : ['changed', 'error', 'lease', 'status', 'version'];
319
+ if (!exactKeys(result, resultKeys)
320
+ || result.version !== 1 || !['opened', 'closed', 'failed', 'verified'].includes(result.status as string)) {
321
+ clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller result schema is invalid', false, true);
322
+ }
323
+ if (result.status === 'failed') {
324
+ const error = result.error as { code?: unknown; message?: unknown; retryable?: unknown } | null;
325
+ if (response.exitCode !== 1 || result.changed !== false || result.lease !== null
326
+ || !error || !exactKeys(error as Record<string, unknown>, ['code', 'message', 'retryable'])
327
+ || typeof error.code !== 'string' || !/^[A-Z][A-Z0-9_]+$/u.test(error.code)
328
+ || typeof error.message !== 'string' || error.retryable !== false) {
329
+ clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller failure result is invalid', false, true);
330
+ }
331
+ clientError(error.code, error.message, false, true);
332
+ }
333
+ if (response.exitCode !== 0 || result.error !== null) {
334
+ clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller success result is invalid', false, true);
335
+ }
336
+ if (result.status === 'closed') {
337
+ if (typeof result.changed !== 'boolean' || result.lease !== null) {
338
+ clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller close result is invalid', false, true);
339
+ }
340
+ return result as unknown as CodexControllerClosed;
341
+ }
342
+ if (result.status === 'verified') {
343
+ const binding = result.binding as Record<string, unknown> | null;
344
+ if (result.changed !== false || result.lease !== null || !binding
345
+ || !exactKeys(binding, ['controllerInstanceDigest', 'controlGeneration', 'taskId'])
346
+ || typeof binding.taskId !== 'string' || binding.taskId.length === 0
347
+ || typeof binding.controlGeneration !== 'string' || binding.controlGeneration.length === 0
348
+ || typeof binding.controllerInstanceDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(binding.controllerInstanceDigest)) {
349
+ clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller verify result is invalid', false, true);
350
+ }
351
+ return result as unknown as CodexControllerVerified;
352
+ }
353
+ const lease = result.lease as Record<string, unknown> | null;
354
+ if (result.changed !== true || !lease
355
+ || !exactKeys(lease, [
356
+ 'buildIdentity', 'controlGeneration', 'controllerInstanceDigest', 'controllerProcess',
357
+ 'expiresAt', 'issuedAt', 'leaseId', 'leaseSecret', 'taskId', 'version'
358
+ ])
359
+ || lease.version !== 1
360
+ || typeof lease.leaseId !== 'string' || !/^[a-f0-9]{64}$/u.test(lease.leaseId)
361
+ || typeof lease.leaseSecret !== 'string' || !/^[a-f0-9]{64}$/u.test(lease.leaseSecret)
362
+ || typeof lease.taskId !== 'string' || lease.taskId.length === 0
363
+ || typeof lease.controlGeneration !== 'string' || lease.controlGeneration.length === 0
364
+ || typeof lease.controllerInstanceDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(lease.controllerInstanceDigest)
365
+ || !validProcess(lease.controllerProcess) || !validBuild(lease.buildIdentity)
366
+ || !Number.isSafeInteger(lease.issuedAt) || !Number.isSafeInteger(lease.expiresAt)
367
+ || (lease.expiresAt as number) <= (lease.issuedAt as number)) {
368
+ clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller open result is invalid', false, true);
369
+ }
370
+ return result as unknown as CodexControllerOpened | CodexControllerClosed;
371
+ }
372
+
373
+ function requestCodexController(params: Readonly<{
374
+ command: 'open' | 'close' | 'verify';
375
+ controllerProcess: ProcessIdentity;
376
+ controllerProof: CodexControllerLeaseProofV1 | null;
377
+ channelDir?: string;
378
+ statusDir?: string;
379
+ token?: string;
380
+ generation?: string;
381
+ timeoutMs?: number;
382
+ }>): CodexControllerOpened | CodexControllerClosed | CodexControllerVerified {
383
+ const auth = authority(params);
384
+ const issuedAt = Date.now();
385
+ const request: SandboxCodexControllerRequest = {
386
+ version: 3,
387
+ id: randomUUID(),
388
+ ...auth,
389
+ issuedAt,
390
+ expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
391
+ family: 'codex-controller',
392
+ command: params.command,
393
+ args: [],
394
+ controllerProcess: params.controllerProcess,
395
+ controllerProof: params.controllerProof
396
+ };
397
+ const result = parseCodexControllerResult(exchangeSandboxControl(request, params));
398
+ if (result.status === 'opened'
399
+ && (result.lease.controlGeneration !== auth.generation
400
+ || result.lease.controllerProcess.pid !== params.controllerProcess.pid
401
+ || result.lease.controllerProcess.startTime !== params.controllerProcess.startTime)) {
402
+ clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller result does not match the request', false, true);
403
+ }
404
+ return result;
405
+ }
406
+
407
+ export function requestCodexControllerOpen(params: Omit<Parameters<typeof requestCodexController>[0], 'command' | 'controllerProof'>): CodexControllerOpened {
408
+ const result = requestCodexController({ ...params, command: 'open', controllerProof: null });
409
+ if (result.status !== 'opened') clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller open returned the wrong result', false, true);
410
+ return result;
411
+ }
412
+
413
+ export function requestCodexControllerClose(params: Omit<Parameters<typeof requestCodexController>[0], 'command'>): CodexControllerClosed {
414
+ const result = requestCodexController({ ...params, command: 'close' });
415
+ if (result.status !== 'closed') clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller close returned the wrong result', false, true);
416
+ return result;
417
+ }
418
+
419
+ export function requestCodexControllerVerify(params: Readonly<{
420
+ controllerProof: CodexControllerLeaseProofV1;
421
+ channelDir?: string;
422
+ statusDir?: string;
423
+ token?: string;
424
+ generation?: string;
425
+ timeoutMs?: number;
426
+ }>): CodexControllerVerified {
427
+ const result = requestCodexController({
428
+ ...params,
429
+ controllerProcess: params.controllerProof.controllerProcess,
430
+ command: 'verify'
431
+ });
432
+ if (result.status !== 'verified') {
433
+ clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller verify returned the wrong result', false, true);
434
+ }
435
+ return result;
436
+ }
437
+
191
438
  export function requestSandboxTaskCreate(params: Readonly<{
192
439
  candidate: TaskCreateCandidateV1; channelDir?: string; statusDir?: string;
193
440
  token?: string; generation?: string; timeoutMs?: number;
@@ -195,9 +442,11 @@ export function requestSandboxTaskCreate(params: Readonly<{
195
442
  const auth = authority(params);
196
443
  const issuedAt = Date.now();
197
444
  const request: SandboxTaskCreateRequest = {
198
- version: 2, id: randomUUID(), ...auth, issuedAt,
445
+ version: 3, id: randomUUID(), ...auth, issuedAt,
199
446
  expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
200
- family: 'task-create', candidate: params.candidate
447
+ family: 'task-create', candidate: params.candidate,
448
+ controllerProcess: null,
449
+ controllerProof: null
201
450
  };
202
451
  return exchangeSandboxControl(request, { ...params, timeoutMs: params.timeoutMs ?? 120_000 });
203
452
  }
@@ -0,0 +1,352 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ import type { LifecycleBuildIdentity } from '../../agent-clients/adapters/codex-lifecycle/build-identity.ts';
6
+ import { parseLinuxProcessStat, type ProcessIdentity, type ProcessIdentityState } from '../../server/process-state.ts';
7
+ import { commandForEngine, runProbe } from '../shell.ts';
8
+ import type { SandboxControlManifest } from './protocol.ts';
9
+
10
+ const CONTROLLER_TTL_MS = 4 * 60 * 60 * 1_000;
11
+ const HEX_256 = /^[a-f0-9]{64}$/u;
12
+ const REGISTRATION_KEYS = [
13
+ 'buildIdentity', 'containerId', 'controlGeneration', 'controllerInstanceDigest',
14
+ 'controllerProcess', 'expiresAt', 'issuedAt', 'leaseId', 'leaseSecretHash',
15
+ 'taskId', 'version'
16
+ ].sort().join(',');
17
+
18
+ export type CodexControllerLeaseProofV1 = Readonly<{
19
+ version: 1;
20
+ leaseId: string;
21
+ leaseSecret: string;
22
+ controllerProcess: ProcessIdentity;
23
+ }>;
24
+
25
+ export type CodexControllerRegistrationV1 = Readonly<{
26
+ version: 1;
27
+ taskId: string;
28
+ controlGeneration: string;
29
+ containerId: string;
30
+ leaseId: string;
31
+ leaseSecretHash: string;
32
+ controllerInstanceDigest: string;
33
+ controllerProcess: ProcessIdentity;
34
+ buildIdentity: LifecycleBuildIdentity;
35
+ issuedAt: number;
36
+ expiresAt: number;
37
+ }>;
38
+
39
+ export type CodexControllerLeaseV1 = Readonly<{
40
+ version: 1;
41
+ leaseId: string;
42
+ leaseSecret: string;
43
+ taskId: string;
44
+ controlGeneration: string;
45
+ controllerInstanceDigest: string;
46
+ controllerProcess: ProcessIdentity;
47
+ buildIdentity: LifecycleBuildIdentity;
48
+ issuedAt: number;
49
+ expiresAt: number;
50
+ }>;
51
+
52
+ export type CodexControllerOpened = Readonly<{
53
+ version: 1;
54
+ status: 'opened';
55
+ changed: true;
56
+ lease: CodexControllerLeaseV1;
57
+ error: null;
58
+ }>;
59
+
60
+ type RegistrationOptions = Readonly<{
61
+ now?: () => number;
62
+ probeProcess?: (identity: ProcessIdentity) => ProcessIdentityState;
63
+ randomHex?: () => string;
64
+ beforeCommit?: () => void;
65
+ }>;
66
+
67
+ export class CodexControllerRegistrationError extends Error {
68
+ readonly code: string;
69
+ constructor(code: string, message: string) {
70
+ super(`${code}: ${message}`);
71
+ this.name = code;
72
+ this.code = code;
73
+ }
74
+ }
75
+
76
+ function fail(code: string, message: string): never {
77
+ throw new CodexControllerRegistrationError(code, message);
78
+ }
79
+
80
+ function exactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
81
+ return Object.keys(value).sort().join(',') === [...keys].sort().join(',');
82
+ }
83
+
84
+ function validProcess(value: unknown): value is ProcessIdentity {
85
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
86
+ const processValue = value as Record<string, unknown>;
87
+ return exactKeys(processValue, ['pid', 'startTime'])
88
+ && Number.isSafeInteger(processValue.pid) && (processValue.pid as number) > 0
89
+ && Number.isSafeInteger(processValue.startTime) && (processValue.startTime as number) >= 0;
90
+ }
91
+
92
+ function validBuild(value: unknown): value is LifecycleBuildIdentity {
93
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
94
+ const build = value as Record<string, unknown>;
95
+ return exactKeys(build, [
96
+ 'internalExecutableBuildHash', 'lifecycleContractHash', 'packageVersion', 'protocolVersion'
97
+ ])
98
+ && build.protocolVersion === 3
99
+ && typeof build.packageVersion === 'string' && build.packageVersion.length > 0
100
+ && typeof build.internalExecutableBuildHash === 'string' && HEX_256.test(build.internalExecutableBuildHash)
101
+ && typeof build.lifecycleContractHash === 'string' && HEX_256.test(build.lifecycleContractHash);
102
+ }
103
+
104
+ function parseRegistration(raw: string): CodexControllerRegistrationV1 {
105
+ let value: unknown;
106
+ try {
107
+ value = JSON.parse(raw);
108
+ } catch {
109
+ fail('CODEX_SANDBOX_CONTROLLER_REGISTRATION_INVALID', 'controller registration is malformed');
110
+ }
111
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
112
+ fail('CODEX_SANDBOX_CONTROLLER_REGISTRATION_INVALID', 'controller registration is invalid');
113
+ }
114
+ const registration = value as Record<string, unknown>;
115
+ if (Object.keys(registration).sort().join(',') !== REGISTRATION_KEYS
116
+ || registration.version !== 1
117
+ || typeof registration.taskId !== 'string'
118
+ || typeof registration.controlGeneration !== 'string'
119
+ || typeof registration.containerId !== 'string'
120
+ || typeof registration.leaseId !== 'string' || !HEX_256.test(registration.leaseId)
121
+ || typeof registration.leaseSecretHash !== 'string' || !HEX_256.test(registration.leaseSecretHash)
122
+ || typeof registration.controllerInstanceDigest !== 'string' || !HEX_256.test(registration.controllerInstanceDigest)
123
+ || !validProcess(registration.controllerProcess)
124
+ || !validBuild(registration.buildIdentity)
125
+ || !Number.isSafeInteger(registration.issuedAt)
126
+ || !Number.isSafeInteger(registration.expiresAt)
127
+ || (registration.expiresAt as number) <= (registration.issuedAt as number)) {
128
+ fail('CODEX_SANDBOX_CONTROLLER_REGISTRATION_INVALID', 'controller registration schema is invalid');
129
+ }
130
+ return registration as CodexControllerRegistrationV1;
131
+ }
132
+
133
+ function registrationPath(manifestPath: string): string {
134
+ return path.join(path.dirname(path.resolve(manifestPath)), 'codex-controller.json');
135
+ }
136
+
137
+ function readRaw(file: string): { raw: string; stat: fs.Stats } | null {
138
+ try {
139
+ const stat = fs.lstatSync(file);
140
+ if (!stat.isFile() || stat.isSymbolicLink()
141
+ || (process.platform !== 'win32' && (stat.mode & 0o777) !== 0o600)) {
142
+ fail('CODEX_SANDBOX_CONTROLLER_REGISTRATION_INVALID', 'controller registration file is unsafe');
143
+ }
144
+ return { raw: fs.readFileSync(file, 'utf8'), stat };
145
+ } catch (error) {
146
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
147
+ throw error;
148
+ }
149
+ }
150
+
151
+ function sameFile(left: fs.Stats, right: fs.Stats): boolean {
152
+ return left.dev === right.dev && left.ino === right.ino && left.mode === right.mode
153
+ && left.size === right.size && left.mtimeMs === right.mtimeMs;
154
+ }
155
+
156
+ function secretHash(secret: string): string {
157
+ return crypto.createHash('sha256')
158
+ .update('agent-infra/codex-controller-lease/v1\0')
159
+ .update(secret)
160
+ .digest('hex');
161
+ }
162
+
163
+ function safeEqual(left: string, right: string): boolean {
164
+ if (!HEX_256.test(left) || !HEX_256.test(right)) return false;
165
+ return crypto.timingSafeEqual(Buffer.from(left, 'hex'), Buffer.from(right, 'hex'));
166
+ }
167
+
168
+ function defaultProbe(manifest: SandboxControlManifest, identity: ProcessIdentity): ProcessIdentityState {
169
+ const command = commandForEngine(manifest.engine, 'docker', [
170
+ 'exec', manifest.containerIdentity.id, 'cat', `/proc/${identity.pid}/stat`
171
+ ]);
172
+ const result = runProbe(command.cmd, command.args, { timeout: 2_000 });
173
+ if (result.status !== 0) {
174
+ const stderr = typeof result.stderr === 'string' ? result.stderr : result.stderr?.toString('utf8') ?? '';
175
+ return /no such (?:file|container)|not found|does not exist/iu.test(stderr) ? 'dead' : 'unknown';
176
+ }
177
+ const stdout = typeof result.stdout === 'string' ? result.stdout : result.stdout?.toString('utf8') ?? '';
178
+ const stat = parseLinuxProcessStat(stdout);
179
+ if (!stat) return 'unknown';
180
+ if (stat.state === 'Z' || stat.startTime !== identity.startTime) return 'dead';
181
+ return 'alive';
182
+ }
183
+
184
+ function assertRegistrationBinding(
185
+ registration: CodexControllerRegistrationV1,
186
+ manifest: SandboxControlManifest,
187
+ buildIdentity: LifecycleBuildIdentity
188
+ ): void {
189
+ if (registration.taskId !== manifest.taskId
190
+ || registration.controlGeneration !== manifest.generation
191
+ || registration.containerId !== manifest.containerIdentity.id) {
192
+ fail('CODEX_SANDBOX_CONTROLLER_REGISTRATION_INVALID', 'controller registration binding is invalid');
193
+ }
194
+ if (registration.buildIdentity.protocolVersion !== buildIdentity.protocolVersion) {
195
+ fail('CODEX_LIFECYCLE_PROTOCOL_MISMATCH', 'Codex lifecycle protocol version does not match');
196
+ }
197
+ }
198
+
199
+ function atomicWrite(file: string, value: CodexControllerRegistrationV1): void {
200
+ const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${crypto.randomUUID()}.tmp`);
201
+ try {
202
+ fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600, flag: 'wx' });
203
+ fs.chmodSync(temporary, 0o600);
204
+ fs.renameSync(temporary, file);
205
+ } finally {
206
+ fs.rmSync(temporary, { force: true });
207
+ }
208
+ }
209
+
210
+ export function readCodexControllerRegistration(manifestPath: string): CodexControllerRegistrationV1 {
211
+ const existing = readRaw(registrationPath(manifestPath));
212
+ if (!existing) fail('CODEX_SANDBOX_CONTROLLER_REGISTRATION_MISSING', 'controller registration is missing');
213
+ return parseRegistration(existing.raw);
214
+ }
215
+
216
+ export function openCodexControllerRegistration(params: Readonly<{
217
+ manifest: SandboxControlManifest;
218
+ manifestPath: string;
219
+ controllerProcess: ProcessIdentity;
220
+ buildIdentity: LifecycleBuildIdentity;
221
+ }>, options: RegistrationOptions = {}): CodexControllerOpened {
222
+ if (params.manifest.mode !== 'task-bound' || !params.manifest.taskId) {
223
+ fail('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot register a Codex controller');
224
+ }
225
+ if (!validProcess(params.controllerProcess)) {
226
+ fail('CODEX_SANDBOX_CONTROLLER_PROCESS_INACTIVE', 'requested controller process is invalid');
227
+ }
228
+ const file = registrationPath(params.manifestPath);
229
+ const now = (options.now ?? Date.now)();
230
+ const initial = readRaw(file);
231
+ if (initial) {
232
+ const existing = parseRegistration(initial.raw);
233
+ assertRegistrationBinding(existing, params.manifest, params.buildIdentity);
234
+ if (existing.expiresAt > now) {
235
+ const oldState = (options.probeProcess ?? ((identity) => defaultProbe(params.manifest, identity)))(existing.controllerProcess);
236
+ if (oldState === 'alive') fail('CODEX_SANDBOX_CONTROLLER_BUSY', 'an active controller registration already exists');
237
+ if (oldState === 'unknown') fail('CODEX_SANDBOX_CONTROLLER_PROCESS_UNKNOWN', 'existing controller process state is unknown');
238
+ }
239
+ }
240
+ const requestedState = (options.probeProcess ?? ((identity) => defaultProbe(params.manifest, identity)))(params.controllerProcess);
241
+ if (requestedState === 'dead') fail('CODEX_SANDBOX_CONTROLLER_PROCESS_INACTIVE', 'requested controller process is not alive');
242
+ if (requestedState === 'unknown') fail('CODEX_SANDBOX_CONTROLLER_PROCESS_UNKNOWN', 'requested controller process state is unknown');
243
+
244
+ const randomHex = options.randomHex ?? (() => crypto.randomBytes(32).toString('hex'));
245
+ const leaseId = randomHex();
246
+ const leaseSecret = randomHex();
247
+ const controllerInstanceDigest = randomHex();
248
+ if (![leaseId, leaseSecret, controllerInstanceDigest].every((value) => HEX_256.test(value))) {
249
+ fail('CODEX_SANDBOX_CONTROLLER_CREDENTIAL_INVALID', 'generated controller credential is invalid');
250
+ }
251
+ const registration: CodexControllerRegistrationV1 = Object.freeze({
252
+ version: 1,
253
+ taskId: params.manifest.taskId!,
254
+ controlGeneration: params.manifest.generation,
255
+ containerId: params.manifest.containerIdentity.id,
256
+ leaseId,
257
+ leaseSecretHash: secretHash(leaseSecret),
258
+ controllerInstanceDigest,
259
+ controllerProcess: params.controllerProcess,
260
+ buildIdentity: params.buildIdentity,
261
+ issuedAt: now,
262
+ expiresAt: now + CONTROLLER_TTL_MS
263
+ });
264
+ options.beforeCommit?.();
265
+ const current = readRaw(file);
266
+ if ((!initial && current)
267
+ || (initial && (!current || current.raw !== initial.raw || !sameFile(current.stat, initial.stat)))) {
268
+ fail('CODEX_SANDBOX_CONTROLLER_OWNERSHIP_LOST', 'controller registration changed before commit');
269
+ }
270
+ atomicWrite(file, registration);
271
+ return Object.freeze({
272
+ version: 1,
273
+ status: 'opened',
274
+ changed: true,
275
+ lease: Object.freeze({
276
+ version: 1,
277
+ leaseId,
278
+ leaseSecret,
279
+ taskId: registration.taskId,
280
+ controlGeneration: registration.controlGeneration,
281
+ controllerInstanceDigest,
282
+ controllerProcess: registration.controllerProcess,
283
+ buildIdentity: registration.buildIdentity,
284
+ issuedAt: registration.issuedAt,
285
+ expiresAt: registration.expiresAt
286
+ }),
287
+ error: null
288
+ });
289
+ }
290
+
291
+ export function closeCodexControllerRegistration(params: Readonly<{
292
+ manifest: SandboxControlManifest;
293
+ manifestPath: string;
294
+ proof: CodexControllerLeaseProofV1;
295
+ }>): Readonly<{ version: 1; status: 'closed'; changed: boolean; lease: null; error: null }> {
296
+ if (params.manifest.mode !== 'task-bound' || !params.manifest.taskId) {
297
+ fail('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot close a Codex controller registration');
298
+ }
299
+ const file = registrationPath(params.manifestPath);
300
+ const existingRaw = readRaw(file);
301
+ if (!existingRaw) return Object.freeze({ version: 1, status: 'closed', changed: false, lease: null, error: null });
302
+ const existing = parseRegistration(existingRaw.raw);
303
+ if (existing.taskId !== params.manifest.taskId
304
+ || existing.controlGeneration !== params.manifest.generation
305
+ || existing.containerId !== params.manifest.containerIdentity.id
306
+ || params.proof.version !== 1
307
+ || existing.leaseId !== params.proof.leaseId
308
+ || !safeEqual(existing.leaseSecretHash, secretHash(params.proof.leaseSecret))
309
+ || JSON.stringify(existing.controllerProcess) !== JSON.stringify(params.proof.controllerProcess)) {
310
+ fail('CODEX_SANDBOX_CONTROLLER_PROOF_INVALID', 'controller close proof is invalid');
311
+ }
312
+ const current = readRaw(file);
313
+ if (!current || current.raw !== existingRaw.raw || !sameFile(current.stat, existingRaw.stat)) {
314
+ fail('CODEX_SANDBOX_CONTROLLER_OWNERSHIP_LOST', 'controller registration changed before close');
315
+ }
316
+ fs.unlinkSync(file);
317
+ return Object.freeze({ version: 1, status: 'closed', changed: true, lease: null, error: null });
318
+ }
319
+
320
+ export function resolveCodexControllerBinding(params: Readonly<{
321
+ manifest: SandboxControlManifest;
322
+ manifestPath: string;
323
+ proof: CodexControllerLeaseProofV1;
324
+ buildIdentity: LifecycleBuildIdentity;
325
+ now?: number;
326
+ probeProcess?: (identity: ProcessIdentity) => ProcessIdentityState;
327
+ }>): Readonly<{
328
+ instanceDigest: string;
329
+ controlGeneration: string;
330
+ }> {
331
+ if (params.manifest.mode !== 'task-bound' || !params.manifest.taskId) {
332
+ fail('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot resolve a Codex controller registration');
333
+ }
334
+ const registration = readCodexControllerRegistration(params.manifestPath);
335
+ assertRegistrationBinding(registration, params.manifest, params.buildIdentity);
336
+ if (registration.expiresAt <= (params.now ?? Date.now())) {
337
+ fail('CODEX_SANDBOX_CONTROLLER_LEASE_EXPIRED', 'controller lease is expired');
338
+ }
339
+ if (params.proof.version !== 1
340
+ || registration.leaseId !== params.proof.leaseId
341
+ || !safeEqual(registration.leaseSecretHash, secretHash(params.proof.leaseSecret))
342
+ || JSON.stringify(registration.controllerProcess) !== JSON.stringify(params.proof.controllerProcess)) {
343
+ fail('CODEX_SANDBOX_CONTROLLER_PROOF_INVALID', 'controller proof is invalid');
344
+ }
345
+ const state = (params.probeProcess ?? ((identity) => defaultProbe(params.manifest, identity)))(registration.controllerProcess);
346
+ if (state === 'dead') fail('CODEX_SANDBOX_CONTROLLER_PROCESS_INACTIVE', 'controller process is not alive');
347
+ if (state === 'unknown') fail('CODEX_SANDBOX_CONTROLLER_PROCESS_UNKNOWN', 'controller process state is unknown');
348
+ return Object.freeze({
349
+ instanceDigest: registration.controllerInstanceDigest,
350
+ controlGeneration: registration.controlGeneration
351
+ });
352
+ }