@fitlab-ai/agent-infra 0.11.11 → 0.11.13

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 (154) hide show
  1. package/dist/bin/internal-cli.js +2 -2
  2. package/dist/lib/agent-clients/adapter.js +7 -1
  3. package/dist/lib/agent-clients/adapters/claude-code.js +5 -1
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +78 -63
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/binding.js +114 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +447 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +130 -0
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +131 -0
  9. package/dist/lib/{sandbox/control → agent-clients/adapters/codex-lifecycle}/controller-registration.js +2 -2
  10. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +48 -1
  11. package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +124 -62
  12. package/dist/lib/{internal/codex-sandbox-controller.js → agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js} +3 -3
  13. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -5
  14. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -213
  15. package/dist/lib/agent-clients/adapters/codex-orchestration.js +468 -0
  16. package/dist/lib/agent-clients/adapters/codex.js +40 -1
  17. package/dist/lib/agent-clients/adapters/index.js +2 -10
  18. package/dist/lib/agent-clients/tokens.js +3 -2
  19. package/dist/lib/internal/agent-client.js +132 -72
  20. package/dist/lib/internal/cli-route-inventory.js +2 -2
  21. package/dist/lib/internal/sandbox-control.js +1 -81
  22. package/dist/lib/internal/task-finalization.js +26 -19
  23. package/dist/lib/internal/task-lifecycle.js +1 -1
  24. package/dist/lib/internal/task-operation-registry.js +1 -1
  25. package/dist/lib/internal/task-validate.js +1 -1
  26. package/dist/lib/platform/github-client.js +33 -3
  27. package/dist/lib/platform/issues.js +18 -6
  28. package/dist/lib/runtime/agent-runtime.js +6 -29
  29. package/dist/lib/sandbox/commands/control-status.js +1 -0
  30. package/dist/lib/sandbox/commands/create.js +7 -2
  31. package/dist/lib/sandbox/commands/ls.js +4 -5
  32. package/dist/lib/sandbox/commands/rm.js +1 -1
  33. package/dist/lib/sandbox/control/agent-operation.js +34 -0
  34. package/dist/lib/sandbox/control/client.js +14 -201
  35. package/dist/lib/sandbox/control/executor.js +12 -125
  36. package/dist/lib/sandbox/control/lifecycle.js +66 -4
  37. package/dist/lib/sandbox/control/protocol.js +15 -54
  38. package/dist/lib/sandbox/control/server.js +216 -281
  39. package/dist/lib/sandbox/control/task-view.js +0 -13
  40. package/dist/lib/sandbox/mounts.js +9 -1
  41. package/dist/lib/sandbox/recovery.js +31 -10
  42. package/dist/lib/sandbox/removal.js +28 -5
  43. package/dist/lib/sandbox/workspace-identity.js +18 -6
  44. package/dist/lib/sandbox/workspace-view.js +56 -19
  45. package/dist/lib/task/commands/ls.js +3 -3
  46. package/dist/lib/task/control-authority.js +56 -264
  47. package/dist/lib/task/control-recovery.js +2 -3
  48. package/dist/lib/task/create.js +2 -2
  49. package/dist/lib/task/delegation-receipts.js +74 -161
  50. package/dist/lib/task/events.js +88 -44
  51. package/dist/lib/task/finalization.js +35 -6
  52. package/dist/lib/task/lifecycle.js +274 -115
  53. package/dist/lib/task/orchestration.js +201 -194
  54. package/dist/lib/task/runtime-paths.js +10 -0
  55. package/dist/lib/task/short-id.js +37 -27
  56. package/dist/lib/task/verification-engine.js +32 -26
  57. package/dist/lib/task/workspace-snapshot.js +1 -1
  58. package/dist/lifecycle-build-manifest.json +3 -3
  59. package/lib/agent-clients/adapter.ts +39 -0
  60. package/lib/agent-clients/adapters/claude-code.ts +5 -1
  61. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +105 -80
  62. package/lib/agent-clients/adapters/codex-lifecycle/binding.ts +147 -0
  63. package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +510 -0
  64. package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +158 -0
  65. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +1 -1
  66. package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +151 -0
  67. package/lib/{sandbox/control → agent-clients/adapters/codex-lifecycle}/controller-registration.ts +4 -4
  68. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +43 -0
  69. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +8 -5
  70. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +190 -65
  71. package/lib/{internal/codex-sandbox-controller.ts → agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts} +2 -2
  72. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -5
  73. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +270 -224
  74. package/lib/agent-clients/adapters/codex-orchestration.ts +632 -0
  75. package/lib/agent-clients/adapters/codex.ts +44 -1
  76. package/lib/agent-clients/adapters/index.ts +7 -20
  77. package/lib/agent-clients/tokens.ts +4 -3
  78. package/lib/internal/agent-client.ts +142 -73
  79. package/lib/internal/cli-route-inventory.ts +2 -2
  80. package/lib/internal/sandbox-control.ts +0 -97
  81. package/lib/internal/task-finalization.ts +30 -18
  82. package/lib/internal/task-lifecycle.ts +1 -1
  83. package/lib/internal/task-operation-registry.ts +1 -1
  84. package/lib/internal/task-validate.ts +1 -1
  85. package/lib/platform/github-client.ts +34 -3
  86. package/lib/platform/issues.ts +18 -6
  87. package/lib/runtime/agent-runtime.ts +6 -41
  88. package/lib/sandbox/commands/control-status.ts +1 -0
  89. package/lib/sandbox/commands/create.ts +7 -1
  90. package/lib/sandbox/commands/ls.ts +4 -5
  91. package/lib/sandbox/commands/rm.ts +1 -1
  92. package/lib/sandbox/control/agent-operation.ts +49 -0
  93. package/lib/sandbox/control/client.ts +20 -254
  94. package/lib/sandbox/control/executor.ts +13 -136
  95. package/lib/sandbox/control/lifecycle.ts +67 -4
  96. package/lib/sandbox/control/protocol.ts +22 -66
  97. package/lib/sandbox/control/server.ts +21 -95
  98. package/lib/sandbox/control/task-view.ts +0 -19
  99. package/lib/sandbox/mounts.ts +10 -1
  100. package/lib/sandbox/recovery.ts +38 -17
  101. package/lib/sandbox/removal.ts +33 -7
  102. package/lib/sandbox/workspace-identity.ts +16 -6
  103. package/lib/sandbox/workspace-view.ts +50 -18
  104. package/lib/task/commands/ls.ts +3 -3
  105. package/lib/task/control-authority.ts +66 -378
  106. package/lib/task/control-recovery.ts +3 -4
  107. package/lib/task/create.ts +3 -3
  108. package/lib/task/delegation-receipts.ts +87 -231
  109. package/lib/task/events.ts +100 -42
  110. package/lib/task/finalization.ts +37 -7
  111. package/lib/task/lifecycle.ts +257 -109
  112. package/lib/task/orchestration.ts +260 -237
  113. package/lib/task/runtime-paths.ts +12 -0
  114. package/lib/task/short-id.ts +33 -40
  115. package/lib/task/verification-engine.ts +30 -25
  116. package/lib/task/workspace-snapshot.ts +1 -1
  117. package/package.json +1 -1
  118. package/templates/.agents/hooks/lifecycle-delegation.js +1 -25
  119. package/templates/.agents/rules/README.en.md +2 -1
  120. package/templates/.agents/rules/README.zh-CN.md +2 -1
  121. package/templates/.agents/rules/next-step-output.en.md +6 -19
  122. package/templates/.agents/rules/next-step-output.zh-CN.md +6 -19
  123. package/templates/.agents/rules/strategy-pattern.en.md +25 -0
  124. package/templates/.agents/rules/strategy-pattern.zh-CN.md +25 -0
  125. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
  126. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
  127. package/templates/.agents/skills/check-task/SKILL.en.md +1 -1
  128. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +1 -1
  129. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  130. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
  131. package/templates/.agents/skills/code-task/reference/code-rules.en.md +4 -0
  132. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +4 -0
  133. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +0 -1
  134. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +0 -1
  135. package/templates/.agents/skills/complete-task/SKILL.en.md +5 -29
  136. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -26
  137. package/templates/.agents/skills/plan-task/SKILL.en.md +7 -4
  138. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +7 -4
  139. package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
  140. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
  141. package/templates/.agents/skills/review-code/SKILL.en.md +1 -1
  142. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +1 -1
  143. package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
  144. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
  145. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
  146. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
  147. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  148. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  149. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +0 -546
  150. package/dist/lib/internal/codex-lifecycle.js +0 -385
  151. package/dist/lib/task/codex-orchestration.js +0 -206
  152. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +0 -776
  153. package/lib/internal/codex-lifecycle.ts +0 -390
  154. package/lib/task/codex-orchestration.ts +0 -276
@@ -23,7 +23,7 @@ import { inspectSandboxControlContainer } from '../control/container-identity.js
23
23
  import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.js';
24
24
  import { sandboxCoreBindMounts } from '../mounts.js';
25
25
  import { materializeSandboxGitMetadata } from '../git-metadata.js';
26
- import { assertSandboxTaskSource, finalizeSandboxControlManifest, materializeSandboxControl, materializeSandboxWorkspaceView, prepareSandboxWorkspaceMountTargets, sandboxControlPaths, } from '../workspace-view.js';
26
+ import { assertSandboxTaskSource, finalizeSandboxControlManifest, materializeSandboxControl, materializeSandboxWorkspaceView, prepareSandboxWorkspaceMountTargets, prepareSandboxCompletedTaskMountSource, sandboxControlPaths, } from '../workspace-view.js';
27
27
  import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.js';
28
28
  import { validateSelinuxDisableEnv } from '../engines/selinux.js';
29
29
  import { captureSandboxAuthority, commandForSandboxAuthority, verifySandboxAuthority } from '../engines/authority.js';
@@ -741,6 +741,7 @@ export async function create(args, { runProjectInitCommand = true, workspace: wo
741
741
  const container = containerName(effectiveConfig, branch);
742
742
  const controlPaths = sandboxControlPaths({
743
743
  base: effectiveConfig.controlBase,
744
+ repoRoot: effectiveConfig.repoRoot,
744
745
  project: effectiveConfig.project,
745
746
  container,
746
747
  identity: target.workspace
@@ -1037,7 +1038,10 @@ export async function create(args, { runProjectInitCommand = true, workspace: wo
1037
1038
  const taskSource = target.workspace.mode === 'task-bound'
1038
1039
  ? assertSandboxTaskSource(effectiveConfig.repoRoot, target.workspace.taskId)
1039
1040
  : null;
1040
- prepareSandboxWorkspaceMountTargets(worktree);
1041
+ const completedTaskSource = target.workspace.mode === 'task-bound'
1042
+ ? prepareSandboxCompletedTaskMountSource(effectiveConfig.repoRoot, target.workspace.taskId)
1043
+ : null;
1044
+ prepareSandboxWorkspaceMountTargets(worktree, target.workspace.mode === 'task-bound' ? target.workspace.taskId : undefined);
1041
1045
  if (previousCutoverSnapshot) {
1042
1046
  assertSandboxControlCutoverSnapshot(previousCutoverSnapshot);
1043
1047
  if (previousManifest) {
@@ -1080,6 +1084,7 @@ export async function create(args, { runProjectInitCommand = true, workspace: wo
1080
1084
  ...(target.workspace.mode === 'task-bound'
1081
1085
  ? {
1082
1086
  taskSources: [taskSource],
1087
+ completedTaskSource: completedTaskSource,
1083
1088
  taskId: target.workspace.taskId
1084
1089
  }
1085
1090
  : {})
@@ -11,11 +11,10 @@ export { containerListFormat, parseLabels } from './list-running.js';
11
11
  const USAGE = `Usage: ai sandbox ls
12
12
 
13
13
  Lists all containers for the current project. The '#' column is a
14
- display-only row number; the 'SHORT' column shows the active task short
15
- id bound to each container's branch (via
16
- .agents/workspace/active/.short-ids.json), or '-' if no active task is
17
- bound. Pass the SHORT value to "ai sandbox exec" (e.g. 'ai sandbox exec 11').
18
- A SHORT value of '-' means this container has no active task short id and may
14
+ display-only row number; the 'SHORT' column shows the task short id bound to
15
+ each task-bound container, or '-' if no short id is registered. Pass the
16
+ SHORT value to "ai sandbox exec" (e.g. 'ai sandbox exec 11').
17
+ A SHORT value of '-' means this container has no task short id and may
19
18
  be eligible for "ai sandbox rm --unbound" cleanup. Use a full TASK-id to
20
19
  remove a specific task-bound sandbox.
21
20
 
@@ -7,7 +7,7 @@ import { sandboxBranchLabel, sandboxLabel, sandboxTaskIdLabel, sandboxWorkspaceM
7
7
  import { fetchSandboxRows } from './list-running.js';
8
8
  import { rmOne, rmPurge, rmUnbound } from '../removal.js';
9
9
  const USAGE = `Usage:
10
- ai sandbox rm <branch | TASK-id | short id> Remove one sandbox; use a full TASK-id for a task-bound sandbox and a branch for branch-only sandboxes
10
+ ai sandbox rm <branch | TASK-id | short id> Remove one sandbox; use a retained short id or full TASK-id for a task-bound sandbox and a branch for branch-only sandboxes
11
11
  ai sandbox rm --unbound [--dry-run] [--yes] Remove sandboxes not bound to an active task short id
12
12
  ai sandbox rm --purge Tear down ALL sandboxes for the project (containers, worktrees, image, VM)`;
13
13
  function resolveMissingTaskSandboxBranch(taskId, config) {
@@ -0,0 +1,34 @@
1
+ import { getAgentClientAdapter } from '../../agent-clients/registry.js';
2
+ function operationFor(request) {
3
+ const adapter = getAgentClientAdapter(request.agentClient);
4
+ const operation = adapter.sandboxControlOperation;
5
+ if (!operation)
6
+ throw new Error('SANDBOX_CONTROL_AGENT_OPERATION_UNSUPPORTED');
7
+ return operation;
8
+ }
9
+ function validateAgentClientOperation(request, manifest) {
10
+ operationFor(request).validate({
11
+ operation: request.operation,
12
+ payload: request.payload,
13
+ manifest
14
+ });
15
+ }
16
+ function executeAgentClientOperation(request, manifest, manifestPath, override) {
17
+ return (override ?? operationFor(request)).execute({
18
+ operation: request.operation,
19
+ payload: request.payload,
20
+ manifest,
21
+ manifestPath
22
+ });
23
+ }
24
+ function recoverAgentClientOperation(params) {
25
+ const operation = operationFor(params.request);
26
+ return operation.recover?.({
27
+ operation: params.request.operation,
28
+ manifest: params.manifest,
29
+ manifestPath: params.manifestPath,
30
+ stdout: params.stdout
31
+ }) ?? { consistent: false };
32
+ }
33
+ export { executeAgentClientOperation, recoverAgentClientOperation, validateAgentClientOperation };
34
+ //# sourceMappingURL=agent-operation.js.map
@@ -2,7 +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
+ import { isAgentClientId } from '../../agent-clients/types.js';
6
6
  import { readSandboxControlPayload, readSandboxControlStatus } from './state.js';
7
7
  import { accessSandboxTaskView, taskViewFromStatus } from './task-view.js';
8
8
  import { readSandboxControlIdentitySentinel } from './identity-sentinel.js';
@@ -11,7 +11,6 @@ import { hasTaskBoundMarker, isSandbox } from '../environment.js';
11
11
  import { loadConfig } from '../config.js';
12
12
  import { recoverSandboxControlFromHost } from './host-recovery.js';
13
13
  const SANDBOX_CONTROL_RESPONSE_SETTLE_MS = 250;
14
- const SANDBOX_TASK_FINALIZATION_RECOVERY_BUDGET_MS = 5 * 60_000;
15
14
  export class SandboxControlClientError extends Error {
16
15
  detail;
17
16
  accepted;
@@ -84,8 +83,6 @@ function preflight(statusDir, generation, taskViewEffect = null, now = Date.now(
84
83
  function taskViewEffectForRequest(request) {
85
84
  if (request.family === 'task-lifecycle')
86
85
  return 'progress';
87
- if (request.family === 'task-finalization')
88
- return 'recovery';
89
86
  return null;
90
87
  }
91
88
  function parseResponse(raw, id, accepted = false) {
@@ -179,9 +176,6 @@ function exchangeSandboxControl(request, params) {
179
176
  || (requestTask.kind === 'task' && requestTask.taskId !== identity.taskId))) {
180
177
  clientError('SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH', 'request task does not match the sandbox identity', false);
181
178
  }
182
- if (identity.mode === 'branch-only' && request.family === 'task-finalization') {
183
- clientError('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot finalize tasks', false);
184
- }
185
179
  }
186
180
  const encoded = `${JSON.stringify(request)}\n`;
187
181
  if (Buffer.byteLength(encoded, 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
@@ -368,173 +362,25 @@ function authority(params) {
368
362
  export function requestSandboxControl(params) {
369
363
  if (!isSandboxControlFamily(params.family))
370
364
  clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' is not allowed`, false);
371
- if (params.family === 'task-create' || params.family === 'codex-controller' || params.family === 'task-finalization') {
365
+ if (params.family === 'task-create' || params.family === 'agent-client') {
372
366
  clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' requires a typed request`, false);
373
367
  }
368
+ if (params.args[1] === 'complete') {
369
+ clientError('SANDBOX_CONTROL_COMMAND_DENIED', 'complete must use task-finalization', false);
370
+ }
374
371
  const auth = authority(params);
375
372
  const issuedAt = Date.now();
376
373
  const request = {
377
374
  version: 4, id: randomUUID(), ...auth, issuedAt,
378
375
  expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
379
- family: params.family, args: params.args,
380
- controllerProcess: null,
381
- controllerProof: null
376
+ family: params.family, args: params.args
382
377
  };
383
378
  return exchangeSandboxControl(request, params);
384
379
  }
385
- export function requestSandboxTaskFinalization(params) {
386
- const agent = normalizeAgentToken(params.agent);
387
- if (!agent)
388
- clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization agent is invalid', false);
389
- const auth = authority(params);
390
- const issuedAt = Date.now();
391
- const id = randomUUID();
392
- const request = {
393
- version: 4,
394
- id,
395
- ...auth,
396
- issuedAt,
397
- expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
398
- family: 'task-finalization',
399
- operation: 'complete',
400
- agent,
401
- args: [],
402
- controllerProcess: null,
403
- controllerProof: null
404
- };
405
- let response;
406
- try {
407
- response = exchangeSandboxControl(request, params);
408
- }
409
- catch (error) {
410
- if (!(error instanceof SandboxControlClientError)
411
- || error.detail.code !== 'SANDBOX_CONTROL_RESULT_UNKNOWN'
412
- || !error.accepted || !error.requestId)
413
- throw error;
414
- return recoverAcceptedTaskFinalization(error.requestId, params);
415
- }
416
- if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') {
417
- return recoverAcceptedTaskFinalization(response.id, params);
418
- }
419
- return response;
420
- }
421
- export function recoverAcceptedTaskFinalization(requestId, params, recover = recoverSandboxControl) {
422
- const deadline = Date.now() + (params.recoveryBudgetMs ?? SANDBOX_TASK_FINALIZATION_RECOVERY_BUDGET_MS);
423
- while (Date.now() < deadline) {
424
- const timeoutMs = Math.min(30_000, deadline - Date.now());
425
- if (timeoutMs <= 0)
426
- break;
427
- try {
428
- const response = recover(requestId, { timeoutMs });
429
- if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN')
430
- continue;
431
- return response;
432
- }
433
- catch (error) {
434
- if (error instanceof SandboxControlClientError
435
- && error.detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN'
436
- && error.requestId === requestId)
437
- continue;
438
- throw error;
439
- }
440
- }
441
- clientError('SANDBOX_CONTROL_RESULT_UNKNOWN', 'accepted finalization request remained unknown after the automatic recovery budget; inspect the receipt and resume by request id', false, true, requestId);
442
- }
443
- function exactKeys(value, keys) {
444
- return Object.keys(value).sort().join(',') === [...keys].sort().join(',');
445
- }
446
- function validProcess(value) {
447
- if (!value || typeof value !== 'object' || Array.isArray(value))
448
- return false;
449
- const identity = value;
450
- return exactKeys(identity, ['pid', 'startTime'])
451
- && Number.isSafeInteger(identity.pid) && identity.pid > 0
452
- && Number.isSafeInteger(identity.startTime) && identity.startTime >= 0;
453
- }
454
- function validBuild(value) {
455
- if (!value || typeof value !== 'object' || Array.isArray(value))
456
- return false;
457
- const build = value;
458
- return exactKeys(build, ['internalExecutableBuildHash', 'lifecycleContractHash', 'packageVersion', 'protocolVersion'])
459
- && build.protocolVersion === 3
460
- && typeof build.packageVersion === 'string' && build.packageVersion.length > 0
461
- && typeof build.internalExecutableBuildHash === 'string' && /^[a-f0-9]{64}$/u.test(build.internalExecutableBuildHash)
462
- && typeof build.lifecycleContractHash === 'string' && /^[a-f0-9]{64}$/u.test(build.lifecycleContractHash);
463
- }
464
- export function parseCodexControllerResult(response) {
465
- if (response.phase !== 'completed' || response.error !== null || response.stderr !== '') {
466
- clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller result outer response is invalid', false, true);
467
- }
468
- let value;
469
- try {
470
- if (!response.stdout.endsWith('\n') || response.stdout.slice(0, -1).includes('\n'))
471
- throw new Error('not canonical');
472
- value = JSON.parse(response.stdout);
473
- }
474
- catch {
475
- clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller result payload is invalid', false, true);
476
- }
477
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
478
- clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller result payload is invalid', false, true);
479
- }
480
- const result = value;
481
- const resultKeys = result.status === 'verified'
482
- ? ['binding', 'changed', 'error', 'lease', 'status', 'version']
483
- : ['changed', 'error', 'lease', 'status', 'version'];
484
- if (!exactKeys(result, resultKeys)
485
- || result.version !== 1 || !['opened', 'closed', 'failed', 'verified'].includes(result.status)) {
486
- clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller result schema is invalid', false, true);
487
- }
488
- if (result.status === 'failed') {
489
- const error = result.error;
490
- if (response.exitCode !== 1 || result.changed !== false || result.lease !== null
491
- || !error || !exactKeys(error, ['code', 'message', 'retryable'])
492
- || typeof error.code !== 'string' || !/^[A-Z][A-Z0-9_]+$/u.test(error.code)
493
- || typeof error.message !== 'string' || error.retryable !== false) {
494
- clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller failure result is invalid', false, true);
495
- }
496
- clientError(error.code, error.message, false, true);
380
+ export function requestSandboxAgentOperation(params) {
381
+ if (!isAgentClientId(params.agentClient)) {
382
+ clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'agent-client id is invalid', false);
497
383
  }
498
- if (response.exitCode !== 0 || result.error !== null) {
499
- clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller success result is invalid', false, true);
500
- }
501
- if (result.status === 'closed') {
502
- if (typeof result.changed !== 'boolean' || result.lease !== null) {
503
- clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller close result is invalid', false, true);
504
- }
505
- return result;
506
- }
507
- if (result.status === 'verified') {
508
- const binding = result.binding;
509
- if (result.changed !== false || result.lease !== null || !binding
510
- || !exactKeys(binding, ['controllerInstanceDigest', 'controlGeneration', 'taskId'])
511
- || typeof binding.taskId !== 'string' || binding.taskId.length === 0
512
- || typeof binding.controlGeneration !== 'string' || binding.controlGeneration.length === 0
513
- || typeof binding.controllerInstanceDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(binding.controllerInstanceDigest)) {
514
- clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller verify result is invalid', false, true);
515
- }
516
- return result;
517
- }
518
- const lease = result.lease;
519
- if (result.changed !== true || !lease
520
- || !exactKeys(lease, [
521
- 'buildIdentity', 'controlGeneration', 'controllerInstanceDigest', 'controllerProcess',
522
- 'expiresAt', 'issuedAt', 'leaseId', 'leaseSecret', 'taskId', 'version'
523
- ])
524
- || lease.version !== 1
525
- || typeof lease.leaseId !== 'string' || !/^[a-f0-9]{64}$/u.test(lease.leaseId)
526
- || typeof lease.leaseSecret !== 'string' || !/^[a-f0-9]{64}$/u.test(lease.leaseSecret)
527
- || typeof lease.taskId !== 'string' || lease.taskId.length === 0
528
- || typeof lease.controlGeneration !== 'string' || lease.controlGeneration.length === 0
529
- || typeof lease.controllerInstanceDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(lease.controllerInstanceDigest)
530
- || !validProcess(lease.controllerProcess) || !validBuild(lease.buildIdentity)
531
- || !Number.isSafeInteger(lease.issuedAt) || !Number.isSafeInteger(lease.expiresAt)
532
- || lease.expiresAt <= lease.issuedAt) {
533
- clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller open result is invalid', false, true);
534
- }
535
- return result;
536
- }
537
- function requestCodexController(params) {
538
384
  const auth = authority(params);
539
385
  const issuedAt = Date.now();
540
386
  const request = {
@@ -543,43 +389,12 @@ function requestCodexController(params) {
543
389
  ...auth,
544
390
  issuedAt,
545
391
  expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
546
- family: 'codex-controller',
547
- command: params.command,
548
- args: [],
549
- controllerProcess: params.controllerProcess,
550
- controllerProof: params.controllerProof
392
+ family: 'agent-client',
393
+ agentClient: params.agentClient,
394
+ operation: params.operation,
395
+ payload: params.payload
551
396
  };
552
- const result = parseCodexControllerResult(exchangeSandboxControl(request, params));
553
- if (result.status === 'opened'
554
- && (result.lease.controlGeneration !== auth.generation
555
- || result.lease.controllerProcess.pid !== params.controllerProcess.pid
556
- || result.lease.controllerProcess.startTime !== params.controllerProcess.startTime)) {
557
- clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller result does not match the request', false, true);
558
- }
559
- return result;
560
- }
561
- export function requestCodexControllerOpen(params) {
562
- const result = requestCodexController({ ...params, command: 'open', controllerProof: null });
563
- if (result.status !== 'opened')
564
- clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller open returned the wrong result', false, true);
565
- return result;
566
- }
567
- export function requestCodexControllerClose(params) {
568
- const result = requestCodexController({ ...params, command: 'close' });
569
- if (result.status !== 'closed')
570
- clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller close returned the wrong result', false, true);
571
- return result;
572
- }
573
- export function requestCodexControllerVerify(params) {
574
- const result = requestCodexController({
575
- ...params,
576
- controllerProcess: params.controllerProof.controllerProcess,
577
- command: 'verify'
578
- });
579
- if (result.status !== 'verified') {
580
- clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller verify returned the wrong result', false, true);
581
- }
582
- return result;
397
+ return exchangeSandboxControl(request, params);
583
398
  }
584
399
  export function requestSandboxTaskCreate(params) {
585
400
  const auth = authority(params);
@@ -588,8 +403,6 @@ export function requestSandboxTaskCreate(params) {
588
403
  version: 4, id: randomUUID(), ...auth, issuedAt,
589
404
  expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
590
405
  family: 'task-create', candidate: params.candidate,
591
- controllerProcess: null,
592
- controllerProof: null
593
406
  };
594
407
  return exchangeSandboxControl(request, { ...params, timeoutMs: params.timeoutMs ?? 120_000 });
595
408
  }
@@ -4,16 +4,13 @@ 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, projectTaskCreateResult, taskCreateExitCode, taskCreateFailure } from '../../task/create-service.js';
7
- import { applyTaskFinalization } from '../../task/finalization.js';
8
- import { serializeTaskFinalizationEnvelope } from '../../task/finalization-envelope.js';
9
7
  import { bindSandboxControlTask, validateSandboxControlRequest } from './protocol.js';
10
8
  import { atomicWriteJson, executionPath, readActiveLease, terminateSandboxControlExecution } from './state.js';
11
9
  import { requestAuditFields, resultAuditFields, appendDiagnosticAudit, createSandboxControlAuditContext, appendCriticalAudit, writeSandboxControlTransition } from './audit.js';
12
10
  import { validateSandboxControlIdentity } from './identity-sentinel.js';
13
11
  import { createSandboxExecutorExecutionContext, dispatchTaskControlOperation, parseTaskControlOperation } from '../../task/control-authority.js';
14
12
  import { assertSandboxControlBrokerOwner, readSandboxControlManifest } from './lifecycle.js';
15
- import { computeLifecycleBuildIdentity } from '../../agent-clients/adapters/codex-lifecycle/build-identity.js';
16
- import { closeCodexControllerRegistration, CodexControllerRegistrationError, openCodexControllerRegistration, resolveCodexControllerBinding } from './controller-registration.js';
13
+ import { executeAgentClientOperation, validateAgentClientOperation } from './agent-operation.js';
17
14
  function sameBrokerOwner(left, right) {
18
15
  return left.version === right.version
19
16
  && left.pid === right.pid
@@ -169,23 +166,6 @@ function waitForGate(nonce, timeoutMs = 2_000) {
169
166
  });
170
167
  });
171
168
  }
172
- function controllerFailure(error) {
173
- const code = error instanceof CodexControllerRegistrationError
174
- ? error.code
175
- : /^([A-Z][A-Z0-9_]+)/u.exec(error instanceof Error ? error.message : String(error))?.[1]
176
- ?? 'CODEX_SANDBOX_CONTROLLER_FAILED';
177
- const message = error instanceof CodexControllerRegistrationError
178
- ? error.message
179
- : `${code}: controller operation failed; inspect the sandbox controller and rebuild the sandbox if needed`;
180
- const payload = {
181
- version: 1,
182
- status: 'failed',
183
- changed: false,
184
- lease: null,
185
- error: { code, message, retryable: false }
186
- };
187
- return { exitCode: 1, stdout: `${JSON.stringify(payload)}\n`, stderr: '' };
188
- }
189
169
  function lifecycleFailure(code, message) {
190
170
  return {
191
171
  exitCode: 1,
@@ -210,21 +190,6 @@ function taskCreateExecutionFailure(requestId, code, message) {
210
190
  recovery: 'inspect-domain-state'
211
191
  }));
212
192
  }
213
- function finalizationResult(result) {
214
- const payload = {
215
- version: 1,
216
- status: result.status,
217
- changed: result.changed,
218
- accepted: true,
219
- result,
220
- error: result.error
221
- };
222
- return {
223
- exitCode: result.status === 'completed' ? 0 : result.status === 'blocked' ? 2 : 1,
224
- stdout: `${JSON.stringify(payload)}\n`,
225
- stderr: ''
226
- };
227
- }
228
193
  async function executeRequestInner(manifest, manifestPath, request, options = {}) {
229
194
  if (request.family === 'task-create') {
230
195
  try {
@@ -241,75 +206,9 @@ async function executeRequestInner(manifest, manifestPath, request, options = {}
241
206
  return taskCreateExecutionFailure(request.id, code, detail);
242
207
  }
243
208
  }
244
- if (request.family === 'codex-controller') {
245
- try {
246
- if (request.command === 'verify') {
247
- const binding = (options.resolveControllerBinding ?? resolveCodexControllerBinding)({
248
- manifest,
249
- manifestPath,
250
- proof: request.controllerProof,
251
- buildIdentity: (options.buildIdentity ?? computeLifecycleBuildIdentity)(manifest.repoRoot)
252
- });
253
- return {
254
- exitCode: 0,
255
- stdout: `${JSON.stringify({
256
- version: 1,
257
- status: 'verified',
258
- changed: false,
259
- lease: null,
260
- binding: {
261
- taskId: manifest.taskId,
262
- controlGeneration: binding.controlGeneration,
263
- controllerInstanceDigest: binding.instanceDigest
264
- },
265
- error: null
266
- })}\n`,
267
- stderr: ''
268
- };
269
- }
270
- const result = request.command === 'open'
271
- ? openCodexControllerRegistration({
272
- manifest,
273
- manifestPath,
274
- controllerProcess: request.controllerProcess,
275
- buildIdentity: (options.buildIdentity ?? computeLifecycleBuildIdentity)(manifest.repoRoot)
276
- })
277
- : closeCodexControllerRegistration({
278
- manifest,
279
- manifestPath,
280
- proof: request.controllerProof
281
- });
282
- return { exitCode: 0, stdout: `${JSON.stringify(result)}\n`, stderr: '' };
283
- }
284
- catch (error) {
285
- const errorCode = error instanceof CodexControllerRegistrationError
286
- ? error.code
287
- : /^([A-Z][A-Z0-9_]+)/u.exec(error instanceof Error ? error.message : String(error))?.[1]
288
- ?? 'CODEX_SANDBOX_CONTROLLER_FAILED';
289
- appendExecutorAudit(manifest, 'controller-operation-failed', {
290
- ...requestAuditFields(manifest, request),
291
- controllerCommand: request.command,
292
- errorCode,
293
- errorType: error instanceof Error ? error.name : typeof error,
294
- });
295
- return controllerFailure(error);
296
- }
297
- }
298
- if (request.family === 'task-finalization') {
299
- const parsed = parseTaskControlOperation('task-finalization', [manifest.taskId, 'complete', '--agent', request.agent]);
300
- if (parsed.family !== 'task-finalization')
301
- throw new Error('SANDBOX_CONTROL_FINALIZATION_OPERATION_INVALID');
302
- const operation = parsed;
303
- const result = dispatchTaskControlOperation(createSandboxExecutorExecutionContext({
304
- repoRoot: manifest.repoRoot,
305
- worktreeRoot: manifest.worktreeRoot,
306
- runtimeDir: manifest.runtimeDir,
307
- taskId: manifest.taskId,
308
- generation: manifest.generation,
309
- manifestPath,
310
- requestId: request.id
311
- }), operation);
312
- return finalizationResult(await result);
209
+ if (request.family === 'agent-client') {
210
+ validateAgentClientOperation(request, manifest);
211
+ return executeAgentClientOperation(request, manifest, manifestPath, options.agentClientOperation);
313
212
  }
314
213
  const boundArgs = bindSandboxControlTask(request, manifest.taskId);
315
214
  let operation;
@@ -346,7 +245,8 @@ export async function executeRequest(manifest, manifestPath, request, options =
346
245
  const fields = requestAuditFields(manifest, request);
347
246
  appendExecutorAudit(manifest, 'executor-request-start', fields);
348
247
  try {
349
- const result = await executeRequestInner(manifest, manifestPath, request, options);
248
+ let result;
249
+ result = await executeRequestInner(manifest, manifestPath, request, options);
350
250
  appendExecutorAudit(manifest, 'executor-request-finished', {
351
251
  requestId: request.id,
352
252
  requestFamily: request.family,
@@ -424,13 +324,13 @@ export async function runSandboxControlExecutor(requestPath, nonce) {
424
324
  return null;
425
325
  }
426
326
  })()
427
- : null;
327
+ : request.family === 'agent-client' ? request.operation : null;
428
328
  const context = createSandboxControlAuditContext(manifest, {
429
329
  requestId: request.id,
430
330
  family: request.family,
431
- operation: operation?.family === 'task-lifecycle'
331
+ operation: operation && typeof operation === 'object' && 'family' in operation && operation.family === 'task-lifecycle'
432
332
  ? operation.request.intent
433
- : request.family === 'task-finalization' ? request.operation : null,
333
+ : request.family === 'agent-client' ? request.operation : null,
434
334
  phase: 'started-committed',
435
335
  outcome: 'in-progress'
436
336
  });
@@ -447,22 +347,9 @@ export async function runSandboxControlExecutor(requestPath, nonce) {
447
347
  });
448
348
  const detail = error instanceof Error ? error.message : String(error);
449
349
  const code = /^([A-Z][A-Z0-9_]+)/u.exec(detail)?.[1] ?? 'SANDBOX_CONTROL_EXECUTOR_FAILED';
450
- result = request.family === 'task-finalization'
451
- ? {
452
- exitCode: 1,
453
- stdout: serializeTaskFinalizationEnvelope({
454
- status: code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 'unknown' : 'failed',
455
- changed: code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? null : false,
456
- accepted: true,
457
- requestId: request.id,
458
- result: null,
459
- error: { code, message: detail, retryable: false }
460
- }),
461
- stderr: ''
462
- }
463
- : request.family === 'task-create'
464
- ? taskCreateExecutionFailure(request.id, code, detail)
465
- : lifecycleFailure(code, detail);
350
+ result = request.family === 'task-create'
351
+ ? taskCreateExecutionFailure(request.id, code, detail)
352
+ : lifecycleFailure(code, detail);
466
353
  }
467
354
  process.stdout.write(result.stdout);
468
355
  process.stderr.write(result.stderr);
@@ -9,6 +9,7 @@ import { DEFAULT_SANDBOX_CONTROL_TIMING } from './protocol.js';
9
9
  import { inspectSandboxControlContainer } from './container-identity.js';
10
10
  import { acquireSandboxResourceLock, resolveSandboxLockNamespace } from './native-file-lock.js';
11
11
  import { isSandboxAuthorityEvidence } from '../engines/authority.js';
12
+ import { validateSandboxControlIdentity } from './identity-sentinel.js';
12
13
  import { parseSandboxControlStatus, readExecution, terminateSandboxControlExecution } from './state.js';
13
14
  const DEFAULT_QUIESCE_TIMEOUT_MS = DEFAULT_SANDBOX_CONTROL_TIMING.quiesceDeadlineMs;
14
15
  const QUIESCING_FILE = 'quiescing.json';
@@ -500,6 +501,45 @@ function markSandboxControlRootQuiescing(root) {
500
501
  throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
501
502
  }
502
503
  }
504
+ function readTaskCompletionManifest(root, binding) {
505
+ const manifest = readSandboxControlManifest(path.join(root, 'manifest.json'));
506
+ const identity = validateSandboxControlIdentity({
507
+ publicStatusDir: binding.statusDir,
508
+ root,
509
+ mode: manifest.mode,
510
+ taskId: manifest.taskId,
511
+ generation: manifest.generation,
512
+ controlRootId: manifest.controlRootId
513
+ });
514
+ if (identity.state !== 'valid' || manifest.mode !== 'task-bound'
515
+ || manifest.taskId !== binding.taskId || manifest.generation !== binding.generation
516
+ || manifest.controlRootId !== binding.controlRootId) {
517
+ throw new Error('SANDBOX_CONTROL_IDENTITY_MISMATCH');
518
+ }
519
+ return manifest;
520
+ }
521
+ export async function quiesceSandboxControlForTaskCompletion(root, binding, timeoutMs = DEFAULT_QUIESCE_TIMEOUT_MS) {
522
+ const resolvedRoot = path.resolve(root);
523
+ const manifest = readTaskCompletionManifest(resolvedRoot, binding);
524
+ markSandboxControlRootQuiescing(resolvedRoot);
525
+ const deadline = Date.now() + timeoutMs;
526
+ const startupPath = path.join(resolvedRoot, BROKER_STARTING_FILE);
527
+ const brokerPath = path.join(resolvedRoot, 'broker.json');
528
+ while (true) {
529
+ const startup = readStartupOwner(startupPath);
530
+ if (fs.existsSync(startupPath) && !startup)
531
+ throw new Error('SANDBOX_CONTROL_BROKER_START_TRANSITION');
532
+ const broker = readBrokerOwner(brokerPath);
533
+ if (broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
534
+ throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
535
+ }
536
+ if (!startup && !broker)
537
+ return;
538
+ if (Date.now() >= deadline)
539
+ throw new Error('SANDBOX_CONTROL_BROKER_DRAIN_TIMEOUT');
540
+ await new Promise((resolve) => setTimeout(resolve, 25));
541
+ }
542
+ }
503
543
  function recordSandboxRemovalPending(root, manifest, phase, journal) {
504
544
  const pendingPath = path.join(root, 'removal-pending.json');
505
545
  const temporary = `${pendingPath}.${process.pid}.${randomUUID()}.tmp`;
@@ -934,12 +974,34 @@ function readSandboxControlManifestValue(manifestPath) {
934
974
  || !isSandboxAuthorityEvidence(candidate.authorityEvidence))
935
975
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
936
976
  const root = path.dirname(path.resolve(manifestPath));
937
- if (path.resolve(candidate.channelDir) !== path.join(root, 'channel')
938
- || path.resolve(candidate.publicStatusDir) !== path.join(root, 'public')
939
- || path.resolve(candidate.processingDir) !== path.join(root, 'processing')) {
977
+ const canonicalRoot = fs.realpathSync.native(root);
978
+ const matchesControlChildPath = (candidatePath, name) => {
979
+ const resolved = path.resolve(candidatePath);
980
+ if (path.basename(resolved) !== name)
981
+ return false;
982
+ const expected = path.join(canonicalRoot, name);
983
+ try {
984
+ return fs.realpathSync.native(resolved) === expected;
985
+ }
986
+ catch (error) {
987
+ if (error.code !== 'ENOENT')
988
+ throw error;
989
+ try {
990
+ return fs.realpathSync.native(path.dirname(resolved)) === canonicalRoot;
991
+ }
992
+ catch (parentError) {
993
+ if (parentError.code !== 'ENOENT')
994
+ throw parentError;
995
+ return false;
996
+ }
997
+ }
998
+ };
999
+ if (!matchesControlChildPath(candidate.channelDir, 'channel')
1000
+ || !matchesControlChildPath(candidate.publicStatusDir, 'public')
1001
+ || !matchesControlChildPath(candidate.processingDir, 'processing')) {
940
1002
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
941
1003
  }
942
- if (path.resolve(candidate.runtimeDir) !== path.join(root, 'runtime')) {
1004
+ if (!matchesControlChildPath(candidate.runtimeDir, 'runtime')) {
943
1005
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
944
1006
  }
945
1007
  const normalized = {