@fitlab-ai/agent-infra 0.11.13 → 0.11.15

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 (115) 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-orchestration.js +1 -3
  7. package/dist/lib/agent-clients/adapters/codex.js +0 -2
  8. package/dist/lib/internal/cli-route-inventory.js +0 -1
  9. package/dist/lib/internal/task-finalization.js +7 -5
  10. package/dist/lib/internal/task-lifecycle.js +3 -1
  11. package/dist/lib/internal/task-operation-registry.js +1 -3
  12. package/dist/lib/internal/task-validate.js +14 -124
  13. package/dist/lib/platform/pr-change-report.js +10 -3
  14. package/dist/lib/platform/release-notes.js +2 -2
  15. package/dist/lib/process-data/index.js +15 -4
  16. package/dist/lib/sandbox/commands/control-status.js +0 -1
  17. package/dist/lib/sandbox/commands/create.js +5 -4
  18. package/dist/lib/sandbox/config.js +12 -0
  19. package/dist/lib/sandbox/control/audit.js +1 -2
  20. package/dist/lib/sandbox/control/client.js +12 -47
  21. package/dist/lib/sandbox/control/executor.js +6 -10
  22. package/dist/lib/sandbox/control/host-recovery.js +89 -27
  23. package/dist/lib/sandbox/control/lifecycle.js +22 -37
  24. package/dist/lib/sandbox/control/protocol.js +8 -36
  25. package/dist/lib/sandbox/control/server.js +2 -21
  26. package/dist/lib/sandbox/environment.js +7 -8
  27. package/dist/lib/sandbox/mounts.js +71 -59
  28. package/dist/lib/sandbox/recovery.js +2 -3
  29. package/dist/lib/sandbox/removal.js +19 -4
  30. package/dist/lib/sandbox/workspace-identity.js +30 -15
  31. package/dist/lib/sandbox/workspace-view.js +41 -4
  32. package/dist/lib/task/control-authority.js +39 -17
  33. package/dist/lib/task/control-recovery.js +1 -2
  34. package/dist/lib/task/finalization.js +13 -2
  35. package/dist/lib/task/lifecycle.js +122 -35
  36. package/dist/lib/task/short-id.js +2 -2
  37. package/dist/lifecycle-build-manifest.json +3 -3
  38. package/lib/agent-clients/adapter.ts +0 -27
  39. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +6 -12
  40. package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +1 -5
  41. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +0 -7
  42. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +1 -5
  43. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -4
  44. package/lib/agent-clients/adapters/codex-orchestration.ts +1 -3
  45. package/lib/agent-clients/adapters/codex.ts +0 -2
  46. package/lib/internal/cli-route-inventory.ts +0 -1
  47. package/lib/internal/task-finalization.ts +8 -6
  48. package/lib/internal/task-lifecycle.ts +3 -1
  49. package/lib/internal/task-operation-registry.ts +1 -3
  50. package/lib/internal/task-validate.ts +13 -127
  51. package/lib/platform/pr-change-report.ts +9 -3
  52. package/lib/platform/release-notes.ts +2 -2
  53. package/lib/process-data/index.ts +15 -4
  54. package/lib/sandbox/commands/control-status.ts +0 -1
  55. package/lib/sandbox/commands/create.ts +5 -4
  56. package/lib/sandbox/config.ts +13 -0
  57. package/lib/sandbox/control/audit.ts +1 -2
  58. package/lib/sandbox/control/client.ts +13 -61
  59. package/lib/sandbox/control/executor.ts +7 -13
  60. package/lib/sandbox/control/host-recovery.ts +94 -21
  61. package/lib/sandbox/control/lifecycle.ts +22 -37
  62. package/lib/sandbox/control/protocol.ts +9 -45
  63. package/lib/sandbox/control/server.ts +2 -17
  64. package/lib/sandbox/environment.ts +7 -8
  65. package/lib/sandbox/mounts.ts +92 -75
  66. package/lib/sandbox/recovery.ts +2 -3
  67. package/lib/sandbox/removal.ts +16 -4
  68. package/lib/sandbox/workspace-identity.ts +36 -13
  69. package/lib/sandbox/workspace-view.ts +40 -5
  70. package/lib/task/control-authority.ts +40 -26
  71. package/lib/task/control-recovery.ts +2 -3
  72. package/lib/task/finalization.ts +15 -2
  73. package/lib/task/lifecycle.ts +135 -33
  74. package/lib/task/short-id.ts +5 -2
  75. package/package.json +1 -1
  76. package/templates/.agents/rules/lifecycle-orchestration.en.md +3 -4
  77. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -4
  78. package/templates/.agents/rules/task-short-id.en.md +18 -12
  79. package/templates/.agents/rules/task-short-id.zh-CN.md +12 -7
  80. package/templates/.agents/skills/complete-task/SKILL.en.md +10 -2
  81. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +10 -2
  82. package/templates/.agents/skills/create-pr/reference/change-report.en.md +12 -2
  83. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +12 -2
  84. package/templates/.agents/skills/create-release-note/SKILL.en.md +3 -1
  85. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +3 -1
  86. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +5 -5
  87. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +5 -5
  88. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +13 -2
  89. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +13 -2
  90. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -0
  91. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -0
  92. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  93. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  94. package/templates/.agents/skills/watch-pr/SKILL.en.md +19 -2
  95. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +20 -3
  96. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +14 -0
  97. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +15 -1
  98. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +0 -130
  99. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +0 -127
  100. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +0 -131
  101. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-registration.js +0 -275
  102. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js +0 -106
  103. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +0 -345
  104. package/dist/lib/runtime/agent-runtime.js +0 -33
  105. package/dist/lib/sandbox/control/agent-operation.js +0 -34
  106. package/dist/lib/task/runtime-paths.js +0 -10
  107. package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +0 -158
  108. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +0 -163
  109. package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +0 -151
  110. package/lib/agent-clients/adapters/codex-lifecycle/controller-registration.ts +0 -352
  111. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts +0 -109
  112. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +0 -437
  113. package/lib/runtime/agent-runtime.ts +0 -41
  114. package/lib/sandbox/control/agent-operation.ts +0 -49
  115. package/lib/task/runtime-paths.ts +0 -12
@@ -5,14 +5,13 @@ export const SANDBOX_CONTROL_STATUS_MOUNT = '/run/agent-infra/control-status';
5
5
  const TASK_MARKER_KEYS = [
6
6
  'AGENT_INFRA_SANDBOX',
7
7
  'AGENT_INFRA_TASK_ID',
8
- 'AGENT_INFRA_CONTROL_TOKEN',
9
8
  'AGENT_INFRA_CONTROL_GENERATION',
10
9
  'AGENT_INFRA_CONTROL_DIR',
10
+ 'AGENT_INFRA_CONTROL_ROOT_DIR',
11
11
  'AGENT_INFRA_CONTROL_STATUS_DIR',
12
12
  'AGENT_INFRA_RUNTIME_DIR'
13
13
  ];
14
14
  const TASK_CONTROL_MARKER_KEYS = [
15
- 'AGENT_INFRA_CONTROL_TOKEN',
16
15
  'AGENT_INFRA_CONTROL_GENERATION',
17
16
  'AGENT_INFRA_CONTROL_DIR',
18
17
  'AGENT_INFRA_CONTROL_STATUS_DIR'
@@ -77,7 +76,6 @@ export function hasSandboxControlMarker(env = process.env) {
77
76
  export function resolveSandboxControlTransport(env = process.env, options = {}) {
78
77
  const markerState = sandboxTaskMarkerState(env);
79
78
  const hasAnyMarker = markerState !== 'none'
80
- || Boolean(env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING)
81
79
  || Boolean(env.AGENT_INFRA_EXECUTOR_MANIFEST);
82
80
  const fixedStatusDir = options.statusMountPath ?? SANDBOX_CONTROL_STATUS_MOUNT;
83
81
  const fixedStatusProbe = path.isAbsolute(fixedStatusDir)
@@ -98,6 +96,10 @@ export function resolveSandboxControlTransport(env = process.env, options = {})
98
96
  if (!hasCompleteConfig) {
99
97
  return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
100
98
  }
99
+ if (markerState === 'task-bound'
100
+ && (!env.AGENT_INFRA_CONTROL_ROOT_DIR || !path.isAbsolute(env.AGENT_INFRA_CONTROL_ROOT_DIR))) {
101
+ return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
102
+ }
101
103
  const configuredStatusDir = env.AGENT_INFRA_CONTROL_STATUS_DIR;
102
104
  const configuredStatusProbe = configuredStatusDir && path.isAbsolute(configuredStatusDir)
103
105
  ? nativeDirectoryProbe(configuredStatusDir) : 'absent';
@@ -108,7 +110,7 @@ export function resolveSandboxControlTransport(env = process.env, options = {})
108
110
  ? configuredStatusProbe === 'present' ? configuredStatusDir : null
109
111
  : fixedStatusMounted ? fixedStatusDir : null;
110
112
  const statusMounted = statusDir !== null;
111
- if (env.AGENT_INFRA_EXECUTOR_MANIFEST || env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING) {
113
+ if (env.AGENT_INFRA_EXECUTOR_MANIFEST) {
112
114
  return { kind: 'fail-closed', reasonCode: 'TASK_CONTROL_TRANSPORT_INVALID' };
113
115
  }
114
116
  const taskBound = Boolean(env.AGENT_INFRA_TASK_ID);
@@ -124,9 +126,6 @@ export function resolveSandboxControlTransport(env = process.env, options = {})
124
126
  ? 'SANDBOX_CONTROL_IDENTITY_MISSING'
125
127
  : 'SANDBOX_CONTROL_IDENTITY_MALFORMED' };
126
128
  }
127
- if (sentinel.generation !== env.AGENT_INFRA_CONTROL_GENERATION) {
128
- return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_GENERATION_MISMATCH' };
129
- }
130
129
  if (sentinel.controlRootId !== env.AGENT_INFRA_CONTROL_ROOT_ID) {
131
130
  return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_ROOT_ID_MISMATCH' };
132
131
  }
@@ -134,7 +133,7 @@ export function resolveSandboxControlTransport(env = process.env, options = {})
134
133
  && (sentinel.taskId !== env.AGENT_INFRA_TASK_ID || !runtime)) {
135
134
  return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH' };
136
135
  }
137
- if (sentinel.mode === 'branch-only' && (taskBound || runtime)) {
136
+ if (sentinel.mode === 'branch-only' && (taskBound || runtime || Boolean(env.AGENT_INFRA_CONTROL_ROOT_DIR))) {
138
137
  return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH' };
139
138
  }
140
139
  return { kind: options.localWorkflow ? 'sandbox-local' : 'broker-client', reasonCode: null };
@@ -2,10 +2,13 @@ import path from 'node:path';
2
2
  import { shareBranchDir, shareCommonDir, shellConfigDirCandidates, worktreeDirCandidates } from './constants.js';
3
3
  import { sandboxWorkspaceViewStatePaths } from './workspace-view.js';
4
4
  import { SANDBOX_GIT_COMMON_DIR } from './git-metadata.js';
5
- export function sandboxCoreBindMounts(config, branch, overrides) {
6
- const taskSources = overrides.taskSources ?? [];
7
- const taskBound = Boolean(taskSources.length > 0 && overrides.taskId);
8
- const workspaceMounts = sandboxWorkspaceViewStatePaths(overrides.workspaceViewRoot).map(({ state, hostPath }) => taskBound && state === 'active'
5
+ function workspaceStateMounts(workspaceViewRoot, taskBound) {
6
+ return sandboxWorkspaceViewStatePaths(workspaceViewRoot)
7
+ // The writable completed-task mount below is nested under this path. Keep
8
+ // the worktree target visible so Docker can mount it without creating a
9
+ // mountpoint inside a read-only parent bind mount.
10
+ .filter(({ state }) => !(taskBound && state === 'completed'))
11
+ .map(({ state, hostPath }) => taskBound && state === 'active'
9
12
  ? {
10
13
  hostPaths: [path.join(hostPath, '.short-ids.json')],
11
14
  containerPath: '/workspace/.agents/workspace/active/.short-ids.json',
@@ -16,17 +19,69 @@ export function sandboxCoreBindMounts(config, branch, overrides) {
16
19
  containerPath: path.posix.join('/workspace/.agents/workspace', state),
17
20
  readOnly: true
18
21
  });
22
+ }
23
+ function assertGitMetadataPair(overrides) {
19
24
  if (Boolean(overrides.gitCommonDir) !== Boolean(overrides.worktreeGitFile)) {
20
25
  throw new Error('SANDBOX_GIT_METADATA_INCOMPLETE');
21
26
  }
22
- const gitCommonMount = overrides.gitCommonDir
23
- ? { hostPaths: [overrides.gitCommonDir], containerPath: SANDBOX_GIT_COMMON_DIR, readOnly: false }
24
- : null;
25
- const worktreeGitFileMount = overrides.worktreeGitFile
26
- ? { hostPaths: [overrides.worktreeGitFile], containerPath: '/workspace/.git', readOnly: true }
27
- : null;
27
+ }
28
+ function gitCommonMount(gitCommonDir) {
29
+ return gitCommonDir ? [{ hostPaths: [gitCommonDir], containerPath: SANDBOX_GIT_COMMON_DIR, readOnly: false }] : [];
30
+ }
31
+ function worktreeGitFileMount(worktreeGitFile) {
32
+ return worktreeGitFile ? [{ hostPaths: [worktreeGitFile], containerPath: '/workspace/.git', readOnly: true }] : [];
33
+ }
34
+ function taskWorkspaceMounts(taskSources, taskBound, overrides) {
35
+ if (!taskBound)
36
+ return [];
37
+ const taskId = overrides.taskId;
38
+ const mounts = [{
39
+ hostPaths: taskSources,
40
+ containerPath: path.posix.join('/workspace/.agents/workspace/active', taskId),
41
+ readOnly: false
42
+ }];
43
+ if (overrides.completedTaskSource)
44
+ mounts.push({
45
+ hostPaths: [overrides.completedTaskSource],
46
+ containerPath: path.posix.join('/workspace/.agents/workspace/completed', taskId),
47
+ readOnly: false
48
+ });
49
+ return mounts;
50
+ }
51
+ function sharedMounts(config, branch, shellConfigHostDir) {
52
+ return [{
53
+ hostPaths: [shareCommonDir(config)], containerPath: '/share/common', readOnly: false
54
+ }, {
55
+ hostPaths: [shareBranchDir(config, branch)], containerPath: '/share/branch', readOnly: false
56
+ }, {
57
+ hostPaths: shellConfigHostDir ? [shellConfigHostDir] : shellConfigDirCandidates(config, branch),
58
+ containerPath: '/home/devuser/.host-shell-config', readOnly: true
59
+ }];
60
+ }
61
+ function controlMounts(overrides, taskBound) {
62
+ const mounts = [{
63
+ hostPaths: [overrides.controlStatusDir], containerPath: '/run/agent-infra/control-status', readOnly: true
64
+ }, {
65
+ hostPaths: [overrides.controlDir], containerPath: '/run/agent-infra/control', readOnly: false
66
+ }];
67
+ if (taskBound && overrides.runtimeDir)
68
+ mounts.push({
69
+ hostPaths: [overrides.runtimeDir], containerPath: '/run/agent-infra/runtime', readOnly: false
70
+ });
71
+ if (taskBound && overrides.controlRootDir)
72
+ mounts.push({
73
+ hostPaths: [overrides.controlRootDir], containerPath: '/run/agent-infra/control-root', readOnly: false
74
+ });
75
+ return mounts;
76
+ }
77
+ export function sandboxCoreBindMounts(config, branch, overrides) {
78
+ const taskSources = overrides.taskSources ?? [];
79
+ const taskBound = Boolean(taskSources.length > 0 && overrides.taskId);
80
+ assertGitMetadataPair(overrides);
81
+ if (taskBound && !overrides.controlRootDir)
82
+ throw new Error('SANDBOX_CONTROL_ROOT_MISSING');
28
83
  const mounts = [
29
- ...(gitCommonMount ? [gitCommonMount] : []),
84
+ ...gitCommonMount(overrides.gitCommonDir),
30
85
  {
31
86
  hostPaths: overrides.worktree
32
87
  ? [overrides.worktree]
@@ -34,55 +89,12 @@ export function sandboxCoreBindMounts(config, branch, overrides) {
34
89
  containerPath: '/workspace',
35
90
  readOnly: false
36
91
  },
37
- ...(worktreeGitFileMount ? [worktreeGitFileMount] : []),
38
- ...workspaceMounts,
92
+ ...worktreeGitFileMount(overrides.worktreeGitFile),
93
+ ...workspaceStateMounts(overrides.workspaceViewRoot, taskBound),
94
+ ...taskWorkspaceMounts(taskSources, taskBound, overrides),
95
+ ...sharedMounts(config, branch, overrides.shellConfigHostDir),
96
+ ...controlMounts(overrides, taskBound)
39
97
  ];
40
- if (taskBound) {
41
- const taskMountPath = path.posix.join('/workspace/.agents/workspace/active', overrides.taskId);
42
- mounts.push({
43
- hostPaths: taskSources,
44
- containerPath: taskMountPath,
45
- readOnly: false
46
- });
47
- if (overrides.completedTaskSource) {
48
- mounts.push({
49
- hostPaths: [overrides.completedTaskSource],
50
- containerPath: path.posix.join('/workspace/.agents/workspace/completed', overrides.taskId),
51
- readOnly: false
52
- });
53
- }
54
- }
55
- mounts.push({
56
- hostPaths: [shareCommonDir(config)],
57
- containerPath: '/share/common',
58
- readOnly: false
59
- }, {
60
- hostPaths: [shareBranchDir(config, branch)],
61
- containerPath: '/share/branch',
62
- readOnly: false
63
- }, {
64
- hostPaths: overrides.shellConfigHostDir
65
- ? [overrides.shellConfigHostDir]
66
- : shellConfigDirCandidates(config, branch),
67
- containerPath: '/home/devuser/.host-shell-config',
68
- readOnly: true
69
- });
70
- mounts.push({
71
- hostPaths: [overrides.controlStatusDir],
72
- containerPath: '/run/agent-infra/control-status',
73
- readOnly: true
74
- }, {
75
- hostPaths: [overrides.controlDir],
76
- containerPath: '/run/agent-infra/control',
77
- readOnly: false
78
- });
79
- if (taskBound && overrides.runtimeDir) {
80
- mounts.push({
81
- hostPaths: [overrides.runtimeDir],
82
- containerPath: '/run/agent-infra/runtime',
83
- readOnly: false
84
- });
85
- }
86
98
  return mounts;
87
99
  }
88
100
  //# sourceMappingURL=mounts.js.map
@@ -174,7 +174,6 @@ 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,
178
177
  project: params.config.project,
179
178
  container: params.container,
180
179
  identity: params.workspace
@@ -523,7 +522,6 @@ function expectedMounts(params) {
523
522
  });
524
523
  const control = sandboxControlPaths({
525
524
  base: config.controlBase ?? path.join(config.home, '.agent-infra', 'sandbox-control'),
526
- repoRoot: config.repoRoot,
527
525
  project: config.project,
528
526
  container: params.container,
529
527
  identity: params.workspace
@@ -546,6 +544,7 @@ function expectedMounts(params) {
546
544
  }),
547
545
  workspaceViewRoot: view.root,
548
546
  controlDir: control.channelDir,
547
+ ...(params.workspace.mode === 'task-bound' ? { controlRootDir: control.root } : {}),
549
548
  controlStatusDir: control.statusDir,
550
549
  ...(taskSources === null
551
550
  ? {}
@@ -926,7 +925,7 @@ export async function ensureSandboxReady(params) {
926
925
  let completedReentry = null;
927
926
  if (params.reentry === 'completed' && params.workspace?.mode === 'task-bound') {
928
927
  const control = sandboxControlPaths({
929
- base: params.config.controlBase, repoRoot: params.config.repoRoot, project: params.config.project,
928
+ base: params.config.controlBase, project: params.config.project,
930
929
  container: params.row.name, identity: params.workspace
931
930
  });
932
931
  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, repoRoot: config.repoRoot, project: config.project, container, identity
571
+ base: config.controlBase, 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
@@ -763,6 +763,21 @@ async function rmPurgeCore(config, tools, prompt = {}) {
763
763
  const containers = runEngine(engine, 'docker', [
764
764
  'ps', '-a', '--filter', `label=${sandboxLabel(config)}`, '--format', '{{.Names}}'
765
765
  ]).split('\n').filter(Boolean);
766
+ const controlRoots = rows.flatMap((row) => {
767
+ if (!row.branch)
768
+ return [];
769
+ const identity = row.workspaceMode === 'task-bound' && row.taskId
770
+ ? { mode: 'task-bound', taskId: row.taskId }
771
+ : row.workspaceMode === 'branch-only'
772
+ ? { mode: 'branch-only' }
773
+ : null;
774
+ if (!identity)
775
+ return [];
776
+ return [sandboxControlPaths({
777
+ base: config.controlBase, project: config.project,
778
+ container: row.name, identity
779
+ }).root];
780
+ });
766
781
  const completedTaskIds = [...new Set(rows
767
782
  .filter((row) => row.workspaceMode === 'task-bound' && row.taskId)
768
783
  .map((row) => row.taskId))];
@@ -801,9 +816,9 @@ async function rmPurgeCore(config, tools, prompt = {}) {
801
816
  else
802
817
  cleanupCompleted = false;
803
818
  }
804
- for (const base of [config.workspaceViewBase, config.controlBase]) {
805
- fs.rmSync(path.join(base, config.project), { recursive: true, force: true });
806
- }
819
+ fs.rmSync(path.join(config.workspaceViewBase, config.project), { recursive: true, force: true });
820
+ for (const root of new Set(controlRoots))
821
+ fs.rmSync(root, { recursive: true, force: true });
807
822
  if (cleanupCompleted) {
808
823
  for (const taskId of completedTaskIds)
809
824
  releaseStaleShortIdRegistry(config.repoRoot, taskId);
@@ -1,8 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { isRemovedHashShortIdInput } from '../task/short-id.js';
4
- import { resolveTaskRef } from '../task/resolve-ref.js';
5
- import { resolveTaskWorkspace } from './task-resolver.js';
3
+ import { configuredShortIdLength, inspectShortIdRegistry, isRemovedHashShortIdInput, normalizeShortIdInput } from '../task/short-id.js';
4
+ import { TASK_WORKSPACE_STATES, listTaskWorkspaces, resolveTaskWorkspace } from './task-resolver.js';
6
5
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
7
6
  const SHORT_ID_RE = /^\d+$/;
8
7
  function stripQuotes(value) {
@@ -42,12 +41,30 @@ function taskIdentity(repoRoot, taskId, registry) {
42
41
  function cleanupTaskId(requestedRef, repoRoot) {
43
42
  if (TASK_ID_RE.test(requestedRef))
44
43
  return requestedRef;
45
- if (!SHORT_ID_RE.test(requestedRef))
44
+ const shortIdLength = configuredShortIdLength(repoRoot);
45
+ const normalized = normalizeShortIdInput(requestedRef, { shortIdLength });
46
+ if (normalized.kind === 'error')
47
+ throw new Error(normalized.message);
48
+ if (normalized.kind !== 'shortId')
46
49
  return null;
47
- const task = resolveTaskRef(requestedRef, { repoRoot });
48
- if (!task.ok)
49
- throw new Error(task.message);
50
- return task.taskId;
50
+ const registry = inspectShortIdRegistry(repoRoot, shortIdLength);
51
+ if (registry.status !== 'valid')
52
+ throw new Error(registry.error.message);
53
+ const match = [...registry.shortIds].find(([, shortId]) => shortId === normalized.value);
54
+ if (!match) {
55
+ throw new Error(`Short id '${normalized.value}' not found in active task registry`);
56
+ }
57
+ return match[0];
58
+ }
59
+ function cleanupTaskIdByBranch(requestedRef, repoRoot) {
60
+ const taskIds = new Set(TASK_WORKSPACE_STATES
61
+ .flatMap((state) => listTaskWorkspaces(repoRoot, state))
62
+ .filter((task) => task.branch === requestedRef)
63
+ .map((task) => task.taskId));
64
+ if (taskIds.size > 1) {
65
+ throw new Error(`SANDBOX_TASK_IDENTITY_AMBIGUOUS: branch '${requestedRef}' is bound to multiple tasks`);
66
+ }
67
+ return taskIds.values().next().value ?? null;
51
68
  }
52
69
  export function resolveSandboxTarget(requestedRef, repoRoot) {
53
70
  if (isRemovedHashShortIdInput(requestedRef)) {
@@ -94,7 +111,8 @@ export function resolveSandboxCleanupTarget(requestedRef, repoRoot, options = {}
94
111
  if (isRemovedHashShortIdInput(requestedRef)) {
95
112
  throw new Error(`Invalid task short id '${requestedRef}': task short ids must use bare digits`);
96
113
  }
97
- const taskId = cleanupTaskId(requestedRef, repoRoot);
114
+ const taskId = cleanupTaskId(requestedRef, repoRoot)
115
+ ?? cleanupTaskIdByBranch(requestedRef, repoRoot);
98
116
  if (taskId) {
99
117
  let task;
100
118
  try {
@@ -123,14 +141,11 @@ export function resolveSandboxCleanupTarget(requestedRef, repoRoot, options = {}
123
141
  taskState: task.state
124
142
  };
125
143
  }
126
- const target = resolveSandboxTarget(requestedRef, repoRoot);
127
144
  return {
128
145
  requestedRef,
129
- branch: target.branch,
130
- workspace: target.workspace.mode === 'task-bound'
131
- ? { mode: 'task-bound', taskId: target.workspace.taskId }
132
- : { mode: 'branch-only' },
133
- taskState: target.workspace.mode === 'task-bound' ? 'active' : 'branch-only'
146
+ branch: requestedRef,
147
+ workspace: { mode: 'branch-only' },
148
+ taskState: 'branch-only'
134
149
  };
135
150
  }
136
151
  export function parseSandboxWorkspaceIdentity(labels, keys) {
@@ -2,7 +2,6 @@ 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';
6
5
  import { createSandboxControlIdentitySentinel, writeSandboxControlIdentitySentinel } from './control/identity-sentinel.js';
7
6
  export const SANDBOX_WORKSPACE_VIEW_STATES = Object.freeze([
8
7
  'active',
@@ -27,7 +26,17 @@ export function sandboxWorkspaceViewPaths(params) {
27
26
  }
28
27
  export function sandboxControlPaths(params) {
29
28
  const root = params.identity.mode === 'task-bound'
30
- ? path.join(resolveTaskRuntimeRoot(params.identity.taskId, { repoRoot: params.repoRoot }), 'sandbox-control')
29
+ ? (() => {
30
+ if (!/^TASK-\d{8}-\d{6}$/.test(params.identity.taskId)
31
+ || !params.project || path.basename(params.project) !== params.project || params.project === '.' || params.project === '..') {
32
+ throw new Error('SANDBOX_CONTROL_ROOT_INVALID');
33
+ }
34
+ const candidate = path.resolve(params.base, params.project, 'tasks', params.identity.taskId);
35
+ const relative = path.relative(path.resolve(params.base), candidate);
36
+ if (relative.startsWith('..') || path.isAbsolute(relative))
37
+ throw new Error('SANDBOX_CONTROL_ROOT_INVALID');
38
+ return candidate;
39
+ })()
31
40
  : (() => {
32
41
  const digest = createHash('sha256').update('branch-only').digest('hex').slice(0, 16);
33
42
  return path.resolve(params.base, params.project, params.container, digest);
@@ -50,6 +59,22 @@ function assertSafeDirectory(directory, expectedBase) {
50
59
  throw new Error(`Sandbox workspace view must not be a symbolic link: ${directory}`);
51
60
  }
52
61
  }
62
+ function assertSafeTaskBoundControlRoot(basePath, root) {
63
+ const relative = path.relative(basePath, root);
64
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
65
+ throw new Error(`Sandbox control root escapes its configured base: ${root}`);
66
+ }
67
+ if (fs.existsSync(basePath) && fs.lstatSync(basePath).isSymbolicLink()) {
68
+ throw new Error(`Sandbox control base must not be a symbolic link: ${basePath}`);
69
+ }
70
+ let current = basePath;
71
+ for (const segment of relative.split(path.sep)) {
72
+ current = path.join(current, segment);
73
+ if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) {
74
+ throw new Error(`Sandbox control root must not contain a symbolic link: ${current}`);
75
+ }
76
+ }
77
+ }
53
78
  function assertSafeMountTargetPath(target, worktreeRoot, canonicalWorktreeRoot) {
54
79
  const relative = path.relative(worktreeRoot, target);
55
80
  if (relative.startsWith('..') || path.isAbsolute(relative)) {
@@ -203,7 +228,12 @@ export function materializeSandboxWorkspaceView(params) {
203
228
  }
204
229
  export function materializeSandboxControl(params) {
205
230
  const { root, channelDir, statusDir, processingDir, runtimeDir, manifestPath } = sandboxControlPaths(params);
206
- assertSafeDirectory(root, params.identity.mode === 'task-bound' ? path.dirname(root) : path.resolve(params.base));
231
+ if (params.identity.mode === 'task-bound') {
232
+ assertSafeTaskBoundControlRoot(path.resolve(params.base), root);
233
+ }
234
+ else {
235
+ assertSafeDirectory(root, path.resolve(params.base));
236
+ }
207
237
  if (fs.existsSync(root)) {
208
238
  if (!params.replacementLease || path.resolve(params.replacementLease.root) !== root) {
209
239
  throw new Error('SANDBOX_CONTROL_REPLACEMENT_REQUIRED');
@@ -261,9 +291,16 @@ export function finalizeSandboxControlManifest(setup, identity) {
261
291
  containerIdentity: { id: identity.id, labels: { ...identity.labels } },
262
292
  authorityEvidence: identity.authorityEvidence
263
293
  };
294
+ const persistedManifest = {
295
+ ...manifest,
296
+ channelDir: 'channel',
297
+ publicStatusDir: 'public',
298
+ processingDir: 'processing',
299
+ runtimeDir: 'runtime'
300
+ };
264
301
  const temporary = `${setup.manifestPath}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
265
302
  try {
266
- fs.writeFileSync(temporary, `${JSON.stringify(manifest)}\n`, { mode: 0o600, flag: 'wx' });
303
+ fs.writeFileSync(temporary, `${JSON.stringify(persistedManifest)}\n`, { mode: 0o600, flag: 'wx' });
267
304
  fs.renameSync(temporary, setup.manifestPath);
268
305
  }
269
306
  finally {
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { normalizeAgentToken } from '../agent-clients/tokens.js';
3
3
  import { isAgentClientId } from '../agent-clients/types.js';
4
+ import { sandboxControlPaths } from '../sandbox/workspace-view.js';
4
5
  import { getAgentClientAdapter } from '../agent-clients/registry.js';
5
6
  import { activateMatchingOrchestrationDelegation, activateOrchestrationDelegation, advanceOrchestration, awaitOrchestrationDelegationActivation, beginOrResumeOrchestration, dispatchOrchestrationDelegation, OrchestrationStateError, pauseOrchestration, prepareOrchestrationDelegation, recoverPreparedOrchestrationDelegation, routeOrchestration, sealMatchingOrchestrationDelegation, sealOrchestrationDelegation, statusOrchestration } from './orchestration.js';
6
7
  import { applyTaskFinalization } from './finalization.js';
@@ -25,21 +26,14 @@ function absolute(name, value) {
25
26
  invalidContext(`${name} must be absolute`);
26
27
  return path.resolve(value);
27
28
  }
28
- function binding(value) {
29
- if (value === null || value === undefined)
30
- return value;
31
- if (!/^[a-f0-9]{64}$/u.test(value.instanceDigest) || !value.controlGeneration) {
32
- invalidContext('controller binding is invalid');
33
- }
34
- return value;
35
- }
36
29
  export function createDirectHostExecutionContext(params) {
37
30
  return {
38
31
  source: 'direct-host',
39
32
  mode: 'direct-host',
40
33
  repoRoot: absolute('repoRoot', params.repoRoot),
41
- ...(params.runtimeDir === undefined ? {} : { runtimeDir: absolute('runtimeDir', params.runtimeDir) }),
42
- ...(params.controllerBinding === undefined ? {} : { controllerBinding: binding(params.controllerBinding) }),
34
+ ...(params.controlBase === undefined ? {} : { controlBase: absolute('controlBase', params.controlBase) }),
35
+ ...(params.project === undefined ? {} : { project: params.project }),
36
+ ...(params.runtimeDir === undefined ? {} : { runtimeDir: absolute('runtimeDir', params.runtimeDir) })
43
37
  };
44
38
  }
45
39
  export function createSandboxExecutorExecutionContext(params) {
@@ -59,8 +53,7 @@ export function createSandboxExecutorExecutionContext(params) {
59
53
  taskId: params.taskId,
60
54
  generation: params.generation,
61
55
  manifestPath: absolute('manifestPath', params.manifestPath),
62
- requestId: params.requestId,
63
- ...(params.controllerBinding === undefined ? {} : { controllerBinding: binding(params.controllerBinding) }),
56
+ requestId: params.requestId
64
57
  };
65
58
  }
66
59
  export function assertTaskControlExecutionContext(context) {
@@ -70,7 +63,10 @@ export function assertTaskControlExecutionContext(context) {
70
63
  absolute('repoRoot', context.repoRoot);
71
64
  if (context.runtimeDir !== undefined)
72
65
  absolute('runtimeDir', context.runtimeDir);
73
- binding(context.controllerBinding);
66
+ if (Boolean(context.controlBase) !== Boolean(context.project))
67
+ invalidContext('control configuration is incomplete');
68
+ if (context.controlBase !== undefined)
69
+ absolute('controlBase', context.controlBase);
74
70
  return;
75
71
  }
76
72
  if (context.mode !== 'task-bound-sandbox' || !/^TASK-\d{8}-\d{6}$/u.test(context.taskId)
@@ -84,7 +80,6 @@ export function assertTaskControlExecutionContext(context) {
84
80
  if (path.resolve(context.runtimeDir) !== path.join(path.dirname(path.resolve(context.manifestPath)), 'runtime')) {
85
81
  invalidContext('runtimeDir is not bound to the manifest control root');
86
82
  }
87
- binding(context.controllerBinding);
88
83
  }
89
84
  function boundTaskRef(context, taskRef) {
90
85
  return context.source === 'sandbox-executor' ? context.taskId : taskRef;
@@ -115,6 +110,8 @@ function lifecycleTaskId(request, repoRoot) {
115
110
  }
116
111
  }
117
112
  async function applyLifecycleWithAuthority(context, request) {
113
+ const taskId = lifecycleTaskId(request, context.repoRoot);
114
+ const controlRoot = taskId ? controlRootForTask(context, taskId) : undefined;
118
115
  const execute = async (underTaskLock = false) => {
119
116
  if (request.intent === 'recover-started') {
120
117
  const adapter = isAgentClientId(request.agent)
@@ -130,10 +127,17 @@ async function applyLifecycleWithAuthority(context, request) {
130
127
  return adapter.recoverStarted(request, { repoRoot: context.repoRoot });
131
128
  }
132
129
  return underTaskLock
133
- ? applyTaskLifecycleWithinTaskLock(request, { repoRoot: context.repoRoot }, taskId)
134
- : applyTaskLifecycle(request, { repoRoot: context.repoRoot });
130
+ ? applyTaskLifecycleWithinTaskLock(request, {
131
+ repoRoot: context.repoRoot,
132
+ ...(controlRoot ? { controlRoot } : {}),
133
+ ...(context.source === 'direct-host' && context.project ? { controlProject: context.project } : {})
134
+ }, taskId)
135
+ : applyTaskLifecycle(request, {
136
+ repoRoot: context.repoRoot,
137
+ ...(controlRoot ? { controlRoot } : {}),
138
+ ...(context.source === 'direct-host' && context.project ? { controlProject: context.project } : {})
139
+ });
135
140
  };
136
- const taskId = lifecycleTaskId(request, context.repoRoot);
137
141
  if (!taskId)
138
142
  return await execute();
139
143
  try {
@@ -153,6 +157,18 @@ async function applyLifecycleWithAuthority(context, request) {
153
157
  return taskLifecycleFailure(request, { code: error.code, message: error.message }, taskId);
154
158
  }
155
159
  }
160
+ function controlRootForTask(context, taskId) {
161
+ if (context.source === 'sandbox-executor')
162
+ return path.dirname(context.manifestPath);
163
+ if (!context.controlBase || !context.project)
164
+ return undefined;
165
+ return sandboxControlPaths({
166
+ base: context.controlBase,
167
+ project: context.project,
168
+ container: '',
169
+ identity: { mode: 'task-bound', taskId }
170
+ }).root;
171
+ }
156
172
  function orchestration(context, operation) {
157
173
  const taskRef = boundTaskRef(context, operation.taskRef);
158
174
  const input = operation.input;
@@ -219,6 +235,10 @@ export function dispatchTaskControlOperation(context, operation) {
219
235
  });
220
236
  }
221
237
  if (operation.family === 'task-finalization') {
238
+ const resolved = resolveTaskRef(boundTaskRef(context, operation.request.taskRef), { repoRoot: context.repoRoot });
239
+ const controlRoot = context.source === 'sandbox-executor'
240
+ ? path.dirname(context.manifestPath)
241
+ : resolved.ok ? controlRootForTask(context, resolved.taskId) : undefined;
222
242
  return applyTaskFinalization({
223
243
  ...operation.request,
224
244
  taskRef: boundTaskRef(context, operation.request.taskRef)
@@ -228,6 +248,8 @@ export function dispatchTaskControlOperation(context, operation) {
228
248
  controlBinding: { generation: context.generation, requestId: context.requestId },
229
249
  manifestPath: context.manifestPath
230
250
  } : {}),
251
+ ...(controlRoot ? { controlRoot } : {}),
252
+ ...(context.source === 'direct-host' && context.project ? { controlProject: context.project } : {}),
231
253
  preflight: (request, options) => verifyTaskEvent({ ...request, event: 'complete-task.hard-preflight' }, options)
232
254
  });
233
255
  }
@@ -19,8 +19,7 @@ export const SANDBOX_CONTROL_REQUIRED_COMPLETION_PHASES = Object.freeze([
19
19
  const LIFECYCLE_INTENTS = ['block', 'activate', 'cancel', 'close-codescan', 'close-dependabot', 'restore', 'recover-started'];
20
20
  export const SANDBOX_CONTROL_RECOVERY_OPERATIONS = Object.freeze([
21
21
  ...LIFECYCLE_INTENTS.map((intent) => ({ family: 'task-lifecycle', intent, class: 'lifecycle-mutation' })),
22
- { family: 'task-create', intent: 'create', class: 'task-create' },
23
- ...['controller.close', 'controller.verify'].map((intent) => ({ family: 'agent-client', intent, class: 'agent-client-operation' }))
22
+ { family: 'task-create', intent: 'create', class: 'task-create' }
24
23
  ]);
25
24
  export function digestControlRecoveryIntent(family, intent) {
26
25
  return createHash('sha256').update(`${family}\0${intent}`, 'utf8').digest('hex');
@@ -894,7 +894,13 @@ async function prepareUnderLock(request, taskId, options) {
894
894
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
895
895
  }
896
896
  else {
897
- const result = lifecycle({ taskRef: taskId, intent: 'complete', agent: request.agent }, { repoRoot, prepareOnly: true, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) });
897
+ const result = lifecycle({ taskRef: taskId, intent: 'complete', agent: request.agent }, {
898
+ repoRoot,
899
+ prepareOnly: true,
900
+ ...(options.controlRoot ? { controlRoot: options.controlRoot } : {}),
901
+ ...(options.controlProject ? { controlProject: options.controlProject } : {}),
902
+ ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {})
903
+ });
898
904
  lifecycleResult = lifecycleStep(result);
899
905
  if (result.status !== 'applied' && result.status !== 'no-op') {
900
906
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: lifecycleResult.error });
@@ -1037,7 +1043,12 @@ async function commitPreparedTaskFinalization(request, options) {
1037
1043
  });
1038
1044
  receipt = updateReceipt(repoRoot, receipt, { verification: 'pending' });
1039
1045
  const lifecycleRequest = { taskRef: resolved.taskId, intent: 'complete', agent: request.agent };
1040
- const lifecycleOptions = { repoRoot, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) };
1046
+ const lifecycleOptions = {
1047
+ repoRoot,
1048
+ ...(options.controlRoot ? { controlRoot: options.controlRoot } : {}),
1049
+ ...(options.controlProject ? { controlProject: options.controlProject } : {}),
1050
+ ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {})
1051
+ };
1041
1052
  const result = options.lifecycle
1042
1053
  ? options.lifecycle(lifecycleRequest, lifecycleOptions)
1043
1054
  : applyTaskLifecycleWithinTaskLock(lifecycleRequest, lifecycleOptions, resolved.taskId);