@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
@@ -9,7 +9,7 @@ import { createSandboxCapabilityPlan } from './agent-client-reconciler.js';
9
9
  import { toEnginePath } from './engines/wsl2-paths.js';
10
10
  import { sandboxCoreBindMounts } from './mounts.js';
11
11
  import { resolveSandboxGitMetadata } from './git-metadata.js';
12
- import { assertSandboxTaskSource, sandboxControlPaths, sandboxWorkspaceViewPaths } from './workspace-view.js';
12
+ import { assertSandboxTaskSource, sandboxCompletedTaskMountSource, sandboxControlPaths, sandboxWorkspaceViewPaths } from './workspace-view.js';
13
13
  import { resolveTaskWorkspace } from './task-resolver.js';
14
14
  import { parseSandboxWorkspaceIdentity, sameSandboxWorkspaceIdentity } from './workspace-identity.js';
15
15
  import { inspectWorktree } from './worktree-safety.js';
@@ -174,6 +174,7 @@ export async function startSandboxControlBroker(repoRoot, manifestPath) {
174
174
  async function ensureSandboxControlBroker(params) {
175
175
  const control = sandboxControlPaths({
176
176
  base: params.config.controlBase ?? path.join(params.config.home, '.agent-infra', 'sandbox-control'),
177
+ repoRoot: params.config.repoRoot,
177
178
  project: params.config.project,
178
179
  container: params.container,
179
180
  identity: params.workspace
@@ -459,25 +460,38 @@ export function startCompletedSandboxContainer(params) {
459
460
  const activeRoot = path.join(params.config.repoRoot, '.agents', 'workspace', 'active');
460
461
  const historicalSource = path.join(activeRoot, params.taskId);
461
462
  fs.mkdirSync(activeRoot, { recursive: true });
463
+ let replacedEmptyRoot = false;
464
+ let emptyRootMode = 0o700;
462
465
  try {
463
- fs.lstatSync(historicalSource);
464
- throw new Error(`SANDBOX_COMPLETED_SOURCE_CONFLICT: historical task source already exists at ${historicalSource}`);
466
+ const existing = fs.lstatSync(historicalSource);
467
+ if (existing.isSymbolicLink() || !existing.isDirectory() || fs.readdirSync(historicalSource).length > 0) {
468
+ throw new Error(`SANDBOX_COMPLETED_SOURCE_CONFLICT: historical task source already exists at ${historicalSource}`);
469
+ }
470
+ emptyRootMode = existing.mode & 0o777;
471
+ fs.rmdirSync(historicalSource);
472
+ replacedEmptyRoot = true;
465
473
  }
466
474
  catch (error) {
467
475
  if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
468
476
  throw error;
469
477
  }
470
- fs.symlinkSync(completedSource, historicalSource, process.platform === 'win32' ? 'junction' : 'dir');
478
+ let temporaryLink = false;
471
479
  try {
480
+ fs.symlinkSync(completedSource, historicalSource, process.platform === 'win32' ? 'junction' : 'dir');
481
+ temporaryLink = true;
472
482
  params.start(params.engine, params.container);
473
483
  }
474
484
  finally {
475
- const temporarySource = fs.lstatSync(historicalSource);
476
- const resolvedSource = fs.realpathSync.native(historicalSource);
477
- if (!temporarySource.isSymbolicLink() || resolvedSource !== completedSource) {
478
- throw new Error(`SANDBOX_COMPLETED_SOURCE_CHANGED: refusing to remove unexpected path at ${historicalSource}`);
485
+ if (temporaryLink) {
486
+ const temporarySource = fs.lstatSync(historicalSource);
487
+ const resolvedSource = fs.realpathSync.native(historicalSource);
488
+ if (!temporarySource.isSymbolicLink() || resolvedSource !== completedSource) {
489
+ throw new Error(`SANDBOX_COMPLETED_SOURCE_CHANGED: refusing to remove unexpected path at ${historicalSource}`);
490
+ }
491
+ fs.unlinkSync(historicalSource);
479
492
  }
480
- fs.unlinkSync(historicalSource);
493
+ if (replacedEmptyRoot)
494
+ fs.mkdirSync(historicalSource, { mode: emptyRootMode });
481
495
  }
482
496
  }
483
497
  function sameHostSource(left, right) {
@@ -509,6 +523,7 @@ function expectedMounts(params) {
509
523
  });
510
524
  const control = sandboxControlPaths({
511
525
  base: config.controlBase ?? path.join(config.home, '.agent-infra', 'sandbox-control'),
526
+ repoRoot: config.repoRoot,
512
527
  project: config.project,
513
528
  container: params.container,
514
529
  identity: params.workspace
@@ -517,6 +532,7 @@ function expectedMounts(params) {
517
532
  ? recoveryTaskSources(config.repoRoot, params.workspace.taskId)
518
533
  : null;
519
534
  const taskId = params.workspace.mode === 'task-bound' ? params.workspace.taskId : null;
535
+ const completedTaskSource = taskId ? sandboxCompletedTaskMountSource(config.repoRoot, taskId) : null;
520
536
  const worktrees = worktreeDirCandidates(config, branch).filter((candidate) => fs.existsSync(path.join(candidate, '.git')));
521
537
  const gitMetadata = worktrees.length === 1
522
538
  ? resolveSandboxGitMetadata(worktrees[0], control.channelDir)
@@ -536,6 +552,7 @@ function expectedMounts(params) {
536
552
  : {
537
553
  runtimeDir: control.runtimeDir,
538
554
  taskSources: taskSources.accessiblePaths,
555
+ completedTaskSource: completedTaskSource,
539
556
  taskId: taskId
540
557
  })
541
558
  }).map((mount) => ({
@@ -546,6 +563,10 @@ function expectedMounts(params) {
546
563
  && mount.containerPath === `/workspace/.agents/workspace/active/${taskId}`
547
564
  ? { sourceAccessiblePaths: taskSources.accessiblePaths }
548
565
  : {}),
566
+ ...(completedTaskSource !== null && taskId !== null
567
+ && mount.containerPath === `/workspace/.agents/workspace/completed/${taskId}`
568
+ ? { sourceAccessiblePaths: [completedTaskSource] }
569
+ : {}),
549
570
  expectedRW: !mount.readOnly
550
571
  }));
551
572
  const live = tools.flatMap((tool) => (tool.hostLiveMounts ?? []).flatMap(({ hostPath, containerSubpath }) => {
@@ -905,7 +926,7 @@ export async function ensureSandboxReady(params) {
905
926
  let completedReentry = null;
906
927
  if (params.reentry === 'completed' && params.workspace?.mode === 'task-bound') {
907
928
  const control = sandboxControlPaths({
908
- base: params.config.controlBase, project: params.config.project,
929
+ base: params.config.controlBase, repoRoot: params.config.repoRoot, project: params.config.project,
909
930
  container: params.row.name, identity: params.workspace
910
931
  });
911
932
  const manifest = readSandboxControlManifest(control.manifestPath);
@@ -568,7 +568,7 @@ function resolveRmTarget(config, tools, cleanupTarget, options = {}) {
568
568
  ? matchedContainers
569
569
  : [...new Set([...containerNameCandidates(config, effectiveBranch), ...matchedContainers])];
570
570
  const controlRoots = containers.flatMap((container) => identities.map((identity) => sandboxControlPaths({
571
- base: config.controlBase, project: config.project, container, identity
571
+ base: config.controlBase, repoRoot: config.repoRoot, project: config.project, container, identity
572
572
  }).root));
573
573
  const workspaceViewRoots = containers.flatMap((container) => identities.map((identity) => sandboxWorkspaceViewPaths({
574
574
  base: config.workspaceViewBase, project: config.project, container, identity
@@ -711,7 +711,9 @@ async function removeUncheckedSandbox(config, target, options) {
711
711
  }
712
712
  for (const root of controlRoots) {
713
713
  fs.rmSync(root, { recursive: true, force: true });
714
- removeEmptyManagedParent(path.join(config.controlBase, config.project), root);
714
+ if (target.workspace.mode !== 'task-bound') {
715
+ removeEmptyManagedParent(path.join(config.controlBase, config.project), root);
716
+ }
715
717
  }
716
718
  for (const root of workspaceViewRoots) {
717
719
  fs.rmSync(root, { recursive: true, force: true });
@@ -735,13 +737,15 @@ async function removeUncheckedSandbox(config, target, options) {
735
737
  assertBranchRemovalIdentity(config, effectiveBranch, worktreePermits);
736
738
  runSafe('git', ['-C', config.repoRoot, 'branch', '-D', effectiveBranch]);
737
739
  }
738
- if (target.workspace.mode === 'task-bound') {
739
- releaseStaleShortIdRegistry(config.repoRoot, target.workspace.taskId);
740
- }
740
+ releaseTaskBoundSandboxIdentity(config.repoRoot, target.workspace);
741
741
  if (!options.quiet)
742
742
  p.outro('Sandbox removed');
743
743
  return null;
744
744
  }
745
+ function releaseTaskBoundSandboxIdentity(repoRoot, workspace) {
746
+ if (workspace.mode === 'task-bound')
747
+ releaseStaleShortIdRegistry(repoRoot, workspace.taskId);
748
+ }
745
749
  async function rmOneCore(config, tools, branch, options = {}) {
746
750
  const target = options.target ?? resolveRmTarget(config, tools, options.cleanupTarget ?? resolveSandboxCleanupTarget(branch, config.repoRoot));
747
751
  return removeUncheckedSandbox(config, target, options);
@@ -754,11 +758,20 @@ async function rmPurgeCore(config, tools, prompt = {}) {
754
758
  const confirm = prompt.confirm ?? p.confirm;
755
759
  const isCancel = prompt.isCancel ?? p.isCancel;
756
760
  p.intro(pc.cyan(`Removing all sandboxes for ${config.project}`));
761
+ const listed = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config), { mode: sandboxWorkspaceModeLabel(config), taskId: sandboxTaskIdLabel(config) });
762
+ const rows = [...listed.running, ...listed.nonRunning];
757
763
  const containers = runEngine(engine, 'docker', [
758
764
  'ps', '-a', '--filter', `label=${sandboxLabel(config)}`, '--format', '{{.Names}}'
759
765
  ]).split('\n').filter(Boolean);
766
+ const completedTaskIds = [...new Set(rows
767
+ .filter((row) => row.workspaceMode === 'task-bound' && row.taskId)
768
+ .map((row) => row.taskId))];
760
769
  for (const name of containers)
761
770
  runSafeEngine(engine, 'docker', ['rm', '-f', name]);
771
+ if (!sandboxContainersRemoved(engine, containers)) {
772
+ throw new Error(`SANDBOX_REMOVAL_CONTAINER_STILL_PRESENT: ${containers.join(', ')}`);
773
+ }
774
+ let cleanupCompleted = true;
762
775
  const worktrees = fs.existsSync(config.worktreeBase)
763
776
  ? fs.readdirSync(config.worktreeBase).map((entry) => path.join(config.worktreeBase, entry))
764
777
  : [];
@@ -769,6 +782,8 @@ async function rmPurgeCore(config, tools, prompt = {}) {
769
782
  fs.rmSync(worktree, { recursive: true, force: true });
770
783
  runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
771
784
  }
785
+ else
786
+ cleanupCompleted = false;
772
787
  }
773
788
  for (const dir of projectToolDirs(config, tools))
774
789
  fs.rmSync(dir, { recursive: true, force: true });
@@ -776,15 +791,23 @@ async function rmPurgeCore(config, tools, prompt = {}) {
776
791
  const selected = await confirm({ message: `Remove all shell config dirs in ${config.shellConfigBase}?`, initialValue: true });
777
792
  if (!isCancel(selected) && selected)
778
793
  fs.rmSync(config.shellConfigBase, { recursive: true, force: true });
794
+ else
795
+ cleanupCompleted = false;
779
796
  }
780
797
  if (fs.existsSync(config.shareBase)) {
781
798
  const selected = await confirm({ message: `Remove all share dirs for project (${config.shareBase})?`, initialValue: true });
782
799
  if (!isCancel(selected) && selected)
783
800
  fs.rmSync(config.shareBase, { recursive: true, force: true });
801
+ else
802
+ cleanupCompleted = false;
784
803
  }
785
804
  for (const base of [config.workspaceViewBase, config.controlBase]) {
786
805
  fs.rmSync(path.join(base, config.project), { recursive: true, force: true });
787
806
  }
807
+ if (cleanupCompleted) {
808
+ for (const taskId of completedTaskIds)
809
+ releaseStaleShortIdRegistry(config.repoRoot, taskId);
810
+ }
788
811
  const removeImage = await confirm({ message: `Remove image ${config.imageName}?`, initialValue: false });
789
812
  if (!isCancel(removeImage) && removeImage)
790
813
  runSafeEngine(engine, 'docker', ['rmi', config.imageName]);
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { isRemovedHashShortIdInput } from '../task/short-id.js';
4
+ import { resolveTaskRef } from '../task/resolve-ref.js';
4
5
  import { resolveTaskWorkspace } from './task-resolver.js';
5
6
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
6
7
  const SHORT_ID_RE = /^\d+$/;
@@ -38,6 +39,16 @@ function taskIdentity(repoRoot, taskId, registry) {
38
39
  }
39
40
  return { mode: 'task-bound', taskId, shortId: shortIds[0] };
40
41
  }
42
+ function cleanupTaskId(requestedRef, repoRoot) {
43
+ if (TASK_ID_RE.test(requestedRef))
44
+ return requestedRef;
45
+ if (!SHORT_ID_RE.test(requestedRef))
46
+ return null;
47
+ const task = resolveTaskRef(requestedRef, { repoRoot });
48
+ if (!task.ok)
49
+ throw new Error(task.message);
50
+ return task.taskId;
51
+ }
41
52
  export function resolveSandboxTarget(requestedRef, repoRoot) {
42
53
  if (isRemovedHashShortIdInput(requestedRef)) {
43
54
  throw new Error(`Invalid task short id '${requestedRef}': task short ids must use bare digits`);
@@ -83,26 +94,27 @@ export function resolveSandboxCleanupTarget(requestedRef, repoRoot, options = {}
83
94
  if (isRemovedHashShortIdInput(requestedRef)) {
84
95
  throw new Error(`Invalid task short id '${requestedRef}': task short ids must use bare digits`);
85
96
  }
86
- if (TASK_ID_RE.test(requestedRef)) {
97
+ const taskId = cleanupTaskId(requestedRef, repoRoot);
98
+ if (taskId) {
87
99
  let task;
88
100
  try {
89
- task = resolveTaskWorkspace(requestedRef, repoRoot);
101
+ task = resolveTaskWorkspace(taskId, repoRoot);
90
102
  }
91
103
  catch (error) {
92
- if (!(error instanceof Error && error.message === `Task not found: ${requestedRef}`))
104
+ if (!(error instanceof Error && error.message === `Task not found: ${taskId}`))
93
105
  throw error;
94
- const branch = options.resolveMissingTask?.(requestedRef);
106
+ const branch = options.resolveMissingTask?.(taskId);
95
107
  if (!branch)
96
108
  throw error;
97
109
  return {
98
110
  requestedRef,
99
111
  branch,
100
- workspace: { mode: 'task-bound', taskId: requestedRef },
112
+ workspace: { mode: 'task-bound', taskId },
101
113
  taskState: 'unknown'
102
114
  };
103
115
  }
104
116
  if (!options.allowProtected && (task.state === 'blocked' || task.state === 'archive')) {
105
- throw new Error(`SANDBOX_CLEANUP_STATE_UNSUPPORTED: task ${requestedRef} is ${task.state}`);
117
+ throw new Error(`SANDBOX_CLEANUP_STATE_UNSUPPORTED: task ${taskId} is ${task.state}`);
106
118
  }
107
119
  return {
108
120
  requestedRef,
@@ -2,6 +2,7 @@ import { createHash, randomBytes } from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { resolveTaskWorkspace } from './task-resolver.js';
5
+ import { resolveTaskRuntimeRoot } from '../task/runtime-paths.js';
5
6
  import { createSandboxControlIdentitySentinel, writeSandboxControlIdentitySentinel } from './control/identity-sentinel.js';
6
7
  export const SANDBOX_WORKSPACE_VIEW_STATES = Object.freeze([
7
8
  'active',
@@ -25,11 +26,12 @@ export function sandboxWorkspaceViewPaths(params) {
25
26
  return { root };
26
27
  }
27
28
  export function sandboxControlPaths(params) {
28
- const identityKey = params.identity.mode === 'task-bound'
29
- ? `task-bound:${params.identity.taskId}`
30
- : 'branch-only';
31
- const digest = createHash('sha256').update(identityKey).digest('hex').slice(0, 16);
32
- const root = path.resolve(params.base, params.project, params.container, digest);
29
+ const root = params.identity.mode === 'task-bound'
30
+ ? path.join(resolveTaskRuntimeRoot(params.identity.taskId, { repoRoot: params.repoRoot }), 'sandbox-control')
31
+ : (() => {
32
+ const digest = createHash('sha256').update('branch-only').digest('hex').slice(0, 16);
33
+ return path.resolve(params.base, params.project, params.container, digest);
34
+ })();
33
35
  return {
34
36
  root,
35
37
  channelDir: path.join(root, 'channel'),
@@ -109,13 +111,16 @@ export function assertSandboxTaskSource(repoRoot, taskId) {
109
111
  }
110
112
  return canonical;
111
113
  }
112
- export function prepareSandboxWorkspaceMountTargets(worktreeRoot) {
114
+ export function prepareSandboxWorkspaceMountTargets(worktreeRoot, taskId) {
113
115
  const resolvedWorktreeRoot = path.resolve(worktreeRoot);
114
116
  const canonicalWorktreeRoot = fs.realpathSync.native(resolvedWorktreeRoot);
115
117
  const workspaceRoot = path.resolve(resolvedWorktreeRoot, '.agents', 'workspace');
116
118
  const statePaths = sandboxWorkspaceViewStatePaths(workspaceRoot);
117
119
  const registryTarget = path.join(workspaceRoot, 'active', '.short-ids.json');
118
- for (const target of [workspaceRoot, ...statePaths.map(({ hostPath }) => hostPath), registryTarget]) {
120
+ const completedTaskTarget = taskId ? path.join(workspaceRoot, 'completed', taskId) : null;
121
+ if (taskId && !/^TASK-\d{8}-\d{6}$/.test(taskId))
122
+ throw new Error('SANDBOX_TASK_SOURCE_INVALID');
123
+ for (const target of [workspaceRoot, ...statePaths.map(({ hostPath }) => hostPath), registryTarget, ...(completedTaskTarget ? [completedTaskTarget] : [])]) {
119
124
  assertSafeMountTargetPath(target, resolvedWorktreeRoot, canonicalWorktreeRoot);
120
125
  }
121
126
  fs.mkdirSync(workspaceRoot, { recursive: true, mode: 0o700 });
@@ -123,9 +128,52 @@ export function prepareSandboxWorkspaceMountTargets(worktreeRoot) {
123
128
  fs.mkdirSync(hostPath, { recursive: true, mode: 0o700 });
124
129
  fs.chmodSync(hostPath, 0o700);
125
130
  }
131
+ if (completedTaskTarget)
132
+ fs.mkdirSync(completedTaskTarget, { recursive: true, mode: 0o700 });
126
133
  fs.chmodSync(workspaceRoot, 0o700);
127
134
  fs.closeSync(fs.openSync(registryTarget, 'a', 0o600));
128
135
  }
136
+ export function prepareSandboxCompletedTaskMountSource(repoRoot, taskId) {
137
+ if (!/^TASK-\d{8}-\d{6}$/.test(taskId))
138
+ throw new Error('SANDBOX_TASK_SOURCE_INVALID');
139
+ const workspaceRoot = path.resolve(repoRoot, '.agents', 'workspace');
140
+ fs.mkdirSync(path.join(workspaceRoot, 'completed'), { recursive: true, mode: 0o700 });
141
+ const completedRoot = path.join(workspaceRoot, 'completed');
142
+ const completedStat = fs.lstatSync(completedRoot);
143
+ if (!completedStat.isDirectory() || completedStat.isSymbolicLink())
144
+ throw new Error('SANDBOX_TASK_SOURCE_INVALID');
145
+ const target = path.join(completedRoot, taskId);
146
+ try {
147
+ const stat = fs.lstatSync(target);
148
+ if (!stat.isDirectory() || stat.isSymbolicLink() || fs.readdirSync(target).length > 0) {
149
+ throw new Error(`SANDBOX_COMPLETED_MOUNT_CONFLICT: ${target}`);
150
+ }
151
+ }
152
+ catch (error) {
153
+ if (error.code !== 'ENOENT')
154
+ throw error;
155
+ fs.mkdirSync(target, { mode: 0o700 });
156
+ }
157
+ return sandboxCompletedTaskMountSource(repoRoot, taskId);
158
+ }
159
+ export function sandboxCompletedTaskMountSource(repoRoot, taskId) {
160
+ if (!/^TASK-\d{8}-\d{6}$/.test(taskId))
161
+ throw new Error('SANDBOX_TASK_SOURCE_INVALID');
162
+ const source = path.resolve(repoRoot, '.agents', 'workspace', 'completed', taskId);
163
+ const stat = fs.lstatSync(source);
164
+ if (!stat.isDirectory() || stat.isSymbolicLink())
165
+ throw new Error('SANDBOX_TASK_SOURCE_INVALID');
166
+ const canonicalWorkspace = fs.realpathSync.native(path.join(repoRoot, '.agents', 'workspace'));
167
+ const canonicalSource = fs.realpathSync.native(source);
168
+ const canonicalRepoRoot = fs.realpathSync.native(repoRoot);
169
+ const workspaceRelative = path.relative(canonicalRepoRoot, canonicalWorkspace);
170
+ const relative = path.relative(canonicalWorkspace, canonicalSource);
171
+ if (workspaceRelative.startsWith('..') || path.isAbsolute(workspaceRelative)
172
+ || path.dirname(canonicalSource) !== fs.realpathSync.native(path.dirname(source))
173
+ || relative.startsWith('..') || path.isAbsolute(relative))
174
+ throw new Error('SANDBOX_TASK_SOURCE_INVALID');
175
+ return canonicalSource;
176
+ }
129
177
  export function materializeSandboxWorkspaceView(params) {
130
178
  const projectRoot = path.resolve(params.base, params.project);
131
179
  const { root } = sandboxWorkspaceViewPaths(params);
@@ -155,7 +203,7 @@ export function materializeSandboxWorkspaceView(params) {
155
203
  }
156
204
  export function materializeSandboxControl(params) {
157
205
  const { root, channelDir, statusDir, processingDir, runtimeDir, manifestPath } = sandboxControlPaths(params);
158
- assertSafeDirectory(root, path.resolve(params.base));
206
+ assertSafeDirectory(root, params.identity.mode === 'task-bound' ? path.dirname(root) : path.resolve(params.base));
159
207
  if (fs.existsSync(root)) {
160
208
  if (!params.replacementLease || path.resolve(params.replacementLease.root) !== root) {
161
209
  throw new Error('SANDBOX_CONTROL_REPLACEMENT_REQUIRED');
@@ -168,18 +216,7 @@ export function materializeSandboxControl(params) {
168
216
  fs.mkdirSync(consumedDir, { recursive: true, mode: 0o700 });
169
217
  for (const directory of [statusDir, processingDir, runtimeDir]) {
170
218
  assertSafeDirectory(directory, root);
171
- if (directory === runtimeDir)
172
- fs.rmSync(directory, { recursive: true, force: true });
173
- fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
174
- }
175
- for (const directory of [
176
- path.join(runtimeDir, 'clients'),
177
- path.join(runtimeDir, 'clients', 'codex', 'capabilities'),
178
- path.join(runtimeDir, 'clients', 'codex', 'lifecycle')
179
- ]) {
180
- assertSafeDirectory(directory, runtimeDir);
181
219
  fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
182
- fs.chmodSync(directory, 0o700);
183
220
  }
184
221
  for (const queue of ['requests', 'responses']) {
185
222
  const directory = path.join(channelDir, queue);
@@ -58,9 +58,9 @@ function collectTasks(repoRoot, state) {
58
58
  const dir = path.join(repoRoot, '.agents', 'workspace', state);
59
59
  if (!fs.existsSync(dir))
60
60
  return [];
61
- // Short ids live only in the registry and only for active tasks; archived
62
- // (blocked/completed) tasks have released their short id and render '-'.
63
- const shortIdByTaskId = state === 'active' ? loadShortIdByTaskId(repoRoot) : new Map();
61
+ const shortIdByTaskId = state === 'active' || state === 'completed'
62
+ ? loadShortIdByTaskId(repoRoot)
63
+ : new Map();
64
64
  const rows = [];
65
65
  for (const entry of fs.readdirSync(dir).sort()) {
66
66
  if (!TASK_ID_RE.test(entry))