@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
package/lib/run/index.ts CHANGED
@@ -38,54 +38,90 @@ export type RunSkillOptions = {
38
38
  writeStderr?: (chunk: string) => void;
39
39
  };
40
40
 
41
- const USAGE = `Usage: ai run <skill> [task-ref] [args...] [--tui <name>] [--recreate]
41
+ const USAGE = `Usage: ai run --skill <skill> [--task <task-ref>] [--tui <name>] [--recreate] [-- <skill-args...>]
42
42
 
43
43
  Task skills are scheduled inside the sandbox tmux session; ai run returns once
44
44
  the tmux window is created. --recreate authorizes container-only replacement
45
45
  when in-place sandbox recovery fails; it is not included in the TUI prompt.
46
46
 
47
47
  Examples:
48
- ai run create-task "describe the task" --tui codex
49
- ai run code-task #7 --tui codex --recreate`;
48
+ ai run --skill create-task "describe the task" --tui codex
49
+ ai run --skill code-task --task 7 --tui codex --recreate`;
50
50
 
51
- function extractTui(args: string[]): { rest: string[]; tui: string | null } {
52
- const rest: string[] = [];
51
+ function optionValue(args: string[], index: number, option: string): string {
52
+ const value = args[index + 1];
53
+ if (!value || value.startsWith('--')) throw new Error(`${option} requires a value`);
54
+ return value;
55
+ }
56
+
57
+ function extractHostOptions(args: string[]): {
58
+ positionals: string[];
59
+ forwarded: string[];
60
+ skill: string | null;
61
+ taskRef: string | null;
62
+ tui: string | null;
63
+ recreate: boolean;
64
+ } {
65
+ const positionals: string[] = [];
66
+ const forwarded: string[] = [];
67
+ let skill: string | null = null;
68
+ let taskRef: string | null = null;
53
69
  let tui: string | null = null;
70
+ let recreate = false;
71
+ let separator = false;
54
72
  for (let i = 0; i < args.length; i += 1) {
55
73
  const arg = args[i] as string;
56
- if (arg === '--tui') {
57
- const value = args[i + 1];
58
- if (!value) throw new Error('--tui requires a value');
59
- tui = value;
74
+ if (separator) {
75
+ forwarded.push(arg);
76
+ } else if (arg === '--') {
77
+ separator = true;
78
+ } else if (arg === '--skill') {
79
+ if (skill !== null) throw new Error("duplicate option '--skill'");
80
+ skill = optionValue(args, i, '--skill');
60
81
  i += 1;
61
- continue;
82
+ } else if (arg === '--task') {
83
+ if (taskRef !== null) throw new Error("duplicate option '--task'");
84
+ taskRef = optionValue(args, i, '--task');
85
+ i += 1;
86
+ } else if (arg === '--tui') {
87
+ if (tui !== null) throw new Error("duplicate option '--tui'");
88
+ tui = optionValue(args, i, '--tui');
89
+ i += 1;
90
+ } else if (arg === '--recreate') {
91
+ if (recreate) throw new Error("duplicate option '--recreate'");
92
+ recreate = true;
93
+ } else if (arg.startsWith('-')) {
94
+ throw new Error(`Unknown option '${arg}'`);
95
+ } else {
96
+ positionals.push(arg);
62
97
  }
63
- rest.push(arg);
64
98
  }
65
- return { rest, tui };
99
+ return { positionals, forwarded, skill, taskRef, tui, recreate };
66
100
  }
67
101
 
68
102
  export function parseRunArgs(args: string[]): ParsedRunArgs {
69
103
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
70
104
  throw new Error(USAGE);
71
105
  }
72
- const { rest: withRecoveryFlags, tui } = extractTui(args);
73
- const recreate = withRecoveryFlags.includes('--recreate');
74
- const rest = withRecoveryFlags.filter((arg) => arg !== '--recreate');
75
- const [skill = '', maybeTaskRef, ...remaining] = rest;
106
+ const { positionals, forwarded, skill, taskRef, tui, recreate } = extractHostOptions(args);
107
+ if (!skill) throw new Error("option '--skill' is required");
76
108
  const spec = getSkillRunSpec(skill);
77
109
  if (!spec) throw new Error(`Unknown skill '${skill}'`);
78
110
  if (spec.kind === 'create') {
111
+ if (taskRef !== null) throw new Error("create-task does not accept '--task'");
79
112
  if (recreate) throw new Error('--recreate is only valid for sandbox task runs');
80
- const createArgs = rest.slice(1);
113
+ const createArgs = [...positionals, ...forwarded];
81
114
  if (createArgs.length === 0) throw new Error('create-task requires a description');
82
115
  return { skill, taskRef: null, args: createArgs, tui };
83
116
  }
84
- if (!maybeTaskRef) throw new Error(`${skill} requires a task-ref`);
117
+ if (positionals.length > 0) {
118
+ throw new Error('ai run requires --skill and --task; positional host arguments are not supported');
119
+ }
120
+ if (taskRef === null) throw new Error(`${skill} requires a task-ref`);
85
121
  return {
86
122
  skill,
87
- taskRef: maybeTaskRef,
88
- args: remaining,
123
+ taskRef,
124
+ args: forwarded,
89
125
  tui,
90
126
  ...(recreate ? { recreate: true as const } : {})
91
127
  };
@@ -200,8 +236,7 @@ export async function runSkill(args: string[], options: RunSkillOptions = {}): P
200
236
  const commandConfig = options.command ?? config?.command ?? {};
201
237
  assertAllowedByConfig(parsed.skill, commandConfig);
202
238
  const tui = selectTui(parsed.skill, { cliTui: parsed.tui, command: commandConfig });
203
- const promptArgs = parsed.taskRef === null ? parsed.args : [parsed.taskRef, ...parsed.args];
204
- const prompt = renderPrompt({ tui, skill: parsed.skill, args: promptArgs });
239
+ const prompt = renderPrompt({ tui, skill: parsed.skill, args: parsed.args });
205
240
  const [file, argv] = buildTuiCommand(tui, prompt);
206
241
  const command = [file, ...argv];
207
242
 
@@ -20,7 +20,7 @@ import type {
20
20
  } from './controller-registration.ts';
21
21
  import type { ProcessIdentity } from '../../server/process-state.ts';
22
22
  import { normalizeAgentToken } from '../../agent-clients/tokens.ts';
23
- import { readSandboxControlStatus } from './state.ts';
23
+ import { readSandboxControlPayload, readSandboxControlStatus } from './state.ts';
24
24
  import type { TaskCreateCandidateV1 } from '../../task/create.ts';
25
25
 
26
26
  const SANDBOX_CONTROL_RESPONSE_SETTLE_MS = 250;
@@ -28,11 +28,13 @@ const SANDBOX_CONTROL_RESPONSE_SETTLE_MS = 250;
28
28
  export class SandboxControlClientError extends Error {
29
29
  readonly detail: SandboxControlError;
30
30
  readonly accepted: boolean;
31
- constructor(detail: SandboxControlError, accepted = false) {
31
+ readonly requestId: string | null;
32
+ constructor(detail: SandboxControlError, accepted = false, requestId: string | null = null) {
32
33
  super(detail.message);
33
34
  this.name = 'SandboxControlClientError';
34
35
  this.detail = detail;
35
36
  this.accepted = accepted;
37
+ this.requestId = requestId;
36
38
  }
37
39
  }
38
40
 
@@ -40,8 +42,14 @@ function sleep(milliseconds: number): void {
40
42
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
41
43
  }
42
44
 
43
- function clientError(code: string, message: string, retryable: boolean, accepted = false): never {
44
- throw new SandboxControlClientError({ code, message: `${code}: ${message}`, retryable }, accepted);
45
+ function clientError(
46
+ code: string,
47
+ message: string,
48
+ retryable: boolean,
49
+ accepted = false,
50
+ requestId: string | null = null
51
+ ): never {
52
+ throw new SandboxControlClientError({ code, message: `${code}: ${message}`, retryable }, accepted, requestId);
45
53
  }
46
54
 
47
55
  function preflight(statusDir: string, generation: string, now = Date.now()): void {
@@ -68,13 +76,35 @@ function preflight(statusDir: string, generation: string, now = Date.now()): voi
68
76
  function parseResponse(raw: string, id: string): SandboxControlResponse {
69
77
  const response = JSON.parse(raw) as SandboxControlResponse;
70
78
  if (response.version !== 2 || response.id !== id
71
- || !['accepted', 'completed', 'rejected'].includes(response.phase)
72
- || (response.phase === 'completed' && !Number.isInteger(response.exitCode))) {
73
- clientError('SANDBOX_CONTROL_RESPONSE_INVALID', 'broker response is invalid', false);
79
+ || !['completed', 'rejected'].includes(response.phase)
80
+ || (response.phase === 'completed' && !Number.isInteger(response.exitCode))
81
+ || (response.outputState !== undefined && response.outputState !== 'available' && response.outputState !== 'unavailable')
82
+ || (response.outputState === 'available' && !response.payload)
83
+ || (response.outputState === 'unavailable' && response.payload !== null && response.payload !== undefined)) {
84
+ clientError('SANDBOX_CONTROL_RESPONSE_INVALID', 'broker response is invalid', false, false, id);
74
85
  }
75
86
  return response;
76
87
  }
77
88
 
89
+ function readPublishedResponse(raw: string, id: string, channelDir: string): SandboxControlResponse {
90
+ const response = parseResponse(raw, id);
91
+ if (response.outputState !== 'available') return response;
92
+ const filePath = path.join(channelDir, 'responses', `${id}.payload.json`);
93
+ let payload;
94
+ try {
95
+ payload = readSandboxControlPayload(filePath);
96
+ } catch {
97
+ clientError('SANDBOX_CONTROL_RESPONSE_INVALID', 'broker payload is missing or invalid', false, true, id);
98
+ }
99
+ if (!response.payload || response.payload.version !== payload.version
100
+ || response.payload.id !== payload.id || response.payload.generation !== payload.generation
101
+ || response.payload.stdoutBytes !== payload.stdoutBytes || response.payload.stderrBytes !== payload.stderrBytes
102
+ || response.payload.stdoutSha256 !== payload.stdoutSha256 || response.payload.stderrSha256 !== payload.stderrSha256) {
103
+ clientError('SANDBOX_CONTROL_RESPONSE_INVALID', 'broker payload reference is invalid', false, true, id);
104
+ }
105
+ return { ...response, stdout: payload.stdout, stderr: payload.stderr };
106
+ }
107
+
78
108
  function cancelPendingRequest(requestPath: string): boolean {
79
109
  try {
80
110
  fs.unlinkSync(requestPath);
@@ -95,13 +125,16 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
95
125
  preflight(statusDir, request.generation);
96
126
  const encoded = `${JSON.stringify(request)}\n`;
97
127
  if (Buffer.byteLength(encoded, 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
98
- clientError('SANDBOX_CONTROL_REQUEST_TOO_LARGE', 'request exceeds the control limit', false);
128
+ clientError('SANDBOX_CONTROL_REQUEST_TOO_LARGE', 'request exceeds the control limit', false, false, request.id);
99
129
  }
100
130
  const requestsDir = path.join(channelDir, 'requests');
101
131
  const responsesDir = path.join(channelDir, 'responses');
102
132
  const temporary = path.join(requestsDir, `.${request.id}.tmp`);
103
133
  const requestPath = path.join(requestsDir, `${request.id}.json`);
104
134
  const responsePath = path.join(responsesDir, `${request.id}.json`);
135
+ if (fs.existsSync(responsePath)) {
136
+ clientError('SANDBOX_CONTROL_REQUEST_REPLAYED', 'request id already has a terminal response', false, false, request.id);
137
+ }
105
138
  fs.writeFileSync(temporary, encoded, { mode: 0o600, flag: 'wx' });
106
139
  fs.renameSync(temporary, requestPath);
107
140
  const deadline = Date.now() + (params.timeoutMs ?? 30_000);
@@ -109,6 +142,8 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
109
142
  let malformedResponseRaw: string | null = null;
110
143
  let malformedResponseObservedAt = 0;
111
144
  while (Date.now() < deadline) {
145
+ const acceptedPath = path.join(responsesDir, `${request.id}.accepted.json`);
146
+ if (fs.existsSync(acceptedPath)) accepted = true;
112
147
  if (fs.existsSync(responsePath)) {
113
148
  let raw: string;
114
149
  try {
@@ -119,7 +154,7 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
119
154
  }
120
155
  let response: SandboxControlResponse;
121
156
  try {
122
- response = parseResponse(raw, request.id);
157
+ response = readPublishedResponse(raw, request.id, channelDir);
123
158
  malformedResponseRaw = null;
124
159
  } catch (error) {
125
160
  if (!(error instanceof SyntaxError)) throw error;
@@ -128,7 +163,7 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
128
163
  malformedResponseRaw = raw;
129
164
  malformedResponseObservedAt = now;
130
165
  } else if (now - malformedResponseObservedAt >= SANDBOX_CONTROL_RESPONSE_SETTLE_MS) {
131
- clientError('SANDBOX_CONTROL_RESPONSE_INVALID', 'broker response remained malformed', false, accepted);
166
+ clientError('SANDBOX_CONTROL_RESPONSE_INVALID', 'broker response remained malformed', false, accepted, request.id);
132
167
  }
133
168
  sleep(25);
134
169
  continue;
@@ -136,7 +171,6 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
136
171
  if (response.phase === 'accepted') {
137
172
  accepted = true;
138
173
  } else {
139
- fs.rmSync(responsePath, { force: true });
140
174
  return response;
141
175
  }
142
176
  }
@@ -149,7 +183,8 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
149
183
  'SANDBOX_CONTROL_RESULT_UNKNOWN',
150
184
  'request was claimed before broker availability was lost; inspect domain state before retrying',
151
185
  false,
152
- true
186
+ true,
187
+ request.id
153
188
  );
154
189
  }
155
190
  }
@@ -160,7 +195,8 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
160
195
  'SANDBOX_CONTROL_RESULT_UNKNOWN',
161
196
  'accepted request did not produce a final result; inspect domain state before retrying',
162
197
  false,
163
- true
198
+ true,
199
+ request.id
164
200
  );
165
201
  }
166
202
  if (!cancelPendingRequest(requestPath)) {
@@ -168,10 +204,64 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
168
204
  'SANDBOX_CONTROL_RESULT_UNKNOWN',
169
205
  'request was claimed but no acceptance result was observed; inspect domain state before retrying',
170
206
  false,
171
- true
207
+ true,
208
+ request.id
172
209
  );
173
210
  }
174
- clientError('SANDBOX_CONTROL_BROKER_UNAVAILABLE', `broker did not accept the request within ${params.timeoutMs ?? 30_000}ms`, true);
211
+ clientError('SANDBOX_CONTROL_BROKER_UNAVAILABLE', `broker did not accept the request within ${params.timeoutMs ?? 30_000}ms`, true, false, request.id);
212
+ }
213
+
214
+ export function recoverSandboxControl(requestId: string, params: Readonly<{
215
+ channelDir?: string;
216
+ timeoutMs?: number;
217
+ }> = {}): SandboxControlResponse {
218
+ if (!/^[a-f0-9-]{16,64}$/u.test(requestId)) {
219
+ clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'request id is invalid', false, true);
220
+ }
221
+ const channelDir = params.channelDir ?? process.env.AGENT_INFRA_CONTROL_DIR ?? '/run/agent-infra/control';
222
+ const responsePath = path.join(channelDir, 'responses', `${requestId}.json`);
223
+ const deadline = Date.now() + (params.timeoutMs ?? 30_000);
224
+ let accepted = false;
225
+ let malformedResponseRaw: string | null = null;
226
+ let malformedResponseObservedAt = 0;
227
+ while (Date.now() < deadline) {
228
+ if (fs.existsSync(path.join(channelDir, 'responses', `${requestId}.accepted.json`))) accepted = true;
229
+ if (fs.existsSync(responsePath)) {
230
+ let raw: string;
231
+ try {
232
+ raw = fs.readFileSync(responsePath, 'utf8');
233
+ } catch (error) {
234
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') continue;
235
+ throw error;
236
+ }
237
+ let response: SandboxControlResponse;
238
+ try {
239
+ response = readPublishedResponse(raw, requestId, channelDir);
240
+ malformedResponseRaw = null;
241
+ } catch (error) {
242
+ if (!(error instanceof SyntaxError)) throw error;
243
+ const now = Date.now();
244
+ if (malformedResponseRaw !== raw) {
245
+ malformedResponseRaw = raw;
246
+ malformedResponseObservedAt = now;
247
+ } else if (now - malformedResponseObservedAt >= SANDBOX_CONTROL_RESPONSE_SETTLE_MS) {
248
+ clientError('SANDBOX_CONTROL_RESPONSE_INVALID', 'broker response remained malformed', false, accepted, requestId);
249
+ }
250
+ sleep(25);
251
+ continue;
252
+ }
253
+ if (response.phase === 'accepted') accepted = true;
254
+ else return response;
255
+ }
256
+ sleep(25);
257
+ }
258
+ clientError(
259
+ 'SANDBOX_CONTROL_RESULT_UNKNOWN',
260
+ 'request did not produce a final result; inspect domain state before retrying',
261
+ false,
262
+ accepted,
263
+ requestId
264
+ );
175
265
  }
176
266
 
177
267
  function authority(params: { token?: string; generation?: string }): { token: string; generation: string } {
@@ -218,7 +218,7 @@ function lifecycleFailure(code: string, message: string): SandboxControlExecutio
218
218
  };
219
219
  }
220
220
 
221
- function finalizationResult(result: ReturnType<typeof applyTaskFinalization>): SandboxControlExecutionResult {
221
+ function finalizationResult(result: Awaited<ReturnType<typeof applyTaskFinalization>>): SandboxControlExecutionResult {
222
222
  const payload = {
223
223
  version: 1,
224
224
  status: result.status,
@@ -246,7 +246,7 @@ export async function executeRequest(
246
246
  options: ExecuteRequestOptions = {}
247
247
  ): Promise<SandboxControlExecutionResult> {
248
248
  if (request.family === 'task-create') {
249
- const result = createTask(request.candidate, { repoRoot: manifest.repoRoot });
249
+ const result = await createTask(request.candidate, { repoRoot: manifest.repoRoot });
250
250
  return {
251
251
  exitCode: result.status === 'blocked' ? 2 : result.status === 'failed' ? 1 : 0,
252
252
  stdout: `${JSON.stringify(result)}\n`,
@@ -313,7 +313,7 @@ export async function executeRequest(
313
313
  }),
314
314
  operation
315
315
  );
316
- return finalizationResult(result);
316
+ return finalizationResult(await result);
317
317
  }
318
318
  const boundArgs = bindSandboxControlTask(request, manifest.taskId!);
319
319
  if (request.family === 'task-orchestration') {
@@ -387,7 +387,7 @@ export async function executeRequest(
387
387
  return format(await dispatchTaskControlOperation(context, operation));
388
388
  }
389
389
  if (operation.family !== 'task-lifecycle') throw new Error('SANDBOX_CONTROL_LIFECYCLE_OPERATION_INVALID');
390
- return format(dispatchTaskControlOperation(context, operation));
390
+ return format(await dispatchTaskControlOperation(context, operation));
391
391
  }
392
392
 
393
393
  export async function runSandboxControlExecutor(requestPath: string, nonce: string): Promise<void> {
@@ -1,9 +1,14 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { validateTaskCreateCandidate, type TaskCreateCandidateV1 } from '../../task/create.ts';
2
3
  import { normalizeAgentToken } from '../../agent-clients/tokens.ts';
3
4
  import type { ProcessIdentity } from '../../server/process-state.ts';
4
5
  import type { CodexControllerLeaseProofV1 } from './controller-registration.ts';
5
6
 
6
7
  export const SANDBOX_CONTROL_MAX_BYTES = 64 * 1024;
8
+ export const SANDBOX_CONTROL_MAX_LOGICAL_RECORDS = 1024;
9
+ export const SANDBOX_CONTROL_MAX_RESPONSE_BYTES = 64 * 1024 * 1024;
10
+ export const SANDBOX_CONTROL_MAX_TERMINAL_RECORD_BYTES = 1024 * 1024;
11
+ export const SANDBOX_CONTROL_RESERVATION_BYTES = SANDBOX_CONTROL_MAX_TERMINAL_RECORD_BYTES + 4 * 1024;
7
12
  export const SANDBOX_CONTROL_ADMISSION_WINDOW_MS = 2_000;
8
13
  export const SANDBOX_CONTROL_STATUS_INTERVAL_MS = 250;
9
14
  export const SANDBOX_CONTROL_STATUS_STALE_MS = 1_500;
@@ -63,9 +68,43 @@ export type SandboxCodexControllerRequest = RequestBase & Readonly<{
63
68
  }>;
64
69
  export type SandboxControlRequest = SandboxTaskCommandRequest | SandboxTaskFinalizationRequest | SandboxTaskCreateRequest | SandboxCodexControllerRequest;
65
70
  export type SandboxControlError = Readonly<{ code: string; message: string; retryable: boolean }>;
71
+ export type SandboxControlResultEvidence = Readonly<{
72
+ version: 1;
73
+ id: string;
74
+ generation: string;
75
+ exitCode: number;
76
+ stdoutBytes: number;
77
+ stderrBytes: number;
78
+ stdoutSha256: string;
79
+ stderrSha256: string;
80
+ captureState: 'metadata-only';
81
+ }>;
82
+ export type SandboxControlPayloadReference = Readonly<{
83
+ version: 1;
84
+ id: string;
85
+ generation: string;
86
+ stdoutBytes: number;
87
+ stderrBytes: number;
88
+ stdoutSha256: string;
89
+ stderrSha256: string;
90
+ }>;
91
+ export type SandboxControlPayload = SandboxControlPayloadReference & Readonly<{
92
+ stdout: string;
93
+ stderr: string;
94
+ }>;
95
+ export type SandboxControlReservation = Readonly<{
96
+ version: 1;
97
+ id: string;
98
+ generation: string;
99
+ logicalRecords: 1;
100
+ bytes: number;
101
+ createdAt: number;
102
+ }>;
66
103
  export type SandboxControlResponse = Readonly<{
67
104
  version: 2; id: string; phase: 'accepted' | 'completed' | 'rejected'; exitCode: number | null;
68
105
  stdout: string; stderr: string; error: SandboxControlError | null;
106
+ outputState?: 'available' | 'unavailable';
107
+ payload?: SandboxControlPayloadReference | null;
69
108
  }>;
70
109
  export type SandboxControlStatus = Readonly<{
71
110
  version: 2; generation: string; broker: ProcessIdentity & { brokerId: string };
@@ -97,6 +136,80 @@ function fail(code: string, message: string, retryable = false): never {
97
136
  throw new SandboxControlProtocolError(code, message, retryable);
98
137
  }
99
138
 
139
+ export function parseSandboxControlResultEvidence(value: unknown): SandboxControlResultEvidence {
140
+ const evidence = value as Partial<SandboxControlResultEvidence> | null;
141
+ const keys = evidence && typeof evidence === 'object' ? Object.keys(evidence).sort().join(',') : '';
142
+ if (keys !== 'captureState,exitCode,generation,id,stderrBytes,stderrSha256,stdoutBytes,stdoutSha256,version'
143
+ || evidence?.version !== 1
144
+ || typeof evidence.id !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(evidence.id)
145
+ || typeof evidence.generation !== 'string' || evidence.generation.length === 0
146
+ || !Number.isSafeInteger(evidence.exitCode)
147
+ || !Number.isSafeInteger(evidence.stdoutBytes) || (evidence.stdoutBytes as number) < 0
148
+ || !Number.isSafeInteger(evidence.stderrBytes) || (evidence.stderrBytes as number) < 0
149
+ || (evidence.stdoutBytes as number) + (evidence.stderrBytes as number) > SANDBOX_CONTROL_MAX_RESPONSE_BYTES
150
+ || typeof evidence.stdoutSha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(evidence.stdoutSha256)
151
+ || typeof evidence.stderrSha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(evidence.stderrSha256)
152
+ || evidence.captureState !== 'metadata-only') {
153
+ throw new Error('SANDBOX_CONTROL_RESULT_EVIDENCE_INVALID');
154
+ }
155
+ return evidence as SandboxControlResultEvidence;
156
+ }
157
+
158
+ function validPayloadReference(value: unknown, id: string, generation: string): value is SandboxControlPayloadReference {
159
+ const payload = value as Partial<SandboxControlPayloadReference> | null;
160
+ const keys = payload && typeof payload === 'object' ? Object.keys(payload).sort().join(',') : '';
161
+ return keys === 'generation,id,stderrBytes,stderrSha256,stdoutBytes,stdoutSha256,version'
162
+ && payload?.version === 1
163
+ && payload.id === id
164
+ && payload.generation === generation
165
+ && Number.isSafeInteger(payload.stdoutBytes) && (payload.stdoutBytes as number) >= 0
166
+ && Number.isSafeInteger(payload.stderrBytes) && (payload.stderrBytes as number) >= 0
167
+ && (payload.stdoutBytes as number) + (payload.stderrBytes as number) <= SANDBOX_CONTROL_MAX_RESPONSE_BYTES
168
+ && typeof payload.stdoutSha256 === 'string' && /^[a-f0-9]{64}$/u.test(payload.stdoutSha256)
169
+ && typeof payload.stderrSha256 === 'string' && /^[a-f0-9]{64}$/u.test(payload.stderrSha256);
170
+ }
171
+
172
+ export function parseSandboxControlPayload(value: unknown): SandboxControlPayload {
173
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('SANDBOX_CONTROL_PAYLOAD_INVALID');
174
+ const payload = value as Partial<SandboxControlPayload>;
175
+ const reference: Partial<SandboxControlPayloadReference> = {
176
+ version: payload.version,
177
+ id: payload.id,
178
+ generation: payload.generation,
179
+ stdoutBytes: payload.stdoutBytes,
180
+ stderrBytes: payload.stderrBytes,
181
+ stdoutSha256: payload.stdoutSha256,
182
+ stderrSha256: payload.stderrSha256
183
+ };
184
+ const keys = Object.keys(payload).sort().join(',');
185
+ if (keys !== 'generation,id,stderr,stderrBytes,stderrSha256,stdout,stdoutBytes,stdoutSha256,version'
186
+ || !validPayloadReference(reference, reference.id as string, reference.generation as string)
187
+ || typeof payload.stdout !== 'string' || typeof payload.stderr !== 'string'
188
+ || Buffer.byteLength(payload.stdout, 'utf8') !== payload.stdoutBytes
189
+ || Buffer.byteLength(payload.stderr, 'utf8') !== payload.stderrBytes
190
+ || createHash('sha256').update(payload.stdout, 'utf8').digest('hex') !== payload.stdoutSha256
191
+ || createHash('sha256').update(payload.stderr, 'utf8').digest('hex') !== payload.stderrSha256) {
192
+ throw new Error('SANDBOX_CONTROL_PAYLOAD_INVALID');
193
+ }
194
+ return payload as SandboxControlPayload;
195
+ }
196
+
197
+ export function parseSandboxControlReservation(value: unknown): SandboxControlReservation {
198
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('SANDBOX_CONTROL_RESERVATION_INVALID');
199
+ const reservation = value as Partial<SandboxControlReservation>;
200
+ const keys = Object.keys(reservation).sort().join(',');
201
+ if (keys !== 'bytes,createdAt,generation,id,logicalRecords,version'
202
+ || reservation.version !== 1
203
+ || typeof reservation.id !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(reservation.id)
204
+ || typeof reservation.generation !== 'string' || reservation.generation.length === 0
205
+ || reservation.logicalRecords !== 1
206
+ || !Number.isSafeInteger(reservation.bytes) || (reservation.bytes as number) < SANDBOX_CONTROL_RESERVATION_BYTES
207
+ || !Number.isSafeInteger(reservation.createdAt)) {
208
+ throw new Error('SANDBOX_CONTROL_RESERVATION_INVALID');
209
+ }
210
+ return reservation as SandboxControlReservation;
211
+ }
212
+
100
213
  export function isSandboxControlFamily(value: string): value is SandboxControlFamily {
101
214
  return SANDBOX_CONTROL_FAMILIES.includes(value as SandboxControlFamily);
102
215
  }