@fitlab-ai/agent-infra 0.9.4 → 0.9.6

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 (130) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +13 -3
  4. package/dist/lib/agent-clients/adapter.js +2 -2
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +492 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +175 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +255 -0
  8. package/dist/lib/agent-clients/adapters/codex.js +3 -3
  9. package/dist/lib/git/workflow.js +0 -1
  10. package/dist/lib/git/worktree-identity.js +64 -0
  11. package/dist/lib/internal/codex-lifecycle.js +300 -0
  12. package/dist/lib/internal/release-workflow.js +108 -11
  13. package/dist/lib/internal/sandbox-control.js +43 -6
  14. package/dist/lib/internal/task-create.js +56 -0
  15. package/dist/lib/internal/task-orchestration.js +71 -31
  16. package/dist/lib/platform/issue-comments.js +2 -1
  17. package/dist/lib/release/workflow.js +29 -21
  18. package/dist/lib/sandbox/commands/create.js +16 -16
  19. package/dist/lib/sandbox/commands/prune.js +40 -16
  20. package/dist/lib/sandbox/commands/rm.js +318 -89
  21. package/dist/lib/sandbox/control/client.js +152 -22
  22. package/dist/lib/sandbox/control/executor.js +136 -0
  23. package/dist/lib/sandbox/control/lifecycle.js +348 -0
  24. package/dist/lib/sandbox/control/protocol.js +73 -18
  25. package/dist/lib/sandbox/control/server.js +235 -80
  26. package/dist/lib/sandbox/control/state.js +181 -0
  27. package/dist/lib/sandbox/index.js +2 -2
  28. package/dist/lib/sandbox/managed-fs.js +14 -2
  29. package/dist/lib/sandbox/mounts.js +35 -24
  30. package/dist/lib/sandbox/recovery.js +262 -45
  31. package/dist/lib/sandbox/workspace-view.js +80 -11
  32. package/dist/lib/sandbox/worktree-safety.js +196 -0
  33. package/dist/lib/server/process-control.js +6 -0
  34. package/dist/lib/task/activity-log.js +32 -5
  35. package/dist/lib/task/artifact-lifecycle.js +5 -4
  36. package/dist/lib/task/codex-orchestration.js +161 -0
  37. package/dist/lib/task/commands/status.js +3 -0
  38. package/dist/lib/task/commands/validate.js +289 -0
  39. package/dist/lib/task/commit-finalization.js +50 -12
  40. package/dist/lib/task/create-service.js +137 -0
  41. package/dist/lib/task/create.js +333 -0
  42. package/dist/lib/task/delegation-receipts.js +25 -0
  43. package/dist/lib/task/events.js +10 -4
  44. package/dist/lib/task/finalization-artifacts.js +2 -1
  45. package/dist/lib/task/implementation-inputs.js +1 -7
  46. package/dist/lib/task/index.js +9 -0
  47. package/dist/lib/task/orchestration.js +283 -18
  48. package/dist/lib/task/review-fingerprint.js +4 -0
  49. package/dist/lib/task/verification-engine.js +38 -6
  50. package/dist/lib/task/verification.js +1 -0
  51. package/dist/lib/task/workspace-snapshot.js +78 -3
  52. package/lib/agent-clients/adapter.ts +4 -4
  53. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +569 -0
  54. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +323 -0
  55. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +298 -0
  56. package/lib/agent-clients/adapters/codex.ts +3 -3
  57. package/lib/git/workflow.ts +0 -1
  58. package/lib/git/worktree-identity.ts +86 -0
  59. package/lib/internal/codex-lifecycle.ts +308 -0
  60. package/lib/internal/release-workflow.ts +107 -11
  61. package/lib/internal/sandbox-control.ts +38 -6
  62. package/lib/internal/task-create.ts +61 -0
  63. package/lib/internal/task-orchestration.ts +57 -31
  64. package/lib/platform/issue-comments.ts +2 -1
  65. package/lib/release/workflow.ts +69 -20
  66. package/lib/sandbox/commands/create.ts +18 -21
  67. package/lib/sandbox/commands/prune.ts +53 -15
  68. package/lib/sandbox/commands/rm.ts +368 -103
  69. package/lib/sandbox/control/client.ts +173 -25
  70. package/lib/sandbox/control/executor.ts +163 -0
  71. package/lib/sandbox/control/lifecycle.ts +342 -0
  72. package/lib/sandbox/control/protocol.ts +110 -48
  73. package/lib/sandbox/control/server.ts +252 -85
  74. package/lib/sandbox/control/state.ts +196 -0
  75. package/lib/sandbox/index.ts +2 -2
  76. package/lib/sandbox/managed-fs.ts +17 -2
  77. package/lib/sandbox/mounts.ts +31 -13
  78. package/lib/sandbox/recovery.ts +301 -60
  79. package/lib/sandbox/workspace-view.ts +89 -12
  80. package/lib/sandbox/worktree-safety.ts +234 -0
  81. package/lib/server/process-control.ts +11 -0
  82. package/lib/task/activity-log.ts +46 -7
  83. package/lib/task/artifact-lifecycle.ts +5 -4
  84. package/lib/task/codex-orchestration.ts +233 -0
  85. package/lib/task/commands/status.ts +2 -0
  86. package/lib/task/commands/validate.ts +282 -0
  87. package/lib/task/commit-finalization.ts +75 -12
  88. package/lib/task/create-service.ts +171 -0
  89. package/lib/task/create.ts +372 -0
  90. package/lib/task/delegation-receipts.ts +52 -3
  91. package/lib/task/events.ts +9 -4
  92. package/lib/task/finalization-artifacts.ts +2 -1
  93. package/lib/task/implementation-inputs.ts +1 -5
  94. package/lib/task/index.ts +8 -0
  95. package/lib/task/orchestration.ts +381 -22
  96. package/lib/task/review-fingerprint.ts +9 -0
  97. package/lib/task/verification-engine.ts +42 -8
  98. package/lib/task/verification.ts +2 -1
  99. package/lib/task/workspace-snapshot.ts +98 -4
  100. package/package.json +7 -7
  101. package/templates/.agents/hooks/lifecycle-delegation.js +88 -8
  102. package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
  103. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
  104. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
  105. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
  106. package/templates/.agents/rules/task-management.en.md +1 -0
  107. package/templates/.agents/rules/task-management.zh-CN.md +1 -0
  108. package/templates/.agents/skills/commit/SKILL.en.md +3 -9
  109. package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -10
  110. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +4 -3
  111. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +5 -4
  112. package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
  113. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
  114. package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
  115. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
  116. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
  117. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
  118. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -0
  119. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -0
  120. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +44 -0
  121. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +44 -0
  122. package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
  123. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
  124. package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -3
  125. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -3
  126. package/templates/.claude/commands/run-manual-validation.en.md +9 -0
  127. package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
  128. package/templates/.codex/hooks.json +50 -0
  129. package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
  130. package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
@@ -2,54 +2,202 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import {
5
+ SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
5
6
  SANDBOX_CONTROL_MAX_BYTES,
7
+ SANDBOX_CONTROL_STATUS_STALE_MS,
6
8
  isSandboxControlFamily,
9
+ type SandboxControlError,
7
10
  type SandboxControlRequest,
8
- type SandboxControlResponse
11
+ type SandboxControlResponse,
12
+ type SandboxTaskCreateRequest
9
13
  } from './protocol.ts';
14
+ import { readSandboxControlStatus } from './state.ts';
15
+ import type { TaskCreateCandidateV1 } from '../../task/create.ts';
16
+
17
+ const SANDBOX_CONTROL_RESPONSE_SETTLE_MS = 250;
18
+
19
+ export class SandboxControlClientError extends Error {
20
+ readonly detail: SandboxControlError;
21
+ readonly accepted: boolean;
22
+ constructor(detail: SandboxControlError, accepted = false) {
23
+ super(detail.message);
24
+ this.name = 'SandboxControlClientError';
25
+ this.detail = detail;
26
+ this.accepted = accepted;
27
+ }
28
+ }
10
29
 
11
30
  function sleep(milliseconds: number): void {
12
31
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
13
32
  }
14
33
 
15
- export function requestSandboxControl(params: Readonly<{
16
- family: string;
17
- args: string[];
34
+ function clientError(code: string, message: string, retryable: boolean, accepted = false): never {
35
+ throw new SandboxControlClientError({ code, message: `${code}: ${message}`, retryable }, accepted);
36
+ }
37
+
38
+ function preflight(statusDir: string, generation: string, now = Date.now()): void {
39
+ let status;
40
+ try {
41
+ status = readSandboxControlStatus(statusDir);
42
+ } catch {
43
+ clientError('SANDBOX_CONTROL_BROKER_UNAVAILABLE', 'broker status is missing or invalid', true);
44
+ }
45
+ if (status.generation !== generation) {
46
+ clientError('SANDBOX_CONTROL_GENERATION_INVALID', 'broker status generation does not match this sandbox', false);
47
+ }
48
+ if (now - status.updatedAt > SANDBOX_CONTROL_STATUS_STALE_MS || status.updatedAt > now + 1_000) {
49
+ clientError('SANDBOX_CONTROL_BROKER_UNAVAILABLE', 'broker status heartbeat is stale', true);
50
+ }
51
+ if (status.state === 'busy') clientError('SANDBOX_CONTROL_BUSY', 'broker is executing another request', true);
52
+ if (status.state === 'parked') {
53
+ const code = status.reasonCode ?? 'SANDBOX_CONTROL_BROKER_UNAVAILABLE';
54
+ clientError(code, 'broker is parked until the host restores a valid binding', true);
55
+ }
56
+ if (status.state !== 'healthy') clientError('SANDBOX_CONTROL_BROKER_UNAVAILABLE', 'broker is not ready', true);
57
+ }
58
+
59
+ function parseResponse(raw: string, id: string): SandboxControlResponse {
60
+ const response = JSON.parse(raw) as SandboxControlResponse;
61
+ if (response.version !== 2 || response.id !== id
62
+ || !['accepted', 'completed', 'rejected'].includes(response.phase)
63
+ || (response.phase === 'completed' && !Number.isInteger(response.exitCode))) {
64
+ clientError('SANDBOX_CONTROL_RESPONSE_INVALID', 'broker response is invalid', false);
65
+ }
66
+ return response;
67
+ }
68
+
69
+ function cancelPendingRequest(requestPath: string): boolean {
70
+ try {
71
+ fs.unlinkSync(requestPath);
72
+ return true;
73
+ } catch (error) {
74
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false;
75
+ throw error;
76
+ }
77
+ }
78
+
79
+ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly<{
18
80
  channelDir?: string;
19
- token?: string;
81
+ statusDir?: string;
20
82
  timeoutMs?: number;
21
83
  }>): SandboxControlResponse {
22
- if (!isSandboxControlFamily(params.family)) {
23
- throw new Error(`SANDBOX_CONTROL_COMMAND_DENIED: '${params.family}' is not allowed`);
24
- }
25
84
  const channelDir = params.channelDir ?? process.env.AGENT_INFRA_CONTROL_DIR ?? '/run/agent-infra/control';
26
- const token = params.token ?? process.env.AGENT_INFRA_CONTROL_TOKEN;
27
- if (!token) throw new Error('SANDBOX_CONTROL_UNAVAILABLE: control token is missing');
28
- const requestsDir = path.join(channelDir, 'requests');
29
- const responsesDir = path.join(channelDir, 'responses');
30
- const id = randomUUID();
31
- const request: SandboxControlRequest = { version: 1, id, token, family: params.family, args: params.args };
85
+ const statusDir = params.statusDir ?? process.env.AGENT_INFRA_CONTROL_STATUS_DIR ?? '/run/agent-infra/control-status';
86
+ preflight(statusDir, request.generation);
32
87
  const encoded = `${JSON.stringify(request)}\n`;
33
88
  if (Buffer.byteLength(encoded, 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
34
- throw new Error('SANDBOX_CONTROL_REQUEST_TOO_LARGE: request exceeds the control limit');
89
+ clientError('SANDBOX_CONTROL_REQUEST_TOO_LARGE', 'request exceeds the control limit', false);
35
90
  }
36
- const temporary = path.join(requestsDir, `.${id}.tmp`);
37
- const requestPath = path.join(requestsDir, `${id}.json`);
38
- const responsePath = path.join(responsesDir, `${id}.json`);
91
+ const requestsDir = path.join(channelDir, 'requests');
92
+ const responsesDir = path.join(channelDir, 'responses');
93
+ const temporary = path.join(requestsDir, `.${request.id}.tmp`);
94
+ const requestPath = path.join(requestsDir, `${request.id}.json`);
95
+ const responsePath = path.join(responsesDir, `${request.id}.json`);
39
96
  fs.writeFileSync(temporary, encoded, { mode: 0o600, flag: 'wx' });
40
97
  fs.renameSync(temporary, requestPath);
41
98
  const deadline = Date.now() + (params.timeoutMs ?? 30_000);
99
+ let accepted = false;
100
+ let malformedResponseRaw: string | null = null;
101
+ let malformedResponseObservedAt = 0;
42
102
  while (Date.now() < deadline) {
43
103
  if (fs.existsSync(responsePath)) {
44
- const raw = fs.readFileSync(responsePath, 'utf8');
45
- fs.unlinkSync(responsePath);
46
- const response = JSON.parse(raw) as SandboxControlResponse;
47
- if (response.version !== 1 || response.id !== id || !Number.isInteger(response.exitCode)) {
48
- throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID: broker response is invalid');
104
+ let raw: string;
105
+ try {
106
+ raw = fs.readFileSync(responsePath, 'utf8');
107
+ } catch (error) {
108
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') continue;
109
+ throw error;
110
+ }
111
+ let response: SandboxControlResponse;
112
+ try {
113
+ response = parseResponse(raw, request.id);
114
+ malformedResponseRaw = null;
115
+ } catch (error) {
116
+ if (!(error instanceof SyntaxError)) throw error;
117
+ const now = Date.now();
118
+ if (malformedResponseRaw !== raw) {
119
+ malformedResponseRaw = raw;
120
+ malformedResponseObservedAt = now;
121
+ } else if (now - malformedResponseObservedAt >= SANDBOX_CONTROL_RESPONSE_SETTLE_MS) {
122
+ clientError('SANDBOX_CONTROL_RESPONSE_INVALID', 'broker response remained malformed', false, accepted);
123
+ }
124
+ sleep(25);
125
+ continue;
126
+ }
127
+ if (response.phase === 'accepted') {
128
+ accepted = true;
129
+ } else {
130
+ fs.rmSync(responsePath, { force: true });
131
+ return response;
132
+ }
133
+ }
134
+ if (!accepted) {
135
+ try {
136
+ preflight(statusDir, request.generation);
137
+ } catch (error) {
138
+ if (cancelPendingRequest(requestPath)) throw error;
139
+ clientError(
140
+ 'SANDBOX_CONTROL_RESULT_UNKNOWN',
141
+ 'request was claimed before broker availability was lost; inspect domain state before retrying',
142
+ false,
143
+ true
144
+ );
49
145
  }
50
- return response;
51
146
  }
52
147
  sleep(25);
53
148
  }
54
- throw new Error(`SANDBOX_CONTROL_TIMEOUT: broker did not respond within ${params.timeoutMs ?? 30_000}ms`);
149
+ if (accepted) {
150
+ clientError(
151
+ 'SANDBOX_CONTROL_RESULT_UNKNOWN',
152
+ 'accepted request did not produce a final result; inspect domain state before retrying',
153
+ false,
154
+ true
155
+ );
156
+ }
157
+ if (!cancelPendingRequest(requestPath)) {
158
+ clientError(
159
+ 'SANDBOX_CONTROL_RESULT_UNKNOWN',
160
+ 'request was claimed but no acceptance result was observed; inspect domain state before retrying',
161
+ false,
162
+ true
163
+ );
164
+ }
165
+ clientError('SANDBOX_CONTROL_BROKER_UNAVAILABLE', `broker did not accept the request within ${params.timeoutMs ?? 30_000}ms`, true);
166
+ }
167
+
168
+ function authority(params: { token?: string; generation?: string }): { token: string; generation: string } {
169
+ const token = params.token ?? process.env.AGENT_INFRA_CONTROL_TOKEN;
170
+ const generation = params.generation ?? process.env.AGENT_INFRA_CONTROL_GENERATION;
171
+ if (!token || !generation) clientError('SANDBOX_CONTROL_UNAVAILABLE', 'control authority is missing', false);
172
+ return { token, generation };
173
+ }
174
+
175
+ export function requestSandboxControl(params: Readonly<{
176
+ family: string; args: string[]; channelDir?: string; statusDir?: string;
177
+ token?: string; generation?: string; timeoutMs?: number;
178
+ }>): SandboxControlResponse {
179
+ if (!isSandboxControlFamily(params.family)) clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' is not allowed`, false);
180
+ if (params.family === 'task-create') clientError('SANDBOX_CONTROL_COMMAND_DENIED', "'task-create' requires a typed candidate", false);
181
+ const auth = authority(params);
182
+ const issuedAt = Date.now();
183
+ const request: SandboxControlRequest = {
184
+ version: 2, id: randomUUID(), ...auth, issuedAt,
185
+ expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
186
+ family: params.family, args: params.args
187
+ };
188
+ return exchangeSandboxControl(request, params);
189
+ }
190
+
191
+ export function requestSandboxTaskCreate(params: Readonly<{
192
+ candidate: TaskCreateCandidateV1; channelDir?: string; statusDir?: string;
193
+ token?: string; generation?: string; timeoutMs?: number;
194
+ }>): SandboxControlResponse {
195
+ const auth = authority(params);
196
+ const issuedAt = Date.now();
197
+ const request: SandboxTaskCreateRequest = {
198
+ version: 2, id: randomUUID(), ...auth, issuedAt,
199
+ expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
200
+ family: 'task-create', candidate: params.candidate
201
+ };
202
+ return exchangeSandboxControl(request, { ...params, timeoutMs: params.timeoutMs ?? 120_000 });
55
203
  }
@@ -0,0 +1,163 @@
1
+ import { spawn, spawnSync, type ChildProcess } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { getProcessStartTime } from '../../server/process-state.ts';
6
+ import { createTask } from '../../task/create-service.ts';
7
+ import { bindSandboxControlTask, type SandboxControlExecution, type SandboxControlManifest, type SandboxControlRequest } from './protocol.ts';
8
+ import { atomicWriteJson, executionPath, terminateSandboxControlExecution } from './state.ts';
9
+
10
+ export type SandboxControlExecutionResult = {
11
+ exitCode: number;
12
+ stdout: string;
13
+ stderr: string;
14
+ };
15
+
16
+ export type PreparedSandboxControlExecution = {
17
+ execution: SandboxControlExecution;
18
+ start(): void;
19
+ completion: Promise<SandboxControlExecutionResult>;
20
+ terminate(): boolean;
21
+ };
22
+
23
+ function safeEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
24
+ return Object.fromEntries(Object.entries(env).filter(([key]) => !key.toUpperCase().startsWith('AGENT_INFRA_CONTROL_')));
25
+ }
26
+
27
+ export function nodeEntryArgs(entry: string, args: string[]): string[] {
28
+ return path.extname(entry) === '.ts'
29
+ ? ['--experimental-strip-types', '--no-warnings', entry, ...args]
30
+ : [entry, ...args];
31
+ }
32
+
33
+ async function waitForStartTime(pid: number, timeoutMs = 2_000): Promise<string> {
34
+ const deadline = Date.now() + timeoutMs;
35
+ while (Date.now() < deadline) {
36
+ const startTime = getProcessStartTime(pid);
37
+ if (startTime) return startTime;
38
+ await new Promise<void>((resolve) => setTimeout(resolve, 10));
39
+ }
40
+ throw new Error('SANDBOX_CONTROL_EXECUTOR_IDENTITY_UNAVAILABLE');
41
+ }
42
+
43
+ export async function prepareSandboxControlExecution(params: {
44
+ manifest: SandboxControlManifest;
45
+ manifestPath: string;
46
+ request: SandboxControlRequest;
47
+ requestPath: string;
48
+ internalCliPath: string;
49
+ }): Promise<PreparedSandboxControlExecution> {
50
+ const nonce = randomUUID();
51
+ const child = spawn(
52
+ process.execPath,
53
+ nodeEntryArgs(params.internalCliPath, [
54
+ 'sandbox-control', 'execute', '--request', params.requestPath, '--nonce', nonce
55
+ ]),
56
+ {
57
+ cwd: params.manifest.repoRoot,
58
+ detached: process.platform !== 'win32',
59
+ stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
60
+ env: { ...safeEnv(process.env), AGENT_INFRA_EXECUTOR_MANIFEST: params.manifestPath }
61
+ }
62
+ );
63
+ if (typeof child.pid !== 'number') throw new Error('SANDBOX_CONTROL_EXECUTOR_SPAWN_FAILED');
64
+ let stdout = '';
65
+ let stderr = '';
66
+ child.stdout?.setEncoding('utf8');
67
+ child.stderr?.setEncoding('utf8');
68
+ child.stdout?.on('data', (chunk: string) => { stdout += chunk; });
69
+ child.stderr?.on('data', (chunk: string) => { stderr += chunk; });
70
+ const completion = new Promise<SandboxControlExecutionResult>((resolve, reject) => {
71
+ child.once('error', reject);
72
+ child.once('close', (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));
73
+ });
74
+ const startTime = await waitForStartTime(child.pid);
75
+ const execution: SandboxControlExecution = {
76
+ version: 1,
77
+ generation: params.manifest.generation,
78
+ requestId: params.request.id,
79
+ nonce,
80
+ child: {
81
+ pid: child.pid,
82
+ startTime,
83
+ processGroupId: process.platform === 'win32' ? null : child.pid
84
+ },
85
+ phase: 'prepared',
86
+ updatedAt: Date.now()
87
+ };
88
+ atomicWriteJson(executionPath(params.manifest, params.request.id), execution);
89
+ return {
90
+ execution,
91
+ start() {
92
+ if (!child.connected) throw new Error('SANDBOX_CONTROL_EXECUTOR_GATE_CLOSED');
93
+ atomicWriteJson(executionPath(params.manifest, params.request.id), { ...execution, phase: 'running', updatedAt: Date.now() });
94
+ child.send({ version: 1, nonce });
95
+ },
96
+ completion,
97
+ terminate() {
98
+ atomicWriteJson(executionPath(params.manifest, params.request.id), {
99
+ ...execution, phase: 'terminating', updatedAt: Date.now()
100
+ });
101
+ return terminateSandboxControlExecution(execution);
102
+ }
103
+ };
104
+ }
105
+
106
+ function waitForGate(nonce: string, timeoutMs = 2_000): Promise<void> {
107
+ return new Promise((resolve, reject) => {
108
+ const timer = setTimeout(() => reject(new Error('SANDBOX_CONTROL_EXECUTOR_GATE_TIMEOUT')), timeoutMs);
109
+ const onDisconnect = () => {
110
+ clearTimeout(timer);
111
+ reject(new Error('SANDBOX_CONTROL_EXECUTOR_GATE_CLOSED'));
112
+ };
113
+ process.once('disconnect', onDisconnect);
114
+ process.once('message', (message: unknown) => {
115
+ clearTimeout(timer);
116
+ process.off('disconnect', onDisconnect);
117
+ const value = message as { version?: unknown; nonce?: unknown } | null;
118
+ if (!value || value.version !== 1 || value.nonce !== nonce) {
119
+ reject(new Error('SANDBOX_CONTROL_EXECUTOR_GATE_INVALID'));
120
+ return;
121
+ }
122
+ resolve();
123
+ });
124
+ });
125
+ }
126
+
127
+ function executeRequest(manifest: SandboxControlManifest, request: SandboxControlRequest): SandboxControlExecutionResult {
128
+ if (request.family === 'task-create') {
129
+ const result = createTask(request.candidate, { repoRoot: manifest.repoRoot });
130
+ return {
131
+ exitCode: result.status === 'blocked' ? 2 : result.status === 'failed' ? 1 : 0,
132
+ stdout: `${JSON.stringify(result)}\n`,
133
+ stderr: ''
134
+ };
135
+ }
136
+ const boundArgs = bindSandboxControlTask(request, manifest.taskId!);
137
+ if (request.family === 'task-orchestration') boundArgs.push('--git-worktree-root', manifest.worktreeRoot);
138
+ const result = spawnSync(
139
+ process.execPath,
140
+ nodeEntryArgs(process.argv[1]!, [request.family, ...boundArgs]),
141
+ { cwd: manifest.repoRoot, encoding: 'utf8', env: safeEnv(process.env) }
142
+ );
143
+ return { exitCode: result.status ?? 1, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
144
+ }
145
+
146
+ export async function runSandboxControlExecutor(requestPath: string, nonce: string): Promise<void> {
147
+ await waitForGate(nonce);
148
+ process.disconnect?.();
149
+ const root = fs.realpathSync.native(process.cwd());
150
+ const request = JSON.parse(fs.readFileSync(requestPath, 'utf8')) as SandboxControlRequest;
151
+ const manifestPath = process.env.AGENT_INFRA_EXECUTOR_MANIFEST;
152
+ if (!manifestPath) throw new Error('SANDBOX_CONTROL_EXECUTOR_MANIFEST_MISSING');
153
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as SandboxControlManifest;
154
+ if (fs.realpathSync.native(manifest.repoRoot) !== root) throw new Error('SANDBOX_CONTROL_EXECUTOR_ROOT_INVALID');
155
+ const result = executeRequest(manifest, request);
156
+ process.stdout.write(result.stdout);
157
+ process.stderr.write(result.stderr);
158
+ process.exitCode = result.exitCode;
159
+ }
160
+
161
+ export function disconnectExecutor(child: ChildProcess): void {
162
+ if (child.connected) child.disconnect();
163
+ }