@fitlab-ai/agent-infra 0.9.12 → 0.9.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/README.md +11 -11
  2. package/README.zh-CN.md +11 -11
  3. package/dist/bin/internal-cli.js +24 -19
  4. package/dist/lib/agent-clients/next-steps.js +6 -1
  5. package/dist/lib/decide.js +3 -3
  6. package/dist/lib/internal/platform-checks.js +3 -3
  7. package/dist/lib/internal/platform-comment.js +9 -9
  8. package/dist/lib/internal/platform-context.js +2 -2
  9. package/dist/lib/internal/platform-issue.js +8 -9
  10. package/dist/lib/internal/platform-pr-review.js +10 -10
  11. package/dist/lib/internal/platform-pr.js +63 -24
  12. package/dist/lib/internal/platform-release-notes.js +3 -3
  13. package/dist/lib/internal/pr-review-grade.js +11 -9
  14. package/dist/lib/internal/release-workflow.js +4 -4
  15. package/dist/lib/internal/sandbox-control.js +30 -4
  16. package/dist/lib/internal/task-activity.js +3 -3
  17. package/dist/lib/internal/task-artifact.js +76 -24
  18. package/dist/lib/internal/task-create.js +2 -2
  19. package/dist/lib/internal/task-event.js +6 -3
  20. package/dist/lib/internal/task-finalization.js +2 -2
  21. package/dist/lib/internal/task-invalidation.js +60 -0
  22. package/dist/lib/internal/task-ledger.js +9 -6
  23. package/dist/lib/internal/task-lifecycle.js +2 -2
  24. package/dist/lib/internal/task-override.js +4 -4
  25. package/dist/lib/internal/task-review.js +3 -3
  26. package/dist/lib/internal/task-verify.js +6 -6
  27. package/dist/lib/internal/task-warning.js +3 -3
  28. package/dist/lib/platform/adapters.js +1 -3
  29. package/dist/lib/platform/change-request-git-evidence.js +27 -7
  30. package/dist/lib/platform/completion-backfill.js +6 -5
  31. package/dist/lib/platform/context.js +70 -130
  32. package/dist/lib/platform/github-client.js +1 -1
  33. package/dist/lib/platform/github-provider.js +903 -0
  34. package/dist/lib/platform/in-label-sync.js +143 -0
  35. package/dist/lib/platform/issue-comments.js +94 -41
  36. package/dist/lib/platform/issues.js +335 -189
  37. package/dist/lib/platform/metadata-labels.js +6 -2
  38. package/dist/lib/platform/none-provider.js +28 -0
  39. package/dist/lib/platform/pr-change-report.js +551 -0
  40. package/dist/lib/platform/pr-checks.js +119 -104
  41. package/dist/lib/platform/pr-review.js +88 -72
  42. package/dist/lib/platform/pr-summary.js +520 -50
  43. package/dist/lib/platform/provider-bridge.js +45 -0
  44. package/dist/lib/platform/provider-contract.d.ts +487 -0
  45. package/dist/lib/platform/provider-contract.js +95 -0
  46. package/dist/lib/platform/provider-loader.js +251 -0
  47. package/dist/lib/platform/provider-validation.js +504 -0
  48. package/dist/lib/platform/pull-request-metadata.js +8 -6
  49. package/dist/lib/platform/pull-requests.js +783 -537
  50. package/dist/lib/platform/release-notes.js +78 -55
  51. package/dist/lib/platform/releases.js +79 -50
  52. package/dist/lib/platform/resource-identity.d.ts +26 -0
  53. package/dist/lib/platform/resource-identity.js +92 -0
  54. package/dist/lib/platform/task-identities.js +31 -0
  55. package/dist/lib/platform/verification-required.js +11 -12
  56. package/dist/lib/platform/verification-sync.js +112 -238
  57. package/dist/lib/pr-review/evidence-grading.js +19 -6
  58. package/dist/lib/run/index.js +63 -23
  59. package/dist/lib/sandbox/control/client.js +96 -15
  60. package/dist/lib/sandbox/control/executor.js +3 -3
  61. package/dist/lib/sandbox/control/protocol.js +77 -0
  62. package/dist/lib/sandbox/control/server.js +398 -25
  63. package/dist/lib/sandbox/control/state.js +204 -1
  64. package/dist/lib/server/protocol.js +38 -1
  65. package/dist/lib/task/activity-log.js +8 -1
  66. package/dist/lib/task/artifact-lifecycle.js +33 -5
  67. package/dist/lib/task/capabilities.js +239 -0
  68. package/dist/lib/task/commands/status.js +32 -0
  69. package/dist/lib/task/commit-operation.js +12 -1
  70. package/dist/lib/task/control-authority.js +8 -5
  71. package/dist/lib/task/create-service.js +8 -8
  72. package/dist/lib/task/current-contract.js +5 -1
  73. package/dist/lib/task/decision-details.js +1 -1
  74. package/dist/lib/task/events.js +297 -17
  75. package/dist/lib/task/finalization.js +37 -13
  76. package/dist/lib/task/human-override.js +58 -36
  77. package/dist/lib/task/invalidation-command.js +86 -0
  78. package/dist/lib/task/invalidation.js +260 -0
  79. package/dist/lib/task/ledger-intents.js +37 -0
  80. package/dist/lib/task/local-artifact-finalization.js +374 -0
  81. package/dist/lib/task/orchestration.js +49 -93
  82. package/dist/lib/task/pr-delivery-fact.js +102 -101
  83. package/dist/lib/task/review-fingerprint.js +24 -2
  84. package/dist/lib/task/rework-intent.js +138 -0
  85. package/dist/lib/task/skill-scope.js +23 -0
  86. package/dist/lib/task/task-execution-lock.js +27 -17
  87. package/dist/lib/task/verification-engine.js +43 -16
  88. package/dist/lib/task/verification.js +3 -3
  89. package/dist/lib/task/workflow-warning-intents.js +1 -1
  90. package/dist/lib/task/write.js +11 -0
  91. package/dist/lib/version.js +11 -1
  92. package/dist/lifecycle-build-manifest.json +3 -3
  93. package/lib/agent-clients/next-steps.ts +6 -1
  94. package/lib/decide.ts +3 -3
  95. package/lib/init.ts +1 -1
  96. package/lib/internal/platform-checks.ts +3 -3
  97. package/lib/internal/platform-comment.ts +8 -8
  98. package/lib/internal/platform-context.ts +2 -2
  99. package/lib/internal/platform-issue.ts +7 -8
  100. package/lib/internal/platform-pr-review.ts +9 -9
  101. package/lib/internal/platform-pr.ts +47 -22
  102. package/lib/internal/platform-release-notes.ts +3 -3
  103. package/lib/internal/pr-review-grade.ts +10 -8
  104. package/lib/internal/release-workflow.ts +4 -4
  105. package/lib/internal/sandbox-control.ts +27 -3
  106. package/lib/internal/task-activity.ts +3 -3
  107. package/lib/internal/task-artifact.ts +65 -22
  108. package/lib/internal/task-create.ts +2 -2
  109. package/lib/internal/task-event.ts +6 -3
  110. package/lib/internal/task-finalization.ts +3 -3
  111. package/lib/internal/task-invalidation.ts +39 -0
  112. package/lib/internal/task-ledger.ts +11 -8
  113. package/lib/internal/task-lifecycle.ts +2 -2
  114. package/lib/internal/task-override.ts +6 -6
  115. package/lib/internal/task-review.ts +3 -3
  116. package/lib/internal/task-verify.ts +6 -6
  117. package/lib/internal/task-warning.ts +3 -3
  118. package/lib/platform/adapters.ts +3 -2
  119. package/lib/platform/change-request-git-evidence.ts +36 -9
  120. package/lib/platform/completion-backfill.ts +9 -8
  121. package/lib/platform/context.ts +97 -134
  122. package/lib/platform/github-client.ts +2 -2
  123. package/lib/platform/github-provider.ts +910 -0
  124. package/lib/platform/in-label-sync.ts +208 -0
  125. package/lib/platform/issue-comments.ts +97 -47
  126. package/lib/platform/issues.ts +344 -174
  127. package/lib/platform/metadata-labels.ts +5 -2
  128. package/lib/platform/none-provider.ts +35 -0
  129. package/lib/platform/pr-change-report.ts +645 -0
  130. package/lib/platform/pr-checks.ts +126 -106
  131. package/lib/platform/pr-review.ts +88 -85
  132. package/lib/platform/pr-summary.ts +560 -53
  133. package/lib/platform/provider-bridge.ts +82 -0
  134. package/lib/platform/provider-contract.ts +536 -0
  135. package/lib/platform/provider-loader.ts +307 -0
  136. package/lib/platform/provider-validation.ts +529 -0
  137. package/lib/platform/pull-request-metadata.ts +10 -7
  138. package/lib/platform/pull-requests.ts +826 -543
  139. package/lib/platform/release-notes.ts +76 -59
  140. package/lib/platform/releases.ts +77 -43
  141. package/lib/platform/resource-identity.ts +121 -0
  142. package/lib/platform/task-identities.ts +40 -0
  143. package/lib/platform/types.ts +4 -2
  144. package/lib/platform/verification-required.ts +11 -13
  145. package/lib/platform/verification-sync.ts +112 -292
  146. package/lib/pr-review/evidence-grading.ts +25 -8
  147. package/lib/run/index.ts +57 -22
  148. package/lib/sandbox/control/client.ts +105 -15
  149. package/lib/sandbox/control/executor.ts +4 -4
  150. package/lib/sandbox/control/protocol.ts +113 -0
  151. package/lib/sandbox/control/server.ts +431 -23
  152. package/lib/sandbox/control/state.ts +251 -1
  153. package/lib/server/protocol.ts +36 -1
  154. package/lib/task/activity-log.ts +8 -0
  155. package/lib/task/artifact-lifecycle.ts +32 -6
  156. package/lib/task/capabilities.ts +262 -0
  157. package/lib/task/commands/status.ts +38 -0
  158. package/lib/task/commit-operation.ts +12 -1
  159. package/lib/task/control-authority.ts +11 -8
  160. package/lib/task/create-service.ts +8 -8
  161. package/lib/task/current-contract.ts +6 -1
  162. package/lib/task/decision-details.ts +6 -2
  163. package/lib/task/events.ts +296 -16
  164. package/lib/task/finalization.ts +50 -14
  165. package/lib/task/human-override.ts +70 -46
  166. package/lib/task/invalidation-command.ts +86 -0
  167. package/lib/task/invalidation.ts +332 -0
  168. package/lib/task/ledger-intents.ts +35 -1
  169. package/lib/task/local-artifact-finalization.ts +563 -0
  170. package/lib/task/orchestration.ts +52 -85
  171. package/lib/task/pr-delivery-fact.ts +131 -148
  172. package/lib/task/review-fingerprint.ts +23 -2
  173. package/lib/task/rework-intent.ts +165 -0
  174. package/lib/task/skill-scope.ts +24 -0
  175. package/lib/task/task-execution-lock.ts +23 -14
  176. package/lib/task/verification-engine.ts +52 -17
  177. package/lib/task/verification.ts +4 -4
  178. package/lib/task/workflow-warning-intents.ts +1 -1
  179. package/lib/task/write.ts +19 -0
  180. package/lib/update.ts +1 -1
  181. package/lib/version.ts +15 -1
  182. package/package.json +7 -1
  183. package/templates/.agents/rules/README.en.md +1 -1
  184. package/templates/.agents/rules/README.zh-CN.md +1 -1
  185. package/templates/.agents/rules/issue-pr-commands.en.md +1 -1
  186. package/templates/.agents/rules/issue-pr-commands.zh-CN.md +1 -1
  187. package/templates/.agents/rules/issue-sync.en.md +2 -0
  188. package/templates/.agents/rules/issue-sync.zh-CN.md +2 -0
  189. package/templates/.agents/rules/local-artifact-repair.en.md +9 -2
  190. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +9 -2
  191. package/templates/.agents/rules/pr-sync.en.md +10 -4
  192. package/templates/.agents/rules/pr-sync.zh-CN.md +17 -4
  193. package/templates/.agents/rules/task-short-id.en.md +1 -4
  194. package/templates/.agents/rules/task-short-id.zh-CN.md +1 -3
  195. package/templates/.agents/skills/analyze-task/SKILL.en.md +13 -3
  196. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +10 -3
  197. package/templates/.agents/skills/check-task/SKILL.en.md +16 -37
  198. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +16 -37
  199. package/templates/.agents/skills/code-task/SKILL.en.md +27 -8
  200. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +29 -10
  201. package/templates/.agents/skills/code-task/config/verify.en.json +1 -0
  202. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -0
  203. package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +2 -0
  204. package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +2 -0
  205. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +3 -3
  206. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +3 -3
  207. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +5 -3
  208. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +5 -3
  209. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +4 -3
  210. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +4 -3
  211. package/templates/.agents/skills/complete-task/SKILL.en.md +21 -3
  212. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +20 -3
  213. package/templates/.agents/skills/create-pr/SKILL.en.md +7 -6
  214. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +7 -7
  215. package/templates/.agents/skills/create-pr/config/verify.json +1 -0
  216. package/templates/.agents/skills/create-pr/reference/change-report.en.md +16 -4
  217. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +16 -4
  218. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +5 -3
  219. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +5 -3
  220. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
  221. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
  222. package/templates/.agents/skills/create-pr/scripts/change-report.mjs +3 -0
  223. package/templates/.agents/skills/plan-task/SKILL.en.md +13 -3
  224. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +10 -3
  225. package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -3
  226. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -3
  227. package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
  228. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
  229. package/templates/.agents/skills/review-plan/SKILL.en.md +3 -3
  230. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -3
  231. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +4 -2
  232. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +4 -2
  233. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
  234. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
  235. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +6 -0
  236. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +6 -0
  237. package/templates/.agents/templates/task.en.md +18 -1
  238. package/templates/.agents/templates/task.zh-CN.md +18 -1
  239. package/templates/.github/workflows/pr-label.yml +7 -26
@@ -1,15 +1,17 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { randomUUID } from 'node:crypto';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
4
  import { getProcessStartTime } from '../../server/process-state.js';
5
5
  import { assertGitWorktreeBinding } from '../../git/worktree-identity.js';
6
- import { controlError, DEFAULT_SANDBOX_CONTROL_TIMING } from './protocol.js';
6
+ import { controlError, DEFAULT_SANDBOX_CONTROL_TIMING, SANDBOX_CONTROL_MAX_RESPONSE_BYTES, SANDBOX_CONTROL_MAX_TERMINAL_RECORD_BYTES } from './protocol.js';
7
7
  import { prepareSandboxControlExecution } from './executor.js';
8
- import { appendSandboxControlAudit, atomicWriteJson, cleanupStaleSandboxControlLease, executionPath, readActiveLease, readExecution, terminateSandboxControlExecution, writeSandboxControlStatus } from './state.js';
8
+ import { appendSandboxControlAudit, atomicWriteJsonNoReplace, createSandboxControlPayload, cleanupStaleSandboxControlLease, executionPath, readJsonFile, readActiveLease, readExecution, readSandboxControlPayload, readSandboxControlResultEvidence, sandboxControlEncodedJsonBytes, sandboxControlGenerationUsage, sanitizeSandboxControlOutput, sanitizeSandboxControlResult, payloadPath, writeSandboxControlPayload, writeSandboxControlReservation, writeSandboxControlResultEvidence, terminateSandboxControlExecution, writeSandboxControlStatus } from './state.js';
9
9
  import { validateSandboxControlRequest } from './protocol.js';
10
10
  import { inspectSandboxControlContainer } from './container-identity.js';
11
11
  import { acquireSandboxControlBrokerStartup, garbageCollectSandboxControlRoot, isSandboxControlRootQuiescing, readSandboxControlManifest } from './lifecycle.js';
12
12
  import { nextSandboxControlBackoff } from './timing.js';
13
+ import { readTaskFinalizationReceipt } from '../../task/finalization.js';
14
+ import { resolveTaskRef } from '../../task/resolve-ref.js';
13
15
  function assertRealDirectory(directory, parent) {
14
16
  const stat = fs.lstatSync(directory);
15
17
  if (!stat.isDirectory() || stat.isSymbolicLink())
@@ -21,8 +23,239 @@ function assertRealDirectory(directory, parent) {
21
23
  function responsePath(manifest, id) {
22
24
  return path.join(manifest.channelDir, 'responses', `${id}.json`);
23
25
  }
24
- function writeResponse(manifest, response) {
25
- atomicWriteJson(responsePath(manifest, response.id), response);
26
+ function acceptedResponsePath(manifest, id) {
27
+ return path.join(manifest.channelDir, 'responses', `${id}.accepted.json`);
28
+ }
29
+ export function writeSandboxControlResponse(manifest, response) {
30
+ const filePath = responsePath(manifest, response.id);
31
+ const normalized = {
32
+ ...response,
33
+ stdout: sanitizeSandboxControlOutput(manifest, response.stdout),
34
+ stderr: sanitizeSandboxControlOutput(manifest, response.stderr)
35
+ };
36
+ const terminal = sandboxControlEncodedJsonBytes(normalized) <= SANDBOX_CONTROL_MAX_TERMINAL_RECORD_BYTES
37
+ ? normalized
38
+ : {
39
+ ...normalized,
40
+ stdout: '',
41
+ stderr: 'SANDBOX_CONTROL_OUTPUT_UNAVAILABLE: terminal output exceeded the compact record limit\n',
42
+ error: {
43
+ code: 'SANDBOX_CONTROL_OUTPUT_UNAVAILABLE',
44
+ message: 'SANDBOX_CONTROL_OUTPUT_UNAVAILABLE: terminal output exceeded the compact record limit',
45
+ retryable: false
46
+ }
47
+ };
48
+ try {
49
+ atomicWriteJsonNoReplace(filePath, terminal);
50
+ }
51
+ catch (error) {
52
+ if (!(error instanceof Error) || error.message !== 'SANDBOX_CONTROL_TERMINAL_ALREADY_EXISTS')
53
+ throw error;
54
+ let persisted;
55
+ try {
56
+ persisted = readJsonFile(filePath);
57
+ }
58
+ catch {
59
+ throw new Error('SANDBOX_CONTROL_TERMINAL_READBACK_FAILED');
60
+ }
61
+ if (JSON.stringify(persisted) !== JSON.stringify(terminal)) {
62
+ throw new Error('SANDBOX_CONTROL_TERMINAL_CONFLICT');
63
+ }
64
+ return true;
65
+ }
66
+ let persisted;
67
+ try {
68
+ persisted = readJsonFile(filePath);
69
+ }
70
+ catch {
71
+ throw new Error('SANDBOX_CONTROL_TERMINAL_READBACK_FAILED');
72
+ }
73
+ if (JSON.stringify(persisted) !== JSON.stringify(terminal)) {
74
+ throw new Error('SANDBOX_CONTROL_TERMINAL_READBACK_FAILED');
75
+ }
76
+ return true;
77
+ }
78
+ function finalizationRecoveryResponse(manifest, requestId, exitCode) {
79
+ if (!manifest.taskId || exitCode !== 0)
80
+ return { status: 'not-applicable' };
81
+ let receipt;
82
+ try {
83
+ receipt = readTaskFinalizationReceipt(manifest.repoRoot, manifest.taskId);
84
+ if (!receipt || !receipt.controlBinding
85
+ || receipt.controlBinding.generation !== manifest.generation
86
+ || receipt.controlBinding.requestId !== requestId)
87
+ return { status: 'deferred' };
88
+ const resolved = resolveTaskRef(manifest.taskId, { repoRoot: manifest.repoRoot });
89
+ if (!resolved.ok || resolved.state !== 'completed' || receipt.lifecycle !== 'done')
90
+ return { status: 'deferred' };
91
+ }
92
+ catch {
93
+ return { status: 'deferred' };
94
+ }
95
+ const pendingSteps = [
96
+ receipt.taskComment === 'pending' ? 'task-comment' : null,
97
+ receipt.verification === 'pending' ? 'verification' : null
98
+ ].filter((step) => step !== null);
99
+ const completedSteps = ['lifecycle', receipt.taskComment === 'pending' ? null : 'task-comment', receipt.verification === 'pending' ? null : 'verification']
100
+ .filter((step) => step !== null);
101
+ const warnings = receipt.warnings
102
+ .filter((warning) => warning.status === 'open')
103
+ .map(({ status: _status, resolvedAt: _resolvedAt, ...warning }) => warning);
104
+ const result = {
105
+ status: 'completed', changed: false, taskId: manifest.taskId,
106
+ lifecycle: { status: 'no-op', changed: false, error: null },
107
+ taskComment: receipt.taskComment === 'pending' ? null : { status: 'no-op', changed: false, error: null },
108
+ verification: receipt.verification === 'pending' ? null : { status: 'no-op', changed: false, error: null },
109
+ completedSteps, pendingSteps,
110
+ result: pendingSteps.length > 0 || receipt.warningProjection === 'pending' || warnings.length > 0
111
+ ? 'completed_with_warnings' : 'completed',
112
+ warnings, error: null
113
+ };
114
+ return {
115
+ status: 'matched',
116
+ response: {
117
+ version: 2, id: requestId, phase: 'completed', exitCode: 0,
118
+ stdout: `${JSON.stringify({ version: 1, status: 'completed', changed: false, accepted: true, result, error: null })}\n`,
119
+ stderr: '', error: null
120
+ }
121
+ };
122
+ }
123
+ function payloadReference(payload) {
124
+ return {
125
+ version: payload.version,
126
+ id: payload.id,
127
+ generation: payload.generation,
128
+ stdoutBytes: payload.stdoutBytes,
129
+ stderrBytes: payload.stderrBytes,
130
+ stdoutSha256: payload.stdoutSha256,
131
+ stderrSha256: payload.stderrSha256
132
+ };
133
+ }
134
+ function payloadMatchesEvidence(payload, evidence) {
135
+ return payload.id === evidence.id
136
+ && payload.generation === evidence.generation
137
+ && payload.stdoutBytes === evidence.stdoutBytes
138
+ && payload.stderrBytes === evidence.stderrBytes
139
+ && payload.stdoutSha256 === evidence.stdoutSha256
140
+ && payload.stderrSha256 === evidence.stderrSha256;
141
+ }
142
+ function outputMatchesEvidence(output, bytes, sha256) {
143
+ return Buffer.byteLength(output, 'utf8') === bytes
144
+ && createHash('sha256').update(output, 'utf8').digest('hex') === sha256;
145
+ }
146
+ function genericRecoveryResponse(requestId, evidence, payload) {
147
+ return {
148
+ version: 2,
149
+ id: requestId,
150
+ phase: 'completed',
151
+ exitCode: evidence.exitCode,
152
+ stdout: '',
153
+ stderr: payload ? '' : 'SANDBOX_CONTROL_OUTPUT_UNAVAILABLE: broker restarted after executor completion\n',
154
+ error: null,
155
+ outputState: payload ? 'available' : 'unavailable',
156
+ payload: payload ? payloadReference(payload) : null
157
+ };
158
+ }
159
+ function recoveryResponse(manifest, request, evidence, payload) {
160
+ if (request.family === 'task-finalization' && evidence.exitCode === 0) {
161
+ const finalization = finalizationRecoveryResponse(manifest, request.id, evidence.exitCode);
162
+ return finalization.status === 'matched' ? finalization.response ?? null : null;
163
+ }
164
+ return genericRecoveryResponse(request.id, evidence, payload);
165
+ }
166
+ function terminalMatchesEvidence(manifest, request, response, evidence, payload, payloadInvalid) {
167
+ if (request.family === 'task-finalization' && evidence.exitCode === 0) {
168
+ const expected = recoveryResponse(manifest, request, evidence, payload);
169
+ return {
170
+ valid: expected !== null && JSON.stringify(response) === JSON.stringify(expected),
171
+ payloadReferenced: false
172
+ };
173
+ }
174
+ if (response.version !== 2 || response.id !== request.id || response.phase !== 'completed'
175
+ || response.exitCode !== evidence.exitCode || response.error !== null) {
176
+ return { valid: false, payloadReferenced: false };
177
+ }
178
+ if (response.outputState === 'available') {
179
+ const reference = response.payload;
180
+ return {
181
+ valid: !payloadInvalid && payload !== null && response.stdout === '' && response.stderr === ''
182
+ && reference !== undefined && reference !== null
183
+ && reference.version === payload.version
184
+ && reference.id === payload.id
185
+ && reference.generation === payload.generation
186
+ && reference.stdoutBytes === payload.stdoutBytes
187
+ && reference.stderrBytes === payload.stderrBytes
188
+ && reference.stdoutSha256 === payload.stdoutSha256
189
+ && reference.stderrSha256 === payload.stderrSha256
190
+ && payloadMatchesEvidence(payload, evidence),
191
+ payloadReferenced: true
192
+ };
193
+ }
194
+ if (response.outputState === 'unavailable') {
195
+ const expected = genericRecoveryResponse(request.id, evidence, null);
196
+ return { valid: JSON.stringify(response) === JSON.stringify(expected), payloadReferenced: false };
197
+ }
198
+ return {
199
+ valid: response.payload === undefined
200
+ && outputMatchesEvidence(response.stdout, evidence.stdoutBytes, evidence.stdoutSha256)
201
+ && outputMatchesEvidence(response.stderr, evidence.stderrBytes, evidence.stderrSha256),
202
+ payloadReferenced: false
203
+ };
204
+ }
205
+ function publishExecutionResult(manifest, request, result, brokerOwns) {
206
+ const normalized = sanitizeSandboxControlResult(manifest, result);
207
+ let terminal = null;
208
+ if (request.family === 'task-finalization') {
209
+ const finalization = finalizationRecoveryResponse(manifest, request.id, normalized.exitCode);
210
+ if (finalization.status === 'deferred')
211
+ return false;
212
+ terminal = finalization.response ?? null;
213
+ }
214
+ if (!terminal) {
215
+ const inline = {
216
+ version: 2, id: request.id, phase: 'completed', exitCode: normalized.exitCode,
217
+ stdout: normalized.stdout, stderr: normalized.stderr, error: null
218
+ };
219
+ if (sandboxControlEncodedJsonBytes(inline) <= SANDBOX_CONTROL_MAX_TERMINAL_RECORD_BYTES) {
220
+ terminal = inline;
221
+ }
222
+ let payload = null;
223
+ if (!terminal) {
224
+ try {
225
+ payload = createSandboxControlPayload(manifest, request.id, normalized);
226
+ const usage = sandboxControlGenerationUsage(manifest);
227
+ if (usage.bytes + sandboxControlEncodedJsonBytes(payload) <= SANDBOX_CONTROL_MAX_RESPONSE_BYTES) {
228
+ writeSandboxControlPayload(manifest, request.id, normalized);
229
+ }
230
+ else {
231
+ payload = null;
232
+ }
233
+ }
234
+ catch {
235
+ payload = null;
236
+ }
237
+ terminal = {
238
+ version: 2,
239
+ id: request.id,
240
+ phase: 'completed',
241
+ exitCode: normalized.exitCode,
242
+ stdout: '',
243
+ stderr: payload ? '' : 'SANDBOX_CONTROL_OUTPUT_UNAVAILABLE: output payload was not retained\n',
244
+ error: null,
245
+ outputState: payload ? 'available' : 'unavailable',
246
+ payload: payload ? payloadReference(payload) : null
247
+ };
248
+ }
249
+ }
250
+ if (!brokerOwns())
251
+ return false;
252
+ return writeSandboxControlResponse(manifest, terminal);
253
+ }
254
+ function writeAcceptedResponse(manifest, response) {
255
+ atomicWriteJsonNoReplace(acceptedResponsePath(manifest, response.id), response);
256
+ }
257
+ function removeAcceptedResponse(manifest, id) {
258
+ fs.rmSync(acceptedResponsePath(manifest, id), { force: true });
26
259
  }
27
260
  function rejected(id, error) {
28
261
  const detail = controlError(error);
@@ -84,8 +317,49 @@ function recoverProcessing(manifest, brokerOwns) {
84
317
  return false;
85
318
  const descriptor = executionPath(manifest, entry.name);
86
319
  const existingResponse = responsePath(manifest, entry.name);
320
+ const processingDirectory = path.join(manifest.processingDir, entry.name);
321
+ let payloadReferenced = false;
87
322
  if (fs.existsSync(descriptor)) {
88
323
  const execution = readExecution(descriptor);
324
+ let resultEvidence = null;
325
+ let payload = null;
326
+ let payloadInvalid = false;
327
+ let request = null;
328
+ let existingTerminalResponse = null;
329
+ try {
330
+ const rawRequest = JSON.parse(fs.readFileSync(path.join(processingDirectory, 'request.json'), 'utf8'));
331
+ request = validateSandboxControlRequest(rawRequest, manifest, {
332
+ now: typeof rawRequest.issuedAt === 'number' ? rawRequest.issuedAt : undefined
333
+ });
334
+ }
335
+ catch {
336
+ // Missing or malformed request evidence remains fail-closed below.
337
+ }
338
+ const resultPath = path.join(manifest.processingDir, entry.name, 'result.json');
339
+ if (fs.existsSync(resultPath)) {
340
+ try {
341
+ resultEvidence = readSandboxControlResultEvidence(resultPath);
342
+ if (resultEvidence.id !== entry.name || resultEvidence.generation !== manifest.generation) {
343
+ resultEvidence = null;
344
+ }
345
+ }
346
+ catch {
347
+ resultEvidence = null;
348
+ }
349
+ }
350
+ const publishedPayloadPath = payloadPath(manifest, entry.name);
351
+ if (fs.existsSync(publishedPayloadPath)) {
352
+ try {
353
+ payload = readSandboxControlPayload(publishedPayloadPath);
354
+ if (payload.id !== entry.name || payload.generation !== manifest.generation
355
+ || (resultEvidence !== null && !payloadMatchesEvidence(payload, resultEvidence))) {
356
+ payloadInvalid = true;
357
+ }
358
+ }
359
+ catch {
360
+ payloadInvalid = true;
361
+ }
362
+ }
89
363
  if (!terminateSandboxControlExecution(execution)) {
90
364
  throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${entry.name}`);
91
365
  }
@@ -96,27 +370,63 @@ function recoverProcessing(manifest, brokerOwns) {
96
370
  if (fs.existsSync(existingResponse)) {
97
371
  try {
98
372
  const response = JSON.parse(fs.readFileSync(existingResponse, 'utf8'));
373
+ existingTerminalResponse = response;
99
374
  terminal = response.version === 2 && response.id === entry.name
100
375
  && (response.phase === 'completed' || response.phase === 'rejected');
376
+ if (terminal && response.outputState === 'available') {
377
+ if (!response.payload || payloadInvalid || !payload
378
+ || response.payload.version !== payload.version
379
+ || response.payload.id !== payload.id
380
+ || response.payload.generation !== payload.generation
381
+ || response.payload.stdoutBytes !== payload.stdoutBytes
382
+ || response.payload.stderrBytes !== payload.stderrBytes
383
+ || response.payload.stdoutSha256 !== payload.stdoutSha256
384
+ || response.payload.stderrSha256 !== payload.stderrSha256)
385
+ throw new Error('payload missing or mismatched');
386
+ payloadReferenced = true;
387
+ }
388
+ else if (terminal && (response.outputState !== undefined
389
+ || response.payload !== undefined && response.payload !== null)) {
390
+ throw new Error('payload state invalid');
391
+ }
392
+ if (!terminal)
393
+ throw new Error('SANDBOX_CONTROL_RESPONSE_LAYOUT_INVALID');
101
394
  }
102
395
  catch {
103
- terminal = false;
396
+ throw new Error('SANDBOX_CONTROL_RESPONSE_LAYOUT_INVALID');
104
397
  }
105
398
  }
399
+ if (terminal && (!request || !resultEvidence))
400
+ continue;
401
+ if (terminal && request && resultEvidence) {
402
+ const reconciliation = terminalMatchesEvidence(manifest, request, existingTerminalResponse, resultEvidence, payload, payloadInvalid);
403
+ if (!reconciliation.valid)
404
+ continue;
405
+ payloadReferenced = reconciliation.payloadReferenced;
406
+ }
106
407
  if (!terminal) {
107
408
  if (!brokerOwns())
108
409
  return false;
109
- writeResponse(manifest, unknown(entry.name));
410
+ if (!resultEvidence || !request || payloadInvalid)
411
+ continue;
412
+ const recovered = recoveryResponse(manifest, request, resultEvidence, payload);
413
+ if (!recovered)
414
+ continue;
415
+ writeSandboxControlResponse(manifest, recovered);
416
+ payloadReferenced = Boolean(payload) && request.family !== 'task-finalization';
110
417
  }
111
418
  }
112
419
  else {
113
420
  if (!brokerOwns())
114
421
  return false;
115
- writeResponse(manifest, notExecuted(entry.name));
422
+ writeSandboxControlResponse(manifest, notExecuted(entry.name));
116
423
  }
117
424
  if (!brokerOwns())
118
425
  return false;
119
- fs.rmSync(path.join(manifest.processingDir, entry.name), { recursive: true, force: true });
426
+ removeAcceptedResponse(manifest, entry.name);
427
+ fs.rmSync(processingDirectory, { recursive: true, force: true });
428
+ if (!payloadReferenced)
429
+ fs.rmSync(payloadPath(manifest, entry.name), { force: true });
120
430
  }
121
431
  return true;
122
432
  }
@@ -130,6 +440,7 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
130
440
  const timing = options.timing ?? DEFAULT_SANDBOX_CONTROL_TIMING;
131
441
  const inspectContainer = options.inspectContainer ?? ((value) => inspectSandboxControlContainer(value));
132
442
  const bindingCheck = options.bindingCheck ?? bindingReason;
443
+ const prepareExecution = options.prepareExecution ?? prepareSandboxControlExecution;
133
444
  const manifest = readSandboxControlManifest(manifestPath);
134
445
  const root = path.dirname(manifestPath);
135
446
  const startTime = getProcessStartTime(process.pid);
@@ -209,6 +520,18 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
209
520
  }
210
521
  if (current.token !== manifest.token || current.generation !== manifest.generation)
211
522
  break;
523
+ if (active?.result && !active.resultEvidenceWritten) {
524
+ try {
525
+ active.result = sanitizeSandboxControlResult(manifest, active.result);
526
+ writeSandboxControlResultEvidence(manifest, active.request.id, active.result);
527
+ active.resultEvidenceWritten = true;
528
+ active.failure = null;
529
+ active.settled = true;
530
+ }
531
+ catch (error) {
532
+ active.failure = error;
533
+ }
534
+ }
212
535
  const heartbeatNow = Date.now();
213
536
  if (heartbeatNow >= nextContainerHeartbeatAt) {
214
537
  let observation;
@@ -302,17 +625,18 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
302
625
  if (settledExecution) {
303
626
  if (!brokerOwns())
304
627
  break;
305
- if (settledExecution.result) {
306
- writeResponse(manifest, {
307
- version: 2, id: settledExecution.request.id, phase: 'completed', exitCode: settledExecution.result.exitCode,
308
- stdout: settledExecution.result.stdout, stderr: settledExecution.result.stderr, error: null
309
- });
628
+ let terminalCommitted = false;
629
+ if (settledExecution.result && settledExecution.resultEvidenceWritten) {
630
+ terminalCommitted = publishExecutionResult(manifest, settledExecution.request, settledExecution.result, brokerOwns);
310
631
  }
311
632
  else {
312
- writeResponse(manifest, unknown(settledExecution.request.id));
633
+ terminalCommitted = writeSandboxControlResponse(manifest, unknown(settledExecution.request.id));
313
634
  }
635
+ if (!terminalCommitted)
636
+ return;
314
637
  if (!brokerOwns())
315
638
  break;
639
+ removeAcceptedResponse(manifest, settledExecution.request.id);
316
640
  fs.rmSync(path.join(manifest.processingDir, settledExecution.request.id), { recursive: true, force: true });
317
641
  }
318
642
  let retiring = false;
@@ -346,12 +670,26 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
346
670
  throw new Error('SANDBOX_WORKTREE_BINDING_LOST');
347
671
  if (readActiveLease(manifest))
348
672
  throw new Error('SANDBOX_CONTROL_HANDOFF_ACTIVE');
349
- const prepared = await prepareSandboxControlExecution({
350
- manifest, manifestPath, request, requestPath: claimed, internalCliPath: process.argv[1]
673
+ writeSandboxControlReservation(manifest, request.id, sandboxControlGenerationUsage(manifest));
674
+ const prepared = await prepareExecution({
675
+ manifest, manifestPath, request, requestPath: claimed,
676
+ internalCliPath: options.internalCliPath ?? process.argv[1]
351
677
  });
352
- const execution = { request, prepared, result: null, failure: null, settled: false };
678
+ const execution = {
679
+ request, prepared, result: null, resultEvidenceWritten: false, failure: null, settled: false
680
+ };
353
681
  active = execution;
354
- prepared.completion.then((result) => { execution.result = result; execution.settled = true; }, (error) => { execution.failure = error; execution.settled = true; });
682
+ prepared.completion.then((result) => {
683
+ execution.result = sanitizeSandboxControlResult(manifest, result);
684
+ try {
685
+ writeSandboxControlResultEvidence(manifest, request.id, execution.result);
686
+ execution.resultEvidenceWritten = true;
687
+ execution.settled = true;
688
+ }
689
+ catch (error) {
690
+ execution.failure = error;
691
+ }
692
+ }, (error) => { execution.failure = error; execution.settled = true; });
355
693
  if (!brokerOwns()) {
356
694
  prepared.terminate(false);
357
695
  active = null;
@@ -364,7 +702,7 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
364
702
  retiring = true;
365
703
  continue;
366
704
  }
367
- writeResponse(manifest, {
705
+ writeAcceptedResponse(manifest, {
368
706
  version: 2, id, phase: 'accepted', exitCode: null, stdout: '', stderr: '', error: null
369
707
  });
370
708
  if (!brokerOwns()) {
@@ -384,12 +722,13 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
384
722
  retiring = true;
385
723
  break;
386
724
  }
387
- writeResponse(manifest, unknown(id));
725
+ writeSandboxControlResponse(manifest, unknown(id));
388
726
  if (!brokerOwns()) {
389
727
  active = null;
390
728
  retiring = true;
391
729
  break;
392
730
  }
731
+ removeAcceptedResponse(manifest, id);
393
732
  fs.rmSync(path.join(manifest.processingDir, id), { recursive: true, force: true });
394
733
  active = null;
395
734
  claimed = null;
@@ -406,7 +745,7 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
406
745
  retiring = true;
407
746
  break;
408
747
  }
409
- writeResponse(manifest, rejected(id, error));
748
+ writeSandboxControlResponse(manifest, rejected(id, error));
410
749
  if (!brokerOwns()) {
411
750
  retiring = true;
412
751
  break;
@@ -423,9 +762,43 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
423
762
  finally {
424
763
  if (active) {
425
764
  const owned = brokerOwns();
426
- active.prepared.terminate(owned);
427
- if (owned && brokerOwns())
428
- writeResponse(manifest, unknown(active.request.id));
765
+ if (owned && active.result && !active.resultEvidenceWritten) {
766
+ try {
767
+ active.result = sanitizeSandboxControlResult(manifest, active.result);
768
+ writeSandboxControlResultEvidence(manifest, active.request.id, active.result);
769
+ active.resultEvidenceWritten = true;
770
+ active.failure = null;
771
+ active.settled = true;
772
+ }
773
+ catch (error) {
774
+ active.failure = error;
775
+ }
776
+ }
777
+ if (owned && active.result && active.resultEvidenceWritten) {
778
+ if (publishExecutionResult(manifest, active.request, active.result, brokerOwns)) {
779
+ if (brokerOwns()) {
780
+ removeAcceptedResponse(manifest, active.request.id);
781
+ fs.rmSync(path.join(manifest.processingDir, active.request.id), { recursive: true, force: true });
782
+ active = null;
783
+ }
784
+ }
785
+ }
786
+ if (active) {
787
+ const terminationConfirmed = active.prepared.terminate(owned);
788
+ if (owned && brokerOwns() && active.request.family === 'task-finalization') {
789
+ const recovered = finalizationRecoveryResponse(manifest, active.request.id, 0);
790
+ if (recovered.status === 'matched' && recovered.response && writeSandboxControlResponse(manifest, recovered.response)) {
791
+ if (terminationConfirmed && brokerOwns()) {
792
+ removeAcceptedResponse(manifest, active.request.id);
793
+ fs.rmSync(path.join(manifest.processingDir, active.request.id), { recursive: true, force: true });
794
+ active = null;
795
+ }
796
+ }
797
+ }
798
+ if (owned && brokerOwns() && active && active.request.family !== 'task-finalization' && !active.resultEvidenceWritten) {
799
+ writeSandboxControlResponse(manifest, unknown(active.request.id));
800
+ }
801
+ }
429
802
  }
430
803
  if (brokerOwns()) {
431
804
  appendSandboxControlAudit(manifest, 'broker-stop', { pid: broker.pid, brokerId: broker.brokerId });