@fitlab-ai/agent-infra 0.9.14 → 0.9.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 (250) hide show
  1. package/README.md +10 -0
  2. package/README.zh-CN.md +10 -0
  3. package/bin/internal-cli.sh +45 -0
  4. package/dist/bin/internal-cli.js +93 -30
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +165 -27
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -2
  7. package/dist/lib/defaults.json +103 -103
  8. package/dist/lib/fs/durable-write.js +37 -0
  9. package/dist/lib/fs/file-lock.js +111 -0
  10. package/dist/lib/host-control/audit.js +19 -0
  11. package/dist/lib/host-control/client.js +142 -0
  12. package/dist/lib/host-control/command.js +49 -0
  13. package/dist/lib/host-control/path.js +160 -0
  14. package/dist/lib/host-control/secure-fs.js +140 -0
  15. package/dist/lib/host-control/server.js +159 -0
  16. package/dist/lib/host-control/service.js +112 -0
  17. package/dist/lib/init.js +11 -2
  18. package/dist/lib/internal/cli-route-inventory.js +3 -1
  19. package/dist/lib/internal/codex-lifecycle.js +6 -6
  20. package/dist/lib/internal/demo-promotion.js +322 -0
  21. package/dist/lib/internal/demo-transcript.js +454 -0
  22. package/dist/lib/internal/host-control.js +28 -0
  23. package/dist/lib/internal/manual-validation.js +322 -0
  24. package/dist/lib/internal/platform-pr.js +18 -4
  25. package/dist/lib/internal/release-workflow.js +63 -84
  26. package/dist/lib/internal/sandbox-control.js +149 -2
  27. package/dist/lib/internal/task-artifact.js +15 -262
  28. package/dist/lib/internal/task-create.js +6 -5
  29. package/dist/lib/internal/task-event.js +37 -37
  30. package/dist/lib/internal/task-operation-registry.js +101 -3
  31. package/dist/lib/internal/task-review.js +8 -53
  32. package/dist/lib/internal/task-validate.js +30 -10
  33. package/dist/lib/owned-path.js +33 -0
  34. package/dist/lib/platform/check-status.js +11 -0
  35. package/dist/lib/platform/comment-safety.js +4 -1
  36. package/dist/lib/platform/completion-backfill.js +1 -1
  37. package/dist/lib/platform/github-data.js +241 -0
  38. package/dist/lib/platform/github-provider.js +15 -20
  39. package/dist/lib/platform/issue-comments.js +2 -2
  40. package/dist/lib/platform/issues.js +1 -179
  41. package/dist/lib/platform/pr-checks.js +12 -65
  42. package/dist/lib/platform/pr-summary.js +55 -3
  43. package/dist/lib/platform/pull-requests.js +2 -105
  44. package/dist/lib/platform/snapshots.js +2 -0
  45. package/dist/lib/platform/verification-sync.js +127 -142
  46. package/dist/lib/release/workflow.js +1 -1
  47. package/dist/lib/sandbox/commands/create.js +7 -2
  48. package/dist/lib/sandbox/commands/rm.js +3 -1514
  49. package/dist/lib/sandbox/control/audit.js +203 -0
  50. package/dist/lib/sandbox/control/client.js +93 -54
  51. package/dist/lib/sandbox/control/executor.js +129 -65
  52. package/dist/lib/sandbox/control/finalization-response.js +27 -0
  53. package/dist/lib/sandbox/control/identity-sentinel.js +107 -0
  54. package/dist/lib/sandbox/control/lifecycle.js +36 -2
  55. package/dist/lib/sandbox/control/native-file-lock.js +6 -98
  56. package/dist/lib/sandbox/control/protocol.js +55 -4
  57. package/dist/lib/sandbox/control/server.js +491 -120
  58. package/dist/lib/sandbox/control/state.js +58 -0
  59. package/dist/lib/sandbox/control/task-workflow.js +74 -0
  60. package/dist/lib/sandbox/control/workflow-executor.js +99 -0
  61. package/dist/lib/sandbox/index.js +2 -2
  62. package/dist/lib/sandbox/recovery.js +28 -4
  63. package/dist/lib/sandbox/removal.js +1717 -0
  64. package/dist/lib/sandbox/task-cleanup.js +217 -0
  65. package/dist/lib/sandbox/workspace-view.js +13 -14
  66. package/dist/lib/task/activity-intent.js +2 -1
  67. package/dist/lib/task/activity-log.js +30 -24
  68. package/dist/lib/task/artifact-command.js +101 -0
  69. package/dist/lib/task/artifact-lifecycle.js +30 -28
  70. package/dist/lib/task/artifact-name.js +36 -0
  71. package/dist/lib/task/artifact-operations.js +23 -69
  72. package/dist/lib/task/artifact-receipts.js +3 -15
  73. package/dist/lib/task/artifact-repair-intent.js +87 -16
  74. package/dist/lib/task/artifact-schema.js +6 -14
  75. package/dist/lib/task/capabilities.js +12 -19
  76. package/dist/lib/task/codex-orchestration.js +71 -25
  77. package/dist/lib/task/commit-identity.js +28 -0
  78. package/dist/lib/task/commit-operation.js +2 -16
  79. package/dist/lib/task/control-authority.js +371 -5
  80. package/dist/lib/task/control-recovery.js +128 -0
  81. package/dist/lib/task/create.js +62 -9
  82. package/dist/lib/task/decision-details.js +2 -46
  83. package/dist/lib/task/events.js +120 -9
  84. package/dist/lib/task/finalization-state.js +9 -0
  85. package/dist/lib/task/human-override.js +3 -6
  86. package/dist/lib/task/implementation-inputs.js +3 -3
  87. package/dist/lib/task/intermediate-cleanup.js +383 -0
  88. package/dist/lib/task/invalidation.js +18 -65
  89. package/dist/lib/task/ledger-intents.js +7 -22
  90. package/dist/lib/task/ledger.js +1 -3
  91. package/dist/lib/task/lifecycle.js +19 -1
  92. package/dist/lib/task/local-artifact-finalization.js +132 -157
  93. package/dist/lib/task/manual-validation-completion.js +26 -0
  94. package/dist/lib/task/manual-validation-evidence.js +130 -0
  95. package/dist/lib/task/manual-validation-receipt.js +101 -0
  96. package/dist/lib/task/manual-validation-shared.js +40 -0
  97. package/dist/lib/task/manual-validation-transaction.js +243 -0
  98. package/dist/lib/task/markdown.js +47 -0
  99. package/dist/lib/task/orchestration.js +14 -17
  100. package/dist/lib/task/qualification-audit.js +20 -34
  101. package/dist/lib/task/review-artifacts.js +2 -18
  102. package/dist/lib/task/review-command.js +25 -0
  103. package/dist/lib/task/review-finalization.js +125 -193
  104. package/dist/lib/task/review-fingerprint.js +2 -2
  105. package/dist/lib/task/sections.js +34 -56
  106. package/dist/lib/task/verification-engine.js +116 -323
  107. package/dist/lib/task/verification.js +1 -1
  108. package/dist/lib/task/workflow-command.js +17 -0
  109. package/dist/lib/task/workflow-warning-intents.js +0 -16
  110. package/dist/lib/task/workflow-warnings.js +28 -2
  111. package/dist/lib/update.js +2 -0
  112. package/dist/lifecycle-build-manifest.json +3 -3
  113. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +223 -33
  114. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -2
  115. package/lib/fs/durable-write.ts +29 -0
  116. package/lib/fs/file-lock.ts +113 -0
  117. package/lib/host-control/audit.ts +22 -0
  118. package/lib/host-control/client.ts +172 -0
  119. package/lib/host-control/command.ts +60 -0
  120. package/lib/host-control/path.ts +167 -0
  121. package/lib/host-control/secure-fs.ts +135 -0
  122. package/lib/host-control/server.ts +180 -0
  123. package/lib/host-control/service.ts +133 -0
  124. package/lib/init.ts +15 -2
  125. package/lib/internal/cli-route-inventory.ts +3 -1
  126. package/lib/internal/codex-lifecycle.ts +6 -6
  127. package/lib/internal/demo-promotion.ts +354 -0
  128. package/lib/internal/demo-transcript.ts +419 -0
  129. package/lib/internal/host-control.ts +28 -0
  130. package/lib/internal/manual-validation.ts +306 -0
  131. package/lib/internal/platform-pr.ts +15 -4
  132. package/lib/internal/release-workflow.ts +74 -82
  133. package/lib/internal/sandbox-control.ts +162 -1
  134. package/lib/internal/task-artifact.ts +13 -216
  135. package/lib/internal/task-create.ts +5 -5
  136. package/lib/internal/task-event.ts +20 -11
  137. package/lib/internal/task-operation-registry.ts +111 -3
  138. package/lib/internal/task-review.ts +6 -51
  139. package/lib/internal/task-validate.ts +26 -10
  140. package/lib/owned-path.ts +26 -0
  141. package/lib/platform/check-status.ts +9 -0
  142. package/lib/platform/comment-safety.ts +5 -0
  143. package/lib/platform/completion-backfill.ts +1 -1
  144. package/lib/platform/github-data.ts +322 -0
  145. package/lib/platform/github-provider.ts +15 -20
  146. package/lib/platform/issue-comments.ts +3 -4
  147. package/lib/platform/issues.ts +4 -245
  148. package/lib/platform/pr-checks.ts +13 -77
  149. package/lib/platform/pr-summary.ts +61 -5
  150. package/lib/platform/pull-requests.ts +3 -170
  151. package/lib/platform/snapshots.ts +53 -0
  152. package/lib/platform/verification-sync.ts +145 -155
  153. package/lib/release/workflow.ts +3 -3
  154. package/lib/sandbox/commands/create.ts +7 -6
  155. package/lib/sandbox/commands/rm.ts +3 -1909
  156. package/lib/sandbox/control/audit.ts +253 -0
  157. package/lib/sandbox/control/client.ts +109 -66
  158. package/lib/sandbox/control/executor.ts +134 -74
  159. package/lib/sandbox/control/finalization-response.ts +29 -0
  160. package/lib/sandbox/control/identity-sentinel.ts +137 -0
  161. package/lib/sandbox/control/lifecycle.ts +45 -3
  162. package/lib/sandbox/control/native-file-lock.ts +6 -107
  163. package/lib/sandbox/control/protocol.ts +75 -6
  164. package/lib/sandbox/control/server.ts +538 -123
  165. package/lib/sandbox/control/state.ts +78 -0
  166. package/lib/sandbox/control/task-workflow.ts +95 -0
  167. package/lib/sandbox/control/workflow-executor.ts +102 -0
  168. package/lib/sandbox/index.ts +2 -2
  169. package/lib/sandbox/recovery.ts +28 -5
  170. package/lib/sandbox/removal.ts +2144 -0
  171. package/lib/sandbox/task-cleanup.ts +209 -0
  172. package/lib/sandbox/workspace-view.ts +17 -15
  173. package/lib/task/activity-intent.ts +2 -2
  174. package/lib/task/activity-log.ts +27 -19
  175. package/lib/task/artifact-command.ts +104 -0
  176. package/lib/task/artifact-lifecycle.ts +31 -26
  177. package/lib/task/artifact-name.ts +55 -0
  178. package/lib/task/artifact-operations.ts +22 -68
  179. package/lib/task/artifact-receipts.ts +3 -14
  180. package/lib/task/artifact-repair-intent.ts +99 -18
  181. package/lib/task/artifact-schema.ts +5 -34
  182. package/lib/task/capabilities.ts +13 -22
  183. package/lib/task/codex-orchestration.ts +85 -24
  184. package/lib/task/commit-identity.ts +31 -0
  185. package/lib/task/commit-operation.ts +2 -16
  186. package/lib/task/control-authority.ts +502 -5
  187. package/lib/task/control-recovery.ts +174 -0
  188. package/lib/task/create.ts +63 -8
  189. package/lib/task/decision-details.ts +3 -71
  190. package/lib/task/events.ts +131 -10
  191. package/lib/task/finalization-state.ts +11 -0
  192. package/lib/task/human-override.ts +3 -8
  193. package/lib/task/implementation-inputs.ts +3 -3
  194. package/lib/task/intermediate-cleanup.ts +455 -0
  195. package/lib/task/invalidation.ts +18 -62
  196. package/lib/task/ledger-intents.ts +7 -24
  197. package/lib/task/ledger.ts +1 -3
  198. package/lib/task/lifecycle.ts +17 -1
  199. package/lib/task/local-artifact-finalization.ts +148 -182
  200. package/lib/task/manual-validation-completion.ts +40 -0
  201. package/lib/task/manual-validation-evidence.ts +209 -0
  202. package/lib/task/manual-validation-receipt.ts +152 -0
  203. package/lib/task/manual-validation-shared.ts +50 -0
  204. package/lib/task/manual-validation-transaction.ts +304 -0
  205. package/lib/task/markdown.ts +70 -0
  206. package/lib/task/orchestration.ts +19 -19
  207. package/lib/task/qualification-audit.ts +22 -33
  208. package/lib/task/review-artifacts.ts +1 -20
  209. package/lib/task/review-command.ts +23 -0
  210. package/lib/task/review-finalization.ts +131 -259
  211. package/lib/task/review-fingerprint.ts +2 -2
  212. package/lib/task/sections.ts +33 -54
  213. package/lib/task/verification-engine.ts +110 -366
  214. package/lib/task/verification-types.ts +17 -0
  215. package/lib/task/verification.ts +1 -1
  216. package/lib/task/workflow-command.ts +18 -0
  217. package/lib/task/workflow-warning-intents.ts +0 -13
  218. package/lib/task/workflow-warnings.ts +21 -2
  219. package/lib/update.ts +6 -0
  220. package/package.json +6 -6
  221. package/templates/.agents/hooks/lifecycle-delegation.js +6 -6
  222. package/templates/.agents/rules/testing-discipline.en.md +2 -0
  223. package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -0
  224. package/templates/.agents/scripts/package.json +3 -0
  225. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +32 -20
  226. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +32 -20
  227. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
  228. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
  229. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +2 -0
  230. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +2 -0
  231. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +16 -7
  232. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +16 -7
  233. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  234. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  235. package/templates/.agents/skills/release/SKILL.en.md +7 -3
  236. package/templates/.agents/skills/release/SKILL.zh-CN.md +7 -3
  237. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +20 -3
  238. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +20 -3
  239. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +4 -1
  240. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +4 -1
  241. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -0
  242. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -0
  243. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  244. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  245. package/templates/.agents/skills/test/SKILL.en.md +17 -6
  246. package/templates/.agents/skills/test/SKILL.zh-CN.md +17 -6
  247. package/templates/.claude/commands/run-manual-validation.en.md +1 -1
  248. package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
  249. package/dist/lib/platform/adapters.js +0 -56
  250. package/lib/platform/adapters.ts +0 -197
@@ -1,3 +1,5 @@
1
+ import { isCompletionEvidence, type CleanCompletionEvidence } from '../../task/orchestration.ts';
2
+ import { parseControlOutput } from '../../task/control-recovery.ts';
1
3
  import { execFileSync } from 'node:child_process';
2
4
  import fs from 'node:fs';
3
5
  import path from 'node:path';
@@ -101,6 +103,82 @@ export function reservationPath(manifest: SandboxControlManifest, requestId: str
101
103
  return path.join(manifest.processingDir, requestId, 'reservation.json');
102
104
  }
103
105
 
106
+ export type SandboxControlTerminalResult = Readonly<{
107
+ version: 1;
108
+ requestId: string;
109
+ generation: string;
110
+ taskId: string | null;
111
+ intentDigest: string;
112
+ targetState: string | null;
113
+ status: string;
114
+ changed: boolean | null;
115
+ completionEvidence: CleanCompletionEvidence | null;
116
+ }>;
117
+
118
+ export function terminalResultPath(manifest: SandboxControlManifest, requestId: string): string {
119
+ if (!/^[a-f0-9-]{16,64}$/u.test(requestId)) throw new Error('SANDBOX_CONTROL_TERMINAL_RESULT_INVALID');
120
+ return path.join(manifest.processingDir, requestId, 'terminal-result.json');
121
+ }
122
+
123
+ export function createSandboxControlTerminalResult(
124
+ manifest: SandboxControlManifest,
125
+ request: Readonly<{ id: string; family: string; operation?: string | null }>,
126
+ output: string
127
+ ): SandboxControlTerminalResult {
128
+ const nested = parseControlOutput(output) ?? {};
129
+ const run = nested.run && typeof nested.run === 'object' && !Array.isArray(nested.run)
130
+ ? nested.run as Record<string, unknown> : null;
131
+ const rawCompletion = nested.completionEvidence ?? run?.completionEvidence;
132
+ const operation = request.operation ?? (typeof nested.intent === 'string' ? nested.intent : null) ?? '';
133
+ return {
134
+ version: 1,
135
+ requestId: request.id,
136
+ generation: manifest.generation,
137
+ taskId: manifest.taskId,
138
+ intentDigest: createHash('sha256').update(`${request.family}\0${operation}`, 'utf8').digest('hex'),
139
+ targetState: typeof nested.targetState === 'string' ? nested.targetState : null,
140
+ status: typeof nested.status === 'string' ? nested.status : 'completed',
141
+ changed: typeof nested.changed === 'boolean' ? nested.changed : null,
142
+ completionEvidence: isCompletionEvidence(rawCompletion) ? rawCompletion : null
143
+ };
144
+ }
145
+
146
+ export function writeSandboxControlTerminalResult(
147
+ manifest: SandboxControlManifest,
148
+ request: Readonly<{ id: string; family: string; operation?: string | null }>,
149
+ output: string
150
+ ): SandboxControlTerminalResult {
151
+ const result = createSandboxControlTerminalResult(manifest, request, output);
152
+ const filePath = terminalResultPath(manifest, request.id);
153
+ try {
154
+ atomicWriteJsonNoReplace(filePath, result);
155
+ } catch (error) {
156
+ if (!(error instanceof Error) || error.message !== 'SANDBOX_CONTROL_TERMINAL_ALREADY_EXISTS') throw error;
157
+ const existing = readSandboxControlTerminalResult(filePath);
158
+ if (JSON.stringify(existing) !== JSON.stringify(result)) throw new Error('SANDBOX_CONTROL_TERMINAL_RESULT_CONFLICT');
159
+ }
160
+ return readSandboxControlTerminalResult(filePath);
161
+ }
162
+
163
+ export function readSandboxControlTerminalResult(filePath: string): SandboxControlTerminalResult {
164
+ const value = readJsonFile(filePath) as Partial<SandboxControlTerminalResult> | null;
165
+ if (!value || value.version !== 1 || typeof value.requestId !== 'string'
166
+ || typeof value.generation !== 'string' || (value.taskId !== null && typeof value.taskId !== 'string')
167
+ || typeof value.intentDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(value.intentDigest)
168
+ || (value.targetState !== null && typeof value.targetState !== 'string')
169
+ || typeof value.status !== 'string'
170
+ || (value.changed !== null && typeof value.changed !== 'boolean')) {
171
+ throw new Error('SANDBOX_CONTROL_TERMINAL_RESULT_INVALID');
172
+ }
173
+ if (value.completionEvidence !== null && !isCompletionEvidence(value.completionEvidence)) {
174
+ throw new Error('SANDBOX_CONTROL_TERMINAL_RESULT_INVALID');
175
+ }
176
+ if (value.changed === undefined) {
177
+ throw new Error('SANDBOX_CONTROL_TERMINAL_RESULT_INVALID');
178
+ }
179
+ return value as SandboxControlTerminalResult;
180
+ }
181
+
104
182
  export function readSandboxControlResultEvidence(filePath: string): SandboxControlResultEvidence {
105
183
  assertRegularFile(filePath);
106
184
  const raw = fs.readFileSync(filePath, 'utf8');
@@ -0,0 +1,95 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+
5
+ import { readStableFile, SecureFileError, writeAtomicFile } from '../../host-control/secure-fs.ts';
6
+
7
+ import { TASK_WORKFLOW_COMMANDS, TASK_WORKFLOW_OPERATIONS, type TaskWorkflowOperation, type WorkflowCommand } from '../../task/workflow-command.ts';
8
+
9
+ export type TaskWorkflowRequest = Readonly<{
10
+ version: 1;
11
+ id: string;
12
+ taskId: string;
13
+ generation: string;
14
+ operation: TaskWorkflowOperation;
15
+ args: readonly string[];
16
+ }>;
17
+
18
+ export function validateTaskWorkflowRequest(value: unknown): TaskWorkflowRequest {
19
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('TASK_WORKFLOW_REQUEST_INVALID');
20
+ const request = value as TaskWorkflowRequest;
21
+ if (Object.keys(request).sort().join(',') !== 'args,generation,id,operation,taskId,version'
22
+ || request.version !== 1 || typeof request.id !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(request.id)
23
+ || typeof request.taskId !== 'string' || !/^TASK-\d{8}-\d{6}$/u.test(request.taskId)
24
+ || typeof request.generation !== 'string' || !request.generation
25
+ || !Object.hasOwn(TASK_WORKFLOW_COMMANDS, request.operation)
26
+ || !Array.isArray(request.args) || request.args.length < 2 || request.args.length > 128
27
+ || !request.args.every((arg) => typeof arg === 'string' && !/[\r\n]/u.test(arg))
28
+ || request.args[0] !== request.taskId) throw new Error('TASK_WORKFLOW_REQUEST_INVALID');
29
+ const [, selector] = TASK_WORKFLOW_COMMANDS[request.operation];
30
+ if (selector !== null && request.args[1] !== selector) throw new Error('TASK_WORKFLOW_REQUEST_INVALID');
31
+ return request;
32
+ }
33
+
34
+ /** Preserve argv verbatim; the domain command owns option parsing and validation. */
35
+ export function createTaskWorkflowRequest(
36
+ command: WorkflowCommand | undefined,
37
+ args: readonly string[],
38
+ taskId: string,
39
+ generation: string
40
+ ): TaskWorkflowRequest {
41
+ const operation = TASK_WORKFLOW_OPERATIONS.find((key) => {
42
+ const [name, selector] = TASK_WORKFLOW_COMMANDS[key];
43
+ return name === command && (selector === null || selector === args[1]);
44
+ });
45
+ if (!operation) throw new Error('TASK_WORKFLOW_OPERATION_UNSUPPORTED');
46
+ return validateTaskWorkflowRequest({ version: 1, id: randomUUID(), taskId, generation, operation, args: [...args] });
47
+ }
48
+
49
+ export function canonicalArtifactName(value: string): boolean {
50
+ return /^(?:analysis|plan|code|review-analysis|review-plan|review-code)(?:-r[1-9]\d*)?\.md$/u.test(value)
51
+ && path.basename(value) === value;
52
+ }
53
+
54
+ function assertTaskArtifactDirectory(directory: string): string {
55
+ const resolved = path.resolve(directory);
56
+ let stat: fs.Stats;
57
+ try {
58
+ stat = fs.lstatSync(resolved);
59
+ } catch {
60
+ throw new SecureFileError('TASK_ARTIFACT_WRITE_DENIED', 'task directory is unavailable');
61
+ }
62
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
63
+ throw new SecureFileError('TASK_ARTIFACT_WRITE_DENIED', 'task directory must be a real directory');
64
+ }
65
+ return resolved;
66
+ }
67
+
68
+ export async function readTaskArtifact(
69
+ taskDir: string,
70
+ input: Readonly<{
71
+ artifact: string;
72
+ expectedSha256?: string;
73
+ }>
74
+ ): Promise<Readonly<{ artifact: string; bytes: Buffer; sha256: string }>> {
75
+ if (!canonicalArtifactName(input.artifact)) throw new SecureFileError('TASK_ARTIFACT_WRITE_DENIED', 'artifact must be a canonical top-level basename');
76
+ const root = assertTaskArtifactDirectory(taskDir);
77
+ const candidate = path.join(root, input.artifact);
78
+ if (path.dirname(candidate) !== root) throw new SecureFileError('TASK_ARTIFACT_WRITE_DENIED', 'artifact escapes task directory');
79
+ const stable = await readStableFile(candidate, {
80
+ maxBytes: 1024 * 1024,
81
+ ...(input.expectedSha256 ? { expectedSha256: input.expectedSha256 } : {})
82
+ });
83
+ return { artifact: input.artifact, bytes: stable.bytes, sha256: stable.sha256 };
84
+ }
85
+
86
+ export async function writeTaskArtifact(
87
+ taskDir: string,
88
+ artifact: Readonly<{ artifact: string; bytes: Buffer; expectedSha256?: string }>
89
+ ): Promise<void> {
90
+ if (!canonicalArtifactName(artifact.artifact)) throw new SecureFileError('TASK_ARTIFACT_WRITE_DENIED', 'artifact must be a canonical top-level basename');
91
+ const root = assertTaskArtifactDirectory(taskDir);
92
+ const target = path.join(root, artifact.artifact);
93
+ if (path.dirname(target) !== root) throw new SecureFileError('TASK_ARTIFACT_WRITE_DENIED', 'artifact escapes task directory');
94
+ await writeAtomicFile(target, artifact.bytes, 0o600, artifact.expectedSha256);
95
+ }
@@ -0,0 +1,102 @@
1
+ import { TASK_WORKFLOW_COMMANDS } from '../../task/workflow-command.ts';
2
+
3
+ import { parseArtifactCommand, executeArtifactCommand } from '../../task/artifact-command.ts';
4
+ import { parseReviewCommand } from '../../task/review-command.ts';
5
+ import { prepareLocalArtifact, commitLocalArtifactProvenance } from '../../task/local-artifact-finalization.ts';
6
+ import { prepareReviewSummaryCandidate, commitReviewSummaryProvenance } from '../../task/review-finalization.ts';
7
+ import { withTaskExecutionLock } from '../../task/task-execution-lock.ts';
8
+ import { dispatchHostControlCommand } from '../../host-control/command.ts';
9
+ import { assertSandboxTaskSource } from '../workspace-view.ts';
10
+ import { appendDiagnosticAudit } from './audit.ts';
11
+ import {
12
+ readTaskArtifact, writeTaskArtifact, type TaskWorkflowRequest
13
+ } from './task-workflow.ts';
14
+ import type { SandboxControlManifest } from './protocol.ts';
15
+ import type { SandboxControlExecutionResult } from './executor.ts';
16
+ import type { LifecycleRecoveryAttestationV1 } from '../../task/control-authority.ts';
17
+ import { applyTaskEvent } from '../../task/events.ts';
18
+ import { parseTaskEventRequest } from '../../internal/task-event.ts';
19
+
20
+ function executionResult(result: Record<string, unknown>): SandboxControlExecutionResult {
21
+ return { exitCode: result.status === 'failed' || result.status === 'refused' ? 1 : 0, stdout: `${JSON.stringify(result)}\n`, stderr: '' };
22
+ }
23
+
24
+ /** The broker already authorized this host executor; domain operations own validation. */
25
+ export async function executeTaskWorkflow(
26
+ manifest: SandboxControlManifest,
27
+ request: TaskWorkflowRequest,
28
+ lifecycleRecoveryAttestation: LifecycleRecoveryAttestationV1 | null = null
29
+ ): Promise<SandboxControlExecutionResult> {
30
+ let publicationStarted = false;
31
+ try {
32
+ const taskDir = assertSandboxTaskSource(manifest.repoRoot, request.taskId);
33
+ const [command] = TASK_WORKFLOW_COMMANDS[request.operation];
34
+ if (request.operation === 'event') {
35
+ const eventRequest = parseTaskEventRequest(request.args);
36
+ const boundEventRequest = lifecycleRecoveryAttestation && eventRequest.requestId === undefined
37
+ ? { ...eventRequest, requestId: lifecycleRecoveryAttestation.lifecycleRequestId }
38
+ : eventRequest;
39
+ const result = applyTaskEvent(boundEventRequest, {
40
+ repoRoot: manifest.repoRoot,
41
+ lifecycleRecoveryAttestation,
42
+ deferLifecycleRecoveryConsumption: true
43
+ });
44
+ return executionResult(result);
45
+ }
46
+ if (command !== 'task-artifact' && command !== 'task-review') {
47
+ publicationStarted = true;
48
+ // Inherit this executor's process group so its existing recovery owns the worker too.
49
+ const result = await dispatchHostControlCommand({
50
+ operation: command, payload: { workingDirectory: manifest.repoRoot, args: request.args }
51
+ });
52
+ return result;
53
+ }
54
+ const input = command === 'task-artifact' ? parseArtifactCommand(request.args) : parseReviewCommand(request.args);
55
+ if (command === 'task-artifact' && 'operation' in input && input.operation === 'inspect') {
56
+ return executionResult(executeArtifactCommand(input, { repoRoot: manifest.repoRoot }));
57
+ }
58
+ if ('operation' in input && (input.operation === 'init' || input.operation === 'repair')) {
59
+ return executionResult(executeArtifactCommand(input, { repoRoot: manifest.repoRoot, artifactDir: taskDir }));
60
+ }
61
+ return await withTaskExecutionLock(manifest.repoRoot, request.taskId, `sandbox-control.${request.operation}`, async () => {
62
+ const artifact = await readTaskArtifact(taskDir, { artifact: input.artifact });
63
+ const content = artifact.bytes.toString('utf8');
64
+ let result: Record<string, unknown>;
65
+ if ('operation' in input) {
66
+ const { family } = input;
67
+ if (family !== 'analysis' && family !== 'plan' && family !== 'code') throw new Error('ARTIFACT_IDENTITY_INVALID');
68
+ const local = { taskRef: request.taskId, family, artifact: input.artifact, repoRoot: manifest.repoRoot } as const;
69
+ const prepared = prepareLocalArtifact(local, content, lifecycleRecoveryAttestation ?? undefined);
70
+ if (prepared.result.status === 'failed') return executionResult(commitLocalArtifactProvenance(prepared));
71
+ publicationStarted = true;
72
+ await writeTaskArtifact(taskDir, {
73
+ artifact: artifact.artifact,
74
+ bytes: artifact.bytes,
75
+ expectedSha256: artifact.sha256
76
+ });
77
+ result = commitLocalArtifactProvenance(prepared);
78
+ } else {
79
+ if (input.overrideTicket) throw new Error('TASK_WORKFLOW_OVERRIDE_UNSUPPORTED');
80
+ const prepared = prepareReviewSummaryCandidate(input, content, { repoRoot: manifest.repoRoot });
81
+ if (input.dryRun || prepared.result.status === 'planned') return executionResult(prepared.result);
82
+ if (prepared.result.status === 'failed') return executionResult(commitReviewSummaryProvenance(prepared, manifest.repoRoot));
83
+ publicationStarted = true;
84
+ await writeTaskArtifact(taskDir, {
85
+ artifact: input.artifact,
86
+ bytes: Buffer.from(prepared.content, 'utf8'),
87
+ expectedSha256: artifact.sha256
88
+ });
89
+ result = commitReviewSummaryProvenance(prepared, manifest.repoRoot);
90
+ }
91
+ if (result.status !== 'failed') appendDiagnosticAudit(manifest, 'task-workflow-artifact-finalized', {
92
+ requestId: request.id, sandboxTaskId: request.taskId, workflowOperation: request.operation,
93
+ artifact: input.artifact, sha256: result.artifactSha256 as string, semanticDigest: result.semanticDigest as string
94
+ });
95
+ return executionResult({ ...result, changed: true });
96
+ });
97
+ } catch (error) {
98
+ const message = error instanceof Error ? error.message : String(error);
99
+ const code = (error as { code?: string })?.code ?? /^([A-Z][A-Z0-9_]+)/u.exec(message)?.[1] ?? 'TASK_WORKFLOW_REQUEST_INVALID';
100
+ return executionResult({ status: 'failed', changed: publicationStarted ? null : false, error: { code, message } });
101
+ }
102
+ }
@@ -20,8 +20,8 @@ Commands:
20
20
  rm <branch | TASK-id> | --unbound | --purge
21
21
  Remove one sandbox; use a full TASK-id for task-bound
22
22
  cleanup and a branch for branch-only cleanup. --unbound
23
- removes completed task-bound and branch-only sandboxes;
24
- active, blocked, and archive tasks are protected.
23
+ removes completed task-bound and branch-only sandboxes
24
+ plus verified auxiliary state; active, blocked, and archive tasks are protected.
25
25
  start [--recreate] <branch | TASK-id | N>
26
26
  Verify or recover an existing sandbox container;
27
27
  optionally replace only the container on failure
@@ -60,9 +60,10 @@ import {
60
60
  type BrokerOwner
61
61
  } from './control/lifecycle.ts';
62
62
  import {
63
- appendSandboxControlAudit,
64
63
  readSandboxControlStatus
65
64
  } from './control/state.ts';
65
+ import { validateSandboxControlIdentity } from './control/identity-sentinel.ts';
66
+ import { appendDiagnosticAudit as appendControlDiagnosticAudit } from './control/audit.ts';
66
67
  import { inspectSandboxControlContainer } from './control/container-identity.ts';
67
68
  import {
68
69
  SANDBOX_CONTROL_FUTURE_SKEW_MS,
@@ -224,6 +225,17 @@ export async function startSandboxControlBroker(repoRoot: string, manifestPath:
224
225
  const internalCli = path.resolve(directory, '..', '..', 'bin', `internal-cli${extension}`);
225
226
  const manifest = readSandboxControlManifest(manifestPath);
226
227
  const root = path.dirname(manifestPath);
228
+ const identity = validateSandboxControlIdentity({
229
+ publicStatusDir: manifest.publicStatusDir,
230
+ root,
231
+ mode: manifest.mode,
232
+ taskId: manifest.taskId,
233
+ generation: manifest.generation,
234
+ controlRootId: manifest.controlRootId
235
+ });
236
+ if (identity.state !== 'valid') {
237
+ throw new Error(`SANDBOX_CONTROL_IDENTITY_${identity.state.replaceAll('-', '_').toUpperCase()}`);
238
+ }
227
239
  if (isSandboxControlRootQuiescing(root)) throw new Error('SANDBOX_CONTROL_QUIESCING');
228
240
  const brokerPath = path.join(root, 'broker.json');
229
241
  let brokerSnapshot: string | null = null;
@@ -296,8 +308,8 @@ export async function startSandboxControlBroker(repoRoot: string, manifestPath:
296
308
  }
297
309
  }
298
310
  if (replacedBrokerRecord) {
299
- appendSandboxControlAudit(manifest, 'broker-observed-crash');
300
- appendSandboxControlAudit(manifest, 'broker-restart');
311
+ appendControlDiagnosticAudit(manifest, 'broker-observed-crash');
312
+ appendControlDiagnosticAudit(manifest, 'broker-restart');
301
313
  }
302
314
  child = spawn(
303
315
  process.execPath,
@@ -338,6 +350,17 @@ async function ensureSandboxControlBroker(params: {
338
350
  });
339
351
  if (!fs.existsSync(control.manifestPath)) return;
340
352
  const validatedManifest = readSandboxControlManifest(control.manifestPath);
353
+ const identity = validateSandboxControlIdentity({
354
+ publicStatusDir: validatedManifest.publicStatusDir,
355
+ root: control.root,
356
+ mode: validatedManifest.mode,
357
+ taskId: validatedManifest.taskId,
358
+ generation: validatedManifest.generation,
359
+ controlRootId: validatedManifest.controlRootId
360
+ });
361
+ if (identity.state !== 'valid') {
362
+ throw new Error(`SANDBOX_CONTROL_IDENTITY_${identity.state.replaceAll('-', '_').toUpperCase()}`);
363
+ }
341
364
  const containerObservation = await inspectSandboxControlContainer(validatedManifest);
342
365
  if (containerObservation.state === 'unknown') {
343
366
  throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${containerObservation.reason}`);
@@ -749,7 +772,7 @@ function expectedMounts(params: {
749
772
  ? {}
750
773
  : {
751
774
  runtimeDir: control.runtimeDir,
752
- taskSources: taskSources.mountPaths,
775
+ taskSources: taskSources!.accessiblePaths,
753
776
  taskId: taskId!
754
777
  })
755
778
  }).map((mount) => ({
@@ -758,7 +781,7 @@ function expectedMounts(params: {
758
781
  hostPaths: mount.hostPaths,
759
782
  ...(taskSources !== null && taskId !== null
760
783
  && mount.containerPath === `/workspace/.agents/workspace/active/${taskId}`
761
- ? { sourceAccessiblePaths: taskSources.accessiblePaths }
784
+ ? { sourceAccessiblePaths: taskSources!.accessiblePaths }
762
785
  : {}),
763
786
  expectedRW: !mount.readOnly
764
787
  }));