@fitlab-ai/agent-infra 0.9.10 → 0.9.11

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 (269) hide show
  1. package/README.md +13 -3
  2. package/README.zh-CN.md +13 -3
  3. package/bin/cli.ts +17 -4
  4. package/dist/bin/cli.js +19 -4
  5. package/dist/bin/internal-cli.js +50 -4
  6. package/dist/lib/agent-client.js +0 -1
  7. package/dist/lib/agent-clients/adapters/opencode-sandbox.js +0 -4
  8. package/dist/lib/agent-clients/adapters/traecli.js +7 -8
  9. package/dist/lib/agent-clients/config.js +14 -99
  10. package/dist/lib/agent-clients/reconcile.js +1 -11
  11. package/dist/lib/agent-clients/schema.js +5 -4
  12. package/dist/lib/agent-clients/tokens.js +5 -15
  13. package/dist/lib/decide.js +14 -30
  14. package/dist/lib/defaults.json +4 -0
  15. package/dist/lib/init.js +2 -2
  16. package/dist/lib/internal/release-workflow.js +0 -1
  17. package/dist/lib/internal/task-finalization.js +19 -9
  18. package/dist/lib/internal/task-ledger.js +8 -2
  19. package/dist/lib/internal/task-lifecycle.js +17 -93
  20. package/dist/lib/internal/task-orchestration.js +30 -216
  21. package/dist/lib/internal/task-validate.js +2 -1
  22. package/dist/lib/merge.js +205 -103
  23. package/dist/lib/process-data/normalize.js +2 -9
  24. package/dist/lib/run/host.js +46 -16
  25. package/dist/lib/sandbox/agent-client-reconciler.js +4 -12
  26. package/dist/lib/sandbox/commands/create.js +3 -3
  27. package/dist/lib/sandbox/commands/enter.js +18 -4
  28. package/dist/lib/sandbox/config.js +3 -12
  29. package/dist/lib/sandbox/control/executor.js +131 -39
  30. package/dist/lib/sandbox/control/lifecycle.js +29 -20
  31. package/dist/lib/sandbox/control/server.js +18 -16
  32. package/dist/lib/sandbox/mounts.js +3 -2
  33. package/dist/lib/sandbox/recovery.js +123 -22
  34. package/dist/lib/sandbox/workspace-identity.js +14 -0
  35. package/dist/lib/sandbox/workspace-view.js +8 -9
  36. package/dist/lib/self-update.js +278 -0
  37. package/dist/lib/server/protocol.js +5 -5
  38. package/dist/lib/task/activity-log.js +4 -1
  39. package/dist/lib/task/artifact-lifecycle.js +4 -2
  40. package/dist/lib/task/codex-orchestration.js +1 -1
  41. package/dist/lib/task/command-options.js +1 -3
  42. package/dist/lib/task/commands/cat.js +0 -5
  43. package/dist/lib/task/commands/decisions.js +76 -92
  44. package/dist/lib/task/commands/files.js +6 -6
  45. package/dist/lib/task/commands/grep.js +25 -61
  46. package/dist/lib/task/commands/issue-body.js +6 -6
  47. package/dist/lib/task/commands/log.js +25 -10
  48. package/dist/lib/task/commands/show.js +5 -6
  49. package/dist/lib/task/commands/status.js +8 -8
  50. package/dist/lib/task/commit-operation.js +21 -25
  51. package/dist/lib/task/commit-policy.js +1 -1
  52. package/dist/lib/task/control-authority.js +431 -0
  53. package/dist/lib/task/create.js +6 -1
  54. package/dist/lib/task/current-contract.js +35 -0
  55. package/dist/lib/task/decision-details.js +263 -0
  56. package/dist/lib/task/decision-intents.js +5 -2
  57. package/dist/lib/task/events.js +11 -5
  58. package/dist/lib/task/index.js +19 -20
  59. package/dist/lib/task/ledger-intents.js +8 -9
  60. package/dist/lib/task/ledger.js +13 -6
  61. package/dist/lib/task/lifecycle.js +16 -0
  62. package/dist/lib/task/orchestration.js +50 -13
  63. package/dist/lib/task/review-artifacts.js +31 -12
  64. package/dist/lib/task/review-finalization.js +19 -6
  65. package/dist/lib/task/verification-engine.js +38 -15
  66. package/dist/lib/task/verification-review.js +1 -1
  67. package/dist/lib/task/workspace-snapshot.js +5 -11
  68. package/dist/lib/task/write.js +11 -1
  69. package/dist/lib/update.js +6 -37
  70. package/dist/lib/version.js +5 -1
  71. package/dist/lifecycle-build-manifest.json +3 -3
  72. package/lib/agent-client.ts +0 -1
  73. package/lib/agent-clients/adapters/opencode-sandbox.ts +0 -5
  74. package/lib/agent-clients/adapters/traecli.ts +7 -8
  75. package/lib/agent-clients/config.ts +14 -144
  76. package/lib/agent-clients/reconcile.ts +1 -12
  77. package/lib/agent-clients/schema.ts +5 -4
  78. package/lib/agent-clients/tokens.ts +5 -18
  79. package/lib/decide.ts +12 -22
  80. package/lib/defaults.json +4 -0
  81. package/lib/init.ts +2 -2
  82. package/lib/internal/release-workflow.ts +0 -1
  83. package/lib/internal/task-finalization.ts +27 -13
  84. package/lib/internal/task-ledger.ts +8 -2
  85. package/lib/internal/task-lifecycle.ts +33 -76
  86. package/lib/internal/task-orchestration.ts +33 -190
  87. package/lib/internal/task-validate.ts +3 -2
  88. package/lib/merge.ts +236 -120
  89. package/lib/process-data/normalize.ts +2 -8
  90. package/lib/run/host.ts +63 -14
  91. package/lib/sandbox/agent-client-reconciler.ts +5 -15
  92. package/lib/sandbox/commands/create.ts +4 -4
  93. package/lib/sandbox/commands/enter.ts +24 -4
  94. package/lib/sandbox/config.ts +2 -14
  95. package/lib/sandbox/control/container-identity.ts +2 -2
  96. package/lib/sandbox/control/executor.ts +148 -51
  97. package/lib/sandbox/control/lifecycle.ts +34 -28
  98. package/lib/sandbox/control/protocol.ts +1 -3
  99. package/lib/sandbox/control/server.ts +15 -12
  100. package/lib/sandbox/control/state.ts +7 -7
  101. package/lib/sandbox/mounts.ts +4 -3
  102. package/lib/sandbox/recovery.ts +162 -34
  103. package/lib/sandbox/tools.ts +1 -1
  104. package/lib/sandbox/workspace-identity.ts +28 -0
  105. package/lib/sandbox/workspace-view.ts +14 -10
  106. package/lib/self-update.ts +341 -0
  107. package/lib/server/protocol.ts +5 -5
  108. package/lib/task/activity-log.ts +6 -1
  109. package/lib/task/artifact-lifecycle.ts +5 -3
  110. package/lib/task/codex-orchestration.ts +2 -1
  111. package/lib/task/command-options.ts +1 -2
  112. package/lib/task/commands/cat.ts +0 -4
  113. package/lib/task/commands/decisions.ts +80 -75
  114. package/lib/task/commands/files.ts +6 -6
  115. package/lib/task/commands/grep.ts +23 -51
  116. package/lib/task/commands/issue-body.ts +6 -6
  117. package/lib/task/commands/log.ts +22 -10
  118. package/lib/task/commands/show.ts +5 -6
  119. package/lib/task/commands/status.ts +8 -8
  120. package/lib/task/commit-operation.ts +25 -30
  121. package/lib/task/commit-policy.ts +1 -2
  122. package/lib/task/control-authority.ts +563 -0
  123. package/lib/task/create.ts +6 -1
  124. package/lib/task/current-contract.ts +49 -0
  125. package/lib/task/decision-details.ts +340 -0
  126. package/lib/task/decision-intents.ts +7 -2
  127. package/lib/task/events.ts +10 -5
  128. package/lib/task/index.ts +19 -20
  129. package/lib/task/ledger-intents.ts +6 -8
  130. package/lib/task/ledger.ts +17 -7
  131. package/lib/task/lifecycle.ts +20 -0
  132. package/lib/task/orchestration.ts +40 -15
  133. package/lib/task/review-artifacts.ts +53 -15
  134. package/lib/task/review-finalization.ts +33 -7
  135. package/lib/task/verification-engine.ts +33 -18
  136. package/lib/task/verification-review.ts +1 -1
  137. package/lib/task/workspace-snapshot.ts +4 -13
  138. package/lib/task/write.ts +20 -1
  139. package/lib/update.ts +6 -40
  140. package/lib/version.ts +6 -1
  141. package/package.json +1 -1
  142. package/templates/.agents/README.en.md +5 -3
  143. package/templates/.agents/README.zh-CN.md +5 -3
  144. package/templates/.agents/rules/compatibility-policy.en.md +5 -4
  145. package/templates/.agents/rules/compatibility-policy.zh-CN.md +5 -4
  146. package/templates/.agents/rules/create-issue.github.en.md +1 -1
  147. package/templates/.agents/rules/create-issue.github.zh-CN.md +1 -1
  148. package/templates/.agents/rules/human-decision-context.en.md +9 -7
  149. package/templates/.agents/rules/human-decision-context.zh-CN.md +9 -7
  150. package/templates/.agents/rules/issue-fields.github.en.md +1 -1
  151. package/templates/.agents/rules/issue-fields.github.zh-CN.md +1 -1
  152. package/templates/.agents/rules/issue-pr-commands.en.md +1 -1
  153. package/templates/.agents/rules/issue-pr-commands.github.en.md +1 -1
  154. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +1 -1
  155. package/templates/.agents/rules/issue-pr-commands.zh-CN.md +1 -1
  156. package/templates/.agents/rules/issue-sync.github.en.md +1 -1
  157. package/templates/.agents/rules/issue-sync.github.zh-CN.md +2 -2
  158. package/templates/.agents/rules/milestone-inference.github.en.md +1 -1
  159. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +1 -1
  160. package/templates/.agents/rules/next-step-output.en.md +3 -3
  161. package/templates/.agents/rules/next-step-output.zh-CN.md +3 -3
  162. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -1
  163. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -1
  164. package/templates/.agents/rules/pr-sync.en.md +1 -1
  165. package/templates/.agents/rules/pr-sync.github.en.md +1 -1
  166. package/templates/.agents/rules/pr-sync.github.zh-CN.md +1 -1
  167. package/templates/.agents/rules/pr-sync.zh-CN.md +1 -1
  168. package/templates/.agents/rules/release-commands.github.en.md +6 -0
  169. package/templates/.agents/rules/release-commands.github.zh-CN.md +6 -0
  170. package/templates/.agents/rules/review-handshake.en.md +1 -1
  171. package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
  172. package/templates/.agents/rules/task-management.en.md +1 -2
  173. package/templates/.agents/rules/task-management.zh-CN.md +1 -2
  174. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
  175. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  176. package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
  177. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
  178. package/templates/.agents/skills/cancel-task/SKILL.en.md +2 -2
  179. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
  180. package/templates/.agents/skills/check-task/SKILL.en.md +2 -2
  181. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +2 -2
  182. package/templates/.agents/skills/close-codescan/SKILL.en.md +1 -1
  183. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +1 -1
  184. package/templates/.agents/skills/close-dependabot/SKILL.en.md +1 -1
  185. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +1 -1
  186. package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
  187. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
  188. package/templates/.agents/skills/commit/SKILL.en.md +27 -7
  189. package/templates/.agents/skills/commit/SKILL.zh-CN.md +3 -4
  190. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +5 -5
  191. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +5 -5
  192. package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
  193. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
  194. package/templates/.agents/skills/create-pr/SKILL.en.md +5 -7
  195. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +5 -7
  196. package/templates/.agents/skills/create-pr/reference/change-report.en.md +9 -4
  197. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +9 -4
  198. package/templates/.agents/skills/create-pr/scripts/change-report.mjs +203 -0
  199. package/templates/.agents/skills/create-release-note/SKILL.en.md +5 -6
  200. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +5 -6
  201. package/templates/.agents/skills/create-task/SKILL.en.md +1 -1
  202. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
  203. package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
  204. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
  205. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
  206. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  207. package/templates/.agents/skills/restore-task/SKILL.en.md +1 -1
  208. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +1 -1
  209. package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -3
  210. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -3
  211. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -0
  212. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -0
  213. package/templates/.agents/skills/review-code/SKILL.en.md +5 -3
  214. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -3
  215. package/templates/.agents/skills/review-code/config/verify.en.json +1 -0
  216. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -0
  217. package/templates/.agents/skills/review-code/reference/migration-risks.en.md +2 -1
  218. package/templates/.agents/skills/review-code/reference/migration-risks.zh-CN.md +2 -1
  219. package/templates/.agents/skills/review-plan/SKILL.en.md +5 -3
  220. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -3
  221. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -0
  222. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -0
  223. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +4 -0
  224. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +4 -0
  225. package/templates/.agents/skills/run-task/SKILL.en.md +4 -0
  226. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +4 -0
  227. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +2 -2
  228. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +2 -2
  229. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +140 -109
  230. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
  231. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
  232. package/templates/.agents/templates/task.en.md +1 -1
  233. package/templates/.agents/templates/task.zh-CN.md +1 -1
  234. package/templates/.claude/commands/analyze-task.en.md +1 -1
  235. package/templates/.claude/commands/analyze-task.zh-CN.md +1 -1
  236. package/templates/.claude/commands/block-task.en.md +1 -1
  237. package/templates/.claude/commands/block-task.zh-CN.md +1 -1
  238. package/templates/.claude/commands/cancel-task.en.md +1 -1
  239. package/templates/.claude/commands/cancel-task.zh-CN.md +1 -1
  240. package/templates/.claude/commands/check-task.en.md +1 -1
  241. package/templates/.claude/commands/check-task.zh-CN.md +1 -1
  242. package/templates/.claude/commands/code-task.en.md +1 -1
  243. package/templates/.claude/commands/code-task.zh-CN.md +1 -1
  244. package/templates/.claude/commands/commit.en.md +1 -1
  245. package/templates/.claude/commands/commit.zh-CN.md +1 -1
  246. package/templates/.claude/commands/complete-manual-validation.en.md +1 -1
  247. package/templates/.claude/commands/complete-manual-validation.zh-CN.md +1 -1
  248. package/templates/.claude/commands/complete-task.en.md +1 -1
  249. package/templates/.claude/commands/complete-task.zh-CN.md +1 -1
  250. package/templates/.claude/commands/create-pr.en.md +1 -1
  251. package/templates/.claude/commands/create-pr.zh-CN.md +1 -1
  252. package/templates/.claude/commands/plan-task.en.md +1 -1
  253. package/templates/.claude/commands/plan-task.zh-CN.md +1 -1
  254. package/templates/.claude/commands/review-analysis.en.md +1 -1
  255. package/templates/.claude/commands/review-analysis.zh-CN.md +1 -1
  256. package/templates/.claude/commands/review-code.en.md +1 -1
  257. package/templates/.claude/commands/review-code.zh-CN.md +1 -1
  258. package/templates/.claude/commands/review-plan.en.md +1 -1
  259. package/templates/.claude/commands/review-plan.zh-CN.md +1 -1
  260. package/templates/.claude/commands/run-manual-validation.en.md +1 -1
  261. package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
  262. package/templates/.claude/commands/run-task.en.md +1 -1
  263. package/templates/.claude/commands/run-task.zh-CN.md +1 -1
  264. package/templates/.claude/commands/watch-pr.en.md +1 -1
  265. package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
  266. package/templates/{.trae/skills → .traecli/commands}/update-agent-infra.en.md +0 -1
  267. package/templates/{.trae/skills → .traecli/commands}/update-agent-infra.zh-CN.md +0 -1
  268. package/dist/lib/builtin-tuis.js +0 -40
  269. package/lib/builtin-tuis.ts +0 -56
@@ -4,7 +4,6 @@ import { homedir, platform } from 'node:os';
4
4
  import { execFileSync } from 'node:child_process';
5
5
  import pc from 'picocolors';
6
6
  import { normalizeAgentClients } from '../agent-clients/config.ts';
7
- import { isAgentClientId } from '../agent-clients/types.ts';
8
7
  import type { AgentClientState } from '../agent-clients/types.ts';
9
8
  import { validateSandboxEngine } from './engine.ts';
10
9
  import type { SandboxEngineInput } from './engine.ts';
@@ -64,7 +63,6 @@ export type SandboxConfig = {
64
63
  tools: string[];
65
64
  customTools: SandboxTool[];
66
65
  agentClientState: AgentClientState;
67
- agentClientSource: 'canonical' | 'legacy';
68
66
  refreshIntervalDays: number;
69
67
  dockerfile: string | null;
70
68
  vm: SandboxVmConfig;
@@ -74,7 +72,6 @@ type AircConfig = {
74
72
  project?: unknown;
75
73
  org?: unknown;
76
74
  agentClients?: unknown;
77
- tuis?: unknown;
78
75
  sandbox?: SandboxConfigInput;
79
76
  };
80
77
 
@@ -110,17 +107,10 @@ function cloneDefaults(): SandboxConfigInput & { vm: SandboxVmConfig; runtimes:
110
107
 
111
108
  function resolveSandboxToolIds(
112
109
  sandbox: SandboxConfigInput,
113
- agentClients: ReturnType<typeof normalizeAgentClients>,
114
110
  defaults: readonly string[]
115
111
  ): string[] {
116
112
  if (Array.isArray(sandbox.tools)) {
117
- const tools = sandbox.tools.filter((tool): tool is string => typeof tool === 'string');
118
- return agentClients.source === 'canonical'
119
- ? tools.filter((tool) => !isAgentClientId(tool))
120
- : tools;
121
- }
122
- if (agentClients.remainingSandboxTools !== undefined) {
123
- return [...agentClients.remainingSandboxTools];
113
+ return sandbox.tools.filter((tool): tool is string => typeof tool === 'string');
124
114
  }
125
115
  return [...defaults];
126
116
  }
@@ -142,7 +132,6 @@ export function loadConfig({
142
132
  }
143
133
 
144
134
  const airc = JSON.parse(fs.readFileSync(configPath, 'utf8')) as AircConfig;
145
- const sandboxInput = airc.sandbox;
146
135
  const agentClients = normalizeAgentClients(airc);
147
136
  const defaults = cloneDefaults();
148
137
  const sandbox = airc.sandbox ?? {};
@@ -178,7 +167,7 @@ export function loadConfig({
178
167
  }
179
168
 
180
169
  const customTools = parseCustomTools(sandbox.customTools, { home });
181
- const tools = resolveSandboxToolIds(sandbox, agentClients, defaults.tools);
170
+ const tools = resolveSandboxToolIds(sandbox, defaults.tools);
182
171
 
183
172
  return {
184
173
  repoRoot,
@@ -199,7 +188,6 @@ export function loadConfig({
199
188
  tools,
200
189
  customTools,
201
190
  agentClientState: agentClients.state,
202
- agentClientSource: agentClients.source,
203
191
  refreshIntervalDays: asNonNegativeIntegerOrDefault(
204
192
  sandbox.refreshIntervalDays,
205
193
  defaults.refreshIntervalDays
@@ -1,4 +1,4 @@
1
- import type { SandboxControlManifest, SandboxControlManifestLike } from './protocol.ts';
1
+ import type { SandboxControlManifest } from './protocol.ts';
2
2
  import { commandForEngine, runEngine, runProbe } from '../shell.ts';
3
3
 
4
4
  export type ContainerObservation =
@@ -48,7 +48,7 @@ export function classifySandboxContainerInspection(
48
48
  }
49
49
 
50
50
  export async function inspectSandboxControlContainer(
51
- manifest: SandboxControlManifestLike,
51
+ manifest: SandboxControlManifest,
52
52
  options: { run?: typeof runEngine; probe?: typeof runProbe; timeoutMs?: number } = {}
53
53
  ): Promise<ContainerObservation> {
54
54
  try {
@@ -1,13 +1,19 @@
1
- import { spawn, spawnSync, type ChildProcess } from 'node:child_process';
1
+ import { spawn, type ChildProcess } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { randomUUID } from 'node:crypto';
5
5
  import { getProcessStartTime } from '../../server/process-state.ts';
6
6
  import { createTask } from '../../task/create-service.ts';
7
7
  import { applyTaskFinalization } from '../../task/finalization.ts';
8
- import { verifyTaskEvent } from '../../task/verification.ts';
9
- import { bindSandboxControlTask, type SandboxControlExecution, type SandboxControlManifest, type SandboxControlRequest } from './protocol.ts';
10
- import { atomicWriteJson, executionPath, terminateSandboxControlExecution } from './state.ts';
8
+ import { bindSandboxControlTask, validateSandboxControlRequest, type SandboxControlExecution, type SandboxControlManifest, type SandboxControlRequest } from './protocol.ts';
9
+ import { atomicWriteJson, executionPath, readActiveLease, terminateSandboxControlExecution } from './state.ts';
10
+ import {
11
+ createSandboxExecutorExecutionContext,
12
+ dispatchTaskControlOperation,
13
+ parseTaskControlOperation,
14
+ type TaskControlOperation
15
+ } from '../../task/control-authority.ts';
16
+ import { assertSandboxControlBrokerOwner, readSandboxControlManifest, type BrokerOwner } from './lifecycle.ts';
11
17
  import { computeLifecycleBuildIdentity } from '../../agent-clients/adapters/codex-lifecycle/build-identity.ts';
12
18
  import {
13
19
  closeCodexControllerRegistration,
@@ -29,6 +35,26 @@ export type PreparedSandboxControlExecution = {
29
35
  terminate(updateState?: boolean): boolean;
30
36
  };
31
37
 
38
+ function sameBrokerOwner(left: BrokerOwner, right: BrokerOwner): boolean {
39
+ return left.version === right.version
40
+ && left.pid === right.pid
41
+ && left.startTime === right.startTime
42
+ && left.brokerId === right.brokerId
43
+ && left.token === right.token
44
+ && left.generation === right.generation;
45
+ }
46
+
47
+ export function assertSandboxControlExecutorAuthority(
48
+ manifest: SandboxControlManifest,
49
+ gateOwner?: BrokerOwner
50
+ ): void {
51
+ const owner = assertSandboxControlBrokerOwner(manifest);
52
+ if (gateOwner && !sameBrokerOwner(owner, gateOwner)) {
53
+ throw new Error('SANDBOX_CONTROL_GATE_OWNER_MISMATCH');
54
+ }
55
+ if (readActiveLease(manifest)) throw new Error('SANDBOX_CONTROL_HANDOFF_ACTIVE');
56
+ }
57
+
32
58
  function safeEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
33
59
  return Object.fromEntries(Object.entries(env).filter(([key]) => !key.toUpperCase().startsWith('AGENT_INFRA_CONTROL_')));
34
60
  }
@@ -104,9 +130,11 @@ export async function prepareSandboxControlExecution(params: {
104
130
  start(canWrite = () => true) {
105
131
  if (!child.connected) throw new Error('SANDBOX_CONTROL_EXECUTOR_GATE_CLOSED');
106
132
  if (!canWrite()) throw new Error('SANDBOX_CONTROL_OWNER_LOST');
133
+ const gateOwner = assertSandboxControlBrokerOwner(params.manifest);
134
+ if (!canWrite()) throw new Error('SANDBOX_CONTROL_OWNER_LOST');
107
135
  atomicWriteJson(executionPath(params.manifest, params.request.id), { ...execution, phase: 'running', updatedAt: Date.now() });
108
136
  if (!canWrite()) throw new Error('SANDBOX_CONTROL_OWNER_LOST');
109
- child.send({ version: 1, nonce });
137
+ child.send({ version: 1, nonce, owner: gateOwner });
110
138
  },
111
139
  completion,
112
140
  terminate(updateState = true) {
@@ -120,7 +148,7 @@ export async function prepareSandboxControlExecution(params: {
120
148
  };
121
149
  }
122
150
 
123
- function waitForGate(nonce: string, timeoutMs = 2_000): Promise<void> {
151
+ function waitForGate(nonce: string, timeoutMs = 2_000): Promise<BrokerOwner> {
124
152
  return new Promise((resolve, reject) => {
125
153
  const timer = setTimeout(() => reject(new Error('SANDBOX_CONTROL_EXECUTOR_GATE_TIMEOUT')), timeoutMs);
126
154
  const onDisconnect = () => {
@@ -131,12 +159,21 @@ function waitForGate(nonce: string, timeoutMs = 2_000): Promise<void> {
131
159
  process.once('message', (message: unknown) => {
132
160
  clearTimeout(timer);
133
161
  process.off('disconnect', onDisconnect);
134
- const value = message as { version?: unknown; nonce?: unknown } | null;
135
- if (!value || value.version !== 1 || value.nonce !== nonce) {
162
+ const value = message as {
163
+ version?: unknown;
164
+ nonce?: unknown;
165
+ owner?: Partial<BrokerOwner> | null;
166
+ } | null;
167
+ const owner = value?.owner;
168
+ if (!value || value.version !== 1 || value.nonce !== nonce
169
+ || !owner || owner.version !== 3 || !Number.isSafeInteger(owner.pid) || (owner.pid ?? 0) <= 0
170
+ || typeof owner.startTime !== 'number' || !Number.isSafeInteger(owner.startTime)
171
+ || typeof owner.brokerId !== 'string' || owner.brokerId.length === 0
172
+ || typeof owner.token !== 'string' || typeof owner.generation !== 'string') {
136
173
  reject(new Error('SANDBOX_CONTROL_EXECUTOR_GATE_INVALID'));
137
174
  return;
138
175
  }
139
- resolve();
176
+ resolve(owner as BrokerOwner);
140
177
  });
141
178
  });
142
179
  }
@@ -170,6 +207,17 @@ function orchestrationFailure(code: string, message: string): SandboxControlExec
170
207
  };
171
208
  }
172
209
 
210
+ function lifecycleFailure(code: string, message: string): SandboxControlExecutionResult {
211
+ return {
212
+ exitCode: 1,
213
+ stdout: `${JSON.stringify({
214
+ status: 'failed', changed: false,
215
+ error: { code, message }
216
+ })}\n`,
217
+ stderr: ''
218
+ };
219
+ }
220
+
173
221
  function finalizationResult(result: ReturnType<typeof applyTaskFinalization>): SandboxControlExecutionResult {
174
222
  const payload = {
175
223
  version: 1,
@@ -186,28 +234,17 @@ function finalizationResult(result: ReturnType<typeof applyTaskFinalization>): S
186
234
  };
187
235
  }
188
236
 
189
- function isCodexPrepare(args: readonly string[]): boolean {
190
- if (args[1] !== 'prepare') return false;
191
- const values: string[] = [];
192
- for (let index = 2; index < args.length; index += 1) {
193
- if (args[index] === '--client') values.push(args[index + 1] ?? '');
194
- else if (args[index]?.startsWith('--client=')) values.push(args[index]!.slice('--client='.length));
195
- }
196
- return values.length === 1 && values[0] === 'codex';
197
- }
198
-
199
237
  type ExecuteRequestOptions = Readonly<{
200
238
  buildIdentity?: typeof computeLifecycleBuildIdentity;
201
239
  resolveControllerBinding?: typeof resolveCodexControllerBinding;
202
- spawnDomain?: typeof spawnSync;
203
240
  }>;
204
241
 
205
- export function executeRequest(
242
+ export async function executeRequest(
206
243
  manifest: SandboxControlManifest,
207
244
  manifestPath: string,
208
245
  request: SandboxControlRequest,
209
246
  options: ExecuteRequestOptions = {}
210
- ): SandboxControlExecutionResult {
247
+ ): Promise<SandboxControlExecutionResult> {
211
248
  if (request.family === 'task-create') {
212
249
  const result = createTask(request.candidate, { repoRoot: manifest.repoRoot });
213
250
  return {
@@ -260,20 +297,47 @@ export function executeRequest(
260
297
  }
261
298
  }
262
299
  if (request.family === 'task-finalization') {
263
- return finalizationResult(applyTaskFinalization(
264
- { taskRef: manifest.taskId!, intent: 'complete', agent: request.agent },
265
- { repoRoot: manifest.repoRoot, preflight: (input, options) => verifyTaskEvent(
266
- { ...input, event: 'complete-task.hard-preflight' }, options
267
- ) }
268
- ));
300
+ const operation = parseTaskControlOperation(
301
+ 'task-finalization', [manifest.taskId!, 'complete', '--agent', request.agent]
302
+ );
303
+ if (operation.family !== 'task-finalization') throw new Error('SANDBOX_CONTROL_FINALIZATION_OPERATION_INVALID');
304
+ const result = dispatchTaskControlOperation(
305
+ createSandboxExecutorExecutionContext({
306
+ repoRoot: manifest.repoRoot,
307
+ worktreeRoot: manifest.worktreeRoot,
308
+ runtimeDir: manifest.runtimeDir,
309
+ taskId: manifest.taskId!,
310
+ generation: manifest.generation,
311
+ manifestPath,
312
+ requestId: request.id
313
+ }),
314
+ operation
315
+ );
316
+ return finalizationResult(result);
269
317
  }
270
318
  const boundArgs = bindSandboxControlTask(request, manifest.taskId!);
319
+ if (request.family === 'task-orchestration') {
320
+ boundArgs.push('--git-worktree-root', manifest.worktreeRoot);
321
+ }
322
+ let operation: TaskControlOperation;
323
+ try {
324
+ operation = parseTaskControlOperation(request.family, boundArgs);
325
+ } catch (error) {
326
+ const message = error instanceof Error ? error.message : String(error);
327
+ const code = /^([A-Z][A-Z0-9_]+)/u.exec(message)?.[1] ?? 'TASK_CONTROL_OPERATION_INVALID';
328
+ return request.family === 'task-lifecycle'
329
+ ? lifecycleFailure('LIFECYCLE_PAYLOAD_INVALID', message)
330
+ : orchestrationFailure(code, message);
331
+ }
271
332
  let controllerBinding: Readonly<{
272
333
  instanceDigest: string;
273
334
  controlGeneration: string;
274
335
  }> | null = null;
275
336
  if (request.family === 'task-orchestration') {
276
- if (isCodexPrepare(request.args)) {
337
+ const codexPrepare = operation.family === 'task-orchestration'
338
+ && operation.intent === 'prepare'
339
+ && operation.input.client === 'codex';
340
+ if (codexPrepare) {
277
341
  if (!request.controllerProof) {
278
342
  return orchestrationFailure(
279
343
  'CODEX_SANDBOX_CONTROLLER_PROOF_REQUIRED',
@@ -299,36 +363,69 @@ export function executeRequest(
299
363
  'Controller proof is only accepted for canonical Codex prepare'
300
364
  );
301
365
  }
302
- boundArgs.push('--git-worktree-root', manifest.worktreeRoot);
303
366
  }
304
- const result = (options.spawnDomain ?? spawnSync)(
305
- process.execPath,
306
- nodeEntryArgs(process.argv[1]!, [request.family, ...boundArgs]),
307
- {
308
- cwd: manifest.repoRoot,
309
- encoding: 'utf8',
310
- env: {
311
- ...safeEnv(process.env),
312
- AGENT_INFRA_RUNTIME_DIR: manifest.runtimeDir,
313
- ...(controllerBinding
314
- ? { AGENT_INFRA_CONTROL_CONTROLLER_BINDING: JSON.stringify(controllerBinding) }
315
- : {})
316
- }
317
- }
318
- );
319
- return { exitCode: result.status ?? 1, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
367
+ const context = createSandboxExecutorExecutionContext({
368
+ repoRoot: manifest.repoRoot,
369
+ worktreeRoot: manifest.worktreeRoot,
370
+ runtimeDir: manifest.runtimeDir,
371
+ taskId: manifest.taskId!,
372
+ generation: manifest.generation,
373
+ manifestPath,
374
+ requestId: request.id,
375
+ ...(controllerBinding ? { controllerBinding } : {})
376
+ });
377
+ const format = (value: unknown): SandboxControlExecutionResult => {
378
+ const result = value as { status?: string };
379
+ return {
380
+ exitCode: result.status === 'failed' ? 1 : 0,
381
+ stdout: `${JSON.stringify(value)}\n`,
382
+ stderr: ''
383
+ };
384
+ };
385
+ if (request.family === 'task-orchestration') {
386
+ if (operation.family !== 'task-orchestration') throw new Error('SANDBOX_CONTROL_ORCHESTRATION_OPERATION_INVALID');
387
+ return format(await dispatchTaskControlOperation(context, operation));
388
+ }
389
+ if (operation.family !== 'task-lifecycle') throw new Error('SANDBOX_CONTROL_LIFECYCLE_OPERATION_INVALID');
390
+ return format(dispatchTaskControlOperation(context, operation));
320
391
  }
321
392
 
322
393
  export async function runSandboxControlExecutor(requestPath: string, nonce: string): Promise<void> {
323
- await waitForGate(nonce);
394
+ const gateOwner = await waitForGate(nonce);
324
395
  process.disconnect?.();
325
- const root = fs.realpathSync.native(process.cwd());
326
- const request = JSON.parse(fs.readFileSync(requestPath, 'utf8')) as SandboxControlRequest;
327
396
  const manifestPath = process.env.AGENT_INFRA_EXECUTOR_MANIFEST;
328
397
  if (!manifestPath) throw new Error('SANDBOX_CONTROL_EXECUTOR_MANIFEST_MISSING');
329
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as SandboxControlManifest;
398
+ const manifest = readSandboxControlManifest(manifestPath);
399
+ const root = fs.realpathSync.native(process.cwd());
330
400
  if (fs.realpathSync.native(manifest.repoRoot) !== root) throw new Error('SANDBOX_CONTROL_EXECUTOR_ROOT_INVALID');
331
- const result = executeRequest(manifest, manifestPath, request);
401
+ const raw = JSON.parse(fs.readFileSync(requestPath, 'utf8')) as unknown;
402
+ const request = validateSandboxControlRequest(raw, manifest);
403
+ const requestDirectory = path.resolve(path.dirname(requestPath));
404
+ const processingDirectory = path.resolve(path.join(manifest.processingDir, request.id));
405
+ const channelRequestDirectory = path.resolve(path.join(manifest.channelDir, 'requests'));
406
+ if (requestDirectory !== processingDirectory && requestDirectory !== channelRequestDirectory) {
407
+ throw new Error('SANDBOX_CONTROL_EXECUTOR_REQUEST_INVALID');
408
+ }
409
+ let result: SandboxControlExecutionResult;
410
+ try {
411
+ assertSandboxControlExecutorAuthority(manifest, gateOwner);
412
+ result = await executeRequest(manifest, manifestPath, request);
413
+ } catch (error) {
414
+ const detail = error instanceof Error ? error.message : String(error);
415
+ const code = /^([A-Z][A-Z0-9_]+)/u.exec(detail)?.[1] ?? 'SANDBOX_CONTROL_EXECUTOR_FAILED';
416
+ result = request.family === 'task-finalization'
417
+ ? {
418
+ exitCode: 1,
419
+ stdout: `${JSON.stringify({
420
+ version: 1, status: 'failed', changed: false, accepted: true, result: null,
421
+ error: { code, message: detail, retryable: false }
422
+ })}\n`,
423
+ stderr: ''
424
+ }
425
+ : request.family === 'task-lifecycle'
426
+ ? lifecycleFailure(code, detail)
427
+ : orchestrationFailure(code, detail);
428
+ }
332
429
  process.stdout.write(result.stdout);
333
430
  process.stderr.write(result.stderr);
334
431
  process.exitCode = result.exitCode;
@@ -10,9 +10,7 @@ import {
10
10
  import type { ProcessIdentity, ProcessIdentityProbe, ProcessIdentityState } from '../../server/process-state.ts';
11
11
  import type {
12
12
  SandboxControlExecution,
13
- SandboxControlManifestLike,
14
13
  SandboxControlManifest,
15
- SandboxControlLegacyManifest,
16
14
  SandboxControlStatus,
17
15
  SandboxControlTimingPolicy
18
16
  } from './protocol.ts';
@@ -468,7 +466,6 @@ function readProcessingSnapshot(root: string): SandboxControlCutoverSnapshot['pr
468
466
  export function captureSandboxControlCutoverSnapshot(root: string): SandboxControlCutoverSnapshot {
469
467
  const resolvedRoot = path.resolve(root);
470
468
  const manifestPath = path.join(resolvedRoot, 'manifest.json');
471
- if (fs.existsSync(manifestPath)) readSandboxControlManifestForTransition(manifestPath);
472
469
  return {
473
470
  root: resolvedRoot,
474
471
  manifestRaw: readOptionalRaw(manifestPath),
@@ -485,10 +482,7 @@ export function assertSandboxControlCutoverSnapshot(snapshot: SandboxControlCuto
485
482
  }
486
483
  }
487
484
 
488
- function readSandboxControlManifestValue(
489
- manifestPath: string,
490
- allowLegacy: boolean
491
- ): SandboxControlManifestLike {
485
+ function readSandboxControlManifestValue(manifestPath: string): SandboxControlManifest {
492
486
  if (!regularFile(manifestPath)) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
493
487
  let manifest: Record<string, unknown>;
494
488
  try {
@@ -500,9 +494,15 @@ function readSandboxControlManifestValue(
500
494
  } catch {
501
495
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
502
496
  }
503
- const candidate = manifest as Partial<SandboxControlManifest> & Partial<SandboxControlLegacyManifest>;
504
- if (candidate.version !== 4 && candidate.version !== 5) {
505
- throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 5; container-only recreation is required');
497
+ const candidate = manifest as Partial<SandboxControlManifest> & { version?: unknown };
498
+ const expectedKeys = [
499
+ 'branch', 'channelDir', 'container', 'containerIdentity', 'engine', 'generation',
500
+ 'mode', 'processingDir', 'project', 'publicStatusDir', 'repoRoot', 'runtimeDir',
501
+ 'taskId', 'token', 'worktreeRoot'
502
+ ];
503
+ if ('version' in candidate || typeof candidate.runtimeDir !== 'string'
504
+ || Object.keys(manifest).sort().join(',') !== expectedKeys.sort().join(',')) {
505
+ throw new Error('SANDBOX_CONTROL_MANIFEST_REBUILD_REQUIRED: container-only recreation/rebuild is required');
506
506
  }
507
507
  if (typeof candidate.containerIdentity !== 'object' || candidate.containerIdentity === null
508
508
  || Array.isArray(candidate.containerIdentity)) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
@@ -518,31 +518,23 @@ function readSandboxControlManifestValue(
518
518
  || (candidate.taskId !== null && typeof candidate.taskId !== 'string')
519
519
  || typeof candidate.channelDir !== 'string' || typeof candidate.publicStatusDir !== 'string'
520
520
  || typeof candidate.processingDir !== 'string' || typeof candidate.token !== 'string'
521
- || typeof candidate.generation !== 'string') throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
521
+ || typeof candidate.generation !== 'string'
522
+ || (candidate.mode === 'task-bound' && (typeof candidate.taskId !== 'string' || candidate.taskId.length === 0))
523
+ || (candidate.mode === 'branch-only' && candidate.taskId !== null)) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
522
524
  const root = path.dirname(path.resolve(manifestPath));
523
525
  if (path.resolve(candidate.channelDir) !== path.join(root, 'channel')
524
526
  || path.resolve(candidate.publicStatusDir) !== path.join(root, 'public')
525
527
  || path.resolve(candidate.processingDir) !== path.join(root, 'processing')) {
526
528
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
527
529
  }
528
- if (candidate.version === 4) {
529
- if (!allowLegacy) {
530
- throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 5; container-only recreation is required');
531
- }
532
- return candidate as unknown as SandboxControlLegacyManifest;
533
- }
534
- if (typeof candidate.runtimeDir !== 'string' || path.resolve(candidate.runtimeDir) !== path.join(root, 'runtime')) {
530
+ if (path.resolve(candidate.runtimeDir) !== path.join(root, 'runtime')) {
535
531
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
536
532
  }
537
- return candidate as unknown as SandboxControlManifest;
533
+ return candidate as SandboxControlManifest;
538
534
  }
539
535
 
540
536
  export function readSandboxControlManifest(manifestPath: string): SandboxControlManifest {
541
- return readSandboxControlManifestValue(manifestPath, false) as SandboxControlManifest;
542
- }
543
-
544
- export function readSandboxControlManifestForTransition(manifestPath: string): SandboxControlManifestLike {
545
- return readSandboxControlManifestValue(manifestPath, true);
537
+ return readSandboxControlManifestValue(manifestPath);
546
538
  }
547
539
 
548
540
  function readBrokerOwner(filePath: string): BrokerOwner | null {
@@ -561,6 +553,20 @@ function readBrokerOwner(filePath: string): BrokerOwner | null {
561
553
  return owner as BrokerOwner;
562
554
  }
563
555
 
556
+ export function assertSandboxControlBrokerOwner(
557
+ manifest: SandboxControlManifest,
558
+ options: Readonly<{ identityProbe?: ProcessIdentityProbe }> = {}
559
+ ): BrokerOwner {
560
+ const root = path.dirname(path.resolve(manifest.publicStatusDir));
561
+ const owner = readBrokerOwner(path.join(root, 'broker.json'));
562
+ if (!owner) throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
563
+ if (owner.token !== manifest.token || owner.generation !== manifest.generation) {
564
+ throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
565
+ }
566
+ if (!ownerLive(owner, options.identityProbe)) throw new Error('SANDBOX_CONTROL_OWNER_LOST');
567
+ return owner;
568
+ }
569
+
564
570
  function readStatusOwner(filePath: string): SandboxControlStatus | null {
565
571
  if (!fs.existsSync(filePath)) return null;
566
572
  if (!regularFile(filePath)) throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
@@ -585,7 +591,7 @@ function ownerLive(
585
591
  return state === 'alive';
586
592
  }
587
593
 
588
- function readExecutions(manifest: SandboxControlManifestLike): SandboxControlExecution[] {
594
+ function readExecutions(manifest: SandboxControlManifest): SandboxControlExecution[] {
589
595
  if (!fs.existsSync(manifest.processingDir)) return [];
590
596
  const stat = fs.lstatSync(manifest.processingDir);
591
597
  if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
@@ -705,7 +711,7 @@ export async function quiesceSandboxControlRoot(
705
711
  await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()), identityProbe);
706
712
 
707
713
  const manifestPath = path.join(resolvedRoot, 'manifest.json');
708
- const manifest = fs.existsSync(manifestPath) ? readSandboxControlManifestForTransition(manifestPath) : null;
714
+ const manifest = fs.existsSync(manifestPath) ? readSandboxControlManifest(manifestPath) : null;
709
715
  const broker = readBrokerOwner(path.join(resolvedRoot, 'broker.json'));
710
716
  const status = readStatusOwner(path.join(resolvedRoot, 'public', 'status.json'));
711
717
  const statusOwner = status ? status.broker : null;
@@ -799,7 +805,7 @@ export async function removeSandboxControlRoot(
799
805
  const stat = fs.lstatSync(resolvedRoot);
800
806
  if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
801
807
  const manifestPath = path.join(resolvedRoot, 'manifest.json');
802
- const manifest = readSandboxControlManifestForTransition(manifestPath);
808
+ const manifest = readSandboxControlManifest(manifestPath);
803
809
  const inspectContainer = options.inspectContainer
804
810
  ?? ((timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }));
805
811
  const timeoutMs = options.timeoutMs ?? options.timing?.quiesceDeadlineMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
@@ -870,7 +876,7 @@ export async function removeSandboxControlRoot(
870
876
  }
871
877
  }
872
878
 
873
- const currentManifest = readSandboxControlManifestForTransition(manifestPath);
879
+ const currentManifest = readSandboxControlManifest(manifestPath);
874
880
  if (currentManifest.token !== manifest.token || currentManifest.generation !== manifest.generation
875
881
  || !isSandboxControlRootQuiescing(resolvedRoot)) {
876
882
  throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
@@ -35,9 +35,7 @@ export type SandboxControlManifestBase = Readonly<{
35
35
  branch: string; mode: 'task-bound' | 'branch-only'; taskId: string | null; token: string;
36
36
  generation: string; channelDir: string; publicStatusDir: string; processingDir: string;
37
37
  }>;
38
- export type SandboxControlLegacyManifest = SandboxControlManifestBase & Readonly<{ version: 4 }>;
39
- export type SandboxControlManifest = SandboxControlManifestBase & Readonly<{ version: 5; runtimeDir: string }>;
40
- export type SandboxControlManifestLike = SandboxControlLegacyManifest | SandboxControlManifest;
38
+ export type SandboxControlManifest = SandboxControlManifestBase & Readonly<{ runtimeDir: string }>;
41
39
  export type SandboxControlBrokerOwner = ProcessIdentity & Readonly<{
42
40
  version: 3;
43
41
  brokerId: string;
@@ -228,6 +228,7 @@ export async function serveSandboxControl(
228
228
  appendSandboxControlAudit(manifest, 'broker-start', { pid: broker.pid });
229
229
  if (!recoverProcessing(manifest, brokerOwns)) return;
230
230
  while (!signal.aborted) {
231
+ let settledExecution: ActiveExecution | null = null;
231
232
  if (!brokerOwns()) break;
232
233
  let current: SandboxControlManifest;
233
234
  try {
@@ -277,18 +278,7 @@ export async function serveSandboxControl(
277
278
 
278
279
  if (active?.settled) {
279
280
  if (!brokerOwns()) break;
280
- if (active.result) {
281
- if (!brokerOwns()) break;
282
- writeResponse(manifest, {
283
- version: 2, id: active.request.id, phase: 'completed', exitCode: active.result.exitCode,
284
- stdout: active.result.stdout, stderr: active.result.stderr, error: null
285
- });
286
- } else {
287
- if (!brokerOwns()) break;
288
- writeResponse(manifest, unknown(active.request.id));
289
- }
290
- if (!brokerOwns()) break;
291
- fs.rmSync(path.join(manifest.processingDir, active.request.id), { recursive: true, force: true });
281
+ settledExecution = active;
292
282
  active = null;
293
283
  }
294
284
 
@@ -327,6 +317,19 @@ export async function serveSandboxControl(
327
317
  appendSandboxControlAudit(manifest, 'broker-state', { state, reasonCode, requestId: active?.request.id ?? null });
328
318
  lastState = stateKey;
329
319
  }
320
+ if (settledExecution) {
321
+ if (!brokerOwns()) break;
322
+ if (settledExecution.result) {
323
+ writeResponse(manifest, {
324
+ version: 2, id: settledExecution.request.id, phase: 'completed', exitCode: settledExecution.result.exitCode,
325
+ stdout: settledExecution.result.stdout, stderr: settledExecution.result.stderr, error: null
326
+ });
327
+ } else {
328
+ writeResponse(manifest, unknown(settledExecution.request.id));
329
+ }
330
+ if (!brokerOwns()) break;
331
+ fs.rmSync(path.join(manifest.processingDir, settledExecution.request.id), { recursive: true, force: true });
332
+ }
330
333
 
331
334
  let retiring = false;
332
335
  for (const name of fs.readdirSync(requestsDir).sort()) {
@@ -8,7 +8,7 @@ import type { ProcessIdentity, ProcessIdentityProbe } from '../../server/process
8
8
  import type {
9
9
  SandboxControlExecution,
10
10
  SandboxControlLease,
11
- SandboxControlManifestLike,
11
+ SandboxControlManifest,
12
12
  SandboxControlStatus
13
13
  } from './protocol.ts';
14
14
 
@@ -36,12 +36,12 @@ export function readJsonFile(filePath: string): unknown {
36
36
  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
37
37
  }
38
38
 
39
- export function statusPath(manifest: SandboxControlManifestLike): string {
39
+ export function statusPath(manifest: SandboxControlManifest): string {
40
40
  return path.join(manifest.publicStatusDir, 'status.json');
41
41
  }
42
42
 
43
43
  export function writeSandboxControlStatus(
44
- manifest: SandboxControlManifestLike,
44
+ manifest: SandboxControlManifest,
45
45
  broker: ProcessIdentity & { brokerId: string },
46
46
  state: SandboxControlStatus['state'],
47
47
  reasonCode: string | null,
@@ -72,7 +72,7 @@ export function readSandboxControlStatus(directory: string): SandboxControlStatu
72
72
  return parseSandboxControlStatus(readJsonFile(path.join(directory, 'status.json')));
73
73
  }
74
74
 
75
- export function readActiveLease(manifest: SandboxControlManifestLike, now = Date.now()): SandboxControlLease | null {
75
+ export function readActiveLease(manifest: SandboxControlManifest, now = Date.now()): SandboxControlLease | null {
76
76
  const filePath = path.join(path.dirname(manifest.publicStatusDir), 'lease.json');
77
77
  if (!fs.existsSync(filePath)) return null;
78
78
  const lease = readJsonFile(filePath) as Partial<SandboxControlLease> | null;
@@ -91,7 +91,7 @@ export function readActiveLease(manifest: SandboxControlManifestLike, now = Date
91
91
  }
92
92
 
93
93
  export function cleanupStaleSandboxControlLease(
94
- manifest: SandboxControlManifestLike,
94
+ manifest: SandboxControlManifest,
95
95
  now = Date.now(),
96
96
  options: Readonly<{ identityProbe?: ProcessIdentityProbe }> = {}
97
97
  ): boolean {
@@ -121,7 +121,7 @@ export function cleanupStaleSandboxControlLease(
121
121
  }
122
122
 
123
123
  export function appendSandboxControlAudit(
124
- manifest: SandboxControlManifestLike,
124
+ manifest: SandboxControlManifest,
125
125
  event: string,
126
126
  fields: Record<string, string | number | boolean | null> = {},
127
127
  now = Date.now()
@@ -136,7 +136,7 @@ export function appendSandboxControlAudit(
136
136
  });
137
137
  }
138
138
 
139
- export function executionPath(manifest: SandboxControlManifestLike, requestId: string): string {
139
+ export function executionPath(manifest: SandboxControlManifest, requestId: string): string {
140
140
  return path.join(manifest.processingDir, requestId, 'execution.json');
141
141
  }
142
142
 
@@ -27,11 +27,12 @@ export function sandboxCoreBindMounts(
27
27
  controlDir: string;
28
28
  controlStatusDir: string;
29
29
  runtimeDir?: string;
30
- taskSource?: string;
30
+ taskSources?: string[];
31
31
  taskId?: string;
32
32
  }
33
33
  ): SandboxBindMountDeclaration[] {
34
- const taskBound = Boolean(overrides.taskSource && overrides.taskId);
34
+ const taskSources = overrides.taskSources ?? [];
35
+ const taskBound = Boolean(taskSources.length > 0 && overrides.taskId);
35
36
  const workspaceMounts = sandboxWorkspaceViewStatePaths(overrides.workspaceViewRoot).map(
36
37
  ({ state, hostPath }) => taskBound && state === 'active'
37
38
  ? {
@@ -57,7 +58,7 @@ export function sandboxCoreBindMounts(
57
58
  ];
58
59
  if (taskBound) {
59
60
  mounts.push({
60
- hostPaths: [overrides.taskSource!],
61
+ hostPaths: taskSources,
61
62
  containerPath: path.posix.join('/workspace/.agents/workspace/active', overrides.taskId!),
62
63
  readOnly: false
63
64
  });