@fitlab-ai/agent-infra 0.9.5 → 0.9.7

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 (172) hide show
  1. package/dist/bin/internal-cli.js +8 -3
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +171 -20
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +68 -8
  8. package/dist/lib/agent-clients/adapters/codex.js +1 -1
  9. package/dist/lib/defaults.json +10 -2
  10. package/dist/lib/git/workflow.js +30 -4
  11. package/dist/lib/internal/codex-lifecycle.js +169 -3
  12. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  13. package/dist/lib/internal/release-workflow.js +108 -11
  14. package/dist/lib/internal/sandbox-control.js +43 -6
  15. package/dist/lib/internal/task-create.js +12 -4
  16. package/dist/lib/internal/task-orchestration.js +40 -7
  17. package/dist/lib/platform/context.js +6 -4
  18. package/dist/lib/platform/github-client.js +55 -2
  19. package/dist/lib/platform/issue-comments.js +2 -1
  20. package/dist/lib/process-data/checkpoint.js +319 -0
  21. package/dist/lib/process-data/index.js +128 -60
  22. package/dist/lib/process-data/materialize.js +84 -0
  23. package/dist/lib/process-data/normalize.js +34 -1
  24. package/dist/lib/process-data/reconcile.js +55 -1
  25. package/dist/lib/process-data/sources.js +422 -1
  26. package/dist/lib/process-data/store.js +341 -11
  27. package/dist/lib/release/workflow.js +29 -21
  28. package/dist/lib/sandbox/commands/create.js +111 -60
  29. package/dist/lib/sandbox/commands/prune.js +40 -16
  30. package/dist/lib/sandbox/commands/rm.js +372 -94
  31. package/dist/lib/sandbox/commands/start.js +4 -3
  32. package/dist/lib/sandbox/control/client.js +138 -22
  33. package/dist/lib/sandbox/control/container-identity.js +64 -0
  34. package/dist/lib/sandbox/control/executor.js +142 -0
  35. package/dist/lib/sandbox/control/lifecycle.js +557 -0
  36. package/dist/lib/sandbox/control/protocol.js +69 -26
  37. package/dist/lib/sandbox/control/server.js +395 -99
  38. package/dist/lib/sandbox/control/state.js +191 -0
  39. package/dist/lib/sandbox/control/timing.js +8 -0
  40. package/dist/lib/sandbox/index.js +2 -2
  41. package/dist/lib/sandbox/managed-fs.js +14 -2
  42. package/dist/lib/sandbox/mounts.js +22 -19
  43. package/dist/lib/sandbox/recovery.js +284 -50
  44. package/dist/lib/sandbox/workspace-view.js +90 -10
  45. package/dist/lib/sandbox/worktree-safety.js +196 -0
  46. package/dist/lib/server/daemon.js +1 -1
  47. package/dist/lib/server/process-control.js +17 -1
  48. package/dist/lib/server/process-state.js +57 -20
  49. package/dist/lib/task/artifact-lifecycle.js +87 -15
  50. package/dist/lib/task/artifact-receipts.js +128 -0
  51. package/dist/lib/task/codex-orchestration.js +236 -0
  52. package/dist/lib/task/commands/status.js +3 -0
  53. package/dist/lib/task/commands/validate.js +289 -0
  54. package/dist/lib/task/commit-finalization.js +36 -9
  55. package/dist/lib/task/create.js +10 -2
  56. package/dist/lib/task/delegation-receipts.js +109 -2
  57. package/dist/lib/task/events.js +149 -6
  58. package/dist/lib/task/finalization-artifacts.js +2 -1
  59. package/dist/lib/task/implementation-inputs.js +1 -7
  60. package/dist/lib/task/index.js +9 -0
  61. package/dist/lib/task/ledger.js +13 -1
  62. package/dist/lib/task/orchestration.js +365 -15
  63. package/dist/lib/task/sections.js +5 -1
  64. package/dist/lib/task/task-execution-lock.js +5 -4
  65. package/dist/lib/task/verification-engine.js +78 -14
  66. package/dist/lib/task/verification.js +1 -0
  67. package/dist/lifecycle-build-manifest.json +6 -0
  68. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +202 -18
  69. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  70. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
  71. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
  72. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  73. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
  74. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +83 -6
  75. package/lib/agent-clients/adapters/codex.ts +1 -1
  76. package/lib/defaults.json +10 -2
  77. package/lib/git/workflow.ts +25 -3
  78. package/lib/internal/codex-lifecycle.ts +175 -4
  79. package/lib/internal/codex-sandbox-controller.ts +109 -0
  80. package/lib/internal/release-workflow.ts +107 -11
  81. package/lib/internal/sandbox-control.ts +38 -6
  82. package/lib/internal/task-create.ts +16 -4
  83. package/lib/internal/task-orchestration.ts +34 -7
  84. package/lib/platform/context.ts +8 -5
  85. package/lib/platform/github-client.ts +61 -3
  86. package/lib/platform/issue-comments.ts +2 -1
  87. package/lib/process-data/checkpoint.ts +335 -0
  88. package/lib/process-data/index.ts +112 -60
  89. package/lib/process-data/materialize.ts +83 -0
  90. package/lib/process-data/normalize.ts +29 -1
  91. package/lib/process-data/reconcile.ts +64 -2
  92. package/lib/process-data/sources.ts +449 -2
  93. package/lib/process-data/store.ts +304 -11
  94. package/lib/process-data/types.ts +124 -2
  95. package/lib/release/workflow.ts +69 -20
  96. package/lib/sandbox/commands/create.ts +70 -25
  97. package/lib/sandbox/commands/prune.ts +53 -15
  98. package/lib/sandbox/commands/rm.ts +423 -108
  99. package/lib/sandbox/commands/start.ts +4 -3
  100. package/lib/sandbox/control/client.ts +152 -28
  101. package/lib/sandbox/control/container-identity.ts +93 -0
  102. package/lib/sandbox/control/executor.ts +167 -0
  103. package/lib/sandbox/control/lifecycle.ts +573 -0
  104. package/lib/sandbox/control/protocol.ts +121 -68
  105. package/lib/sandbox/control/server.ts +398 -109
  106. package/lib/sandbox/control/state.ts +213 -0
  107. package/lib/sandbox/control/timing.ts +12 -0
  108. package/lib/sandbox/index.ts +2 -2
  109. package/lib/sandbox/managed-fs.ts +17 -2
  110. package/lib/sandbox/mounts.ts +16 -8
  111. package/lib/sandbox/recovery.ts +335 -65
  112. package/lib/sandbox/workspace-view.ts +102 -11
  113. package/lib/sandbox/worktree-safety.ts +234 -0
  114. package/lib/server/daemon.ts +1 -1
  115. package/lib/server/process-control.ts +24 -3
  116. package/lib/server/process-state.ts +82 -27
  117. package/lib/task/artifact-lifecycle.ts +78 -14
  118. package/lib/task/artifact-receipts.ts +161 -0
  119. package/lib/task/codex-orchestration.ts +319 -0
  120. package/lib/task/commands/status.ts +2 -0
  121. package/lib/task/commands/validate.ts +282 -0
  122. package/lib/task/commit-finalization.ts +52 -9
  123. package/lib/task/create.ts +11 -2
  124. package/lib/task/delegation-receipts.ts +208 -7
  125. package/lib/task/events.ts +131 -5
  126. package/lib/task/finalization-artifacts.ts +2 -1
  127. package/lib/task/implementation-inputs.ts +1 -5
  128. package/lib/task/index.ts +8 -0
  129. package/lib/task/ledger.ts +16 -1
  130. package/lib/task/orchestration.ts +552 -18
  131. package/lib/task/sections.ts +6 -2
  132. package/lib/task/task-execution-lock.ts +11 -9
  133. package/lib/task/verification-engine.ts +93 -24
  134. package/lib/task/verification.ts +2 -1
  135. package/package.json +1 -1
  136. package/templates/.agents/README.en.md +7 -0
  137. package/templates/.agents/README.zh-CN.md +5 -0
  138. package/templates/.agents/hooks/lifecycle-delegation.js +72 -12
  139. package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
  140. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
  141. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
  142. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
  143. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  144. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  145. package/templates/.agents/rules/review-handshake.en.md +3 -1
  146. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  147. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  148. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  149. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  150. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  151. package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
  152. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
  153. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
  154. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
  155. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +25 -0
  156. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +25 -0
  157. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  158. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  159. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +69 -0
  160. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +69 -0
  161. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  162. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  163. package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -3
  164. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -3
  165. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  166. package/templates/.claude/commands/run-manual-validation.en.md +9 -0
  167. package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
  168. package/templates/.codex/hooks.json +8 -8
  169. package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
  170. package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
  171. package/templates/.agents/workspace/README.en.md +0 -26
  172. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -20,6 +20,10 @@ function value(runner: GitRunner, cwd: string, args: readonly string[], trim = t
20
20
  return result.status === 0 ? (trim ? result.stdout.trim() : result.stdout.replace(/\n$/, '')) : null;
21
21
  }
22
22
 
23
+ function nulPaths(output: string): string[] {
24
+ return output.split('\0').filter(Boolean);
25
+ }
26
+
23
27
  function cleanMessage(message: string): string {
24
28
  return message.replace(/(https?:\/\/)[^\s/@:]+(?::[^\s/@]*)?@/gi, '$1***@').trim();
25
29
  }
@@ -68,8 +72,27 @@ function commitExplicitPaths(input: { cwd: string; paths: readonly string[]; mes
68
72
  const selected = new Set(input.paths);
69
73
  const unrelatedStaged = before.snapshot.staged.filter((candidate) => !selected.has(candidate));
70
74
  if (unrelatedStaged.length > 0) return { status: 'failed' as const, changed: false, snapshot: before.snapshot, operations: [], error: { code: 'GIT_STAGED_SCOPE_MISMATCH', message: `Unrelated staged paths: ${unrelatedStaged.join(', ')}` } };
71
- const add = run(runner, input.cwd, ['add', '--', ...input.paths]);
72
- if (add.status !== 0) return { status: 'failed' as const, changed: false, snapshot: inspectGitWorkflow(input.cwd, runner).snapshot, operations: [{ name: 'stage', status: 'failed' as const, message: add.stderr.trim() }], error: { code: 'GIT_STAGE_FAILED', message: add.stderr.trim() } };
75
+ const stageFailure = (message: string) => ({ status: 'failed' as const, changed: false, snapshot: inspectGitWorkflow(input.cwd, runner).snapshot, operations: [{ name: 'stage', status: 'failed' as const, message }], error: { code: 'GIT_STAGE_FAILED', message } });
76
+ const indexed = run(runner, input.cwd, ['ls-files', '-z', '--', ...input.paths]);
77
+ if (indexed.status !== 0) return stageFailure(indexed.stderr.trim());
78
+ const headed = run(runner, input.cwd, ['ls-tree', '-r', '--name-only', '-z', 'HEAD', '--', ...input.paths]);
79
+ if (headed.status !== 0) return stageFailure(headed.stderr.trim());
80
+ const indexedPaths = new Set(nulPaths(indexed.stdout));
81
+ const headPaths = new Set(nulPaths(headed.stdout));
82
+ const trackedPaths: string[] = [];
83
+ const addPaths: string[] = [];
84
+ for (const candidate of input.paths) {
85
+ if (indexedPaths.has(candidate)) trackedPaths.push(candidate);
86
+ else if (!headPaths.has(candidate) || fs.existsSync(path.join(input.cwd, candidate))) addPaths.push(candidate);
87
+ }
88
+ if (trackedPaths.length > 0) {
89
+ const update = run(runner, input.cwd, ['add', '-u', '--', ...trackedPaths]);
90
+ if (update.status !== 0) return stageFailure(update.stderr.trim());
91
+ }
92
+ if (addPaths.length > 0) {
93
+ const add = run(runner, input.cwd, ['add', '--', ...addPaths]);
94
+ if (add.status !== 0) return stageFailure(add.stderr.trim());
95
+ }
73
96
  const staged = run(runner, input.cwd, ['diff', '--cached', '--quiet', '--']);
74
97
  if (staged.status === 0) return { status: 'no-op' as const, changed: false, snapshot: inspectGitWorkflow(input.cwd, runner).snapshot, operations: [{ name: 'stage', status: 'no-op' as const }], error: null };
75
98
  if (input.expectedTree) {
@@ -129,7 +152,6 @@ function pushRebasedBranch(input: PushRebasedInput, runner: GitRunner = defaultR
129
152
  .map((name) => value(runner, input.cwd, ['rev-parse', '--git-path', name]))
130
153
  .filter((candidate): candidate is string => Boolean(candidate));
131
154
  if (snapshot.worktree.length > 0 || snapshot.staged.length > 0 ||
132
- run(runner, input.cwd, ['rev-parse', '-q', '--verify', 'REBASE_HEAD']).status === 0 ||
133
155
  rebasePaths.some((candidate) => fs.existsSync(path.isAbsolute(candidate) ? candidate : path.join(input.cwd, candidate)))) {
134
156
  return { status: 'failed' as const, changed: false, snapshot, operations: [], error: { code: 'GIT_LOCAL_STATE_UNSAFE', message: 'Working tree must be clean with no rebase in progress' } };
135
157
  }
@@ -9,12 +9,24 @@ import {
9
9
  resolveCodexThread
10
10
  } from '../agent-clients/adapters/codex-lifecycle/app-server.ts';
11
11
  import { createCodexLifecycleStore } from '../agent-clients/adapters/codex-lifecycle/store.ts';
12
+ import { createCodexCapabilityStore } from '../agent-clients/adapters/codex-lifecycle/capability-store.ts';
13
+ import { computeLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.ts';
14
+ import { verifyCodexSandboxControllerContext } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.ts';
12
15
  import type { CodexLifecycleEvent } from '../agent-clients/adapters/codex-lifecycle/evidence.ts';
16
+ import { resolveTaskRef } from '../task/resolve-ref.ts';
17
+ import {
18
+ activateCodexOrchestrationDelegation,
19
+ activateCodexSpawnDelegation,
20
+ reconcileCodexOrchestrationDelegation,
21
+ sealCodexOrchestrationDelegation,
22
+ sealCodexParentDelegation
23
+ } from '../task/codex-orchestration.ts';
13
24
 
14
- const USAGE = 'Usage: agent-infra-internal codex-lifecycle <hook-event|resolve-start|resolve-stop|preflight|consume> [options]\n';
25
+ const USAGE = 'Usage: agent-infra-internal codex-lifecycle <capability-arm|hook-event|resolve-start|resolve-stop|preflight|consume> [options]\n';
15
26
  const MANAGED_AGENT = /^agent-infra-lifecycle-(executor|reviewer)$/;
16
27
 
17
28
  type Parsed = Readonly<{ operation: string; values: Readonly<Record<string, string>> }>;
29
+ type UnresolvedHookChild = Omit<Extract<CodexLifecycleEvent, { type: 'hook-child' }>, 'parentThreadId'>;
18
30
 
19
31
  function output(value: unknown): void {
20
32
  process.stdout.write(`${JSON.stringify(value)}\n`);
@@ -25,6 +37,15 @@ function failure(code: string, message: string): void {
25
37
  process.exitCode = 1;
26
38
  }
27
39
 
40
+ function outputBridgeResult(result: Awaited<ReturnType<typeof activateCodexOrchestrationDelegation>>): void {
41
+ if (result.error?.code === 'ORCHESTRATION_DELEGATION_MISSING') {
42
+ output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
43
+ return;
44
+ }
45
+ output(result);
46
+ if (result.status !== 'running') process.exitCode = 1;
47
+ }
48
+
28
49
  function parse(args: string[]): Parsed | null {
29
50
  if (args[0] === '--help' || args[0] === '-h') {
30
51
  process.stdout.write(USAGE);
@@ -36,7 +57,8 @@ function parse(args: string[]): Parsed | null {
36
57
  return null;
37
58
  }
38
59
  const allowed = {
39
- 'hook-event': ['--event'],
60
+ 'capability-arm': ['--task-id'],
61
+ 'hook-event': ['--event', '--bridge'],
40
62
  'resolve-start': ['--child-id'],
41
63
  'resolve-stop': ['--child-id'],
42
64
  preflight: ['--format', '--session-id', '--turn-id', '--tool-use-id'],
@@ -75,6 +97,16 @@ function hookDefinitionHash(): string {
75
97
  return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
76
98
  }
77
99
 
100
+ function controllerBinding(taskId: string) {
101
+ const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
102
+ if (!contextPath) return undefined;
103
+ const context = verifyCodexSandboxControllerContext(contextPath, taskId, { repoRoot: process.cwd() });
104
+ return {
105
+ instanceDigest: context.controllerInstanceDigest,
106
+ controlGeneration: context.controlGeneration
107
+ };
108
+ }
109
+
78
110
  async function readStdin(): Promise<unknown> {
79
111
  let input = '';
80
112
  process.stdin.setEncoding('utf8');
@@ -85,7 +117,7 @@ async function readStdin(): Promise<unknown> {
85
117
  return JSON.parse(input || '{}') as unknown;
86
118
  }
87
119
 
88
- function recordFromPayload(phase: string, payload: unknown): CodexLifecycleEvent | null {
120
+ function recordFromPayload(phase: string, payload: unknown): CodexLifecycleEvent | UnresolvedHookChild | null {
89
121
  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
90
122
  throw new Error('Codex lifecycle hook payload must be an object');
91
123
  }
@@ -120,6 +152,13 @@ function recordFromPayload(phase: string, payload: unknown): CodexLifecycleEvent
120
152
  throw new Error(`unknown Codex lifecycle hook phase '${phase}'`);
121
153
  }
122
154
 
155
+ function payloadText(payload: unknown, key: string): string {
156
+ return payload && typeof payload === 'object' && !Array.isArray(payload)
157
+ && typeof (payload as Record<string, unknown>)[key] === 'string'
158
+ ? (payload as Record<string, string>)[key]!
159
+ : '';
160
+ }
161
+
123
162
  async function codexLifecycle(args: string[] = []): Promise<void> {
124
163
  const parsed = parse(args);
125
164
  if (!parsed || process.exitCode) return;
@@ -147,18 +186,150 @@ async function codexLifecycle(args: string[] = []): Promise<void> {
147
186
  return;
148
187
  }
149
188
 
189
+ if (parsed.operation === 'capability-arm') {
190
+ const taskId = parsed.values['--task-id'];
191
+ if (!taskId) throw new Error('capability-arm requires --task-id');
192
+ const resolved = resolveTaskRef(taskId, { repoRoot: process.cwd() });
193
+ if (!resolved.ok) throw new Error(`${resolved.code}: ${resolved.message}`);
194
+ const armed = createCodexCapabilityStore().arm({
195
+ taskId: resolved.taskId,
196
+ buildIdentity: computeLifecycleBuildIdentity(process.cwd()),
197
+ controller: controllerBinding(resolved.taskId)
198
+ });
199
+ output({
200
+ status: 'armed',
201
+ changed: true,
202
+ capabilityToken: armed.token,
203
+ marker: armed.marker,
204
+ expiresAt: armed.expiresAt,
205
+ buildIdentity: armed.buildIdentity,
206
+ error: null
207
+ });
208
+ return;
209
+ }
210
+
150
211
  const store = createCodexLifecycleStore({ cliVersion: cliVersion() });
151
212
  if (parsed.operation === 'hook-event') {
152
213
  const phase = parsed.values['--event'];
153
214
  if (!phase || !['pre-tool', 'subagent-start', 'post-tool', 'subagent-stop'].includes(phase)) {
154
215
  throw new Error('hook-event requires a known --event');
155
216
  }
156
- const event = recordFromPayload(phase, await readStdin());
217
+ if (parsed.values['--bridge'] !== undefined && parsed.values['--bridge'] !== 'true') {
218
+ throw new Error("hook-event --bridge must be 'true'");
219
+ }
220
+ const payload = await readStdin();
221
+ if (phase === 'post-tool') {
222
+ const capabilityToken = payloadText(payload, 'capabilityToken');
223
+ if (capabilityToken) {
224
+ const capabilityStore = createCodexCapabilityStore();
225
+ const armed = capabilityStore.inspect(capabilityToken);
226
+ const capability = createCodexCapabilityStore().attest({
227
+ token: capabilityToken,
228
+ sessionId: payloadText(payload, 'sessionId'),
229
+ turnId: payloadText(payload, 'turnId'),
230
+ toolUseId: payloadText(payload, 'toolUseId'),
231
+ hookDefinitionHash: payloadText(payload, 'hookDefinitionHash'),
232
+ buildIdentity: computeLifecycleBuildIdentity(process.cwd()),
233
+ controller: controllerBinding(armed.taskId)
234
+ });
235
+ output({
236
+ status: capability.status,
237
+ changed: true,
238
+ evidence: {
239
+ revision: capability.revision,
240
+ sessionId: capability.sessionId,
241
+ turnId: capability.turnId,
242
+ toolUseId: capability.toolUseId,
243
+ expiresAt: capability.expiresAt
244
+ },
245
+ diagnostics: [],
246
+ error: null
247
+ });
248
+ return;
249
+ }
250
+ }
251
+ if (phase === 'post-tool' && parsed.values['--bridge'] === 'true') {
252
+ const toolName = payloadText(payload, 'toolName');
253
+ if (toolName === 'collaborationspawn_agent') {
254
+ const nativeAgent = payloadText(payload, 'nativeAgent');
255
+ if (!MANAGED_AGENT.test(nativeAgent)) {
256
+ output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
257
+ return;
258
+ }
259
+ const requestedModel = payloadText(payload, 'requestedModel');
260
+ const requestedReasoningEffort = payloadText(payload, 'requestedReasoningEffort');
261
+ const bridged = await activateCodexSpawnDelegation({
262
+ sessionId: payloadText(payload, 'sessionId'),
263
+ turnId: payloadText(payload, 'turnId'),
264
+ toolUseId: payloadText(payload, 'toolUseId'),
265
+ transcriptPath: payloadText(payload, 'transcriptPath'),
266
+ nativeAgent,
267
+ taskName: payloadText(payload, 'taskName'),
268
+ ...(requestedModel ? { requestedModel } : {}),
269
+ ...(requestedReasoningEffort ? { requestedReasoningEffort } : {})
270
+ }, { store });
271
+ outputBridgeResult(bridged);
272
+ return;
273
+ }
274
+ if (toolName === 'collaborationwait_agent') {
275
+ outputBridgeResult(await sealCodexParentDelegation(payloadText(payload, 'sessionId'), { store }));
276
+ return;
277
+ }
278
+ output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
279
+ return;
280
+ }
281
+ const event = recordFromPayload(phase, payload);
157
282
  if (!event) {
283
+ if (phase === 'post-tool' && parsed.values['--bridge'] === 'true') {
284
+ const value = payload as Record<string, unknown>;
285
+ const childThreadId = String(value.childThreadId ?? '');
286
+ const reconciled = reconcileCodexOrchestrationDelegation(childThreadId);
287
+ outputBridgeResult(reconciled);
288
+ return;
289
+ }
158
290
  output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
159
291
  return;
160
292
  }
293
+ if (parsed.values['--bridge'] === 'true' && event.type === 'hook-stop') {
294
+ try {
295
+ if (store.read(event.childThreadId).consumer) {
296
+ const bridged = await sealCodexOrchestrationDelegation(event.childThreadId, { store });
297
+ outputBridgeResult(bridged);
298
+ return;
299
+ }
300
+ } catch {
301
+ // The normal apply path below owns missing or invalid evidence errors.
302
+ }
303
+ }
304
+ if (parsed.values['--bridge'] === 'true' && event.type === 'hook-child') {
305
+ const resolved = await resolveCodexThread(event.childThreadId);
306
+ store.apply({
307
+ ...event,
308
+ parentThreadId: resolved.resolution.thread.parentThreadId
309
+ });
310
+ const bridged = await activateCodexOrchestrationDelegation(event.childThreadId, {
311
+ store,
312
+ resolveThread: async () => resolved
313
+ });
314
+ outputBridgeResult(bridged);
315
+ return;
316
+ }
317
+ if (event.type === 'hook-child') {
318
+ const resolved = await resolveCodexThread(event.childThreadId);
319
+ const result = store.apply({
320
+ ...event,
321
+ parentThreadId: resolved.resolution.thread.parentThreadId
322
+ });
323
+ output({ status: result.state.status, changed: true, evidence: result.state, diagnostics: resolved.diagnostics, error: result.state.error });
324
+ if (result.state.status === 'invalid') process.exitCode = 1;
325
+ return;
326
+ }
161
327
  const result = store.apply(event);
328
+ if (parsed.values['--bridge'] === 'true' && event.type === 'hook-stop') {
329
+ const bridged = await sealCodexOrchestrationDelegation(event.childThreadId, { store });
330
+ outputBridgeResult(bridged);
331
+ return;
332
+ }
162
333
  output({ status: result.state.status, changed: true, evidence: result.state, diagnostics: [], error: result.state.error });
163
334
  if (result.state.status === 'invalid') process.exitCode = 1;
164
335
  return;
@@ -0,0 +1,109 @@
1
+ import {
2
+ runCodexSandboxController,
3
+ verifyCodexSandboxControllerContext
4
+ } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.ts';
5
+
6
+ const USAGE = 'Usage: agent-infra-internal codex-sandbox-controller <run|verify-context> [options]\n';
7
+
8
+ function fail(code: string, message: string, exitCode = 1): void {
9
+ process.stdout.write(`${JSON.stringify({
10
+ status: 'failed',
11
+ changed: false,
12
+ error: { code, message }
13
+ })}\n`);
14
+ process.exitCode = exitCode;
15
+ }
16
+
17
+ function parse(args: string[]): Readonly<{ operation: string; values: Readonly<Record<string, string>> }> | null {
18
+ if (args[0] === '--help' || args[0] === '-h') {
19
+ process.stdout.write(USAGE);
20
+ return null;
21
+ }
22
+ const operation = args[0];
23
+ if (!operation || !['run', 'verify-context'].includes(operation)) {
24
+ process.stderr.write(USAGE);
25
+ fail('CODEX_SANDBOX_CONTROLLER_PAYLOAD_INVALID', 'operation must be run or verify-context', 2);
26
+ return null;
27
+ }
28
+ const allowed = new Set([
29
+ '--task-id',
30
+ '--task-ref',
31
+ '--context',
32
+ '--executor-model',
33
+ '--executor-reasoning-effort',
34
+ '--reviewer-model',
35
+ '--reviewer-reasoning-effort'
36
+ ]);
37
+ const values: Record<string, string> = {};
38
+ for (let index = 1; index < args.length; index += 1) {
39
+ const flag = args[index]!;
40
+ const value = args[++index];
41
+ if (!allowed.has(flag) || Object.hasOwn(values, flag) || !value || value.startsWith('--')) {
42
+ fail('CODEX_SANDBOX_CONTROLLER_PAYLOAD_INVALID', `invalid option '${flag}'`, 2);
43
+ return null;
44
+ }
45
+ values[flag] = value;
46
+ }
47
+ return { operation, values };
48
+ }
49
+
50
+ async function codexSandboxController(args: string[] = []): Promise<void> {
51
+ const parsed = parse(args);
52
+ if (!parsed || process.exitCode) return;
53
+ const taskId = parsed.values['--task-id'];
54
+ if (!taskId) {
55
+ fail('CODEX_SANDBOX_CONTROLLER_PAYLOAD_INVALID', '--task-id is required', 2);
56
+ return;
57
+ }
58
+ try {
59
+ if (parsed.operation === 'verify-context') {
60
+ const contextPath = parsed.values['--context']
61
+ ?? process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
62
+ if (!contextPath) throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_MISSING');
63
+ const context = verifyCodexSandboxControllerContext(contextPath, taskId);
64
+ process.stdout.write(`${JSON.stringify({
65
+ status: 'ready',
66
+ changed: false,
67
+ context: {
68
+ taskId: context.taskId,
69
+ controlGeneration: context.controlGeneration,
70
+ controllerInstanceDigest: context.controllerInstanceDigest,
71
+ expiresAt: context.expiresAt,
72
+ buildIdentity: context.buildIdentity,
73
+ hookDefinitionHash: context.hookDefinitionHash,
74
+ lifecycleProfilesHash: context.lifecycleProfilesHash
75
+ },
76
+ error: null
77
+ })}\n`);
78
+ return;
79
+ }
80
+ const taskRef = parsed.values['--task-ref'];
81
+ if (!taskRef) throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_REF_REQUIRED');
82
+ const policy = [
83
+ '--executor-model',
84
+ '--executor-reasoning-effort',
85
+ '--reviewer-model',
86
+ '--reviewer-reasoning-effort'
87
+ ];
88
+ if (policy.some((flag) => parsed.values[flag] !== undefined)
89
+ && policy.some((flag) => parsed.values[flag] === undefined)) {
90
+ throw new Error('CODEX_SANDBOX_CONTROLLER_MODEL_POLICY_INVALID');
91
+ }
92
+ const exitCode = await runCodexSandboxController({
93
+ taskId,
94
+ taskRef,
95
+ executorModel: parsed.values['--executor-model'],
96
+ executorReasoningEffort: parsed.values['--executor-reasoning-effort'],
97
+ reviewerModel: parsed.values['--reviewer-model'],
98
+ reviewerReasoningEffort: parsed.values['--reviewer-reasoning-effort']
99
+ });
100
+ process.exitCode = exitCode;
101
+ } catch (error) {
102
+ const message = error instanceof Error ? error.message : String(error);
103
+ const code = /^([A-Z][A-Z0-9_]+)/u.exec(message)?.[1]
104
+ ?? 'CODEX_SANDBOX_CONTROLLER_FAILED';
105
+ fail(code, message);
106
+ }
107
+ }
108
+
109
+ export { codexSandboxController };
@@ -8,7 +8,7 @@ import { commitExplicitPaths, inspectGitWorkflow, pushGitRefs } from '../git/wor
8
8
  import { inspectPlatformRelease, reconcileReleaseMilestones } from '../platform/releases.ts';
9
9
  import { inspectHomebrewChannel, inspectNpmChannel } from '../release/channels.ts';
10
10
  import { releaseSnapshot } from '../release/workflow.ts';
11
- import type { ReleaseFacts } from '../release/workflow.ts';
11
+ import type { PostReleaseFacts, ReleaseFacts } from '../release/workflow.ts';
12
12
 
13
13
  function command(cwd: string, executable: string, args: string[]) {
14
14
  return spawnSync(executable, args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
@@ -168,12 +168,52 @@ function inspectLocalReleaseFacts(cwd: string, version: string, run: CommandRunn
168
168
  const localTagConflict = Boolean(tagCommit && !localTag && !localTagAncestor);
169
169
  const postSubject = `chore: prepare next dev iteration after v${version}`;
170
170
  const postLog = localTag || localTagAncestor
171
- ? git(cwd, ['log', '--format=%s', `${tag}..HEAD`], run) ?? ''
171
+ ? git(cwd, ['log', '--format=%H%x00%s', `${tag}..HEAD`], run) ?? ''
172
172
  : '';
173
- const postCommit = postLog.split('\n').some((subject) => subject === postSubject);
173
+ const postCommit = postLog.split('\n').map((line) => line.split('\0'))
174
+ .find(([, subject]) => subject === postSubject)?.[0] ?? null;
174
175
  return { localTag, localTagAncestor, localTagConflict, postCommit };
175
176
  }
176
177
 
178
+ function inspectPostReleaseFacts(
179
+ cwd: string,
180
+ commit: string | null,
181
+ run: CommandRunner = command
182
+ ): PostReleaseFacts {
183
+ const inspected = inspectGitWorkflow(cwd);
184
+ const snapshot = inspected.snapshot;
185
+ const branch = snapshot?.branch ?? '';
186
+ const remoteLine = branch ? git(cwd, ['ls-remote', '--heads', 'origin', `refs/heads/${branch}`], run) : null;
187
+ const remoteHead = remoteLine?.split(/\s+/)[0] ?? null;
188
+ const packageJson = commit ? git(cwd, ['show', `${commit}:package.json`], run, false) : null;
189
+ let newVersion: string | null = null;
190
+ if (packageJson) {
191
+ try {
192
+ const version = (JSON.parse(packageJson) as { version?: unknown }).version;
193
+ newVersion = typeof version === 'string' ? version : null;
194
+ } catch {
195
+ newVersion = null;
196
+ }
197
+ }
198
+ const paths = commit ? git(cwd, ['diff-tree', '--root', '--no-commit-id', '--name-only', '-r', '-z', commit], run, false) : null;
199
+ const changedPaths = paths ? paths.split('\0').filter(Boolean)
200
+ .sort((left, right) => Buffer.from(left).compare(Buffer.from(right))) : [];
201
+ const digest = commit ? git(cwd, ['show', `${commit}:${DEMO_DIGEST_PATH}`], run) : null;
202
+ return {
203
+ commit,
204
+ isHead: Boolean(commit && snapshot?.head === commit),
205
+ published: Boolean(commit && remoteHead === commit),
206
+ branch,
207
+ upstream: snapshot?.upstream ?? null,
208
+ remoteHead,
209
+ newVersion,
210
+ changedPaths,
211
+ demoInputSha256: digest && /^[0-9a-f]{64}$/.test(digest) ? digest : null,
212
+ worktree: snapshot?.worktree ?? [],
213
+ staged: snapshot?.staged ?? []
214
+ };
215
+ }
216
+
177
217
  function releaseSmokeStatus(workflows: Array<Record<string, unknown>>, version: string, tagCommit: string | null): ReleaseFacts['smoke'] {
178
218
  const manualTitle = `Post-Release Smoke v${version}`;
179
219
  const targets = workflows.filter((run) => {
@@ -220,12 +260,15 @@ async function inspectFacts(cwd: string, version: string): Promise<ReleaseFacts>
220
260
  const workflows = platform.workflows as Array<Record<string, unknown>>;
221
261
  const smoke = releaseSmokeStatus(workflows, version, localTagTarget);
222
262
  return {
223
- ...local,
263
+ localTag: local.localTag,
264
+ localTagAncestor: local.localTagAncestor,
265
+ localTagConflict: local.localTagConflict,
224
266
  remoteBranch: Boolean(remoteBranch && remoteBranch.split(/\s+/)[0] === head), remoteTag: Boolean(remoteTag),
225
267
  githubRelease: platform.platform.type !== 'github'
226
268
  ? true
227
269
  : platform.status === 'blocked' ? null : Boolean(platform.release?.published),
228
- npm: npm.published, homebrew: homebrew.published, smoke
270
+ npm: npm.published, homebrew: homebrew.published, smoke,
271
+ post: inspectPostReleaseFacts(cwd, local.postCommit)
229
272
  };
230
273
  }
231
274
 
@@ -258,13 +301,21 @@ function updateReleaseMetadata(cwd: string, version: string): void {
258
301
  }
259
302
  }
260
303
 
304
+ function optionValues(args: string[], name: string): string[] {
305
+ const values: string[] = [];
306
+ for (let index = 0; index < args.length; index += 1) {
307
+ if (args[index] === name) values.push(args[index + 1] ?? '');
308
+ }
309
+ return values;
310
+ }
311
+
261
312
  async function releaseWorkflow(args: string[] = []): Promise<void> {
262
313
  const [action, rawVersion, ...rest] = args;
263
314
  const version = String(rawVersion || '').replace(/^v/, '');
264
315
  const cwdIndex = rest.indexOf('--cwd');
265
316
  const cwd = path.resolve(cwdIndex >= 0 && rest[cwdIndex + 1] ? rest[cwdIndex + 1]! : process.cwd());
266
- if (!['inspect', 'prepare', 'publish', 'post'].includes(action || '') || !semver.valid(version)) {
267
- process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'RELEASE_INPUT_INVALID', message: 'Usage: release-workflow inspect|prepare|publish|post <version>' } })}\n`);
317
+ if (!['inspect', 'prepare', 'publish', 'post-prepare', 'post-publish'].includes(action || '') || !semver.valid(version)) {
318
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'RELEASE_INPUT_INVALID', message: 'Usage: release-workflow inspect|prepare|publish|post-prepare|post-publish <version>' } })}\n`);
268
319
  process.exitCode = 1; return;
269
320
  }
270
321
  const before = releaseSnapshot(version, await inspectFacts(cwd, version));
@@ -310,10 +361,51 @@ async function releaseWorkflow(args: string[] = []): Promise<void> {
310
361
  const result = pushGitRefs({ cwd, remote: 'origin', refs });
311
362
  process.stdout.write(`${JSON.stringify({ ...result, snapshot: releaseSnapshot(version, await inspectFacts(cwd, version)) })}\n`); process.exitCode = result.status === 'failed' ? 1 : result.status === 'degraded' ? 2 : 0; return;
312
363
  }
364
+ if (action === 'post-publish') {
365
+ const expectedValues = optionValues(rest, '--expected-sha256');
366
+ if (expectedValues.length !== 1 || !/^sha256:[0-9a-f]{64}$/.test(expectedValues[0]!)) {
367
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, snapshot: before, error: { code: 'RELEASE_INPUT_INVALID', message: 'post-publish requires one --expected-sha256 sha256:<64 hex>' } })}\n`);
368
+ process.exitCode = 1; return;
369
+ }
370
+ if (before.phase === 'complete') {
371
+ process.stdout.write(`${JSON.stringify({ status: 'no-op', changed: false, snapshot: before, error: null })}\n`);
372
+ return;
373
+ }
374
+ const confirmation = 'postConfirmation' in before ? before.postConfirmation : null;
375
+ if (before.phase !== 'post-prepared' || !confirmation) {
376
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, snapshot: before, error: { code: confirmation ? 'RELEASE_PHASE_INVALID' : 'RELEASE_POST_CONFIRMATION_UNAVAILABLE', message: 'Post-release facts cannot authorize publish' } })}\n`);
377
+ process.exitCode = 1; return;
378
+ }
379
+ if (confirmation.sha256 !== expectedValues[0]) {
380
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, snapshot: before, error: { code: 'RELEASE_POST_SNAPSHOT_MISMATCH', message: 'Post-release confirmation snapshot changed' } })}\n`);
381
+ process.exitCode = 1; return;
382
+ }
383
+ const channelsComplete = before.facts.githubRelease === true && before.facts.npm === true && before.facts.homebrew === true;
384
+ if (!channelsComplete || before.facts.smoke !== 'success') {
385
+ process.stdout.write(`${JSON.stringify({ status: before.facts.smoke === 'failed' ? 'failed' : 'blocked', changed: false, snapshot: before, error: { code: 'RELEASE_CHANNELS_PENDING', message: 'Release channels or smoke workflow are not complete' } })}\n`);
386
+ process.exitCode = before.facts.smoke === 'failed' ? 1 : 2; return;
387
+ }
388
+ const pushed = pushGitRefs({ cwd, remote: 'origin', refs: [before.facts.post.branch] });
389
+ const snapshot = releaseSnapshot(version, await inspectFacts(cwd, version));
390
+ if (snapshot.phase === 'complete') {
391
+ process.stdout.write(`${JSON.stringify({ ...pushed, status: 'applied', changed: true, snapshot, error: null })}\n`);
392
+ return;
393
+ }
394
+ const blocked = pushed.status !== 'failed';
395
+ process.stdout.write(`${JSON.stringify({ ...pushed, status: blocked ? 'blocked' : 'failed', snapshot, error: pushed.error ?? { code: 'RELEASE_PROGRESS_PENDING', message: 'Post-release push is not visible at the expected remote head' } })}\n`);
396
+ process.exitCode = blocked ? 2 : 1; return;
397
+ }
398
+
313
399
  if (before.phase === 'complete') {
314
400
  process.stdout.write(`${JSON.stringify({ status: 'no-op', changed: false, snapshot: before, error: null })}\n`);
315
401
  return;
316
402
  }
403
+ if (before.phase === 'post-prepared') {
404
+ const confirmation = 'postConfirmation' in before ? before.postConfirmation : null;
405
+ const status = confirmation ? 'no-op' : 'blocked';
406
+ process.stdout.write(`${JSON.stringify({ status, changed: false, snapshot: before, error: confirmation ? null : { code: 'RELEASE_POST_CONFIRMATION_UNAVAILABLE', message: 'Post commit is not the clean current HEAD' } })}\n`);
407
+ process.exitCode = confirmation ? 0 : 2; return;
408
+ }
317
409
  const channelsComplete = before.facts.githubRelease === true && before.facts.npm === true && before.facts.homebrew === true;
318
410
  if (!['published', 'post-pending'].includes(before.phase) || !channelsComplete || before.facts.smoke !== 'success') {
319
411
  process.stdout.write(`${JSON.stringify({ status: before.facts.smoke === 'failed' ? 'failed' : 'blocked', changed: false, snapshot: before, error: { code: 'RELEASE_CHANNELS_PENDING', message: 'Release channels or smoke workflow are not complete' } })}\n`); process.exitCode = before.facts.smoke === 'failed' ? 1 : 2; return;
@@ -338,10 +430,14 @@ async function releaseWorkflow(args: string[] = []): Promise<void> {
338
430
  }
339
431
  const committed = commitExplicitPaths({ cwd, paths: changedPaths(cwd), message: `chore: prepare next dev iteration after v${version}` });
340
432
  if (committed.status === 'failed') { process.stdout.write(`${JSON.stringify(committed)}\n`); process.exitCode = 1; return; }
341
- const branch = git(cwd, ['branch', '--show-current']) || '';
342
- const pushed = pushGitRefs({ cwd, remote: 'origin', refs: [branch] });
343
- process.stdout.write(`${JSON.stringify({ ...pushed, demo, snapshot: releaseSnapshot(version, await inspectFacts(cwd, version)) })}\n`); process.exitCode = pushed.status === 'failed' ? 1 : pushed.status === 'degraded' ? 2 : 0;
433
+ const snapshot = releaseSnapshot(version, await inspectFacts(cwd, version));
434
+ const confirmation = 'postConfirmation' in snapshot ? snapshot.postConfirmation : null;
435
+ if (snapshot.phase !== 'post-prepared' || !confirmation) {
436
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: true, demo, snapshot, error: { code: 'RELEASE_POST_CONFIRMATION_UNAVAILABLE', message: 'Prepared post commit did not produce a confirmation snapshot' } })}\n`);
437
+ process.exitCode = 1; return;
438
+ }
439
+ process.stdout.write(`${JSON.stringify({ status: 'applied', changed: true, demo, snapshot, error: null })}\n`);
344
440
  }
345
441
 
346
- export { changedPaths, computeDemoInputDigest, inspectFacts, inspectLocalReleaseFacts, inspectPostWorktree, releaseSmokeStatus, releaseWorkflow, runOptionalDemo };
442
+ export { changedPaths, computeDemoInputDigest, inspectFacts, inspectLocalReleaseFacts, inspectPostReleaseFacts, inspectPostWorktree, releaseSmokeStatus, releaseWorkflow, runOptionalDemo };
347
443
  export type { CommandRunner, DemoResult };
@@ -1,24 +1,56 @@
1
- import { requestSandboxControl } from '../sandbox/control/client.ts';
1
+ import { requestSandboxControl, SandboxControlClientError } from '../sandbox/control/client.ts';
2
2
  import { serveSandboxControl } from '../sandbox/control/server.ts';
3
+ import { runSandboxControlExecutor } from '../sandbox/control/executor.ts';
3
4
 
4
- function sandboxControl(args: string[]): void {
5
+ async function sandboxControl(args: string[]): Promise<void> {
5
6
  const [operation, ...rest] = args;
6
7
  if (operation === 'serve') {
7
8
  const manifestIndex = rest.indexOf('--manifest');
8
9
  const manifest = manifestIndex >= 0 ? rest[manifestIndex + 1] : undefined;
9
10
  if (!manifest) throw new Error('sandbox-control serve requires --manifest');
10
- serveSandboxControl(manifest);
11
+ const controller = new AbortController();
12
+ const abort = () => controller.abort();
13
+ process.once('SIGINT', abort);
14
+ process.once('SIGTERM', abort);
15
+ try {
16
+ await serveSandboxControl(manifest, controller.signal);
17
+ } finally {
18
+ process.off('SIGINT', abort);
19
+ process.off('SIGTERM', abort);
20
+ }
21
+ return;
22
+ }
23
+ if (operation === 'execute') {
24
+ const requestIndex = rest.indexOf('--request');
25
+ const nonceIndex = rest.indexOf('--nonce');
26
+ const request = requestIndex >= 0 ? rest[requestIndex + 1] : undefined;
27
+ const nonce = nonceIndex >= 0 ? rest[nonceIndex + 1] : undefined;
28
+ if (!request || !nonce) throw new Error('sandbox-control execute requires --request and --nonce');
29
+ await runSandboxControlExecutor(request, nonce);
11
30
  return;
12
31
  }
13
32
  if (operation === 'client') {
14
33
  const [family = '', ...commandArgs] = rest;
15
- const response = requestSandboxControl({ family, args: commandArgs });
34
+ let response;
35
+ try {
36
+ response = requestSandboxControl({ family, args: commandArgs });
37
+ } catch (error) {
38
+ if (!(error instanceof SandboxControlClientError)) throw error;
39
+ process.stderr.write(`${error.detail.message}\n`);
40
+ process.exitCode = error.detail.retryable ? 75 : 1;
41
+ return;
42
+ }
16
43
  process.stdout.write(response.stdout);
17
44
  process.stderr.write(response.stderr);
18
- process.exitCode = response.exitCode;
45
+ if (response.phase === 'rejected') {
46
+ process.stderr.write(response.error?.message ?? response.stderr);
47
+ process.exitCode = response.error?.retryable ? 75 : 1;
48
+ } else {
49
+ process.exitCode = response.exitCode ?? 1;
50
+ }
19
51
  return;
20
52
  }
21
- throw new Error('Usage: agent-infra-internal sandbox-control serve --manifest <path> | client <family> [args...]');
53
+ throw new Error('Usage: agent-infra-internal sandbox-control serve --manifest <path> | execute --request <path> --nonce <nonce> | client <family> [args...]');
22
54
  }
23
55
 
24
56
  export { sandboxControl };