@fitlab-ai/agent-infra 0.9.6 → 0.9.8

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 (178) hide show
  1. package/dist/bin/internal-cli.js +10 -0
  2. package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +47 -5
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +245 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +398 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +23 -6
  8. package/dist/lib/decide.js +68 -14
  9. package/dist/lib/defaults.json +10 -2
  10. package/dist/lib/git/workflow.js +30 -3
  11. package/dist/lib/init.js +1 -1
  12. package/dist/lib/internal/codex-lifecycle.js +69 -1
  13. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  14. package/dist/lib/internal/release-workflow.js +1 -3
  15. package/dist/lib/internal/task-activity.js +57 -7
  16. package/dist/lib/internal/task-event.js +57 -4
  17. package/dist/lib/internal/task-ledger.js +59 -5
  18. package/dist/lib/internal/task-lifecycle.js +59 -8
  19. package/dist/lib/internal/task-orchestration.js +48 -8
  20. package/dist/lib/internal/task-override.js +131 -0
  21. package/dist/lib/internal/task-review.js +64 -6
  22. package/dist/lib/internal/task-verify.js +65 -3
  23. package/dist/lib/internal/task-warning.js +57 -4
  24. package/dist/lib/platform/context.js +6 -4
  25. package/dist/lib/platform/github-client.js +55 -2
  26. package/dist/lib/platform/pr-summary.js +5 -3
  27. package/dist/lib/process-data/checkpoint.js +319 -0
  28. package/dist/lib/process-data/index.js +128 -60
  29. package/dist/lib/process-data/materialize.js +84 -0
  30. package/dist/lib/process-data/normalize.js +34 -1
  31. package/dist/lib/process-data/reconcile.js +55 -1
  32. package/dist/lib/process-data/sources.js +422 -1
  33. package/dist/lib/process-data/store.js +341 -11
  34. package/dist/lib/runtime/agent-runtime.js +56 -0
  35. package/dist/lib/sandbox/commands/create.js +394 -264
  36. package/dist/lib/sandbox/commands/rm.js +64 -15
  37. package/dist/lib/sandbox/commands/start.js +4 -3
  38. package/dist/lib/sandbox/control/container-identity.js +64 -0
  39. package/dist/lib/sandbox/control/executor.js +23 -9
  40. package/dist/lib/sandbox/control/lifecycle.js +590 -61
  41. package/dist/lib/sandbox/control/protocol.js +7 -0
  42. package/dist/lib/sandbox/control/server.js +202 -26
  43. package/dist/lib/sandbox/control/state.js +51 -25
  44. package/dist/lib/sandbox/control/timing.js +8 -0
  45. package/dist/lib/sandbox/engine.js +14 -7
  46. package/dist/lib/sandbox/mounts.js +7 -0
  47. package/dist/lib/sandbox/recovery.js +135 -50
  48. package/dist/lib/sandbox/workspace-view.js +59 -20
  49. package/dist/lib/server/daemon.js +1 -1
  50. package/dist/lib/server/process-control.js +11 -1
  51. package/dist/lib/server/process-state.js +107 -20
  52. package/dist/lib/task/activity-intent.js +4 -1
  53. package/dist/lib/task/artifact-lifecycle.js +82 -11
  54. package/dist/lib/task/artifact-receipts.js +128 -0
  55. package/dist/lib/task/codex-orchestration.js +81 -6
  56. package/dist/lib/task/commands/validate.js +1 -1
  57. package/dist/lib/task/create.js +10 -2
  58. package/dist/lib/task/decision-intents.js +4 -2
  59. package/dist/lib/task/delegation-receipts.js +120 -17
  60. package/dist/lib/task/events.js +148 -7
  61. package/dist/lib/task/guard-override.js +31 -0
  62. package/dist/lib/task/human-override.js +1240 -0
  63. package/dist/lib/task/ledger-intents.js +6 -3
  64. package/dist/lib/task/ledger.js +13 -1
  65. package/dist/lib/task/lifecycle.js +69 -14
  66. package/dist/lib/task/orchestration.js +293 -18
  67. package/dist/lib/task/review-finalization.js +3 -2
  68. package/dist/lib/task/sections.js +5 -1
  69. package/dist/lib/task/task-execution-lock.js +5 -4
  70. package/dist/lib/task/verification-engine.js +80 -22
  71. package/dist/lib/task/verification.js +13 -1
  72. package/dist/lib/task/workflow-warning-intents.js +4 -2
  73. package/dist/lib/task/write.js +2 -1
  74. package/dist/lifecycle-build-manifest.json +6 -0
  75. package/lib/agent-clients/adapters/claude-code.ts +3 -3
  76. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +62 -4
  77. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  78. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +322 -0
  79. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  80. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +480 -0
  81. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +35 -5
  82. package/lib/decide.ts +59 -14
  83. package/lib/defaults.json +10 -2
  84. package/lib/git/workflow.ts +25 -2
  85. package/lib/init.ts +1 -1
  86. package/lib/internal/codex-lifecycle.ts +68 -1
  87. package/lib/internal/codex-sandbox-controller.ts +109 -0
  88. package/lib/internal/release-workflow.ts +1 -3
  89. package/lib/internal/task-activity.ts +46 -6
  90. package/lib/internal/task-event.ts +53 -4
  91. package/lib/internal/task-ledger.ts +48 -4
  92. package/lib/internal/task-lifecycle.ts +56 -12
  93. package/lib/internal/task-orchestration.ts +43 -8
  94. package/lib/internal/task-override.ts +138 -0
  95. package/lib/internal/task-review.ts +51 -6
  96. package/lib/internal/task-verify.ts +52 -3
  97. package/lib/internal/task-warning.ts +44 -3
  98. package/lib/platform/context.ts +8 -5
  99. package/lib/platform/github-client.ts +61 -3
  100. package/lib/platform/pr-summary.ts +10 -3
  101. package/lib/process-data/checkpoint.ts +335 -0
  102. package/lib/process-data/index.ts +112 -60
  103. package/lib/process-data/materialize.ts +83 -0
  104. package/lib/process-data/normalize.ts +29 -1
  105. package/lib/process-data/reconcile.ts +64 -2
  106. package/lib/process-data/sources.ts +449 -2
  107. package/lib/process-data/store.ts +304 -11
  108. package/lib/process-data/types.ts +124 -2
  109. package/lib/runtime/agent-runtime.ts +76 -0
  110. package/lib/sandbox/commands/create.ts +139 -6
  111. package/lib/sandbox/commands/rm.ts +64 -14
  112. package/lib/sandbox/commands/start.ts +4 -3
  113. package/lib/sandbox/config.ts +2 -1
  114. package/lib/sandbox/control/container-identity.ts +93 -0
  115. package/lib/sandbox/control/executor.ts +24 -12
  116. package/lib/sandbox/control/lifecycle.ts +685 -67
  117. package/lib/sandbox/control/protocol.ts +35 -6
  118. package/lib/sandbox/control/server.ts +184 -24
  119. package/lib/sandbox/control/state.ts +67 -33
  120. package/lib/sandbox/control/timing.ts +12 -0
  121. package/lib/sandbox/engine.ts +20 -9
  122. package/lib/sandbox/mounts.ts +8 -0
  123. package/lib/sandbox/recovery.ts +159 -49
  124. package/lib/sandbox/workspace-view.ts +73 -22
  125. package/lib/server/daemon.ts +1 -1
  126. package/lib/server/process-control.ts +13 -3
  127. package/lib/server/process-state.ts +129 -27
  128. package/lib/task/activity-intent.ts +4 -2
  129. package/lib/task/artifact-lifecycle.ts +73 -10
  130. package/lib/task/artifact-receipts.ts +161 -0
  131. package/lib/task/codex-orchestration.ts +91 -5
  132. package/lib/task/commands/validate.ts +1 -1
  133. package/lib/task/create.ts +11 -2
  134. package/lib/task/decision-intents.ts +4 -2
  135. package/lib/task/delegation-receipts.ts +205 -25
  136. package/lib/task/events.ts +132 -6
  137. package/lib/task/guard-override.ts +50 -0
  138. package/lib/task/human-override.ts +1503 -0
  139. package/lib/task/ledger-intents.ts +6 -3
  140. package/lib/task/ledger.ts +16 -1
  141. package/lib/task/lifecycle.ts +71 -14
  142. package/lib/task/orchestration.ts +443 -22
  143. package/lib/task/review-finalization.ts +6 -2
  144. package/lib/task/sections.ts +6 -2
  145. package/lib/task/task-execution-lock.ts +11 -9
  146. package/lib/task/verification-engine.ts +93 -32
  147. package/lib/task/verification.ts +15 -1
  148. package/lib/task/workflow-warning-intents.ts +4 -2
  149. package/lib/task/write.ts +4 -1
  150. package/package.json +1 -1
  151. package/templates/.agents/README.en.md +7 -0
  152. package/templates/.agents/README.zh-CN.md +5 -0
  153. package/templates/.agents/hooks/lifecycle-delegation.js +33 -4
  154. package/templates/.agents/rules/lifecycle-orchestration.en.md +10 -3
  155. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +10 -3
  156. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  157. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  158. package/templates/.agents/rules/review-handshake.en.md +3 -1
  159. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  160. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  161. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  162. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  163. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  164. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +6 -6
  165. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +6 -6
  166. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -3
  167. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -3
  168. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  169. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  170. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +25 -0
  171. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +25 -0
  172. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  173. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  174. package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -1
  175. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -1
  176. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  177. package/templates/.agents/workspace/README.en.md +0 -26
  178. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -11,13 +11,28 @@ import { sandboxCoreBindMounts } from './mounts.js';
11
11
  import { assertSandboxTaskSource, sandboxControlPaths, sandboxWorkspaceViewPaths } from './workspace-view.js';
12
12
  import { parseSandboxWorkspaceIdentity, sameSandboxWorkspaceIdentity } from './workspace-identity.js';
13
13
  import { inspectWorktree } from './worktree-safety.js';
14
- import { runEngine, runOkEngine, runVerboseEngine } from './shell.js';
14
+ import { commandForEngine, runEngine, runOkEngine, runProbe, runVerboseEngine } from './shell.js';
15
15
  import { declaredTmpfsSeedEntries, resolveTools, toolConfigDirCandidates } from './tools.js';
16
16
  import { fetchSandboxRows, selectSandboxContainer, startSandboxContainer } from './commands/list-running.js';
17
- import { getProcessStartTime, processIdentityMatches, removePidFileIfMatches } from '../server/process-state.js';
18
- import { acquireSandboxControlBrokerStartup, isSandboxControlRootQuiescing } from './control/lifecycle.js';
17
+ import { getProcessIdentityState, getProcessStartTime, removePidFileIfMatches } from '../server/process-state.js';
18
+ import { acquireSandboxControlBrokerStartup, garbageCollectSandboxControlRoot, isSandboxControlRootQuiescing, readSandboxControlManifest } from './control/lifecycle.js';
19
19
  import { appendSandboxControlAudit, readSandboxControlStatus } from './control/state.js';
20
+ import { inspectSandboxControlContainer } from './control/container-identity.js';
20
21
  import { SANDBOX_CONTROL_FUTURE_SKEW_MS, SANDBOX_CONTROL_STATUS_STALE_MS } from './control/protocol.js';
22
+ export function worktreeProbeForEngine(engine, probe = runProbe) {
23
+ return (cmd, args, opts = {}) => {
24
+ const engineArgs = [...args];
25
+ if (engine === 'wsl2' && cmd === 'git') {
26
+ for (let index = 0; index < engineArgs.length - 1; index += 1) {
27
+ if (engineArgs[index] === '-C') {
28
+ engineArgs[index + 1] = toEnginePath(engine, engineArgs[index + 1]);
29
+ }
30
+ }
31
+ }
32
+ const command = commandForEngine(engine, cmd, engineArgs);
33
+ return probe(command.cmd, command.args, opts);
34
+ };
35
+ }
21
36
  async function waitForSandboxControlBrokerStatus(params, timeoutMs = 5_000) {
22
37
  const deadline = Date.now() + timeoutMs;
23
38
  while (Date.now() < deadline) {
@@ -42,7 +57,7 @@ export async function startSandboxControlBroker(repoRoot, manifestPath) {
42
57
  const directory = path.dirname(fileURLToPath(import.meta.url));
43
58
  const extension = path.extname(fileURLToPath(import.meta.url));
44
59
  const internalCli = path.resolve(directory, '..', '..', 'bin', `internal-cli${extension}`);
45
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
60
+ const manifest = readSandboxControlManifest(manifestPath);
46
61
  const root = path.dirname(manifestPath);
47
62
  if (isSandboxControlRootQuiescing(root))
48
63
  throw new Error('SANDBOX_CONTROL_QUIESCING');
@@ -64,14 +79,24 @@ export async function startSandboxControlBroker(repoRoot, manifestPath) {
64
79
  catch {
65
80
  // Malformed owner records are stale and replaced below.
66
81
  }
67
- const owner = broker !== null && typeof broker.pid === 'number' && typeof broker.startTime === 'string'
68
- ? { version: 1, pid: broker.pid, startTime: broker.startTime }
82
+ const owner = broker !== null && broker.version === 3 && typeof broker.pid === 'number'
83
+ && Number.isSafeInteger(broker.pid) && broker.pid > 0 && typeof broker.startTime === 'number'
84
+ && Number.isSafeInteger(broker.startTime) && typeof broker.brokerId === 'string'
85
+ && broker.brokerId.length > 0 && typeof broker.token === 'string'
86
+ && typeof broker.generation === 'string'
87
+ ? broker
69
88
  : null;
70
- const live = owner !== null && processIdentityMatches(owner);
89
+ if (broker === null || broker.version !== 3 || owner === null) {
90
+ throw new Error('SANDBOX_CONTROL_BROKER_VERSION_INVALID: rebuild the sandbox container before starting a new broker');
91
+ }
92
+ const ownerState = getProcessIdentityState({ pid: owner.pid, startTime: owner.startTime });
93
+ if (ownerState === 'unknown')
94
+ throw new Error('SANDBOX_CONTROL_BROKER_OWNER_UNAVAILABLE');
95
+ const live = ownerState === 'alive';
71
96
  if (live
72
97
  && owner !== null
73
98
  && broker !== null
74
- && broker.version === 2
99
+ && broker.version === 3
75
100
  && broker.token === manifest.token
76
101
  && broker.generation === manifest.generation) {
77
102
  await waitForSandboxControlBrokerStatus({
@@ -83,28 +108,6 @@ export async function startSandboxControlBroker(repoRoot, manifestPath) {
83
108
  });
84
109
  return;
85
110
  }
86
- if (live && owner) {
87
- const deadline = Date.now() + 5_000;
88
- while (Date.now() < deadline) {
89
- await new Promise((resolve) => setTimeout(resolve, 25));
90
- try {
91
- if (fs.readFileSync(brokerPath, 'utf8') !== brokerSnapshot) {
92
- return startSandboxControlBroker(repoRoot, manifestPath);
93
- }
94
- }
95
- catch (error) {
96
- if (error.code === 'ENOENT') {
97
- return startSandboxControlBroker(repoRoot, manifestPath);
98
- }
99
- throw error;
100
- }
101
- if (!processIdentityMatches(owner)) {
102
- removePidFileIfMatches(brokerPath, brokerSnapshot);
103
- return startSandboxControlBroker(repoRoot, manifestPath);
104
- }
105
- }
106
- throw new Error('SANDBOX_CONTROL_BROKER_OWNER_ACTIVE');
107
- }
108
111
  }
109
112
  const startupStartTime = getProcessStartTime(process.pid);
110
113
  if (!startupStartTime)
@@ -162,22 +165,42 @@ async function ensureSandboxControlBroker(params) {
162
165
  });
163
166
  if (!fs.existsSync(control.manifestPath))
164
167
  return;
165
- const manifest = JSON.parse(fs.readFileSync(control.manifestPath, 'utf8'));
166
- if (manifest.version !== 3) {
167
- throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 3; container-only recreation is required');
168
+ const validatedManifest = readSandboxControlManifest(control.manifestPath);
169
+ const containerObservation = await inspectSandboxControlContainer(validatedManifest);
170
+ if (containerObservation.state === 'unknown') {
171
+ throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${containerObservation.reason}`);
172
+ }
173
+ if (containerObservation.state === 'absent') {
174
+ await garbageCollectSandboxControlRoot(control.root, {
175
+ inspectContainer: (timeoutMs) => inspectSandboxControlContainer(validatedManifest, { timeoutMs })
176
+ });
177
+ throw new Error('SANDBOX_CONTROL_CONTAINER_ABSENT: recreate the sandbox container before broker recovery');
168
178
  }
169
179
  const brokerPath = path.join(control.root, 'broker.json');
170
180
  try {
171
181
  const broker = JSON.parse(fs.readFileSync(brokerPath, 'utf8'));
172
- if (broker.version === 2
182
+ const validIdentity = typeof broker.pid === 'number'
183
+ && Number.isSafeInteger(broker.pid)
184
+ && typeof broker.startTime === 'number'
185
+ && Number.isSafeInteger(broker.startTime);
186
+ const ownerState = validIdentity
187
+ ? getProcessIdentityState({ pid: broker.pid, startTime: broker.startTime })
188
+ : 'dead';
189
+ if (ownerState === 'unknown')
190
+ throw new Error('SANDBOX_CONTROL_BROKER_OWNER_UNAVAILABLE');
191
+ if (broker.version === 3
173
192
  && typeof broker.pid === 'number'
174
- && typeof broker.startTime === 'string'
175
- && broker.token === manifest.token
176
- && broker.generation === manifest.generation
177
- && processIdentityMatches({ version: 1, pid: broker.pid, startTime: broker.startTime })) {
193
+ && Number.isSafeInteger(broker.pid)
194
+ && typeof broker.startTime === 'number'
195
+ && Number.isSafeInteger(broker.startTime)
196
+ && typeof broker.brokerId === 'string'
197
+ && broker.brokerId.length > 0
198
+ && broker.token === validatedManifest.token
199
+ && broker.generation === validatedManifest.generation
200
+ && ownerState === 'alive') {
178
201
  try {
179
202
  const status = readSandboxControlStatus(control.statusDir);
180
- if (status.generation === manifest.generation && status.broker.pid === broker.pid
203
+ if (status.generation === validatedManifest.generation && status.broker.pid === broker.pid
181
204
  && status.broker.startTime === broker.startTime
182
205
  && Date.now() - status.updatedAt <= SANDBOX_CONTROL_STATUS_STALE_MS
183
206
  && status.updatedAt <= Date.now() + SANDBOX_CONTROL_FUTURE_SKEW_MS)
@@ -189,14 +212,16 @@ async function ensureSandboxControlBroker(params) {
189
212
  await waitForSandboxControlBrokerStatus({
190
213
  root: control.root,
191
214
  statusDir: control.statusDir,
192
- generation: manifest.generation,
215
+ generation: validatedManifest.generation,
193
216
  pid: broker.pid,
194
217
  startTime: broker.startTime
195
218
  });
196
219
  return;
197
220
  }
198
221
  }
199
- catch {
222
+ catch (error) {
223
+ if (error instanceof Error && error.message === 'SANDBOX_CONTROL_BROKER_OWNER_UNAVAILABLE')
224
+ throw error;
200
225
  // A missing, stale, or malformed broker record is replaced below.
201
226
  }
202
227
  await startSandboxControlBroker(params.config.repoRoot, control.manifestPath);
@@ -231,6 +256,30 @@ export function classifySandboxRecovery(snapshot) {
231
256
  message: 'Container runtime capability signature does not match the current sandbox plan.'
232
257
  });
233
258
  }
259
+ if (snapshot.workspaceTopology === 'legacy-parent') {
260
+ findings.push({
261
+ repairKind: 'hard-failure',
262
+ code: 'SANDBOX_WORKSPACE_TOPOLOGY_MISMATCH',
263
+ message: 'Sandbox uses the legacy workspace parent-mount topology; recreate the container explicitly to migrate it.',
264
+ path: '/workspace/.agents/workspace'
265
+ });
266
+ }
267
+ if (snapshot.taskView && !snapshot.taskView.readable) {
268
+ findings.push({
269
+ repairKind: 'hard-failure',
270
+ code: 'SANDBOX_TASK_VIEW_UNREADABLE',
271
+ message: `Bound task view is not readable at ${snapshot.taskView.path}.`,
272
+ path: snapshot.taskView.path
273
+ });
274
+ }
275
+ if (snapshot.runtimeStoreOk === false) {
276
+ findings.push({
277
+ repairKind: 'hard-failure',
278
+ code: 'SANDBOX_RUNTIME_STORE_UNAVAILABLE',
279
+ message: 'Task-bound runtime store is not writable and readable by devuser.',
280
+ path: '/run/agent-infra/runtime'
281
+ });
282
+ }
234
283
  for (const mountPath of snapshot.unexpectedCapabilityMounts ?? []) {
235
284
  findings.push({
236
285
  repairKind: 'hard-failure',
@@ -239,6 +288,9 @@ export function classifySandboxRecovery(snapshot) {
239
288
  });
240
289
  }
241
290
  for (const mount of snapshot.mounts) {
291
+ if (snapshot.workspaceTopology === 'legacy-parent'
292
+ && mount.path.startsWith('/workspace/.agents/workspace'))
293
+ continue;
242
294
  if (mount.actualType !== mount.expectedType
243
295
  || !mount.sourceMatches
244
296
  || mount.actualRW !== mount.expectedRW
@@ -320,6 +372,20 @@ function inspectContainer(engine, container, runFn) {
320
372
  }
321
373
  return parsed[0];
322
374
  }
375
+ function workspaceTopology(mountsByDestination) {
376
+ if (mountsByDestination.has('/workspace/.agents/workspace'))
377
+ return 'legacy-parent';
378
+ const perState = [
379
+ '/workspace/.agents/workspace/active',
380
+ '/workspace/.agents/workspace/active/.short-ids.json',
381
+ '/workspace/.agents/workspace/completed',
382
+ '/workspace/.agents/workspace/blocked',
383
+ '/workspace/.agents/workspace/archive'
384
+ ];
385
+ return perState.some((destination) => mountsByDestination.has(destination))
386
+ ? 'per-state'
387
+ : 'unknown';
388
+ }
323
389
  function probe(engine, container, script, args, runOkFn, user = 'devuser') {
324
390
  return runOkFn(engine, 'docker', [
325
391
  'exec', '--user', user, container, 'sh', '-c', script, 'agent-infra-recovery', ...args
@@ -380,6 +446,7 @@ function expectedMounts(params) {
380
446
  controlStatusDir: control.statusDir,
381
447
  ...(params.workspace.mode === 'task-bound'
382
448
  ? {
449
+ runtimeDir: control.runtimeDir,
383
450
  taskSource: assertSandboxTaskSource(config.repoRoot, params.workspace.taskId),
384
451
  taskId: params.workspace.taskId
385
452
  }
@@ -478,6 +545,10 @@ export function collectSandboxRecoverySnapshot(params) {
478
545
  const capabilityPlan = createSandboxCapabilityPlan(params.config);
479
546
  const workspace = params.workspace ?? { mode: 'branch-only' };
480
547
  const tools = [...capabilityPlan.tools];
548
+ const topology = workspaceTopology(mountsByDestination);
549
+ const taskViewPath = workspace.mode === 'task-bound'
550
+ ? `/workspace/.agents/workspace/active/${workspace.taskId}/task.md`
551
+ : null;
481
552
  const seeds = declaredTmpfsSeedEntries(tools).map((seed) => {
482
553
  const mounted = mountsByDestination.get(seed.stagingPath)?.Type === 'bind';
483
554
  return {
@@ -496,6 +567,9 @@ export function collectSandboxRecoverySnapshot(params) {
496
567
  permissionsOk: probe(params.engine, params.container, 'test "$(stat -c %u:%g:%a -- "$1")" = "$(id -u devuser):$(id -g devuser):700"', [tool.containerMount], runOkFn),
497
568
  writable: probe(params.engine, params.container, 'probe="$1/.agent-infra-ready-$$"; trap \'rm -f -- "$probe"\' EXIT; : > "$probe"', [tool.containerMount], runOkFn)
498
569
  }));
570
+ const runtimeStoreOk = workspace.mode === 'task-bound'
571
+ ? probe(params.engine, params.container, 'probe="$1/.agent-infra-runtime-$$"; trap \'rm -f -- "$probe"\' EXIT; printf runtime-probe > "$probe" && test "$(cat -- "$probe")" = runtime-probe', ['/run/agent-infra/runtime'], runOkFn)
572
+ : undefined;
499
573
  const labels = inspection.Config?.Labels ?? {};
500
574
  const branchLabel = labels[sandboxBranchLabel(params.config)];
501
575
  const containerWorkspace = parseSandboxWorkspaceIdentity(labels, {
@@ -551,6 +625,14 @@ export function collectSandboxRecoverySnapshot(params) {
551
625
  actualBranch: typeof branchLabel === 'string' ? branchLabel : null,
552
626
  expectedWorkspace: workspace,
553
627
  actualWorkspace: containerWorkspace,
628
+ workspaceTopology: topology,
629
+ ...(taskViewPath === null ? {} : {
630
+ taskView: {
631
+ path: taskViewPath,
632
+ readable: probe(params.engine, params.container, 'test -r "$1"', [taskViewPath], runOkFn)
633
+ }
634
+ }),
635
+ ...(runtimeStoreOk === undefined ? {} : { runtimeStoreOk }),
554
636
  identityOk: typeof inspection.Id === 'string' && inspection.Id.length > 0
555
637
  && branchLabel === params.branch
556
638
  && sameSandboxWorkspaceIdentity(containerWorkspace, workspace),
@@ -666,10 +748,12 @@ function assess(params) {
666
748
  }
667
749
  function canRetryFreshReadiness(findings) {
668
750
  return findings.length > 0 && findings.every((finding) => finding.repairKind === 'hard-failure'
751
+ && finding.code !== 'SANDBOX_WORKSPACE_TOPOLOGY_MISMATCH'
669
752
  && finding.path !== undefined
670
753
  && (finding.path.startsWith('/workspace/.agents/workspace')
671
754
  || finding.path === '/home/devuser/.host-shell-config'
672
- || finding.path === '/home/devuser/.bash_aliases'));
755
+ || finding.path === '/home/devuser/.bash_aliases'
756
+ || finding.path === '/run/agent-infra/runtime'));
673
757
  }
674
758
  export async function assertFreshSandboxReady(params) {
675
759
  validateTmpfsSeedEntries({
@@ -729,6 +813,9 @@ export async function ensureSandboxReady(params) {
729
813
  workspace: params.workspace ?? { mode: 'branch-only' }
730
814
  });
731
815
  }
816
+ if (params.forceRecreate) {
817
+ throw new Error('Explicit container recreation requested.');
818
+ }
732
819
  if (!params.row.running) {
733
820
  startFn(params.engine, params.row.name);
734
821
  const initial = assess({
@@ -862,7 +949,8 @@ export async function ensureSandboxReady(params) {
862
949
  + 'keep the worktree in use, remove the stale directory, then retry.';
863
950
  throw new Error(`SANDBOX_RECOVERY_WORKTREE_SNAPSHOT_INVALID: expected exactly one existing worktree for '${params.branch}', found ${existingWorktrees.length}. ${remediation}`);
864
951
  }
865
- const beforeInspection = inspectWorktree(existingWorktrees[0]);
952
+ const worktreeProbe = worktreeProbeForEngine(params.engine, deps?.probe);
953
+ const beforeInspection = inspectWorktree(existingWorktrees[0], { probe: worktreeProbe });
866
954
  if (beforeInspection.status === 'failed') {
867
955
  throw new Error(`SANDBOX_RECOVERY_WORKTREE_SNAPSHOT_INVALID: ${beforeInspection.message}`);
868
956
  }
@@ -871,17 +959,14 @@ export async function ensureSandboxReady(params) {
871
959
  throw new Error(`SANDBOX_RECOVERY_WORKTREE_SNAPSHOT_INVALID: expected branch '${params.branch}', found '${before.branch}'. `
872
960
  + `Run 'git -C ${JSON.stringify(before.worktree)} checkout ${params.branch}' to restore the expected branch, then retry.`);
873
961
  }
874
- const warning = `Sandbox recovery failed in place. Replacing only the container. ${dataBoundary}`;
962
+ const warning = params.forceRecreate
963
+ ? `Explicit sandbox recreation requested. Replacing only the container. ${dataBoundary}`
964
+ : `Sandbox recovery failed in place. Replacing only the container. ${dataBoundary}`;
875
965
  warnings.push(warning);
876
966
  (params.writeWarning ?? ((message) => process.stderr.write(`${message}\n`)))(warning);
877
- const runVerboseFn = deps?.runVerbose ?? runVerboseEngine;
878
967
  let replacementResult = null;
879
968
  let replacementFailure = null;
880
969
  try {
881
- if (params.row.running) {
882
- runVerboseFn(params.engine, 'docker', ['stop', params.row.name]);
883
- }
884
- runVerboseFn(params.engine, 'docker', ['rm', params.row.name]);
885
970
  await params.recreate(params.branch);
886
971
  const rows = (deps?.fetchRows ?? fetchSandboxRows)(params.engine, sandboxLabel(params.config), sandboxBranchLabel(params.config));
887
972
  const replacement = selectSandboxContainer([...rows.running, ...rows.nonRunning], containerNameCandidates(params.config, params.branch));
@@ -904,7 +989,7 @@ export async function ensureSandboxReady(params) {
904
989
  catch (error) {
905
990
  replacementFailure = error;
906
991
  }
907
- const afterInspection = inspectWorktree(before.worktree);
992
+ const afterInspection = inspectWorktree(before.worktree, { probe: worktreeProbe });
908
993
  if (afterInspection.status === 'failed' || afterInspection.snapshot.identity !== before.identity) {
909
994
  const detail = afterInspection.status === 'failed'
910
995
  ? afterInspection.message
@@ -38,6 +38,7 @@ export function sandboxControlPaths(params) {
38
38
  channelDir: path.join(root, 'channel'),
39
39
  statusDir: path.join(root, 'public'),
40
40
  processingDir: path.join(root, 'processing'),
41
+ runtimeDir: path.join(root, 'runtime'),
41
42
  manifestPath: path.join(root, 'manifest.json')
42
43
  };
43
44
  }
@@ -73,21 +74,6 @@ function assertSafeMountTargetPath(target, worktreeRoot, canonicalWorktreeRoot)
73
74
  }
74
75
  }
75
76
  }
76
- export function prepareSandboxWorkspaceMountTargets(worktreeRoot) {
77
- const resolvedWorktreeRoot = path.resolve(worktreeRoot);
78
- const canonicalWorktreeRoot = fs.realpathSync.native(resolvedWorktreeRoot);
79
- const workspaceRoot = path.resolve(resolvedWorktreeRoot, '.agents', 'workspace');
80
- const statePaths = sandboxWorkspaceViewStatePaths(workspaceRoot);
81
- const registryTarget = path.join(workspaceRoot, 'active', '.short-ids.json');
82
- for (const target of [workspaceRoot, ...statePaths.map(({ hostPath }) => hostPath), registryTarget]) {
83
- assertSafeMountTargetPath(target, resolvedWorktreeRoot, canonicalWorktreeRoot);
84
- }
85
- fs.mkdirSync(workspaceRoot, { recursive: true, mode: 0o700 });
86
- for (const { hostPath } of statePaths) {
87
- fs.mkdirSync(hostPath, { recursive: true, mode: 0o700 });
88
- }
89
- fs.closeSync(fs.openSync(registryTarget, 'a', 0o600));
90
- }
91
77
  export function assertSandboxTaskSource(repoRoot, taskId) {
92
78
  if (!/^TASK-\d{8}-\d{6}$/.test(taskId))
93
79
  throw new Error('SANDBOX_TASK_SOURCE_INVALID');
@@ -103,6 +89,23 @@ export function assertSandboxTaskSource(repoRoot, taskId) {
103
89
  }
104
90
  return canonical;
105
91
  }
92
+ export function prepareSandboxWorkspaceMountTargets(worktreeRoot) {
93
+ const resolvedWorktreeRoot = path.resolve(worktreeRoot);
94
+ const canonicalWorktreeRoot = fs.realpathSync.native(resolvedWorktreeRoot);
95
+ const workspaceRoot = path.resolve(resolvedWorktreeRoot, '.agents', 'workspace');
96
+ const statePaths = sandboxWorkspaceViewStatePaths(workspaceRoot);
97
+ const registryTarget = path.join(workspaceRoot, 'active', '.short-ids.json');
98
+ for (const target of [workspaceRoot, ...statePaths.map(({ hostPath }) => hostPath), registryTarget]) {
99
+ assertSafeMountTargetPath(target, resolvedWorktreeRoot, canonicalWorktreeRoot);
100
+ }
101
+ fs.mkdirSync(workspaceRoot, { recursive: true, mode: 0o700 });
102
+ for (const { hostPath } of statePaths) {
103
+ fs.mkdirSync(hostPath, { recursive: true, mode: 0o700 });
104
+ fs.chmodSync(hostPath, 0o700);
105
+ }
106
+ fs.chmodSync(workspaceRoot, 0o700);
107
+ fs.closeSync(fs.openSync(registryTarget, 'a', 0o600));
108
+ }
106
109
  export function materializeSandboxWorkspaceView(params) {
107
110
  const projectRoot = path.resolve(params.base, params.project);
108
111
  const { root, taskMountPath } = sandboxWorkspaceViewPaths(params);
@@ -132,16 +135,33 @@ export function materializeSandboxWorkspaceView(params) {
132
135
  return { root, taskMountPath };
133
136
  }
134
137
  export function materializeSandboxControl(params) {
135
- const { root, channelDir, statusDir, processingDir, manifestPath } = sandboxControlPaths(params);
138
+ const { root, channelDir, statusDir, processingDir, runtimeDir, manifestPath } = sandboxControlPaths(params);
136
139
  assertSafeDirectory(root, path.resolve(params.base));
140
+ if (fs.existsSync(root)) {
141
+ if (!params.replacementLease || path.resolve(params.replacementLease.root) !== root) {
142
+ throw new Error('SANDBOX_CONTROL_REPLACEMENT_REQUIRED');
143
+ }
144
+ params.replacementLease.assertOwned();
145
+ }
137
146
  const consumedDir = path.join(root, 'consumed');
138
147
  assertSafeDirectory(consumedDir, root);
139
148
  fs.rmSync(consumedDir, { recursive: true, force: true });
140
149
  fs.mkdirSync(consumedDir, { recursive: true, mode: 0o700 });
141
- for (const directory of [statusDir, processingDir]) {
150
+ for (const directory of [statusDir, processingDir, runtimeDir]) {
142
151
  assertSafeDirectory(directory, root);
152
+ if (directory === runtimeDir)
153
+ fs.rmSync(directory, { recursive: true, force: true });
143
154
  fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
144
155
  }
156
+ for (const directory of [
157
+ path.join(runtimeDir, 'clients'),
158
+ path.join(runtimeDir, 'clients', 'codex', 'capabilities'),
159
+ path.join(runtimeDir, 'clients', 'codex', 'lifecycle')
160
+ ]) {
161
+ assertSafeDirectory(directory, runtimeDir);
162
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
163
+ fs.chmodSync(directory, 0o700);
164
+ }
145
165
  for (const queue of ['requests', 'responses']) {
146
166
  const directory = path.join(channelDir, queue);
147
167
  assertSafeDirectory(directory, root);
@@ -152,11 +172,13 @@ export function materializeSandboxControl(params) {
152
172
  const generation = randomBytes(16).toString('hex');
153
173
  const repoRoot = fs.realpathSync.native(params.repoRoot);
154
174
  const manifest = {
155
- version: 3,
175
+ version: 5,
176
+ engine: params.engine ?? 'docker',
156
177
  repoRoot,
157
178
  worktreeRoot: fs.realpathSync.native(params.worktreeRoot),
158
179
  project: params.project,
159
180
  container: params.container,
181
+ containerIdentity: { id: '', labels: {} },
160
182
  branch: params.branch,
161
183
  mode: params.identity.mode,
162
184
  taskId: params.identity.mode === 'task-bound' ? params.identity.taskId : null,
@@ -164,9 +186,26 @@ export function materializeSandboxControl(params) {
164
186
  generation,
165
187
  channelDir,
166
188
  publicStatusDir: statusDir,
167
- processingDir
189
+ processingDir,
190
+ runtimeDir
168
191
  };
169
192
  fs.writeFileSync(manifestPath, `${JSON.stringify(manifest)}\n`, { mode: 0o600 });
170
- return { channelDir, statusDir, manifestPath, token, generation };
193
+ return { root, channelDir, statusDir, runtimeDir, manifestPath, token, generation };
194
+ }
195
+ export function finalizeSandboxControlManifest(setup, identity) {
196
+ if (!identity.engine || !identity.id)
197
+ throw new Error('SANDBOX_CONTROL_CONTAINER_ID_INVALID');
198
+ const manifest = JSON.parse(fs.readFileSync(setup.manifestPath, 'utf8'));
199
+ manifest.version = 5;
200
+ manifest.engine = identity.engine;
201
+ manifest.containerIdentity = { id: identity.id, labels: { ...identity.labels } };
202
+ const temporary = `${setup.manifestPath}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
203
+ try {
204
+ fs.writeFileSync(temporary, `${JSON.stringify(manifest)}\n`, { mode: 0o600, flag: 'wx' });
205
+ fs.renameSync(temporary, setup.manifestPath);
206
+ }
207
+ finally {
208
+ fs.rmSync(temporary, { force: true });
209
+ }
171
210
  }
172
211
  //# sourceMappingURL=workspace-view.js.map
@@ -110,7 +110,7 @@ export async function runDaemon() {
110
110
  logger.close();
111
111
  if (ownStartTime !== null) {
112
112
  removePidRecordIfMatches(config.pidFile, {
113
- version: 1,
113
+ version: 2,
114
114
  pid: process.pid,
115
115
  startTime: ownStartTime
116
116
  });
@@ -26,7 +26,7 @@ async function waitForProcessIdentity(pid, timeoutMs = 2000) {
26
26
  while (Date.now() < deadline) {
27
27
  const startTime = getProcessStartTime(pid);
28
28
  if (startTime !== null)
29
- return { version: 1, pid, startTime };
29
+ return { version: 2, pid, startTime };
30
30
  await delay(25);
31
31
  }
32
32
  return null;
@@ -58,6 +58,9 @@ export async function start({ foreground = false } = {}) {
58
58
  process.stdout.write(`server already running (pid ${existing.record.pid})\n`);
59
59
  return;
60
60
  }
61
+ if (existing.kind === 'legacy-json' || existing.kind === 'legacy-pid-only' || existing.kind === 'legacy-unknown') {
62
+ throw new Error('server: legacy pid record requires the matching previous version to stop it before starting');
63
+ }
61
64
  if (existing.kind !== 'missing') {
62
65
  removePidFileIfMatches(pidPath, existing.snapshot);
63
66
  }
@@ -104,6 +107,9 @@ export async function stop() {
104
107
  return;
105
108
  }
106
109
  if (state.kind !== 'running') {
110
+ if (state.kind === 'legacy-json' || state.kind === 'legacy-pid-only' || state.kind === 'legacy-unknown') {
111
+ throw new Error('server: legacy pid record requires the matching previous version to stop it');
112
+ }
107
113
  removePidFileIfMatches(config.pidFile, state.snapshot);
108
114
  process.stdout.write('server is not running (removed stale pid file)\n');
109
115
  return;
@@ -135,6 +141,10 @@ export function status() {
135
141
  const config = loadServerConfig();
136
142
  const state = readProcessState(config.pidFile);
137
143
  if (state.kind !== 'running') {
144
+ if (state.kind === 'legacy-json' || state.kind === 'legacy-pid-only' || state.kind === 'legacy-unknown') {
145
+ process.stdout.write('server: legacy pid record requires the matching previous version to stop it\n');
146
+ return;
147
+ }
138
148
  if (state.kind !== 'missing')
139
149
  removePidFileIfMatches(config.pidFile, state.snapshot);
140
150
  process.stdout.write('server: stopped\n');