@fitlab-ai/agent-infra 0.11.12 → 0.11.14

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 (120) hide show
  1. package/dist/bin/internal-cli.js +2 -7
  2. package/dist/lib/agent-clients/adapter.js +0 -3
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +5 -9
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +1 -5
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +1 -5
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +11 -25
  7. package/dist/lib/agent-clients/adapters/codex-orchestration.js +1 -3
  8. package/dist/lib/agent-clients/adapters/codex.js +0 -2
  9. package/dist/lib/internal/cli-route-inventory.js +0 -1
  10. package/dist/lib/internal/sandbox-control.js +1 -81
  11. package/dist/lib/internal/task-finalization.js +29 -20
  12. package/dist/lib/internal/task-lifecycle.js +4 -2
  13. package/dist/lib/internal/task-operation-registry.js +1 -3
  14. package/dist/lib/internal/task-validate.js +14 -124
  15. package/dist/lib/platform/github-client.js +33 -3
  16. package/dist/lib/platform/release-notes.js +2 -2
  17. package/dist/lib/sandbox/commands/control-status.js +0 -1
  18. package/dist/lib/sandbox/commands/create.js +11 -6
  19. package/dist/lib/sandbox/commands/ls.js +4 -5
  20. package/dist/lib/sandbox/commands/rm.js +1 -1
  21. package/dist/lib/sandbox/config.js +12 -0
  22. package/dist/lib/sandbox/control/audit.js +1 -2
  23. package/dist/lib/sandbox/control/client.js +15 -110
  24. package/dist/lib/sandbox/control/executor.js +9 -59
  25. package/dist/lib/sandbox/control/host-recovery.js +89 -27
  26. package/dist/lib/sandbox/control/lifecycle.js +75 -9
  27. package/dist/lib/sandbox/control/protocol.js +11 -50
  28. package/dist/lib/sandbox/control/server.js +203 -253
  29. package/dist/lib/sandbox/control/task-view.js +0 -13
  30. package/dist/lib/sandbox/environment.js +7 -8
  31. package/dist/lib/sandbox/mounts.js +71 -52
  32. package/dist/lib/sandbox/recovery.js +30 -12
  33. package/dist/lib/sandbox/removal.js +37 -3
  34. package/dist/lib/sandbox/workspace-identity.js +18 -6
  35. package/dist/lib/sandbox/workspace-view.js +89 -6
  36. package/dist/lib/task/commands/ls.js +3 -3
  37. package/dist/lib/task/control-authority.js +41 -17
  38. package/dist/lib/task/control-recovery.js +2 -4
  39. package/dist/lib/task/finalization.js +27 -2
  40. package/dist/lib/task/lifecycle.js +294 -63
  41. package/dist/lib/task/orchestration.js +26 -72
  42. package/dist/lib/task/short-id.js +37 -27
  43. package/dist/lib/task/verification-engine.js +4 -11
  44. package/dist/lifecycle-build-manifest.json +3 -3
  45. package/lib/agent-clients/adapter.ts +0 -27
  46. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +6 -12
  47. package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +1 -5
  48. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +0 -7
  49. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +1 -5
  50. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +21 -27
  51. package/lib/agent-clients/adapters/codex-orchestration.ts +1 -3
  52. package/lib/agent-clients/adapters/codex.ts +0 -2
  53. package/lib/internal/cli-route-inventory.ts +0 -1
  54. package/lib/internal/sandbox-control.ts +0 -97
  55. package/lib/internal/task-finalization.ts +33 -19
  56. package/lib/internal/task-lifecycle.ts +4 -2
  57. package/lib/internal/task-operation-registry.ts +1 -3
  58. package/lib/internal/task-validate.ts +13 -127
  59. package/lib/platform/github-client.ts +34 -3
  60. package/lib/platform/release-notes.ts +2 -2
  61. package/lib/sandbox/commands/control-status.ts +0 -1
  62. package/lib/sandbox/commands/create.ts +11 -5
  63. package/lib/sandbox/commands/ls.ts +4 -5
  64. package/lib/sandbox/commands/rm.ts +1 -1
  65. package/lib/sandbox/config.ts +13 -0
  66. package/lib/sandbox/control/audit.ts +1 -2
  67. package/lib/sandbox/control/client.ts +17 -138
  68. package/lib/sandbox/control/executor.ts +8 -65
  69. package/lib/sandbox/control/host-recovery.ts +94 -21
  70. package/lib/sandbox/control/lifecycle.ts +75 -10
  71. package/lib/sandbox/control/protocol.ts +12 -62
  72. package/lib/sandbox/control/server.ts +11 -61
  73. package/lib/sandbox/control/task-view.ts +0 -19
  74. package/lib/sandbox/environment.ts +7 -8
  75. package/lib/sandbox/mounts.ts +93 -68
  76. package/lib/sandbox/recovery.ts +37 -19
  77. package/lib/sandbox/removal.ts +36 -5
  78. package/lib/sandbox/workspace-identity.ts +16 -6
  79. package/lib/sandbox/workspace-view.ts +81 -7
  80. package/lib/task/commands/ls.ts +3 -3
  81. package/lib/task/control-authority.ts +41 -26
  82. package/lib/task/control-recovery.ts +3 -5
  83. package/lib/task/finalization.ts +29 -2
  84. package/lib/task/lifecycle.ts +282 -57
  85. package/lib/task/orchestration.ts +30 -68
  86. package/lib/task/short-id.ts +33 -40
  87. package/lib/task/verification-engine.ts +3 -9
  88. package/package.json +1 -1
  89. package/templates/.agents/rules/README.en.md +1 -0
  90. package/templates/.agents/rules/README.zh-CN.md +1 -0
  91. package/templates/.agents/rules/lifecycle-orchestration.en.md +3 -4
  92. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -4
  93. package/templates/.agents/rules/strategy-pattern.en.md +25 -0
  94. package/templates/.agents/rules/strategy-pattern.zh-CN.md +25 -0
  95. package/templates/.agents/rules/task-short-id.en.md +18 -12
  96. package/templates/.agents/rules/task-short-id.zh-CN.md +12 -7
  97. package/templates/.agents/skills/code-task/reference/code-rules.en.md +4 -0
  98. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +4 -0
  99. package/templates/.agents/skills/complete-task/SKILL.en.md +5 -26
  100. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -23
  101. package/templates/.agents/skills/create-release-note/SKILL.en.md +3 -1
  102. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +3 -1
  103. package/templates/.agents/skills/plan-task/SKILL.en.md +7 -4
  104. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +7 -4
  105. package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
  106. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
  107. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +0 -130
  108. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +0 -127
  109. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +0 -131
  110. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-registration.js +0 -275
  111. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js +0 -106
  112. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +0 -345
  113. package/dist/lib/sandbox/control/agent-operation.js +0 -34
  114. package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +0 -158
  115. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +0 -163
  116. package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +0 -151
  117. package/lib/agent-clients/adapters/codex-lifecycle/controller-registration.ts +0 -352
  118. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts +0 -109
  119. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +0 -437
  120. package/lib/sandbox/control/agent-operation.ts +0 -49
@@ -17,9 +17,9 @@ const taskWorkflowCommand = command === 'task-artifact'
17
17
  const taskPlatformRecoveryCommand = command === 'task-platform-recovery';
18
18
  const manualValidationWorkflowCommand = command === 'manual-validation';
19
19
  const taskControlMarkers = [
20
- 'AGENT_INFRA_TASK_ID', 'AGENT_INFRA_CONTROL_TOKEN', 'AGENT_INFRA_CONTROL_GENERATION',
20
+ 'AGENT_INFRA_TASK_ID', 'AGENT_INFRA_CONTROL_GENERATION',
21
21
  'AGENT_INFRA_CONTROL_DIR', 'AGENT_INFRA_CONTROL_STATUS_DIR', 'AGENT_INFRA_RUNTIME_DIR',
22
- 'AGENT_INFRA_CONTROL_CONTROLLER_BINDING', 'AGENT_INFRA_EXECUTOR_MANIFEST'
22
+ 'AGENT_INFRA_EXECUTOR_MANIFEST'
23
23
  ];
24
24
  const markerlessHelp = ['--help', '-h'].includes(process.argv[3] ?? '')
25
25
  && taskControlMarkers.every((key) => !process.env[key]);
@@ -115,11 +115,6 @@ if (!controlRouted && !taskViewGuardFailed && internalRouteRegistered)
115
115
  await codexLifecycle(process.argv.slice(3));
116
116
  break;
117
117
  }
118
- case 'codex-sandbox-controller': {
119
- const { codexSandboxController } = await import('../lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js');
120
- await codexSandboxController(process.argv.slice(3));
121
- break;
122
- }
123
118
  case 'git-workflow': {
124
119
  const { gitWorkflow } = await import('../lib/internal/git-workflow.js');
125
120
  gitWorkflow(process.argv.slice(3));
@@ -434,9 +434,6 @@ function defineAgentClientAdapter(candidate) {
434
434
  ...(candidate.orchestrationAdapter === undefined
435
435
  ? {}
436
436
  : { orchestrationAdapter: Object.freeze({ ...candidate.orchestrationAdapter }) }),
437
- ...(candidate.sandboxControlOperation === undefined
438
- ? {}
439
- : { sandboxControlOperation: Object.freeze({ ...candidate.sandboxControlOperation }) }),
440
437
  project: Object.freeze({
441
438
  ownedPathPrefixes: Object.freeze(paths),
442
439
  ...projectAssets,
@@ -114,7 +114,7 @@ function validateCodexLifecycleHookConfig(value) {
114
114
  if (!valid)
115
115
  throw new Error('CODEX_PREFLIGHT_HOOKS_INVALID: lifecycle hooks are invalid');
116
116
  }
117
- function parseCodexHooksList(value, repoRoot, expectedSource = 'direct-host') {
117
+ function parseCodexHooksList(value, repoRoot) {
118
118
  const root = object(value);
119
119
  const entries = Array.isArray(root?.data) ? root.data : [];
120
120
  const entry = entries.map(object).find((candidate) => candidate?.cwd === repoRoot);
@@ -145,10 +145,8 @@ function parseCodexHooksList(value, repoRoot, expectedSource = 'direct-host') {
145
145
  && hook.currentHash.length > 0
146
146
  && hook.sourcePathDigest.length === 64
147
147
  && hook.pluginId === null
148
- && (expectedSource === 'isolated-user'
149
- ? hook.source === 'user' && hook.trustStatus !== 'modified'
150
- : (hook.source === 'project' && hook.trustStatus === 'trusted')
151
- || hook.isManaged && hook.trustStatus === 'managed')));
148
+ && (hook.source === 'project' && hook.trustStatus === 'trusted'
149
+ || hook.isManaged && hook.trustStatus === 'managed')));
152
150
  if (!valid)
153
151
  throw new Error('CODEX_PREFLIGHT_HOOKS_NOT_LOADED: lifecycle hooks are not loaded for this workspace');
154
152
  return Object.freeze(hooks);
@@ -546,7 +544,7 @@ async function discoverLifecycleHooks(repoRoot) {
546
544
  let discoveredHooks;
547
545
  try {
548
546
  await transport.initialize();
549
- discoveredHooks = parseCodexHooksList(await transport.request('hooks/list', { cwds: [repoRoot] }), repoRoot, process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT ? 'isolated-user' : 'direct-host');
547
+ discoveredHooks = parseCodexHooksList(await transport.request('hooks/list', { cwds: [repoRoot] }), repoRoot);
550
548
  }
551
549
  finally {
552
550
  transport.close();
@@ -564,9 +562,7 @@ async function discoverLifecycleHooks(repoRoot) {
564
562
  throw new Error('CODEX_PREFLIGHT_HOOK_SOURCE_AMBIGUOUS: lifecycle hooks do not share one exact source');
565
563
  }
566
564
  const source = [...sources][0];
567
- const hookSource = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT
568
- ? 'isolated-user'
569
- : source === 'managed' ? 'managed' : 'project';
565
+ const hookSource = source === 'managed' ? 'managed' : 'project';
570
566
  return {
571
567
  discoveredHooks,
572
568
  hookProvenance: Object.freeze({
@@ -4,7 +4,6 @@ import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { preflightCodexLifecycleEvidence, resolveCodexTerminal, resolveCodexThread } from './app-server.js';
6
6
  import { createCodexLifecycleStore } from './store.js';
7
- import { verifyCodexSandboxControllerContextWithWarnings } from './sandbox-controller.js';
8
7
  import { resolveTaskRef } from '../../../task/resolve-ref.js';
9
8
  import { resolveTaskContext } from '../../../task/resolve-ref.js';
10
9
  import { hasSealableOrchestrationDelegation, readRun } from '../../../task/orchestration.js';
@@ -78,10 +77,7 @@ function hookDefinitionHash() {
78
77
  return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
79
78
  }
80
79
  function resolveLifecycleTaskContext() {
81
- const controllerPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
82
- const taskId = controllerPath
83
- ? verifyCodexSandboxControllerContextWithWarnings(controllerPath, { repoRoot: process.cwd() }).context.taskId
84
- : process.env.AGENT_INFRA_TASK_ID;
80
+ const taskId = process.env.AGENT_INFRA_TASK_ID;
85
81
  const context = taskId
86
82
  ? resolveTaskRef(taskId, { repoRoot: process.cwd() })
87
83
  : resolveTaskContext(undefined, { repoRoot: process.cwd() });
@@ -136,14 +136,10 @@ function hostEvidenceMatches(record, receipt, host, start) {
136
136
  && host.spawnToolUseId === start.spawnToolUseId
137
137
  && Boolean(host.spawnObservedAt)
138
138
  && host.spawnObservedAt === record.spawnObservedAt
139
- && typeof host.controllerInstanceDigest === 'string'
140
- && typeof host.controlGeneration === 'string'
141
139
  && hostRevisionMatches(record, receipt, host);
142
140
  }
143
141
  function hostRevisionMatches(record, receipt, host) {
144
- return typeof host.controllerInstanceDigest === 'string'
145
- && typeof host.controlGeneration === 'string'
146
- && host.startRevision >= 1
142
+ return host.startRevision >= 1
147
143
  && record.revision > host.startRevision
148
144
  && record.state.child?.childThreadId === receipt.childId
149
145
  && record.state.stop?.childThreadId === receipt.childId;
@@ -215,7 +215,7 @@ function createCodexLifecycleStore(options) {
215
215
  throw new Error(`Codex lifecycle child '${childThreadId}' was not found uniquely`);
216
216
  return matches[0];
217
217
  }
218
- function consume(childThreadId, consumer, expectedHookDefinitionHash, expectedTaskBinding) {
218
+ function consumeWithLockMode(childThreadId, consumer, expectedHookDefinitionHash, expectedTaskBinding, taskLockHeld) {
219
219
  if (!consumer.trim())
220
220
  throw new Error('Codex lifecycle consumer is required');
221
221
  const current = read(childThreadId);
@@ -223,8 +223,11 @@ function createCodexLifecycleStore(options) {
223
223
  if (!binding)
224
224
  throw new Error('Codex lifecycle task receipt binding is missing');
225
225
  const spawn = current.state.spawn;
226
- if (!spawn)
227
- throw new Error('Codex lifecycle spawn evidence is missing');
226
+ if (!spawn) {
227
+ throw new Error(taskLockHeld
228
+ ? 'Codex lifecycle task receipt binding is missing'
229
+ : 'Codex lifecycle spawn evidence is missing');
230
+ }
228
231
  const key = recordKey(spawn.sessionId, spawn.turnId, spawn.toolUseId);
229
232
  return update(binding, key, (latest) => {
230
233
  if (!latest || expectedTaskBinding && JSON.stringify(latest.taskBinding) !== JSON.stringify(expectedTaskBinding)) {
@@ -241,30 +244,13 @@ function createCodexLifecycleStore(options) {
241
244
  if (latest.consumer)
242
245
  return latest;
243
246
  return Object.freeze({ ...latest, revision: latest.revision + 1, consumer, consumedAt: now(), updatedAt: now() });
244
- });
247
+ }, taskLockHeld);
248
+ }
249
+ function consume(childThreadId, consumer, expectedHookDefinitionHash, expectedTaskBinding) {
250
+ return consumeWithLockMode(childThreadId, consumer, expectedHookDefinitionHash, expectedTaskBinding, false);
245
251
  }
246
252
  function consumeWithinTaskLock(childThreadId, consumer, expectedHookDefinitionHash, expectedTaskBinding) {
247
- if (!consumer.trim())
248
- throw new Error('Codex lifecycle consumer is required');
249
- const current = read(childThreadId);
250
- const binding = expectedTaskBinding ?? current.taskBinding;
251
- if (!binding || !current.state.spawn)
252
- throw new Error('Codex lifecycle task receipt binding is missing');
253
- const key = recordKey(current.state.spawn.sessionId, current.state.spawn.turnId, current.state.spawn.toolUseId);
254
- return update(binding, key, (latest) => {
255
- if (!latest || expectedTaskBinding && JSON.stringify(latest.taskBinding) !== JSON.stringify(expectedTaskBinding)) {
256
- throw new Error('Codex lifecycle task binding does not match the expected receipt');
257
- }
258
- if (latest.consumer && latest.consumer !== consumer) {
259
- throw new Error(`Codex lifecycle evidence was already consumed by '${latest.consumer}'`);
260
- }
261
- if (latest.state.status !== 'stop-ready')
262
- throw new Error('Codex lifecycle evidence is not stop-ready');
263
- if (expectedHookDefinitionHash && latest.state.startEvidence?.hookDefinitionHash !== expectedHookDefinitionHash) {
264
- throw new Error('Codex lifecycle hook definition hash is stale');
265
- }
266
- return latest.consumer ? latest : Object.freeze({ ...latest, revision: latest.revision + 1, consumer, consumedAt: now(), updatedAt: now() });
267
- }, true);
253
+ return consumeWithLockMode(childThreadId, consumer, expectedHookDefinitionHash, expectedTaskBinding, true);
268
254
  }
269
255
  function expireBefore(cutoff) {
270
256
  let changed = 0;
@@ -215,9 +215,7 @@ async function activateCodexOrchestrationDelegation(childThreadId, options = {})
215
215
  capabilitySessionId: evidence.parentThreadId,
216
216
  capabilityTurnId: evidence.parentTurnId,
217
217
  spawnToolUseId: evidence.spawnToolUseId,
218
- spawnObservedAt: record.spawnObservedAt ?? undefined,
219
- controllerInstanceDigest: null,
220
- controlGeneration: null
218
+ spawnObservedAt: record.spawnObservedAt ?? undefined
221
219
  }
222
220
  }
223
221
  }, coreOptions(options));
@@ -3,7 +3,6 @@ import { hostJoin } from '../../sandbox/engines/wsl2-paths.js';
3
3
  import { codexBeforeContainerCreateHook, codexRecoveryChecks } from './codex-sandbox.js';
4
4
  import { parseCodexLifecycleBinding } from './codex-lifecycle/binding.js';
5
5
  import { isCodexDelegationReceiptEvidence, isCodexLifecycleActivationEvidence } from './codex-lifecycle/evidence.js';
6
- import { codexControllerOperation } from './codex-lifecycle/controller-operation.js';
7
6
  function codexCarrierError(code) {
8
7
  const error = new Error(code);
9
8
  error.name = code;
@@ -72,7 +71,6 @@ const codexAdapter = defineAgentClientAdapter({
72
71
  return recoverStartedLifecycleFromAdapter(...args);
73
72
  }
74
73
  },
75
- sandboxControlOperation: codexControllerOperation,
76
74
  project: {
77
75
  ownedPathPrefixes: ['.codex/'],
78
76
  managed: ['.codex/hooks.json', '.codex/agents/'],
@@ -35,7 +35,6 @@ export const INTERNAL_HANDLER_ROUTE_SELECTORS = Object.freeze({
35
35
  'sandbox-control': ['serve', 'execute', 'recover', 'client'],
36
36
  'agent-client': ['next-steps', 'model-selection', 'launch-carrier'],
37
37
  'codex-lifecycle': ['preflight', 'hook-event', 'resolve-start', 'resolve-stop', 'consume'],
38
- 'codex-sandbox-controller': ['verify-context', 'run'],
39
38
  'git-workflow': ['inspect', 'preview-tree', 'snapshot', 'compare-trees', 'commit', 'push-rebased'],
40
39
  'task-delivery': ['deliver'],
41
40
  'release-workflow': ['inspect', 'prepare', 'publish', 'post-prepare', 'post-publish'],
@@ -1,18 +1,8 @@
1
- import { recoverSandboxControl, requestSandboxControl, requestSandboxTaskFinalization, SandboxControlClientError } from '../sandbox/control/client.js';
2
- import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.js';
1
+ import { recoverSandboxControl, requestSandboxControl, SandboxControlClientError } from '../sandbox/control/client.js';
3
2
  import { serveSandboxControl } from '../sandbox/control/server.js';
4
3
  import { runSandboxControlExecutor } from '../sandbox/control/executor.js';
5
4
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
6
5
  import { parseTaskCreateResult, taskCreateExitCode } from '../task/create-service.js';
7
- import { serializeTaskFinalizationEnvelope } from '../task/finalization-envelope.js';
8
- function writeFinalizationEnvelope(status, accepted, error, changed = false, result = null, requestId = null) {
9
- process.stdout.write(serializeTaskFinalizationEnvelope({ status, changed, accepted, requestId, result, error }));
10
- }
11
- function finalizationErrorStatus(error) {
12
- if (error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN')
13
- return 'unknown';
14
- return error.retryable ? 'blocked' : 'failed';
15
- }
16
6
  function writeClientError(error) {
17
7
  process.stderr.write(`${error.detail.message}\n`);
18
8
  if (error.requestId)
@@ -31,72 +21,6 @@ function recoveredExitCode(response) {
31
21
  }
32
22
  return response.exitCode ?? 1;
33
23
  }
34
- function finalizationResponseOutput(response) {
35
- try {
36
- const value = JSON.parse(response.stdout);
37
- if (value.version === 2
38
- && value.status
39
- && ['completed', 'failed', 'blocked', 'unknown'].includes(value.status)
40
- && (typeof value.changed === 'boolean' || value.changed === null)) {
41
- return serializeTaskFinalizationEnvelope({
42
- status: value.status,
43
- changed: value.changed,
44
- accepted: true,
45
- requestId: response.id,
46
- result: value.result ?? null,
47
- error: value.error ?? null
48
- });
49
- }
50
- }
51
- catch {
52
- // Preserve non-envelope output for diagnostics from the executor.
53
- }
54
- return response.stdout;
55
- }
56
- function sandboxFinalizationClient(args) {
57
- if (args.length !== 4 || !args[0] || args[1] !== 'complete' || args[2] !== '--agent' || !args[3]) {
58
- const error = { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'task ref, complete intent, and --agent are required', retryable: false };
59
- writeFinalizationEnvelope('failed', false, error);
60
- process.stderr.write('Usage: agent-infra-internal task-finalization <N | TASK-id> complete --agent <agent>\n');
61
- process.exitCode = 1;
62
- return;
63
- }
64
- const agent = normalizeAgentToken(args[3]);
65
- if (!agent) {
66
- const error = { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: `invalid --agent: ${AGENT_USAGE_HINT}`, retryable: false };
67
- writeFinalizationEnvelope('failed', false, error);
68
- process.exitCode = 1;
69
- return;
70
- }
71
- let response;
72
- try {
73
- response = requestSandboxTaskFinalization({ agent });
74
- }
75
- catch (error) {
76
- if (!(error instanceof SandboxControlClientError))
77
- throw error;
78
- const status = finalizationErrorStatus(error.detail);
79
- writeFinalizationEnvelope(status, error.accepted, error.detail, status === 'unknown' ? null : false, null, error.requestId);
80
- writeClientError(error);
81
- process.exitCode = error.detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 1 : error.detail.retryable ? 2 : 1;
82
- return;
83
- }
84
- if (response.phase === 'rejected') {
85
- const error = response.error ?? {
86
- code: 'SANDBOX_CONTROL_REJECTED',
87
- message: 'sandbox control rejected the finalization request',
88
- retryable: false
89
- };
90
- const status = finalizationErrorStatus(error);
91
- writeFinalizationEnvelope(status, error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN', error, status === 'unknown' ? null : false, null, response.id);
92
- process.stderr.write(`${error.message}\n`);
93
- process.exitCode = error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 1 : error.retryable ? 2 : 1;
94
- return;
95
- }
96
- process.stdout.write(finalizationResponseOutput(response));
97
- process.stderr.write(response.stderr);
98
- process.exitCode = response.exitCode ?? 1;
99
- }
100
24
  async function sandboxControl(args) {
101
25
  if (!ensureInternalHandlerRoute('sandbox-control', args))
102
26
  return;
@@ -152,10 +76,6 @@ async function sandboxControl(args) {
152
76
  }
153
77
  if (internalHandlerRoute('sandbox-control', 'client', operation ?? '')) {
154
78
  const [family = '', ...commandArgs] = rest;
155
- if (family === 'task-finalization') {
156
- sandboxFinalizationClient(commandArgs);
157
- return;
158
- }
159
79
  let response;
160
80
  try {
161
81
  response = requestSandboxControl({ family, args: commandArgs });
@@ -1,9 +1,12 @@
1
+ import path from 'node:path';
1
2
  import { createDirectHostExecutionContext, dispatchTaskControlOperation, parseTaskControlOperation } from '../task/control-authority.js';
2
3
  import { applyTaskFinalization } from '../task/finalization.js';
3
4
  import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.js';
4
5
  import { resolveSandboxControlTransport } from './task-operation-registry.js';
5
- import { requestSandboxTaskFinalization, SandboxControlClientError } from '../sandbox/control/client.js';
6
+ import { quiesceSandboxControlForTaskCompletion } from '../sandbox/control/lifecycle.js';
7
+ import { loadSandboxControlLocation } from '../sandbox/config.js';
6
8
  import { serializeTaskFinalizationEnvelope } from '../task/finalization-envelope.js';
9
+ import { verifyTaskEvent } from '../task/verification.js';
7
10
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
8
11
  const USAGE = 'Usage: agent-infra-internal task-finalization <N | TASK-id> complete --agent <agent>\n';
9
12
  function envelope(status, changed, accepted, result, error, requestId = null) {
@@ -64,28 +67,34 @@ async function taskFinalization(args = []) {
64
67
  };
65
68
  const transport = resolveSandboxControlTransport(process.env, { localWorkflow: true });
66
69
  if (transport.kind === 'sandbox-local') {
67
- try {
68
- const response = requestSandboxTaskFinalization({ agent: boundOperation.request.agent });
69
- process.stdout.write(response.stdout);
70
- process.stderr.write(response.stderr);
71
- process.exitCode = response.exitCode ?? 1;
72
- return;
73
- }
74
- catch (error) {
75
- const clientError = error instanceof SandboxControlClientError ? error : null;
76
- const detail = clientError
77
- ? clientError.detail
78
- : { code: 'SANDBOX_CONTROL_CLIENT_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false };
79
- const status = detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN'
80
- ? 'unknown'
81
- : detail.retryable ? 'blocked' : 'failed';
82
- const changed = status === 'unknown' ? null : false;
83
- process.stdout.write(envelope(status, changed, clientError?.accepted ?? false, null, detail, clientError?.requestId ?? null));
84
- process.exitCode = exitCode(status);
70
+ const statusDir = process.env.AGENT_INFRA_CONTROL_STATUS_DIR;
71
+ const taskId = process.env.AGENT_INFRA_TASK_ID;
72
+ const generation = process.env.AGENT_INFRA_CONTROL_GENERATION;
73
+ const controlRootId = process.env.AGENT_INFRA_CONTROL_ROOT_ID;
74
+ const controlRoot = process.env.AGENT_INFRA_CONTROL_ROOT_DIR;
75
+ if (!statusDir || !taskId || !generation || !controlRootId || !controlRoot || taskId !== resolved.taskId) {
76
+ const detail = { code: 'TASK_CONTROL_TRANSPORT_INVALID', message: 'sandbox task control identity is incomplete', retryable: false };
77
+ process.stdout.write(envelope('failed', false, true, null, detail));
78
+ process.exitCode = 1;
85
79
  return;
86
80
  }
81
+ const result = await applyTaskFinalization(boundOperation.request, {
82
+ repoRoot,
83
+ beforeLifecycle: async (currentTaskId) => {
84
+ if (currentTaskId !== taskId)
85
+ throw new Error('SANDBOX_CONTROL_IDENTITY_MISMATCH');
86
+ await quiesceSandboxControlForTaskCompletion(controlRoot, { taskId, generation, controlRootId, statusDir });
87
+ },
88
+ preflight: (request, options) => verifyTaskEvent({ ...request, event: 'complete-task.hard-preflight' }, options)
89
+ });
90
+ const status = result.status === 'prepared' ? 'failed' : result.status;
91
+ process.stdout.write(envelope(status, result.changed, true, result, result.error
92
+ ?? (result.status === 'prepared' ? { code: 'TASK_FINALIZATION_PREPARATION_INCOMPLETE', message: 'finalization did not commit', retryable: true } : null)));
93
+ process.exitCode = exitCode(status);
94
+ return;
87
95
  }
88
- const result = await dispatchTaskControlOperation(createDirectHostExecutionContext({ repoRoot }), boundOperation);
96
+ const controlLocation = loadSandboxControlLocation(repoRoot);
97
+ const result = await dispatchTaskControlOperation(createDirectHostExecutionContext({ repoRoot, ...(controlLocation ?? {}) }), boundOperation);
89
98
  if (result.status === 'prepared') {
90
99
  const detail = {
91
100
  code: 'TASK_FINALIZATION_DIRECT_COMMIT_REQUIRED',
@@ -2,7 +2,8 @@ import { createDirectHostExecutionContext, dispatchTaskControlOperation, parseTa
2
2
  import { lifecycleIntentCatalog } from '../task/lifecycle.js';
3
3
  import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.js';
4
4
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
5
- const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.join(', ')}\nrecover-started: --auto\n`;
5
+ import { loadSandboxControlLocation } from '../sandbox/config.js';
6
+ const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.filter((intent) => intent !== 'complete').join(', ')}\nrecover-started: --auto\n`;
6
7
  function usageFailure(message) {
7
8
  process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'LIFECYCLE_PAYLOAD_INVALID', message } })}\n`);
8
9
  process.stderr.write(USAGE);
@@ -52,7 +53,8 @@ async function taskLifecycle(args = []) {
52
53
  }
53
54
  repoRoot = resolved.repoRoot;
54
55
  }
55
- const result = await dispatchTaskControlOperation(createDirectHostExecutionContext({ repoRoot }), operation);
56
+ const controlLocation = loadSandboxControlLocation(repoRoot);
57
+ const result = await dispatchTaskControlOperation(createDirectHostExecutionContext({ repoRoot, ...(controlLocation ?? {}) }), operation);
56
58
  process.stdout.write(`${JSON.stringify(result)}\n`);
57
59
  if (result.status === 'failed' || result.status === 'owner-unknown' || result.status === 'conflict')
58
60
  process.exitCode = 1;
@@ -25,8 +25,6 @@ function internalTaskRoutes() {
25
25
  descriptor('internal', 'codex-lifecycle', 'resolve-start', 'conditional', 'progress'),
26
26
  descriptor('internal', 'codex-lifecycle', 'resolve-stop', 'conditional', 'progress'),
27
27
  descriptor('internal', 'codex-lifecycle', 'consume', 'conditional', 'progress'),
28
- descriptor('internal', 'codex-sandbox-controller', 'verify-context', 'task-bound', 'diagnostic'),
29
- descriptor('internal', 'codex-sandbox-controller', 'run', 'task-bound', 'progress'),
30
28
  descriptor('internal', 'git-workflow', 'inspect', 'conditional', 'diagnostic'),
31
29
  descriptor('internal', 'git-workflow', 'preview-tree', 'conditional', 'diagnostic'),
32
30
  descriptor('internal', 'git-workflow', 'snapshot', 'conditional', 'diagnostic'),
@@ -175,7 +173,7 @@ export const TASK_OPERATION_DESCRIPTORS = Object.freeze([
175
173
  ...PUBLIC_OPERATION_DESCRIPTORS
176
174
  ]);
177
175
  export const INTERNAL_DISPATCHER_ROUTES = Object.freeze([
178
- 'task-create', 'task-qualification', 'sandbox-control', 'agent-client', 'codex-lifecycle', 'codex-sandbox-controller',
176
+ 'task-create', 'task-qualification', 'sandbox-control', 'agent-client', 'codex-lifecycle',
179
177
  'git-workflow', 'task-delivery', 'release-workflow', 'platform-release-notes', 'platform-security', 'platform-metadata', 'platform-context',
180
178
  'platform-comment', 'platform-issue', 'platform-pr', 'platform-pr-review', 'pr-review-grade',
181
179
  'platform-checks', 'task-context', 'task-ledger', 'task-warning', 'task-activity', 'task-artifact',
@@ -2,26 +2,15 @@ import { execFileSync, spawnSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
- import { randomUUID } from 'node:crypto';
6
5
  import { loadConfig } from '../sandbox/config.js';
7
- import { detectEngine } from '../sandbox/engine.js';
8
- import { runEngine } from '../sandbox/shell.js';
9
- import { sandboxBranchLabel, sandboxLabel } from '../sandbox/constants.js';
10
- import { fetchSandboxRows, selectSandboxContainer } from '../sandbox/commands/list-running.js';
11
- import { containerNameCandidates } from '../sandbox/constants.js';
12
6
  import { resolveSandboxTarget } from '../sandbox/workspace-identity.js';
13
- import { sandboxControlPaths } from '../sandbox/workspace-view.js';
14
- import { appendSandboxControlAudit, readActiveLease, readSandboxControlStatus } from '../sandbox/control/state.js';
15
- import { readSandboxControlManifest } from '../sandbox/control/lifecycle.js';
16
- import { SANDBOX_CONTROL_FUTURE_SKEW_MS, SANDBOX_CONTROL_STATUS_STALE_MS } from '../sandbox/control/protocol.js';
17
- import { getProcessStartTime } from '../server/process-state.js';
7
+ import { resolveBranchWorktree } from '../git/branch-worktree.js';
18
8
  import { assertGitWorktreeBinding } from '../git/worktree-identity.js';
19
9
  import { createManualValidationEvidence, manualValidationEvidenceSummary } from '../task/manual-validation-evidence.js';
20
10
  import { writeJsonAtomic } from '../task/manual-validation-shared.js';
21
11
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
22
12
  const USAGE = `Usage: agent-infra-internal task-validate <branch | TASK-id | N> [--scope snapshot|inplace] [--timeout <ms>] [--format text|json] [--evidence-file <path>] -- <command> [args...]`;
23
13
  const MAX_TIMEOUT_MS = 60 * 60 * 1000;
24
- const RECOVERY_GRACE_MS = 30_000;
25
14
  export function parseValidateArgs(args) {
26
15
  if (args.includes('--help') || args.includes('-h')) {
27
16
  return { target: '', scope: 'snapshot', timeoutMs: 300_000, format: 'text', evidenceFile: null, command: [], help: true };
@@ -100,21 +89,6 @@ function runValidationCommand(command, cwd, scope, timeoutMs, json) {
100
89
  function git(root, args) {
101
90
  return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
102
91
  }
103
- function waitForState(statusDir, predicate, timeoutMs = 5_000) {
104
- const deadline = Date.now() + timeoutMs;
105
- while (Date.now() < deadline) {
106
- try {
107
- const status = readSandboxControlStatus(statusDir);
108
- if (predicate(status))
109
- return;
110
- }
111
- catch {
112
- // Atomic status publication may be between renames.
113
- }
114
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
115
- }
116
- throw new Error('SANDBOX_VALIDATION_BROKER_STATE_TIMEOUT');
117
- }
118
92
  function snapshotValidation(repoRoot, commit, options) {
119
93
  const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-infra-validation-'));
120
94
  const worktree = path.join(tempRoot, 'worktree');
@@ -146,117 +120,33 @@ function snapshotValidation(repoRoot, commit, options) {
146
120
  }
147
121
  }
148
122
  function inplaceValidation(config, target, options) {
149
- const engine = detectEngine(config);
150
- const { running, nonRunning } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
151
- const row = selectSandboxContainer([...running, ...nonRunning], containerNameCandidates(config, target.branch));
152
- if (!row)
153
- throw new Error('SANDBOX_VALIDATION_CONTAINER_NOT_FOUND');
154
- const control = sandboxControlPaths({
155
- base: config.controlBase, repoRoot: config.repoRoot, project: config.project,
156
- container: row.name, identity: target.workspace
157
- });
158
- const manifest = readSandboxControlManifest(control.manifestPath);
159
- const initial = readSandboxControlStatus(control.statusDir);
160
- const now = Date.now();
161
- if (initial.generation !== manifest.generation
162
- || now - initial.updatedAt > SANDBOX_CONTROL_STATUS_STALE_MS
163
- || initial.updatedAt > now + SANDBOX_CONTROL_FUTURE_SKEW_MS) {
164
- throw new Error('SANDBOX_VALIDATION_BROKER_STATUS_STALE');
165
- }
166
- if (initial.state !== 'healthy' || initial.activeRequestId !== null) {
167
- throw new Error('SANDBOX_VALIDATION_BROKER_NOT_IDLE');
168
- }
169
- const startTime = getProcessStartTime(process.pid);
170
- if (!startTime)
171
- throw new Error('SANDBOX_VALIDATION_OWNER_IDENTITY_UNAVAILABLE');
172
- const leasePath = path.join(control.root, 'lease.json');
173
- const nonce = randomUUID();
174
- const issuedAt = Date.now();
175
- const lease = {
176
- version: 2, generation: manifest.generation, nonce,
177
- owner: { pid: process.pid, startTime }, issuedAt,
178
- expiresAt: issuedAt + Math.min(options.timeoutMs + RECOVERY_GRACE_MS, MAX_TIMEOUT_MS + RECOVERY_GRACE_MS),
179
- taskId: target.workspace.mode === 'task-bound' ? target.workspace.taskId : null,
180
- branch: target.branch, reason: 'manual-validation'
181
- };
182
- const originalBranch = git(manifest.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
183
- const originalHead = git(manifest.worktreeRoot, ['rev-parse', 'HEAD']);
184
- fs.writeFileSync(leasePath, `${JSON.stringify(lease)}\n`, { flag: 'wx', mode: 0o600 });
185
- let stopped = false;
123
+ const targetRoot = resolveBranchWorktree(config.repoRoot, target.branch);
124
+ if (!targetRoot)
125
+ throw new Error(`TASK_VALIDATION_WORKTREE_NOT_FOUND: ${target.branch}`);
126
+ const worktree = assertGitWorktreeBinding(config.repoRoot, targetRoot, target.branch);
127
+ const originalBranch = git(worktree.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
128
+ const originalHead = git(worktree.worktreeRoot, ['rev-parse', 'HEAD']);
186
129
  let restored = false;
187
130
  try {
188
- appendSandboxControlAudit(manifest, 'lease-acquire', { ownerPid: process.pid });
189
- waitForState(control.statusDir, (status) => status.state === 'parked' && status.reasonCode === 'SANDBOX_CONTROL_HANDOFF_ACTIVE');
190
- if (row.running) {
191
- runEngine(engine, 'docker', ['stop', row.name]);
192
- stopped = true;
193
- }
194
- const refreshedRows = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
195
- const refreshed = selectSandboxContainer([...refreshedRows.running, ...refreshedRows.nonRunning], containerNameCandidates(config, target.branch));
196
- if (!refreshed || refreshed.running)
197
- throw new Error('SANDBOX_VALIDATION_CONTAINER_STILL_RUNNING');
198
- const activeLease = readActiveLease(manifest);
199
- if (!activeLease || activeLease.nonce !== nonce)
200
- throw new Error('SANDBOX_VALIDATION_LEASE_OWNERSHIP_LOST');
201
- assertGitWorktreeBinding(config.repoRoot, manifest.worktreeRoot, target.branch);
202
- const result = runValidationCommand(options.command, manifest.worktreeRoot, 'inplace', options.timeoutMs, options.format === 'json');
131
+ const result = runValidationCommand(options.command, worktree.worktreeRoot, 'inplace', options.timeoutMs, options.format === 'json');
203
132
  return { ...result, cleanup: () => restored ? 'completed' : 'pending' };
204
133
  }
205
134
  finally {
206
- const recoveryErrors = [];
207
135
  try {
208
- const currentBranch = git(manifest.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
136
+ const currentBranch = git(worktree.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
209
137
  if (currentBranch !== originalBranch)
210
- git(manifest.worktreeRoot, ['switch', originalBranch]);
138
+ git(worktree.worktreeRoot, ['switch', originalBranch]);
211
139
  }
212
140
  catch {
213
141
  try {
214
- git(manifest.worktreeRoot, ['checkout', '--detach', originalHead]);
215
- git(manifest.worktreeRoot, ['switch', originalBranch]);
216
- }
217
- catch (fallbackError) {
218
- recoveryErrors.push(`git binding restore failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`);
219
- }
220
- }
221
- if (stopped) {
222
- try {
223
- runEngine(engine, 'docker', ['start', row.name]);
224
- }
225
- catch (error) {
226
- recoveryErrors.push(`container start failed: ${error instanceof Error ? error.message : String(error)}`);
227
- }
228
- }
229
- try {
230
- const currentLease = JSON.parse(fs.readFileSync(leasePath, 'utf8'));
231
- if (currentLease.nonce !== nonce)
232
- throw new Error('SANDBOX_VALIDATION_LEASE_OWNERSHIP_LOST');
233
- }
234
- catch (error) {
235
- recoveryErrors.push(error instanceof Error ? error.message : String(error));
236
- }
237
- if (recoveryErrors.length === 0) {
238
- try {
239
- fs.unlinkSync(leasePath);
240
- appendSandboxControlAudit(manifest, 'lease-release', { ownerPid: process.pid });
241
- waitForState(control.statusDir, (status) => status.state === 'healthy');
242
- restored = true;
142
+ git(worktree.worktreeRoot, ['checkout', '--detach', originalHead]);
143
+ git(worktree.worktreeRoot, ['switch', originalBranch]);
243
144
  }
244
145
  catch (error) {
245
- try {
246
- if (!fs.existsSync(leasePath)) {
247
- fs.writeFileSync(leasePath, `${JSON.stringify(lease)}\n`, { flag: 'wx', mode: 0o600 });
248
- }
249
- appendSandboxControlAudit(manifest, 'lease-release-failed', { ownerPid: process.pid });
250
- }
251
- catch (leaseError) {
252
- recoveryErrors.push(`lease preservation failed: ${leaseError instanceof Error ? leaseError.message : String(leaseError)}`);
253
- }
254
- recoveryErrors.push(error instanceof Error ? error.message : String(error));
146
+ throw new Error(`TASK_VALIDATION_WORKTREE_RESTORE_FAILED: ${error instanceof Error ? error.message : String(error)}`);
255
147
  }
256
148
  }
257
- if (recoveryErrors.length > 0) {
258
- throw new Error(`SANDBOX_VALIDATION_RECOVERY_INCOMPLETE: ${recoveryErrors.join('; ')}; run 'ai sandbox start ${target.branch}'`);
259
- }
149
+ restored = true;
260
150
  }
261
151
  }
262
152
  function errorCode(message) {