@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
@@ -51,6 +51,33 @@ function boundedFailureDetail(result: RunResult): string {
51
51
  : `${diagnostic.slice(0, ERROR_DETAIL_LIMIT)}… [truncated]`;
52
52
  }
53
53
 
54
+ function requestDiagnostics(args: string[], request: RequestOptions, attempt: number, errorMessage: string): string {
55
+ if (process.env.AGENT_INFRA_GH_DIAGNOSTICS !== '1' && !/rate.?limit/iu.test(errorMessage)) return '';
56
+
57
+ const apiIndex = args.indexOf('api');
58
+ const endpoint = apiIndex < 0
59
+ ? args.slice(0, 2).filter((arg) => !arg.startsWith('-')).join(' ')
60
+ : args.slice(apiIndex + 1).find((arg) => !arg.startsWith('-') && !arg.includes('=')) || 'unknown';
61
+ const safeEndpoint = endpoint.startsWith('http')
62
+ ? (() => {
63
+ try {
64
+ const url = new URL(endpoint);
65
+ return `${url.host}${url.pathname}`;
66
+ } catch {
67
+ return 'unknown';
68
+ }
69
+ })()
70
+ : endpoint.split('?')[0] || 'unknown';
71
+ const methodIndex = args.findIndex((arg) => arg === '-X' || arg === '--method');
72
+ const methodFromArgs = methodIndex >= 0 ? args[methodIndex + 1] : undefined;
73
+ const method = methodFromArgs || request.method || (safeEndpoint === 'graphql' ? 'POST' : 'GET');
74
+ const tokenEnvironment = ['GH_TOKEN', 'GITHUB_TOKEN', 'GH_ENTERPRISE_TOKEN', 'GITHUB_ENTERPRISE_TOKEN']
75
+ .filter((name) => Boolean(process.env[name]));
76
+ const configDirOverride = Boolean(process.env.GH_CONFIG_DIR);
77
+
78
+ return ` [gh-diagnostic endpoint=${safeEndpoint} method=${method} attempt=${attempt} tokenEnv=${tokenEnvironment.join(',') || 'none'} ghConfigDirOverride=${configDirOverride}]`;
79
+ }
80
+
54
81
  function defaultRunner(args: string[], options: RunOptions): RunResult {
55
82
  const command = process.env.AGENT_INFRA_GH_BIN || 'gh';
56
83
  let prefix: string[] = [];
@@ -168,13 +195,17 @@ function createGitHubClient(options: ClientOptions = {}): GitHubClient {
168
195
  const result = runner(args, request);
169
196
  if (result.status === 0) return { ok: true, value: result.stdout };
170
197
  const error = classifyGitHubFailure(result);
171
- if (!retryableMethod || !error.requestRetryable) return { ok: false, error: clientError(error) };
198
+ const requestDetail = requestDiagnostics(args, request, attempt + 1, error.message);
199
+ const contextualError = requestDetail
200
+ ? { ...error, message: `${error.message}${requestDetail}` }
201
+ : error;
202
+ if (!retryableMethod || !error.requestRetryable) return { ok: false, error: clientError(contextualError) };
172
203
  if (attempt >= delays.length) {
173
204
  return {
174
205
  ok: false,
175
206
  error: error.code === 'NETWORK_TRANSIENT'
176
- ? { ...clientError(error), code: 'NETWORK_RETRY_EXHAUSTED', message: `${error.message} (retry exhausted)` }
177
- : clientError(error)
207
+ ? { ...clientError(contextualError), code: 'NETWORK_RETRY_EXHAUSTED', message: `${contextualError.message} (retry exhausted)` }
208
+ : clientError(contextualError)
178
209
  };
179
210
  }
180
211
  sleep(delays[attempt]!);
@@ -516,7 +516,8 @@ async function syncPlatformIssueImpl(taskRef: string, options: SyncOptions): Pro
516
516
  let providerMetadata: RepositoryMetadataSnapshot | null = null;
517
517
  let repositoryLabels: string[] = [];
518
518
  let milestones: string[] = [];
519
- if (base.context.capabilities.triage && (options.status !== undefined || options.inLabels !== undefined || options.milestone !== undefined || options.issueType || options.fields)) {
519
+ if (base.context.capabilities.triage && (options.status !== undefined || options.inLabels !== undefined || options.milestone !== undefined
520
+ || (base.context.capabilities.push && (options.issueType || options.fields)))) {
520
521
  const metadata = base.provider.issues?.describeRepository
521
522
  ? await base.provider.issues.describeRepository({ context: providerOperationContext(base.loadedContext) })
522
523
  : unsupportedProviderOperation(base.provider, 'issues.describeRepository');
@@ -592,15 +593,26 @@ async function syncPlatformIssueImpl(taskRef: string, options: SyncOptions): Pro
592
593
  }
593
594
  }
594
595
  if (options.issueType || options.fields) {
595
- plan.operations.push(...(providerMetadata
596
- ? planProviderMetadata(inspected.value, providerMetadata, base.frontmatter, options, base.context.capabilities)
597
- : [{ name: 'issue-type', status: 'skipped' as const, reasonCode: 'ISSUE_TYPES_UNSUPPORTED' }]));
596
+ plan.operations.push(...(!base.context.capabilities.push
597
+ ? [
598
+ ...(options.issueType ? [{ name: 'issue-type', status: 'skipped' as const, reasonCode: 'PUSH_REQUIRED' }] : []),
599
+ ...(options.fields ? [{ name: 'fields', status: 'skipped' as const, reasonCode: 'PUSH_REQUIRED' }] : [])
600
+ ]
601
+ : providerMetadata
602
+ ? planProviderMetadata(inspected.value, providerMetadata, base.frontmatter, options, base.context.capabilities)
603
+ : [{ name: 'issue-type', status: 'skipped' as const, reasonCode: 'ISSUE_TYPES_UNSUPPORTED' }]));
598
604
  }
599
605
  const failure = plan.operations.find((operation) => operation.status === 'failed');
600
606
  if (failure) return result('failed', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: { code: failure.reasonCode || 'ISSUE_SYNC_FAILED', message: `Operation ${failure.name} failed`, retryable: false } });
601
607
  const planned = plan.operations.filter((operation) => operation.status === 'planned');
602
- if (options.dryRun) return result(planned.length ? 'planned' : 'no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
603
- if (planned.length === 0) return result('no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
608
+ if (options.dryRun) return result(planned.length ? 'planned' : 'no-op', base.resolved.taskId, base.issueNumber, {
609
+ platform: base.context.platform, capabilities: base.context.capabilities,
610
+ issue: snapshot, operations: plan.operations, error: null
611
+ });
612
+ if (planned.length === 0) return result('no-op', base.resolved.taskId, base.issueNumber, {
613
+ platform: base.context.platform, capabilities: base.context.capabilities,
614
+ issue: snapshot, operations: plan.operations, error: null
615
+ });
604
616
  const payload = applyRestOperations(snapshot, planned) as Partial<{
605
617
  title: string; body: string; labels: string[]; assignees: string[];
606
618
  milestone: string | null; state: 'open' | 'closed'; fields: Record<string, string | number | null>;
@@ -1,16 +1,5 @@
1
1
  import path from 'node:path';
2
-
3
- export type AgentRuntimeStore = 'capabilities' | 'lifecycle';
4
-
5
- type RuntimeResolutionOptions = Readonly<{
6
- repoRoot?: string;
7
- env?: NodeJS.ProcessEnv;
8
- explicitRoot?: string;
9
- client?: string;
10
- store?: AgentRuntimeStore;
11
- }>;
12
-
13
- const CLIENT_NAMESPACE = /^[a-z][a-z0-9-]{0,31}$/u;
2
+ import { resolveTaskRuntimeRoot } from '../task/runtime-paths.ts';
14
3
 
15
4
  function runtimeError(code: string, message: string): Error {
16
5
  const error = new Error(`${code}: ${message}`);
@@ -22,18 +11,10 @@ function boundControlContext(env: NodeJS.ProcessEnv): boolean {
22
11
  return [
23
12
  'AGENT_INFRA_CONTROL_TOKEN',
24
13
  'AGENT_INFRA_CONTROL_GENERATION',
25
- 'AGENT_INFRA_EXECUTOR_MANIFEST',
26
- 'AGENT_INFRA_CODEX_CONTROLLER_CONTEXT'
14
+ 'AGENT_INFRA_EXECUTOR_MANIFEST'
27
15
  ].some((key) => typeof env[key] === 'string' && env[key]!.length > 0);
28
16
  }
29
17
 
30
- function clientNamespace(client: string): string {
31
- if (!CLIENT_NAMESPACE.test(client)) {
32
- throw runtimeError('AGENT_INFRA_RUNTIME_CLIENT_INVALID', `invalid runtime client namespace '${client}'`);
33
- }
34
- return client;
35
- }
36
-
37
18
  export function resolveAgentRuntimeRoot(options: Readonly<{
38
19
  repoRoot?: string;
39
20
  env?: NodeJS.ProcessEnv;
@@ -46,6 +27,10 @@ export function resolveAgentRuntimeRoot(options: Readonly<{
46
27
  }
47
28
  return path.resolve(configured);
48
29
  }
30
+ const taskId = env.AGENT_INFRA_TASK_ID;
31
+ if (taskId) {
32
+ return resolveTaskRuntimeRoot(taskId, { repoRoot: options.repoRoot });
33
+ }
49
34
  if (boundControlContext(env)) {
50
35
  throw runtimeError(
51
36
  'AGENT_INFRA_RUNTIME_DIR_REQUIRED',
@@ -54,23 +39,3 @@ export function resolveAgentRuntimeRoot(options: Readonly<{
54
39
  }
55
40
  return path.join(path.resolve(options.repoRoot ?? process.cwd()), '.agents', 'workspace', '.runtime');
56
41
  }
57
-
58
- export function resolveAgentRuntimeStoreRoot(options: RuntimeResolutionOptions = {}): string {
59
- const client = clientNamespace(options.client ?? 'codex');
60
- if (options.explicitRoot) return path.resolve(options.explicitRoot);
61
- const env = options.env ?? process.env;
62
- const runtimeRoot = resolveAgentRuntimeRoot({ repoRoot: options.repoRoot, env });
63
- if (env.AGENT_INFRA_RUNTIME_DIR) {
64
- if (!options.store) {
65
- throw runtimeError('AGENT_INFRA_RUNTIME_STORE_INVALID', 'store kind is required for task-bound runtime');
66
- }
67
- return path.join(runtimeRoot, 'clients', client, options.store);
68
- }
69
- if (client === 'codex' && options.store) {
70
- return path.join(runtimeRoot, `codex-${options.store}`);
71
- }
72
- if (!options.store) {
73
- throw runtimeError('AGENT_INFRA_RUNTIME_STORE_INVALID', 'store kind is required');
74
- }
75
- return path.join(runtimeRoot, 'clients', client, options.store);
76
- }
@@ -14,6 +14,7 @@ export function readSandboxControlStatusForRow(
14
14
  : { mode: 'branch-only' as const };
15
15
  const control = sandboxControlPaths({
16
16
  base: config.controlBase,
17
+ repoRoot: config.repoRoot,
17
18
  project: config.project,
18
19
  container: row.name,
19
20
  identity
@@ -91,6 +91,7 @@ import {
91
91
  materializeSandboxControl,
92
92
  materializeSandboxWorkspaceView,
93
93
  prepareSandboxWorkspaceMountTargets,
94
+ prepareSandboxCompletedTaskMountSource,
94
95
  sandboxControlPaths,
95
96
  } from '../workspace-view.ts';
96
97
  import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.ts';
@@ -1107,6 +1108,7 @@ export async function create(
1107
1108
  const container = containerName(effectiveConfig, branch);
1108
1109
  const controlPaths = sandboxControlPaths({
1109
1110
  base: effectiveConfig.controlBase,
1111
+ repoRoot: effectiveConfig.repoRoot,
1110
1112
  project: effectiveConfig.project,
1111
1113
  container,
1112
1114
  identity: target.workspace
@@ -1450,7 +1452,10 @@ export async function create(
1450
1452
  const taskSource = target.workspace.mode === 'task-bound'
1451
1453
  ? assertSandboxTaskSource(effectiveConfig.repoRoot, target.workspace.taskId)
1452
1454
  : null;
1453
- prepareSandboxWorkspaceMountTargets(worktree);
1455
+ const completedTaskSource = target.workspace.mode === 'task-bound'
1456
+ ? prepareSandboxCompletedTaskMountSource(effectiveConfig.repoRoot, target.workspace.taskId)
1457
+ : null;
1458
+ prepareSandboxWorkspaceMountTargets(worktree, target.workspace.mode === 'task-bound' ? target.workspace.taskId : undefined);
1454
1459
  if (previousCutoverSnapshot) {
1455
1460
  assertSandboxControlCutoverSnapshot(previousCutoverSnapshot);
1456
1461
  if (previousManifest) {
@@ -1493,6 +1498,7 @@ export async function create(
1493
1498
  ...(target.workspace.mode === 'task-bound'
1494
1499
  ? {
1495
1500
  taskSources: [taskSource!],
1501
+ completedTaskSource: completedTaskSource!,
1496
1502
  taskId: target.workspace.taskId
1497
1503
  }
1498
1504
  : {})
@@ -18,11 +18,10 @@ export { containerListFormat, parseLabels } from './list-running.ts';
18
18
  const USAGE = `Usage: ai sandbox ls
19
19
 
20
20
  Lists all containers for the current project. The '#' column is a
21
- display-only row number; the 'SHORT' column shows the active task short
22
- id bound to each container's branch (via
23
- .agents/workspace/active/.short-ids.json), or '-' if no active task is
24
- bound. Pass the SHORT value to "ai sandbox exec" (e.g. 'ai sandbox exec 11').
25
- A SHORT value of '-' means this container has no active task short id and may
21
+ display-only row number; the 'SHORT' column shows the task short id bound to
22
+ each task-bound container, or '-' if no short id is registered. Pass the
23
+ SHORT value to "ai sandbox exec" (e.g. 'ai sandbox exec 11').
24
+ A SHORT value of '-' means this container has no task short id and may
26
25
  be eligible for "ai sandbox rm --unbound" cleanup. Use a full TASK-id to
27
26
  remove a specific task-bound sandbox.
28
27
 
@@ -13,7 +13,7 @@ import { fetchSandboxRows } from './list-running.ts';
13
13
  import { rmOne, rmPurge, rmUnbound } from '../removal.ts';
14
14
 
15
15
  const USAGE = `Usage:
16
- 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
16
+ 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
17
17
  ai sandbox rm --unbound [--dry-run] [--yes] Remove sandboxes not bound to an active task short id
18
18
  ai sandbox rm --purge Tear down ALL sandboxes for the project (containers, worktrees, image, VM)`;
19
19
 
@@ -0,0 +1,49 @@
1
+ import { getAgentClientAdapter } from '../../agent-clients/registry.ts';
2
+ import type { AgentClientSandboxControlOperation } from '../../agent-clients/adapter.ts';
3
+ import type { SandboxControlManifest, SandboxAgentClientRequest } from './protocol.ts';
4
+
5
+ function operationFor(request: SandboxAgentClientRequest) {
6
+ const adapter = getAgentClientAdapter(request.agentClient);
7
+ const operation = adapter.sandboxControlOperation;
8
+ if (!operation) throw new Error('SANDBOX_CONTROL_AGENT_OPERATION_UNSUPPORTED');
9
+ return operation;
10
+ }
11
+
12
+ function validateAgentClientOperation(request: SandboxAgentClientRequest, manifest: SandboxControlManifest): void {
13
+ operationFor(request).validate({
14
+ operation: request.operation,
15
+ payload: request.payload,
16
+ manifest
17
+ });
18
+ }
19
+
20
+ function executeAgentClientOperation(
21
+ request: SandboxAgentClientRequest,
22
+ manifest: SandboxControlManifest,
23
+ manifestPath: string,
24
+ override?: AgentClientSandboxControlOperation
25
+ ) {
26
+ return (override ?? operationFor(request)).execute({
27
+ operation: request.operation,
28
+ payload: request.payload,
29
+ manifest,
30
+ manifestPath
31
+ });
32
+ }
33
+
34
+ function recoverAgentClientOperation(params: Readonly<{
35
+ request: SandboxAgentClientRequest;
36
+ manifest: SandboxControlManifest;
37
+ manifestPath: string;
38
+ stdout: string | null;
39
+ }>): Readonly<Record<string, unknown>> {
40
+ const operation = operationFor(params.request);
41
+ return operation.recover?.({
42
+ operation: params.request.operation,
43
+ manifest: params.manifest,
44
+ manifestPath: params.manifestPath,
45
+ stdout: params.stdout
46
+ }) ?? { consistent: false };
47
+ }
48
+
49
+ export { executeAgentClientOperation, recoverAgentClientOperation, validateAgentClientOperation };
@@ -9,16 +9,10 @@ import {
9
9
  type SandboxControlError,
10
10
  type SandboxControlRequest,
11
11
  type SandboxControlResponse,
12
- type SandboxTaskCreateRequest,
13
- type SandboxTaskFinalizationRequest,
14
- type SandboxCodexControllerRequest
12
+ type SandboxAgentClientRequest,
13
+ type SandboxTaskCreateRequest
15
14
  } from './protocol.ts';
16
- import type {
17
- CodexControllerLeaseProofV1,
18
- CodexControllerOpened
19
- } from './controller-registration.ts';
20
- import type { ProcessIdentity } from '../../server/process-state.ts';
21
- import { normalizeAgentToken } from '../../agent-clients/tokens.ts';
15
+ import { isAgentClientId } from '../../agent-clients/types.ts';
22
16
  import { readSandboxControlPayload, readSandboxControlStatus } from './state.ts';
23
17
  import type { TaskCreateCandidateV1 } from '../../task/create.ts';
24
18
  import { accessSandboxTaskView, taskViewFromStatus, type TaskViewAccessEffect } from './task-view.ts';
@@ -29,7 +23,6 @@ import { loadConfig } from '../config.ts';
29
23
  import { recoverSandboxControlFromHost } from './host-recovery.ts';
30
24
 
31
25
  const SANDBOX_CONTROL_RESPONSE_SETTLE_MS = 250;
32
- const SANDBOX_TASK_FINALIZATION_RECOVERY_BUDGET_MS = 5 * 60_000;
33
26
 
34
27
  export class SandboxControlClientError extends Error {
35
28
  readonly detail: SandboxControlError;
@@ -118,7 +111,6 @@ function preflight(
118
111
 
119
112
  function taskViewEffectForRequest(request: SandboxControlRequest): TaskViewAccessEffect | null {
120
113
  if (request.family === 'task-lifecycle') return 'progress';
121
- if (request.family === 'task-finalization') return 'recovery';
122
114
  return null;
123
115
  }
124
116
 
@@ -220,9 +212,6 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
220
212
  || (requestTask.kind === 'task' && requestTask.taskId !== identity.taskId))) {
221
213
  clientError('SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH', 'request task does not match the sandbox identity', false);
222
214
  }
223
- if (identity.mode === 'branch-only' && request.family === 'task-finalization') {
224
- clientError('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot finalize tasks', false);
225
- }
226
215
  }
227
216
  const encoded = `${JSON.stringify(request)}\n`;
228
217
  if (Buffer.byteLength(encoded, 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
@@ -425,270 +414,49 @@ export function requestSandboxControl(params: Readonly<{
425
414
  token?: string; generation?: string; timeoutMs?: number;
426
415
  }>): SandboxControlResponse {
427
416
  if (!isSandboxControlFamily(params.family)) clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' is not allowed`, false);
428
- if (params.family === 'task-create' || params.family === 'codex-controller' || params.family === 'task-finalization') {
417
+ if (params.family === 'task-create' || params.family === 'agent-client') {
429
418
  clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' requires a typed request`, false);
430
419
  }
420
+ if (params.args[1] === 'complete') {
421
+ clientError('SANDBOX_CONTROL_COMMAND_DENIED', 'complete must use task-finalization', false);
422
+ }
431
423
  const auth = authority(params);
432
424
  const issuedAt = Date.now();
433
425
  const request: SandboxControlRequest = {
434
426
  version: 4, id: randomUUID(), ...auth, issuedAt,
435
427
  expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
436
- family: params.family as 'task-lifecycle', args: params.args,
437
- controllerProcess: null,
438
- controllerProof: null
428
+ family: params.family as 'task-lifecycle', args: params.args
439
429
  };
440
430
  return exchangeSandboxControl(request, params);
441
431
  }
442
432
 
443
- export function requestSandboxTaskFinalization(params: Readonly<{
444
- agent: string;
433
+ export function requestSandboxAgentOperation(params: Readonly<{
434
+ agentClient: string;
435
+ operation: string;
436
+ payload: Record<string, unknown>;
445
437
  channelDir?: string;
446
438
  statusDir?: string;
447
439
  token?: string;
448
440
  generation?: string;
449
441
  timeoutMs?: number;
450
- recoveryBudgetMs?: number;
451
442
  }>): SandboxControlResponse {
452
- const agent = normalizeAgentToken(params.agent);
453
- if (!agent) clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization agent is invalid', false);
454
- const auth = authority(params);
455
- const issuedAt = Date.now();
456
- const id = randomUUID();
457
- const request: SandboxTaskFinalizationRequest = {
458
- version: 4,
459
- id,
460
- ...auth,
461
- issuedAt,
462
- expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
463
- family: 'task-finalization',
464
- operation: 'complete',
465
- agent,
466
- args: [],
467
- controllerProcess: null,
468
- controllerProof: null
469
- };
470
- let response: SandboxControlResponse;
471
- try {
472
- response = exchangeSandboxControl(request, params);
473
- } catch (error) {
474
- if (!(error instanceof SandboxControlClientError)
475
- || error.detail.code !== 'SANDBOX_CONTROL_RESULT_UNKNOWN'
476
- || !error.accepted || !error.requestId) throw error;
477
- return recoverAcceptedTaskFinalization(error.requestId, params);
478
- }
479
- if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') {
480
- return recoverAcceptedTaskFinalization(response.id, params);
481
- }
482
- return response;
483
- }
484
-
485
- export function recoverAcceptedTaskFinalization(
486
- requestId: string,
487
- params: Readonly<{ recoveryBudgetMs?: number }>,
488
- recover: typeof recoverSandboxControl = recoverSandboxControl
489
- ): SandboxControlResponse {
490
- const deadline = Date.now() + (params.recoveryBudgetMs ?? SANDBOX_TASK_FINALIZATION_RECOVERY_BUDGET_MS);
491
- while (Date.now() < deadline) {
492
- const timeoutMs = Math.min(30_000, deadline - Date.now());
493
- if (timeoutMs <= 0) break;
494
- try {
495
- const response = recover(requestId, { timeoutMs });
496
- if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') continue;
497
- return response;
498
- } catch (error) {
499
- if (error instanceof SandboxControlClientError
500
- && error.detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN'
501
- && error.requestId === requestId) continue;
502
- throw error;
503
- }
443
+ if (!isAgentClientId(params.agentClient)) {
444
+ clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'agent-client id is invalid', false);
504
445
  }
505
- clientError(
506
- 'SANDBOX_CONTROL_RESULT_UNKNOWN',
507
- 'accepted finalization request remained unknown after the automatic recovery budget; inspect the receipt and resume by request id',
508
- false,
509
- true,
510
- requestId
511
- );
512
- }
513
-
514
- type CodexControllerClosed = Readonly<{
515
- version: 1;
516
- status: 'closed';
517
- changed: boolean;
518
- lease: null;
519
- error: null;
520
- }>;
521
-
522
- type CodexControllerVerified = Readonly<{
523
- version: 1;
524
- status: 'verified';
525
- changed: false;
526
- lease: null;
527
- binding: Readonly<{
528
- taskId: string;
529
- controlGeneration: string;
530
- controllerInstanceDigest: string;
531
- }>;
532
- error: null;
533
- }>;
534
-
535
- function exactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
536
- return Object.keys(value).sort().join(',') === [...keys].sort().join(',');
537
- }
538
-
539
- function validProcess(value: unknown): value is ProcessIdentity {
540
- if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
541
- const identity = value as Record<string, unknown>;
542
- return exactKeys(identity, ['pid', 'startTime'])
543
- && Number.isSafeInteger(identity.pid) && (identity.pid as number) > 0
544
- && Number.isSafeInteger(identity.startTime) && (identity.startTime as number) >= 0;
545
- }
546
-
547
- function validBuild(value: unknown): boolean {
548
- if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
549
- const build = value as Record<string, unknown>;
550
- return exactKeys(build, ['internalExecutableBuildHash', 'lifecycleContractHash', 'packageVersion', 'protocolVersion'])
551
- && build.protocolVersion === 3
552
- && typeof build.packageVersion === 'string' && build.packageVersion.length > 0
553
- && typeof build.internalExecutableBuildHash === 'string' && /^[a-f0-9]{64}$/u.test(build.internalExecutableBuildHash)
554
- && typeof build.lifecycleContractHash === 'string' && /^[a-f0-9]{64}$/u.test(build.lifecycleContractHash);
555
- }
556
-
557
- export function parseCodexControllerResult(response: SandboxControlResponse): CodexControllerOpened | CodexControllerClosed | CodexControllerVerified {
558
- if (response.phase !== 'completed' || response.error !== null || response.stderr !== '') {
559
- clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller result outer response is invalid', false, true);
560
- }
561
- let value: unknown;
562
- try {
563
- if (!response.stdout.endsWith('\n') || response.stdout.slice(0, -1).includes('\n')) throw new Error('not canonical');
564
- value = JSON.parse(response.stdout);
565
- } catch {
566
- clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller result payload is invalid', false, true);
567
- }
568
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
569
- clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller result payload is invalid', false, true);
570
- }
571
- const result = value as Record<string, unknown>;
572
- const resultKeys = result.status === 'verified'
573
- ? ['binding', 'changed', 'error', 'lease', 'status', 'version']
574
- : ['changed', 'error', 'lease', 'status', 'version'];
575
- if (!exactKeys(result, resultKeys)
576
- || result.version !== 1 || !['opened', 'closed', 'failed', 'verified'].includes(result.status as string)) {
577
- clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller result schema is invalid', false, true);
578
- }
579
- if (result.status === 'failed') {
580
- const error = result.error as { code?: unknown; message?: unknown; retryable?: unknown } | null;
581
- if (response.exitCode !== 1 || result.changed !== false || result.lease !== null
582
- || !error || !exactKeys(error as Record<string, unknown>, ['code', 'message', 'retryable'])
583
- || typeof error.code !== 'string' || !/^[A-Z][A-Z0-9_]+$/u.test(error.code)
584
- || typeof error.message !== 'string' || error.retryable !== false) {
585
- clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller failure result is invalid', false, true);
586
- }
587
- clientError(error.code, error.message, false, true);
588
- }
589
- if (response.exitCode !== 0 || result.error !== null) {
590
- clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller success result is invalid', false, true);
591
- }
592
- if (result.status === 'closed') {
593
- if (typeof result.changed !== 'boolean' || result.lease !== null) {
594
- clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller close result is invalid', false, true);
595
- }
596
- return result as unknown as CodexControllerClosed;
597
- }
598
- if (result.status === 'verified') {
599
- const binding = result.binding as Record<string, unknown> | null;
600
- if (result.changed !== false || result.lease !== null || !binding
601
- || !exactKeys(binding, ['controllerInstanceDigest', 'controlGeneration', 'taskId'])
602
- || typeof binding.taskId !== 'string' || binding.taskId.length === 0
603
- || typeof binding.controlGeneration !== 'string' || binding.controlGeneration.length === 0
604
- || typeof binding.controllerInstanceDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(binding.controllerInstanceDigest)) {
605
- clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller verify result is invalid', false, true);
606
- }
607
- return result as unknown as CodexControllerVerified;
608
- }
609
- const lease = result.lease as Record<string, unknown> | null;
610
- if (result.changed !== true || !lease
611
- || !exactKeys(lease, [
612
- 'buildIdentity', 'controlGeneration', 'controllerInstanceDigest', 'controllerProcess',
613
- 'expiresAt', 'issuedAt', 'leaseId', 'leaseSecret', 'taskId', 'version'
614
- ])
615
- || lease.version !== 1
616
- || typeof lease.leaseId !== 'string' || !/^[a-f0-9]{64}$/u.test(lease.leaseId)
617
- || typeof lease.leaseSecret !== 'string' || !/^[a-f0-9]{64}$/u.test(lease.leaseSecret)
618
- || typeof lease.taskId !== 'string' || lease.taskId.length === 0
619
- || typeof lease.controlGeneration !== 'string' || lease.controlGeneration.length === 0
620
- || typeof lease.controllerInstanceDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(lease.controllerInstanceDigest)
621
- || !validProcess(lease.controllerProcess) || !validBuild(lease.buildIdentity)
622
- || !Number.isSafeInteger(lease.issuedAt) || !Number.isSafeInteger(lease.expiresAt)
623
- || (lease.expiresAt as number) <= (lease.issuedAt as number)) {
624
- clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller open result is invalid', false, true);
625
- }
626
- return result as unknown as CodexControllerOpened | CodexControllerClosed;
627
- }
628
-
629
- function requestCodexController(params: Readonly<{
630
- command: 'open' | 'close' | 'verify';
631
- controllerProcess: ProcessIdentity;
632
- controllerProof: CodexControllerLeaseProofV1 | null;
633
- channelDir?: string;
634
- statusDir?: string;
635
- token?: string;
636
- generation?: string;
637
- timeoutMs?: number;
638
- }>): CodexControllerOpened | CodexControllerClosed | CodexControllerVerified {
639
446
  const auth = authority(params);
640
447
  const issuedAt = Date.now();
641
- const request: SandboxCodexControllerRequest = {
448
+ const request: SandboxAgentClientRequest = {
642
449
  version: 4,
643
450
  id: randomUUID(),
644
451
  ...auth,
645
452
  issuedAt,
646
453
  expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
647
- family: 'codex-controller',
648
- command: params.command,
649
- args: [],
650
- controllerProcess: params.controllerProcess,
651
- controllerProof: params.controllerProof
454
+ family: 'agent-client',
455
+ agentClient: params.agentClient,
456
+ operation: params.operation,
457
+ payload: params.payload
652
458
  };
653
- const result = parseCodexControllerResult(exchangeSandboxControl(request, params));
654
- if (result.status === 'opened'
655
- && (result.lease.controlGeneration !== auth.generation
656
- || result.lease.controllerProcess.pid !== params.controllerProcess.pid
657
- || result.lease.controllerProcess.startTime !== params.controllerProcess.startTime)) {
658
- clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller result does not match the request', false, true);
659
- }
660
- return result;
661
- }
662
-
663
- export function requestCodexControllerOpen(params: Omit<Parameters<typeof requestCodexController>[0], 'command' | 'controllerProof'>): CodexControllerOpened {
664
- const result = requestCodexController({ ...params, command: 'open', controllerProof: null });
665
- if (result.status !== 'opened') clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller open returned the wrong result', false, true);
666
- return result;
667
- }
668
-
669
- export function requestCodexControllerClose(params: Omit<Parameters<typeof requestCodexController>[0], 'command'>): CodexControllerClosed {
670
- const result = requestCodexController({ ...params, command: 'close' });
671
- if (result.status !== 'closed') clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller close returned the wrong result', false, true);
672
- return result;
673
- }
674
-
675
- export function requestCodexControllerVerify(params: Readonly<{
676
- controllerProof: CodexControllerLeaseProofV1;
677
- channelDir?: string;
678
- statusDir?: string;
679
- token?: string;
680
- generation?: string;
681
- timeoutMs?: number;
682
- }>): CodexControllerVerified {
683
- const result = requestCodexController({
684
- ...params,
685
- controllerProcess: params.controllerProof.controllerProcess,
686
- command: 'verify'
687
- });
688
- if (result.status !== 'verified') {
689
- clientError('SANDBOX_CONTROL_RESULT_INVALID', 'controller verify returned the wrong result', false, true);
690
- }
691
- return result;
459
+ return exchangeSandboxControl(request, params);
692
460
  }
693
461
 
694
462
  export function requestSandboxTaskCreate(params: Readonly<{
@@ -701,8 +469,6 @@ export function requestSandboxTaskCreate(params: Readonly<{
701
469
  version: 4, id: randomUUID(), ...auth, issuedAt,
702
470
  expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
703
471
  family: 'task-create', candidate: params.candidate,
704
- controllerProcess: null,
705
- controllerProof: null
706
472
  };
707
473
  return exchangeSandboxControl(request, { ...params, timeoutMs: params.timeoutMs ?? 120_000 });
708
474
  }