@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
@@ -1,34 +1,45 @@
1
- import { spawnSync } from 'node:child_process';
2
1
  import fs from 'node:fs';
3
2
  import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
4
  import { getProcessStartTime } from '../../server/process-state.ts';
5
- import { createTask } from '../../task/create-service.ts';
6
5
  import { assertGitWorktreeBinding } from '../../git/worktree-identity.ts';
7
6
  import {
8
- bindSandboxControlTask,
9
- validateSandboxControlRequest,
7
+ controlError,
8
+ DEFAULT_SANDBOX_CONTROL_TIMING,
10
9
  type SandboxControlManifest,
11
- type SandboxControlResponse
10
+ type SandboxControlRequest,
11
+ type SandboxControlResponse,
12
+ type SandboxControlTimingPolicy
12
13
  } from './protocol.ts';
14
+ import { prepareSandboxControlExecution, type PreparedSandboxControlExecution, type SandboxControlExecutionResult } from './executor.ts';
15
+ import {
16
+ appendSandboxControlAudit,
17
+ atomicWriteJson,
18
+ cleanupStaleSandboxControlLease,
19
+ executionPath,
20
+ readActiveLease,
21
+ readExecution,
22
+ terminateSandboxControlExecution,
23
+ writeSandboxControlStatus
24
+ } from './state.ts';
25
+ import { validateSandboxControlRequest } from './protocol.ts';
26
+ import { inspectSandboxControlContainer, type ContainerObservation } from './container-identity.ts';
27
+ import {
28
+ acquireSandboxControlBrokerStartup,
29
+ garbageCollectSandboxControlRoot,
30
+ isSandboxControlRootQuiescing,
31
+ readSandboxControlManifest
32
+ } from './lifecycle.ts';
33
+ import type { BrokerOwner } from './lifecycle.ts';
34
+ import { nextSandboxControlBackoff } from './timing.ts';
13
35
 
14
- function readManifest(manifestPath: string): SandboxControlManifest {
15
- const stat = fs.lstatSync(manifestPath);
16
- if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
17
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as SandboxControlManifest;
18
- if (manifest.version !== 2) {
19
- throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 2; recreate the sandbox');
20
- }
21
- if (
22
- typeof manifest.repoRoot !== 'string'
23
- || typeof manifest.worktreeRoot !== 'string'
24
- || typeof manifest.channelDir !== 'string'
25
- || typeof manifest.token !== 'string'
26
- ) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
27
- if (path.resolve(manifest.channelDir) !== path.join(path.dirname(path.resolve(manifestPath)), 'channel')) {
28
- throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
29
- }
30
- return manifest;
31
- }
36
+ type ActiveExecution = {
37
+ request: SandboxControlRequest;
38
+ prepared: PreparedSandboxControlExecution;
39
+ result: SandboxControlExecutionResult | null;
40
+ failure: unknown;
41
+ settled: boolean;
42
+ };
32
43
 
33
44
  function assertRealDirectory(directory: string, parent?: string): void {
34
45
  const stat = fs.lstatSync(directory);
@@ -38,118 +49,396 @@ function assertRealDirectory(directory: string, parent?: string): void {
38
49
  }
39
50
  }
40
51
 
41
- function writeResponse(directory: string, response: SandboxControlResponse): void {
42
- const temporary = path.join(directory, `.${response.id}.tmp`);
43
- fs.writeFileSync(temporary, `${JSON.stringify(response)}\n`, { mode: 0o600, flag: 'wx' });
44
- fs.renameSync(temporary, path.join(directory, `${response.id}.json`));
52
+ function responsePath(manifest: SandboxControlManifest, id: string): string {
53
+ return path.join(manifest.channelDir, 'responses', `${id}.json`);
45
54
  }
46
55
 
47
- export function sandboxControlSafeEnv(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
48
- return Object.fromEntries(
49
- Object.entries(env).filter(
50
- ([key]) => !key.toUpperCase().startsWith('AGENT_INFRA_CONTROL_')
51
- )
52
- );
56
+ function writeResponse(manifest: SandboxControlManifest, response: SandboxControlResponse): void {
57
+ atomicWriteJson(responsePath(manifest, response.id), response);
58
+ }
59
+
60
+ function rejected(id: string, error: unknown): SandboxControlResponse {
61
+ const detail = controlError(error);
62
+ return { version: 2, id, phase: 'rejected', exitCode: null, stdout: '', stderr: `${detail.message}\n`, error: detail };
63
+ }
64
+
65
+ function unknown(id: string): SandboxControlResponse {
66
+ const error = {
67
+ code: 'SANDBOX_CONTROL_RESULT_UNKNOWN',
68
+ message: 'SANDBOX_CONTROL_RESULT_UNKNOWN: accepted execution ended without a provable result; inspect domain state before retrying',
69
+ retryable: false
70
+ };
71
+ return { version: 2, id, phase: 'rejected', exitCode: null, stdout: '', stderr: `${error.message}\n`, error };
72
+ }
73
+
74
+ function notExecuted(id: string): SandboxControlResponse {
75
+ const error = {
76
+ code: 'SANDBOX_CONTROL_NOT_EXECUTED',
77
+ message: 'SANDBOX_CONTROL_NOT_EXECUTED: request was claimed but never accepted; retry with a new request id',
78
+ retryable: true
79
+ };
80
+ return { version: 2, id, phase: 'rejected', exitCode: null, stdout: '', stderr: `${error.message}\n`, error };
53
81
  }
54
82
 
55
83
  function consumeRequest(directory: string, id: string): void {
56
84
  try {
57
85
  fs.writeFileSync(path.join(directory, id), '', { flag: 'wx', mode: 0o600 });
58
86
  } catch (error) {
59
- if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
60
- throw new Error('SANDBOX_CONTROL_REQUEST_REPLAYED');
61
- }
87
+ if ((error as NodeJS.ErrnoException).code === 'EEXIST') throw new Error('SANDBOX_CONTROL_REQUEST_REPLAYED');
88
+ throw error;
89
+ }
90
+ }
91
+
92
+ function claimRequest(manifest: SandboxControlManifest, requestPath: string, id: string): string {
93
+ const directory = path.join(manifest.processingDir, id);
94
+ fs.mkdirSync(directory, { mode: 0o700 });
95
+ const claimed = path.join(directory, 'request.json');
96
+ try {
97
+ fs.renameSync(requestPath, claimed);
98
+ return claimed;
99
+ } catch (error) {
100
+ fs.rmSync(directory, { recursive: true, force: true });
62
101
  throw error;
63
102
  }
64
103
  }
65
104
 
66
- export function serveSandboxControl(manifestPath: string, signal: AbortSignal = new AbortController().signal): void {
67
- const manifest = readManifest(manifestPath);
68
- assertGitWorktreeBinding(manifest.repoRoot, manifest.worktreeRoot, manifest.branch);
69
- const brokerPath = path.join(path.dirname(manifestPath), 'broker.json');
105
+ function bindingReason(manifest: SandboxControlManifest): string | null {
106
+ try {
107
+ assertGitWorktreeBinding(manifest.repoRoot, manifest.worktreeRoot, manifest.branch);
108
+ return null;
109
+ } catch {
110
+ return 'SANDBOX_WORKTREE_BINDING_LOST';
111
+ }
112
+ }
113
+
114
+ function recoverProcessing(manifest: SandboxControlManifest, brokerOwns: () => boolean): boolean {
115
+ for (const entry of fs.readdirSync(manifest.processingDir, { withFileTypes: true })) {
116
+ if (!entry.isDirectory() || !/^[a-f0-9-]{16,64}$/.test(entry.name)) continue;
117
+ if (!brokerOwns()) return false;
118
+ const descriptor = executionPath(manifest, entry.name);
119
+ const existingResponse = responsePath(manifest, entry.name);
120
+ if (fs.existsSync(descriptor)) {
121
+ const execution = readExecution(descriptor);
122
+ if (!terminateSandboxControlExecution(execution)) {
123
+ throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${entry.name}`);
124
+ }
125
+ if (!brokerOwns()) return false;
126
+ appendSandboxControlAudit(manifest, 'orphan-tree-terminated', { requestId: entry.name });
127
+ let terminal = false;
128
+ if (fs.existsSync(existingResponse)) {
129
+ try {
130
+ const response = JSON.parse(fs.readFileSync(existingResponse, 'utf8')) as SandboxControlResponse;
131
+ terminal = response.version === 2 && response.id === entry.name
132
+ && (response.phase === 'completed' || response.phase === 'rejected');
133
+ } catch {
134
+ terminal = false;
135
+ }
136
+ }
137
+ if (!terminal) {
138
+ if (!brokerOwns()) return false;
139
+ writeResponse(manifest, unknown(entry.name));
140
+ }
141
+ } else {
142
+ if (!brokerOwns()) return false;
143
+ writeResponse(manifest, notExecuted(entry.name));
144
+ }
145
+ if (!brokerOwns()) return false;
146
+ fs.rmSync(path.join(manifest.processingDir, entry.name), { recursive: true, force: true });
147
+ }
148
+ return true;
149
+ }
150
+
151
+ function delay(ms: number): Promise<void> {
152
+ return new Promise((resolve) => setTimeout(resolve, ms));
153
+ }
154
+
155
+ export function sandboxControlSafeEnv(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
156
+ return Object.fromEntries(Object.entries(env).filter(([key]) => !key.toUpperCase().startsWith('AGENT_INFRA_CONTROL_')));
157
+ }
158
+
159
+ export async function serveSandboxControl(
160
+ manifestPath: string,
161
+ signal: AbortSignal = new AbortController().signal,
162
+ options: {
163
+ timing?: SandboxControlTimingPolicy;
164
+ inspectContainer?: (manifest: SandboxControlManifest) => Promise<ContainerObservation>;
165
+ bindingCheck?: (manifest: SandboxControlManifest) => string | null;
166
+ } = {}
167
+ ): Promise<void> {
168
+ const timing = options.timing ?? DEFAULT_SANDBOX_CONTROL_TIMING;
169
+ const inspectContainer = options.inspectContainer ?? ((value: SandboxControlManifest) => inspectSandboxControlContainer(value));
170
+ const bindingCheck = options.bindingCheck ?? bindingReason;
171
+ const manifest = readSandboxControlManifest(manifestPath);
172
+ const root = path.dirname(manifestPath);
70
173
  const startTime = getProcessStartTime(process.pid);
71
174
  if (!startTime) throw new Error('SANDBOX_CONTROL_BROKER_IDENTITY_UNAVAILABLE');
72
- fs.writeFileSync(brokerPath, `${JSON.stringify({
73
- version: 1,
74
- pid: process.pid,
75
- startTime,
76
- token: manifest.token
77
- })}\n`, { mode: 0o600, flag: 'wx' });
175
+ const brokerId = randomUUID();
176
+ const releaseStartup = await acquireSandboxControlBrokerStartup(root, { pid: process.pid, startTime, brokerId });
78
177
  const requestsDir = path.join(manifest.channelDir, 'requests');
79
178
  const responsesDir = path.join(manifest.channelDir, 'responses');
80
- const consumedDir = path.join(path.dirname(manifestPath), 'consumed');
81
- assertRealDirectory(manifest.channelDir);
82
- fs.mkdirSync(requestsDir, { recursive: true, mode: 0o700 });
83
- fs.mkdirSync(responsesDir, { recursive: true, mode: 0o700 });
84
- fs.mkdirSync(consumedDir, { recursive: true, mode: 0o700 });
85
- assertRealDirectory(requestsDir, manifest.channelDir);
86
- assertRealDirectory(responsesDir, manifest.channelDir);
87
- assertRealDirectory(consumedDir, path.dirname(manifestPath));
88
- while (!signal.aborted) {
89
- const current = readManifest(manifestPath);
90
- if (current.token !== manifest.token) return;
91
- assertGitWorktreeBinding(current.repoRoot, current.worktreeRoot, current.branch);
179
+ const consumedDir = path.join(root, 'consumed');
180
+ const broker: BrokerOwner = {
181
+ version: 3,
182
+ pid: process.pid,
183
+ startTime,
184
+ brokerId,
185
+ token: manifest.token,
186
+ generation: manifest.generation
187
+ };
188
+ const brokerPath = path.join(root, 'broker.json');
189
+ const brokerRecord = `${JSON.stringify(broker)}\n`;
190
+ const brokerOwns = (): boolean => {
191
+ try {
192
+ return fs.readFileSync(brokerPath, 'utf8') === brokerRecord;
193
+ } catch {
194
+ return false;
195
+ }
196
+ };
197
+ try {
198
+ for (const directory of [manifest.channelDir, requestsDir, responsesDir, consumedDir, manifest.publicStatusDir, manifest.processingDir]) {
199
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
200
+ }
201
+ assertRealDirectory(manifest.channelDir);
92
202
  assertRealDirectory(requestsDir, manifest.channelDir);
93
203
  assertRealDirectory(responsesDir, manifest.channelDir);
94
- for (const name of fs.readdirSync(requestsDir).sort()) {
95
- if (!/^[a-f0-9-]{16,64}\.json$/.test(name)) continue;
96
- const requestPath = path.join(requestsDir, name);
97
- let id = name.slice(0, -5);
98
- let response: SandboxControlResponse;
204
+ assertRealDirectory(consumedDir, root);
205
+ assertRealDirectory(manifest.publicStatusDir, root);
206
+ assertRealDirectory(manifest.processingDir, root);
207
+ fs.writeFileSync(brokerPath, brokerRecord, { mode: 0o600, flag: 'wx' });
208
+ if (isSandboxControlRootQuiescing(root)) {
209
+ if (fs.readFileSync(brokerPath, 'utf8') === brokerRecord) fs.unlinkSync(brokerPath);
210
+ return;
211
+ }
212
+ } finally {
213
+ releaseStartup();
214
+ }
215
+ let active: ActiveExecution | null = null;
216
+ let lastState = '';
217
+ let lastStatusAt = 0;
218
+ let nextBindingCheckAt = 0;
219
+ let nextContainerHeartbeatAt = Date.now() + timing.containerHeartbeatMs;
220
+ let containerReasonCode: string | null = null;
221
+ let containerBackoffMs = timing.parkedBindingInitialMs;
222
+ let bindingReasonCode: string | null = null;
223
+ let bindingBackoffMs: number = timing.parkedBindingInitialMs;
224
+ try {
225
+ if (!brokerOwns()) return;
226
+ writeSandboxControlStatus(manifest, broker, 'starting', null, null);
227
+ if (!brokerOwns()) return;
228
+ appendSandboxControlAudit(manifest, 'broker-start', { pid: broker.pid });
229
+ if (!recoverProcessing(manifest, brokerOwns)) return;
230
+ while (!signal.aborted) {
231
+ if (!brokerOwns()) break;
232
+ let current: SandboxControlManifest;
99
233
  try {
100
- const stat = fs.lstatSync(requestPath);
101
- if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_REQUEST_INVALID');
102
- const request = validateSandboxControlRequest(
103
- JSON.parse(fs.readFileSync(requestPath, 'utf8')),
104
- manifest
105
- );
106
- id = request.id;
107
- consumeRequest(consumedDir, id);
108
- if (request.family === 'task-create') {
109
- const result = createTask(request.candidate, { repoRoot: manifest.repoRoot });
110
- response = {
111
- version: 1, id,
112
- exitCode: result.status === 'blocked' ? 2 : result.status === 'failed' ? 1 : 0,
113
- stdout: `${JSON.stringify(result)}\n`, stderr: ''
114
- };
234
+ current = readSandboxControlManifest(manifestPath);
235
+ } catch {
236
+ break;
237
+ }
238
+ if (current.token !== manifest.token || current.generation !== manifest.generation) break;
239
+ const heartbeatNow = Date.now();
240
+ if (heartbeatNow >= nextContainerHeartbeatAt) {
241
+ let observation: ContainerObservation;
242
+ try {
243
+ observation = await inspectContainer(manifest);
244
+ } catch (error) {
245
+ observation = { state: 'unknown', reason: controlError(error).code };
246
+ }
247
+ if (!brokerOwns()) break;
248
+ if (observation.state === 'found') {
249
+ containerReasonCode = null;
250
+ containerBackoffMs = timing.parkedBindingInitialMs;
251
+ nextContainerHeartbeatAt = Date.now() + timing.containerHeartbeatMs;
252
+ } else if (observation.state === 'unknown') {
253
+ containerReasonCode = 'SANDBOX_CONTROL_CONTAINER_UNKNOWN';
254
+ appendSandboxControlAudit(manifest, 'container-heartbeat-unknown', { reason: observation.reason });
255
+ const backoff = nextSandboxControlBackoff(containerBackoffMs, timing.slowCheckMs);
256
+ nextContainerHeartbeatAt = Date.now() + backoff.delayMs;
257
+ containerBackoffMs = backoff.nextDelayMs;
115
258
  } else {
116
- const boundArgs = bindSandboxControlTask(request, manifest.taskId!);
117
- if (request.family === 'task-orchestration') {
118
- boundArgs.push('--git-worktree-root', manifest.worktreeRoot);
259
+ try {
260
+ await garbageCollectSandboxControlRoot(root, {
261
+ timeoutMs: timing.quiesceDeadlineMs,
262
+ inspectContainer: (timeoutMs) => inspectContainer(manifest),
263
+ selfOwner: broker
264
+ });
265
+ active = null;
266
+ return;
267
+ } catch (error) {
268
+ if (!brokerOwns()) break;
269
+ containerReasonCode = 'SANDBOX_CONTROL_CONTAINER_ABSENT_GC_FAILED';
270
+ appendSandboxControlAudit(manifest, 'container-gc-failed', { reason: controlError(error).code });
271
+ const backoff = nextSandboxControlBackoff(containerBackoffMs, timing.slowCheckMs);
272
+ nextContainerHeartbeatAt = Date.now() + backoff.delayMs;
273
+ containerBackoffMs = backoff.nextDelayMs;
119
274
  }
120
- const result = spawnSync(
121
- process.execPath,
122
- ['--experimental-strip-types', '--no-warnings', process.argv[1]!, request.family, ...boundArgs],
123
- {
124
- cwd: manifest.repoRoot,
125
- encoding: 'utf8',
126
- env: sandboxControlSafeEnv()
127
- }
275
+ }
276
+ }
277
+
278
+ if (active?.settled) {
279
+ if (!brokerOwns()) break;
280
+ if (active.result) {
281
+ if (!brokerOwns()) break;
282
+ writeResponse(manifest, {
283
+ version: 2, id: active.request.id, phase: 'completed', exitCode: active.result.exitCode,
284
+ stdout: active.result.stdout, stderr: active.result.stderr, error: null
285
+ });
286
+ } else {
287
+ if (!brokerOwns()) break;
288
+ writeResponse(manifest, unknown(active.request.id));
289
+ }
290
+ if (!brokerOwns()) break;
291
+ fs.rmSync(path.join(manifest.processingDir, active.request.id), { recursive: true, force: true });
292
+ active = null;
293
+ }
294
+
295
+ let reasonCode: string | null = null;
296
+ try {
297
+ if (!brokerOwns()) break;
298
+ if (cleanupStaleSandboxControlLease(manifest)) {
299
+ if (!brokerOwns()) break;
300
+ appendSandboxControlAudit(manifest, 'lease-stale-cleanup');
301
+ }
302
+ if (readActiveLease(manifest)) reasonCode = 'SANDBOX_CONTROL_HANDOFF_ACTIVE';
303
+ } catch {
304
+ reasonCode = 'SANDBOX_CONTROL_HANDOFF_ACTIVE';
305
+ }
306
+ const now = Date.now();
307
+ if (now >= nextBindingCheckAt) {
308
+ bindingReasonCode = bindingCheck(manifest);
309
+ if (bindingReasonCode) {
310
+ const backoff = nextSandboxControlBackoff(bindingBackoffMs, timing.slowCheckMs);
311
+ nextBindingCheckAt = now + backoff.delayMs;
312
+ bindingBackoffMs = backoff.nextDelayMs;
313
+ } else {
314
+ nextBindingCheckAt = now + timing.slowCheckMs;
315
+ bindingBackoffMs = timing.parkedBindingInitialMs;
316
+ }
317
+ }
318
+ reasonCode ??= containerReasonCode;
319
+ reasonCode ??= bindingReasonCode;
320
+ const state = reasonCode ? 'parked' : active ? 'busy' : 'healthy';
321
+ const stateKey = `${state}:${reasonCode ?? ''}:${active?.request.id ?? ''}`;
322
+ if (brokerOwns() && (stateKey !== lastState || now - lastStatusAt >= timing.controlTickMs)) {
323
+ writeSandboxControlStatus(manifest, broker, state, reasonCode, active?.request.id ?? null, now);
324
+ lastStatusAt = now;
325
+ }
326
+ if (brokerOwns() && stateKey !== lastState) {
327
+ appendSandboxControlAudit(manifest, 'broker-state', { state, reasonCode, requestId: active?.request.id ?? null });
328
+ lastState = stateKey;
329
+ }
330
+
331
+ let retiring = false;
332
+ for (const name of fs.readdirSync(requestsDir).sort()) {
333
+ if (!/^[a-f0-9-]{16,64}\.json$/.test(name)) continue;
334
+ const id = name.slice(0, -5);
335
+ const source = path.join(requestsDir, name);
336
+ let claimed: string | null = null;
337
+ try {
338
+ if (!brokerOwns()) {
339
+ retiring = true;
340
+ break;
341
+ }
342
+ claimed = claimRequest(manifest, source, id);
343
+ if (!brokerOwns()) {
344
+ retiring = true;
345
+ break;
346
+ }
347
+ consumeRequest(consumedDir, id);
348
+ if (!brokerOwns()) {
349
+ retiring = true;
350
+ break;
351
+ }
352
+ if (reasonCode) throw new Error(reasonCode);
353
+ if (active) throw new Error('SANDBOX_CONTROL_BUSY');
354
+ const request = validateSandboxControlRequest(JSON.parse(fs.readFileSync(claimed, 'utf8')), manifest);
355
+ if (bindingCheck(manifest)) throw new Error('SANDBOX_WORKTREE_BINDING_LOST');
356
+ if (readActiveLease(manifest)) throw new Error('SANDBOX_CONTROL_HANDOFF_ACTIVE');
357
+ const prepared = await prepareSandboxControlExecution({
358
+ manifest, manifestPath, request, requestPath: claimed, internalCliPath: process.argv[1]!
359
+ });
360
+ const execution: ActiveExecution = { request, prepared, result: null, failure: null, settled: false };
361
+ active = execution;
362
+ prepared.completion.then(
363
+ (result) => { execution.result = result; execution.settled = true; },
364
+ (error) => { execution.failure = error; execution.settled = true; }
128
365
  );
129
- response = {
130
- version: 1,
131
- id,
132
- exitCode: result.status ?? 1,
133
- stdout: result.stdout ?? '',
134
- stderr: result.stderr ?? ''
135
- };
366
+ if (!brokerOwns()) {
367
+ prepared.terminate(false);
368
+ active = null;
369
+ retiring = true;
370
+ continue;
371
+ }
372
+ if (!brokerOwns()) {
373
+ prepared.terminate(false);
374
+ active = null;
375
+ retiring = true;
376
+ continue;
377
+ }
378
+ writeResponse(manifest, {
379
+ version: 2, id, phase: 'accepted', exitCode: null, stdout: '', stderr: '', error: null
380
+ });
381
+ if (!brokerOwns()) {
382
+ prepared.terminate(false);
383
+ active = null;
384
+ retiring = true;
385
+ break;
386
+ }
387
+ try {
388
+ prepared.start(brokerOwns);
389
+ } catch (error) {
390
+ const owned = brokerOwns();
391
+ prepared.terminate(owned);
392
+ if (!owned || !brokerOwns()) {
393
+ active = null;
394
+ retiring = true;
395
+ break;
396
+ }
397
+ writeResponse(manifest, unknown(id));
398
+ if (!brokerOwns()) {
399
+ active = null;
400
+ retiring = true;
401
+ break;
402
+ }
403
+ fs.rmSync(path.join(manifest.processingDir, id), { recursive: true, force: true });
404
+ active = null;
405
+ claimed = null;
406
+ if (!brokerOwns()) {
407
+ retiring = true;
408
+ break;
409
+ }
410
+ appendSandboxControlAudit(manifest, 'executor-gate-failed', { requestId: id });
411
+ continue;
412
+ }
413
+ } catch (error) {
414
+ if (!brokerOwns()) {
415
+ retiring = true;
416
+ break;
417
+ }
418
+ writeResponse(manifest, rejected(id, error));
419
+ if (!brokerOwns()) {
420
+ retiring = true;
421
+ break;
422
+ }
423
+ if (claimed) fs.rmSync(path.dirname(claimed), { recursive: true, force: true });
136
424
  }
137
- } catch (error) {
138
- response = {
139
- version: 1,
140
- id,
141
- exitCode: 1,
142
- stdout: '',
143
- stderr: `${error instanceof Error ? error.message : String(error)}\n`
144
- };
145
425
  }
426
+ if (retiring || !brokerOwns()) break;
427
+ await delay(timing.controlTickMs);
428
+ }
429
+ } finally {
430
+ if (active) {
431
+ const owned = brokerOwns();
432
+ active.prepared.terminate(owned);
433
+ if (owned && brokerOwns()) writeResponse(manifest, unknown(active.request.id));
434
+ }
435
+ if (brokerOwns()) {
436
+ appendSandboxControlAudit(manifest, 'broker-stop', { pid: broker.pid, brokerId: broker.brokerId });
146
437
  try {
147
- fs.unlinkSync(requestPath);
148
- writeResponse(responsesDir, response);
149
- } catch (error) {
150
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
438
+ if (fs.readFileSync(brokerPath, 'utf8') === brokerRecord) fs.unlinkSync(brokerPath);
439
+ } catch {
440
+ // A newer owner or recovery path owns the record.
151
441
  }
152
442
  }
153
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
154
443
  }
155
444
  }