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