@fitlab-ai/agent-infra 0.9.7 → 0.9.9

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 (195) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +11 -1
  4. package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +60 -22
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +175 -11
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +147 -0
  9. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
  10. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +86 -69
  11. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
  12. package/dist/lib/decide.js +68 -14
  13. package/dist/lib/git/workflow.js +10 -1
  14. package/dist/lib/init.js +1 -1
  15. package/dist/lib/internal/codex-lifecycle.js +19 -5
  16. package/dist/lib/internal/codex-sandbox-controller.js +6 -13
  17. package/dist/lib/internal/git-workflow.js +5 -7
  18. package/dist/lib/internal/platform-pr.js +20 -7
  19. package/dist/lib/internal/release-workflow.js +3 -4
  20. package/dist/lib/internal/sandbox-control.js +79 -2
  21. package/dist/lib/internal/task-activity.js +57 -7
  22. package/dist/lib/internal/task-event.js +57 -4
  23. package/dist/lib/internal/task-finalization.js +54 -0
  24. package/dist/lib/internal/task-ledger.js +59 -5
  25. package/dist/lib/internal/task-lifecycle.js +59 -8
  26. package/dist/lib/internal/task-orchestration.js +161 -288
  27. package/dist/lib/internal/task-override.js +131 -0
  28. package/dist/lib/internal/task-review.js +64 -6
  29. package/dist/lib/internal/task-verify.js +65 -3
  30. package/dist/lib/internal/task-warning.js +57 -4
  31. package/dist/lib/platform/pr-summary.js +53 -13
  32. package/dist/lib/platform/pull-requests.js +205 -41
  33. package/dist/lib/runtime/agent-runtime.js +56 -0
  34. package/dist/lib/sandbox/commands/create.js +389 -311
  35. package/dist/lib/sandbox/commands/rm.js +316 -55
  36. package/dist/lib/sandbox/control/client.js +190 -6
  37. package/dist/lib/sandbox/control/container-identity.js +1 -1
  38. package/dist/lib/sandbox/control/controller-registration.js +273 -0
  39. package/dist/lib/sandbox/control/executor.js +145 -5
  40. package/dist/lib/sandbox/control/lifecycle.js +395 -73
  41. package/dist/lib/sandbox/control/protocol.js +67 -6
  42. package/dist/lib/sandbox/control/state.js +27 -11
  43. package/dist/lib/sandbox/engine.js +14 -7
  44. package/dist/lib/sandbox/index.js +5 -3
  45. package/dist/lib/sandbox/managed-fs.js +5 -0
  46. package/dist/lib/sandbox/mounts.js +20 -5
  47. package/dist/lib/sandbox/recovery.js +78 -10
  48. package/dist/lib/sandbox/task-resolver.js +50 -6
  49. package/dist/lib/sandbox/workspace-identity.js +27 -0
  50. package/dist/lib/sandbox/workspace-view.js +62 -40
  51. package/dist/lib/sandbox/worktree-safety.js +215 -2
  52. package/dist/lib/server/process-state.js +51 -1
  53. package/dist/lib/task/activity-intent.js +4 -1
  54. package/dist/lib/task/codex-orchestration.js +158 -33
  55. package/dist/lib/task/commands/status.js +59 -1
  56. package/dist/lib/task/commit-operation.js +335 -0
  57. package/dist/lib/task/commit-policy.js +18 -0
  58. package/dist/lib/task/decision-intents.js +4 -2
  59. package/dist/lib/task/delegation-receipts.js +279 -17
  60. package/dist/lib/task/events.js +9 -5
  61. package/dist/lib/task/finalization.js +567 -0
  62. package/dist/lib/task/guard-override.js +31 -0
  63. package/dist/lib/task/human-override.js +1240 -0
  64. package/dist/lib/task/ledger-intents.js +6 -3
  65. package/dist/lib/task/lifecycle.js +69 -14
  66. package/dist/lib/task/operation-outcome.js +33 -0
  67. package/dist/lib/task/orchestration.js +199 -719
  68. package/dist/lib/task/review-finalization.js +3 -2
  69. package/dist/lib/task/short-id.js +59 -1
  70. package/dist/lib/task/task-execution-lock.js +6 -1
  71. package/dist/lib/task/verification-engine.js +138 -59
  72. package/dist/lib/task/verification.js +15 -2
  73. package/dist/lib/task/workflow-warning-intents.js +54 -3
  74. package/dist/lib/task/write.js +2 -1
  75. package/dist/lifecycle-build-manifest.json +3 -3
  76. package/lib/agent-clients/adapters/claude-code.ts +3 -3
  77. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +63 -20
  78. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
  79. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +278 -13
  80. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
  81. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
  82. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
  83. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +112 -93
  84. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
  85. package/lib/decide.ts +59 -14
  86. package/lib/git/workflow.ts +11 -2
  87. package/lib/init.ts +1 -1
  88. package/lib/internal/codex-lifecycle.ts +18 -5
  89. package/lib/internal/codex-sandbox-controller.ts +6 -12
  90. package/lib/internal/git-workflow.ts +5 -6
  91. package/lib/internal/platform-pr.ts +14 -7
  92. package/lib/internal/release-workflow.ts +3 -4
  93. package/lib/internal/sandbox-control.ts +92 -2
  94. package/lib/internal/task-activity.ts +46 -6
  95. package/lib/internal/task-event.ts +53 -4
  96. package/lib/internal/task-finalization.ts +68 -0
  97. package/lib/internal/task-ledger.ts +48 -4
  98. package/lib/internal/task-lifecycle.ts +56 -12
  99. package/lib/internal/task-orchestration.ts +32 -107
  100. package/lib/internal/task-override.ts +138 -0
  101. package/lib/internal/task-review.ts +51 -6
  102. package/lib/internal/task-verify.ts +52 -3
  103. package/lib/internal/task-warning.ts +44 -3
  104. package/lib/platform/pr-summary.ts +65 -14
  105. package/lib/platform/pull-requests.ts +211 -41
  106. package/lib/runtime/agent-runtime.ts +76 -0
  107. package/lib/sandbox/commands/create.ts +88 -4
  108. package/lib/sandbox/commands/rm.ts +413 -61
  109. package/lib/sandbox/config.ts +2 -1
  110. package/lib/sandbox/control/client.ts +255 -6
  111. package/lib/sandbox/control/container-identity.ts +3 -3
  112. package/lib/sandbox/control/controller-registration.ts +347 -0
  113. package/lib/sandbox/control/executor.ts +172 -6
  114. package/lib/sandbox/control/lifecycle.ts +465 -77
  115. package/lib/sandbox/control/protocol.ts +85 -9
  116. package/lib/sandbox/control/state.ts +35 -18
  117. package/lib/sandbox/engine.ts +20 -9
  118. package/lib/sandbox/index.ts +5 -3
  119. package/lib/sandbox/managed-fs.ts +5 -0
  120. package/lib/sandbox/mounts.ts +23 -5
  121. package/lib/sandbox/recovery.ts +90 -9
  122. package/lib/sandbox/task-resolver.ts +58 -7
  123. package/lib/sandbox/workspace-identity.ts +50 -2
  124. package/lib/sandbox/workspace-view.ts +73 -43
  125. package/lib/sandbox/worktree-safety.ts +236 -2
  126. package/lib/server/process-state.ts +48 -1
  127. package/lib/task/activity-intent.ts +4 -2
  128. package/lib/task/codex-orchestration.ts +167 -33
  129. package/lib/task/commands/status.ts +78 -1
  130. package/lib/task/commit-operation.ts +430 -0
  131. package/lib/task/commit-policy.ts +30 -0
  132. package/lib/task/decision-intents.ts +4 -2
  133. package/lib/task/delegation-receipts.ts +304 -19
  134. package/lib/task/events.ts +10 -5
  135. package/lib/task/finalization.ts +719 -0
  136. package/lib/task/guard-override.ts +50 -0
  137. package/lib/task/human-override.ts +1503 -0
  138. package/lib/task/ledger-intents.ts +6 -3
  139. package/lib/task/lifecycle.ts +71 -14
  140. package/lib/task/operation-outcome.ts +48 -0
  141. package/lib/task/orchestration.ts +239 -922
  142. package/lib/task/review-finalization.ts +6 -2
  143. package/lib/task/short-id.ts +65 -1
  144. package/lib/task/task-execution-lock.ts +10 -1
  145. package/lib/task/verification-engine.ts +145 -64
  146. package/lib/task/verification.ts +18 -3
  147. package/lib/task/workflow-warning-intents.ts +61 -4
  148. package/lib/task/write.ts +4 -1
  149. package/package.json +2 -2
  150. package/templates/.agents/QUICKSTART.en.md +2 -2
  151. package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
  152. package/templates/.agents/hooks/lifecycle-delegation.js +9 -24
  153. package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -3
  154. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -3
  155. package/templates/.agents/rules/task-management.en.md +2 -2
  156. package/templates/.agents/rules/task-management.zh-CN.md +2 -2
  157. package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
  158. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
  159. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  160. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  161. package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
  162. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
  163. package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
  164. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
  165. package/templates/.agents/skills/commit/SKILL.en.md +41 -125
  166. package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
  167. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  168. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  169. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
  170. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
  171. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  172. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  173. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
  174. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
  175. package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
  176. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
  177. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  178. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  179. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  180. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  181. package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
  182. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
  183. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  184. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  185. package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
  186. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
  187. package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -1
  188. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -1
  189. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  190. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  191. package/templates/.codex/hooks.json +4 -4
  192. package/dist/lib/task/commit-finalization.js +0 -305
  193. package/dist/lib/task/commit-intent.js +0 -165
  194. package/lib/task/commit-finalization.ts +0 -419
  195. package/lib/task/commit-intent.ts +0 -230
@@ -1,12 +1,14 @@
1
1
  import { validateTaskCreateCandidate, type TaskCreateCandidateV1 } from '../../task/create.ts';
2
+ import { normalizeAgentToken } from '../../agent-clients/tokens.ts';
2
3
  import type { ProcessIdentity } from '../../server/process-state.ts';
4
+ import type { CodexControllerLeaseProofV1 } from './controller-registration.ts';
3
5
 
4
6
  export const SANDBOX_CONTROL_MAX_BYTES = 64 * 1024;
5
7
  export const SANDBOX_CONTROL_ADMISSION_WINDOW_MS = 2_000;
6
8
  export const SANDBOX_CONTROL_STATUS_INTERVAL_MS = 250;
7
9
  export const SANDBOX_CONTROL_STATUS_STALE_MS = 1_500;
8
10
  export const SANDBOX_CONTROL_FUTURE_SKEW_MS = 1_000;
9
- export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-orchestration', 'task-create'] as const;
11
+ export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-orchestration', 'task-finalization', 'task-create', 'codex-controller'] as const;
10
12
  export type SandboxControlTimingPolicy = Readonly<{
11
13
  controlTickMs: number;
12
14
  parkedBindingInitialMs: number;
@@ -27,12 +29,15 @@ export type SandboxControlContainerIdentity = Readonly<{
27
29
  id: string;
28
30
  labels: Readonly<Record<string, string>>;
29
31
  }>;
30
- export type SandboxControlManifest = Readonly<{
31
- version: 4; engine: string; repoRoot: string; worktreeRoot: string; project: string; container: string;
32
+ export type SandboxControlManifestBase = Readonly<{
33
+ engine: string; repoRoot: string; worktreeRoot: string; project: string; container: string;
32
34
  containerIdentity: SandboxControlContainerIdentity;
33
35
  branch: string; mode: 'task-bound' | 'branch-only'; taskId: string | null; token: string;
34
36
  generation: string; channelDir: string; publicStatusDir: string; processingDir: string;
35
37
  }>;
38
+ export type SandboxControlLegacyManifest = SandboxControlManifestBase & Readonly<{ version: 4 }>;
39
+ export type SandboxControlManifest = SandboxControlManifestBase & Readonly<{ version: 5; runtimeDir: string }>;
40
+ export type SandboxControlManifestLike = SandboxControlLegacyManifest | SandboxControlManifest;
36
41
  export type SandboxControlBrokerOwner = ProcessIdentity & Readonly<{
37
42
  version: 3;
38
43
  brokerId: string;
@@ -40,15 +45,25 @@ export type SandboxControlBrokerOwner = ProcessIdentity & Readonly<{
40
45
  generation: string;
41
46
  }>;
42
47
  type RequestBase = Readonly<{
43
- version: 2; id: string; token: string; generation: string; issuedAt: number; expiresAt: number;
48
+ version: 3; id: string; token: string; generation: string; issuedAt: number; expiresAt: number;
49
+ controllerProcess: ProcessIdentity | null;
50
+ controllerProof: CodexControllerLeaseProofV1 | null;
44
51
  }>;
45
52
  export type SandboxTaskCommandRequest = RequestBase & Readonly<{
46
53
  family: 'task-lifecycle' | 'task-orchestration'; args: string[];
47
54
  }>;
55
+ export type SandboxTaskFinalizationRequest = RequestBase & Readonly<{
56
+ family: 'task-finalization'; operation: 'complete'; agent: string; args: [];
57
+ }>;
48
58
  export type SandboxTaskCreateRequest = RequestBase & Readonly<{
49
59
  family: 'task-create'; candidate: TaskCreateCandidateV1;
50
60
  }>;
51
- export type SandboxControlRequest = SandboxTaskCommandRequest | SandboxTaskCreateRequest;
61
+ export type SandboxCodexControllerRequest = RequestBase & Readonly<{
62
+ family: 'codex-controller';
63
+ command: 'open' | 'close' | 'verify';
64
+ args: [];
65
+ }>;
66
+ export type SandboxControlRequest = SandboxTaskCommandRequest | SandboxTaskFinalizationRequest | SandboxTaskCreateRequest | SandboxCodexControllerRequest;
52
67
  export type SandboxControlError = Readonly<{ code: string; message: string; retryable: boolean }>;
53
68
  export type SandboxControlResponse = Readonly<{
54
69
  version: 2; id: string; phase: 'accepted' | 'completed' | 'rejected'; exitCode: number | null;
@@ -98,7 +113,7 @@ export function validateSandboxControlRequest(
98
113
  }
99
114
  const request = value as Record<string, unknown>;
100
115
  if (
101
- request.version !== 2 || typeof request.id !== 'string'
116
+ request.version !== 3 || typeof request.id !== 'string'
102
117
  || !/^[a-f0-9-]{16,64}$/.test(request.id) || request.token !== manifest.token
103
118
  || typeof request.family !== 'string' || !isSandboxControlFamily(request.family)
104
119
  || !Number.isSafeInteger(request.issuedAt) || !Number.isSafeInteger(request.expiresAt)
@@ -117,13 +132,47 @@ export function validateSandboxControlRequest(
117
132
  fail('SANDBOX_CONTROL_REQUEST_TOO_LARGE', 'request exceeds the control limit');
118
133
  }
119
134
  if (request.family === 'task-create') {
120
- const expected = ['candidate', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
135
+ const expected = ['candidate', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
121
136
  if (Object.keys(request).sort().join(',') !== expected.sort().join(',')) {
122
137
  fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
123
138
  }
139
+ if (request.controllerProcess !== null || request.controllerProof !== null) {
140
+ fail('SANDBOX_CONTROL_REQUEST_INVALID', 'task-create cannot carry controller authority');
141
+ }
124
142
  return { ...request, candidate: validateTaskCreateCandidate(request.candidate) } as SandboxTaskCreateRequest;
125
143
  }
126
- const expected = ['args', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
144
+ if (request.family === 'codex-controller') {
145
+ const expected = ['args', 'command', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
146
+ if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
147
+ || !Array.isArray(request.args) || request.args.length !== 0
148
+ || !['open', 'close', 'verify'].includes(request.command as string)
149
+ || !validControllerProcess(request.controllerProcess)
150
+ || (request.command === 'open' && request.controllerProof !== null)
151
+ || (request.command !== 'open' && !validControllerProof(request.controllerProof))
152
+ || (request.command === 'verify'
153
+ && JSON.stringify(request.controllerProcess) !== JSON.stringify((request.controllerProof as CodexControllerLeaseProofV1).controllerProcess))) {
154
+ fail('SANDBOX_CONTROL_REQUEST_INVALID', 'controller request schema is invalid');
155
+ }
156
+ if (manifest.mode !== 'task-bound' || !manifest.taskId) {
157
+ fail('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot register a Codex controller');
158
+ }
159
+ return request as SandboxCodexControllerRequest;
160
+ }
161
+ if (request.family === 'task-finalization') {
162
+ const expected = ['agent', 'args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'operation', 'token', 'version'];
163
+ if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
164
+ || request.operation !== 'complete'
165
+ || !Array.isArray(request.args) || request.args.length !== 0
166
+ || typeof request.agent !== 'string' || normalizeAgentToken(request.agent) !== request.agent
167
+ || request.controllerProcess !== null || request.controllerProof !== null) {
168
+ fail('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization request schema or authorization is invalid');
169
+ }
170
+ if (manifest.mode !== 'task-bound' || !manifest.taskId) {
171
+ fail('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot finalize tasks');
172
+ }
173
+ return request as SandboxTaskFinalizationRequest;
174
+ }
175
+ const expected = ['args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
127
176
  if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
128
177
  || !Array.isArray(request.args) || !request.args.every((arg) => typeof arg === 'string')) {
129
178
  fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
@@ -138,15 +187,42 @@ export function validateSandboxControlRequest(
138
187
  && request.args.some((arg) => arg === '--git-worktree-root' || arg.startsWith('--git-worktree-root='))) {
139
188
  fail('SANDBOX_CONTROL_REQUEST_INVALID', 'worktree binding is reserved for the control broker');
140
189
  }
190
+ if (request.controllerProcess !== null
191
+ || (request.controllerProof !== null && !validControllerProof(request.controllerProof))) {
192
+ fail('SANDBOX_CONTROL_REQUEST_INVALID', 'controller authority is invalid');
193
+ }
194
+ if (request.family !== 'task-orchestration' && request.controllerProof !== null) {
195
+ fail('SANDBOX_CONTROL_REQUEST_INVALID', 'controller proof is not allowed for this family');
196
+ }
141
197
  return request as SandboxTaskCommandRequest;
142
198
  }
143
199
 
144
200
  export function bindSandboxControlTask(request: SandboxControlRequest, taskId: string): string[] {
145
- if (request.family === 'task-create') fail('SANDBOX_CONTROL_REQUEST_INVALID', 'task-create requests do not bind a current task');
201
+ if (request.family === 'task-create' || request.family === 'codex-controller' || request.family === 'task-finalization') {
202
+ fail('SANDBOX_CONTROL_REQUEST_INVALID', `${request.family} requests do not bind a current task`);
203
+ }
146
204
  if (request.args.length === 0) fail('SANDBOX_CONTROL_REQUEST_INVALID', 'command arguments are required');
147
205
  return [taskId, ...request.args.slice(1)];
148
206
  }
149
207
 
208
+ function validControllerProcess(value: unknown): value is ProcessIdentity {
209
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
210
+ const processValue = value as Record<string, unknown>;
211
+ return Object.keys(processValue).sort().join(',') === 'pid,startTime'
212
+ && Number.isSafeInteger(processValue.pid) && (processValue.pid as number) > 0
213
+ && Number.isSafeInteger(processValue.startTime) && (processValue.startTime as number) >= 0;
214
+ }
215
+
216
+ function validControllerProof(value: unknown): value is CodexControllerLeaseProofV1 {
217
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
218
+ const proof = value as Record<string, unknown>;
219
+ return Object.keys(proof).sort().join(',') === 'controllerProcess,leaseId,leaseSecret,version'
220
+ && proof.version === 1
221
+ && typeof proof.leaseId === 'string' && /^[a-f0-9]{64}$/u.test(proof.leaseId)
222
+ && typeof proof.leaseSecret === 'string' && /^[a-f0-9]{64}$/u.test(proof.leaseSecret)
223
+ && validControllerProcess(proof.controllerProcess);
224
+ }
225
+
150
226
  export function controlError(error: unknown): SandboxControlError {
151
227
  if (error instanceof SandboxControlProtocolError) {
152
228
  return { code: error.code, message: error.message, retryable: error.retryable };
@@ -3,12 +3,12 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { randomUUID } from 'node:crypto';
5
5
  import { buildProcessTreeStopCommand } from '../../server/process-control.ts';
6
- import { processIdentityMatches } from '../../server/process-state.ts';
7
- import type { ProcessIdentity } from '../../server/process-state.ts';
6
+ import { getProcessIdentityState } from '../../server/process-state.ts';
7
+ import type { ProcessIdentity, ProcessIdentityProbe } from '../../server/process-state.ts';
8
8
  import type {
9
9
  SandboxControlExecution,
10
10
  SandboxControlLease,
11
- SandboxControlManifest,
11
+ SandboxControlManifestLike,
12
12
  SandboxControlStatus
13
13
  } from './protocol.ts';
14
14
 
@@ -36,12 +36,12 @@ export function readJsonFile(filePath: string): unknown {
36
36
  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
37
37
  }
38
38
 
39
- export function statusPath(manifest: SandboxControlManifest): string {
39
+ export function statusPath(manifest: SandboxControlManifestLike): string {
40
40
  return path.join(manifest.publicStatusDir, 'status.json');
41
41
  }
42
42
 
43
43
  export function writeSandboxControlStatus(
44
- manifest: SandboxControlManifest,
44
+ manifest: SandboxControlManifestLike,
45
45
  broker: ProcessIdentity & { brokerId: string },
46
46
  state: SandboxControlStatus['state'],
47
47
  reasonCode: string | null,
@@ -72,7 +72,7 @@ export function readSandboxControlStatus(directory: string): SandboxControlStatu
72
72
  return parseSandboxControlStatus(readJsonFile(path.join(directory, 'status.json')));
73
73
  }
74
74
 
75
- export function readActiveLease(manifest: SandboxControlManifest, now = Date.now()): SandboxControlLease | null {
75
+ export function readActiveLease(manifest: SandboxControlManifestLike, now = Date.now()): SandboxControlLease | null {
76
76
  const filePath = path.join(path.dirname(manifest.publicStatusDir), 'lease.json');
77
77
  if (!fs.existsSync(filePath)) return null;
78
78
  const lease = readJsonFile(filePath) as Partial<SandboxControlLease> | null;
@@ -90,7 +90,11 @@ export function readActiveLease(manifest: SandboxControlManifest, now = Date.now
90
90
  return lease as SandboxControlLease;
91
91
  }
92
92
 
93
- export function cleanupStaleSandboxControlLease(manifest: SandboxControlManifest, now = Date.now()): boolean {
93
+ export function cleanupStaleSandboxControlLease(
94
+ manifest: SandboxControlManifestLike,
95
+ now = Date.now(),
96
+ options: Readonly<{ identityProbe?: ProcessIdentityProbe }> = {}
97
+ ): boolean {
94
98
  const filePath = path.join(path.dirname(manifest.publicStatusDir), 'lease.json');
95
99
  if (!fs.existsSync(filePath)) return false;
96
100
  const raw = fs.readFileSync(filePath, 'utf8');
@@ -104,10 +108,12 @@ export function cleanupStaleSandboxControlLease(manifest: SandboxControlManifest
104
108
  || typeof lease.branch !== 'string' || typeof lease.reason !== 'string') {
105
109
  throw new Error('SANDBOX_CONTROL_LEASE_INVALID');
106
110
  }
107
- const stale = lease.generation !== manifest.generation || lease.taskId !== manifest.taskId
108
- || lease.branch !== manifest.branch || lease.expiresAt! <= now || !processIdentityMatches({
111
+ const ownerState = (options.identityProbe ?? getProcessIdentityState)({
109
112
  pid: lease.owner.pid!, startTime: lease.owner.startTime!
110
113
  });
114
+ if (ownerState === 'unknown') throw new Error('SANDBOX_CONTROL_LEASE_OWNER_UNAVAILABLE');
115
+ const stale = lease.generation !== manifest.generation || lease.taskId !== manifest.taskId
116
+ || lease.branch !== manifest.branch || lease.expiresAt! <= now || ownerState === 'dead';
111
117
  if (!stale) return false;
112
118
  if (fs.readFileSync(filePath, 'utf8') !== raw) throw new Error('SANDBOX_CONTROL_LEASE_OWNERSHIP_LOST');
113
119
  fs.unlinkSync(filePath);
@@ -115,7 +121,7 @@ export function cleanupStaleSandboxControlLease(manifest: SandboxControlManifest
115
121
  }
116
122
 
117
123
  export function appendSandboxControlAudit(
118
- manifest: SandboxControlManifest,
124
+ manifest: SandboxControlManifestLike,
119
125
  event: string,
120
126
  fields: Record<string, string | number | boolean | null> = {},
121
127
  now = Date.now()
@@ -130,7 +136,7 @@ export function appendSandboxControlAudit(
130
136
  });
131
137
  }
132
138
 
133
- export function executionPath(manifest: SandboxControlManifest, requestId: string): string {
139
+ export function executionPath(manifest: SandboxControlManifestLike, requestId: string): string {
134
140
  return path.join(manifest.processingDir, requestId, 'execution.json');
135
141
  }
136
142
 
@@ -153,31 +159,42 @@ export function terminateSandboxControlExecution(
153
159
  deadlineAt?: number;
154
160
  forceAt?: number;
155
161
  allowForce?: boolean;
162
+ identityProbe?: ProcessIdentityProbe;
156
163
  } = {}
157
164
  ): boolean {
158
165
  const platform = options.platform ?? process.platform;
159
166
  const processGroupId = execution.child.processGroupId;
160
- const processGroupAlive = (groupId: number): boolean => {
167
+ const processGroupState = (groupId: number): 'alive' | 'dead' | 'unknown' => {
161
168
  try {
162
169
  const rows = execFileSync('ps', ['-axo', 'pgid=,stat='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
163
170
  return rows.split('\n').some((row) => {
164
171
  const match = row.trim().match(/^(\d+)\s+(\S+)/);
165
172
  return Number(match?.[1]) === groupId && !match?.[2]?.startsWith('Z');
166
- });
173
+ }) ? 'alive' : 'dead';
167
174
  } catch {
168
175
  try {
169
176
  process.kill(-groupId, 0);
170
- return true;
177
+ return 'alive';
171
178
  } catch (error) {
172
- return (error as NodeJS.ErrnoException).code === 'EPERM';
179
+ const code = (error as NodeJS.ErrnoException).code;
180
+ if (code === 'ESRCH') return 'dead';
181
+ if (code === 'EPERM') return 'alive';
182
+ return 'unknown';
173
183
  }
174
184
  }
175
185
  };
176
- const treeAlive = (): boolean => {
186
+ const treeState = (): 'alive' | 'dead' | 'unknown' => {
177
187
  if (platform === 'win32' || !processGroupId) {
178
- return processIdentityMatches({ pid: execution.child.pid, startTime: execution.child.startTime });
188
+ return (options.identityProbe ?? getProcessIdentityState)({
189
+ pid: execution.child.pid, startTime: execution.child.startTime
190
+ });
179
191
  }
180
- return processGroupAlive(processGroupId);
192
+ return processGroupState(processGroupId);
193
+ };
194
+ const treeAlive = (): boolean => {
195
+ const state = treeState();
196
+ if (state === 'unknown') throw new Error('SANDBOX_CONTROL_EXECUTION_OWNER_UNAVAILABLE');
197
+ return state === 'alive';
181
198
  };
182
199
  if (!treeAlive()) return true;
183
200
  const command = buildProcessTreeStopCommand(processGroupId ?? execution.child.pid, platform);
@@ -18,10 +18,20 @@ const PLATFORM_DEFAULTS = Object.freeze({
18
18
  win32: ENGINES.WSL2
19
19
  });
20
20
 
21
+ export type SandboxEngineInput = string | Record<string, string | null | undefined> | null | undefined;
22
+
21
23
  type EngineConfig = EffectiveSandboxConfig & {
22
- engine?: string | null;
24
+ engine?: SandboxEngineInput;
23
25
  };
24
26
 
27
+ function isPerPlatformEngine(engine: SandboxEngineInput): engine is Record<string, string | null | undefined> {
28
+ return typeof engine === 'object' && engine !== null;
29
+ }
30
+
31
+ function resolveEngineForPlatform(engine: SandboxEngineInput, os: string): string | null | undefined {
32
+ return isPerPlatformEngine(engine) ? engine[os] : engine;
33
+ }
34
+
25
35
  type EngineDependencies = {
26
36
  platformFn?: typeof platform;
27
37
  runFn?: RunFns['run'];
@@ -83,33 +93,34 @@ function ensureBuildKit(engine: string, runOkEngineFn: typeof runOkEngine): void
83
93
  }
84
94
 
85
95
  export function validateSandboxEngine(
86
- engine: string | null | undefined,
96
+ engine: SandboxEngineInput,
87
97
  { platformFn = platform }: Pick<EngineDependencies, 'platformFn'> = {}
88
98
  ): string | null {
89
- if (engine === null || engine === undefined) {
99
+ const os = platformFn();
100
+ const resolved = resolveEngineForPlatform(engine, os);
101
+ if (resolved === null || resolved === undefined) {
90
102
  return null;
91
103
  }
92
104
 
93
- const os = platformFn();
94
- if (!(engine in ADAPTERS)) {
105
+ if (!(resolved in ADAPTERS)) {
95
106
  const known = Object.keys(ADAPTERS).join(', ');
96
107
  throw new Error(
97
- `sandbox: invalid "sandbox.engine" value "${engine}" (unknown sandbox engine). `
108
+ `sandbox: invalid "sandbox.engine" value "${resolved}" (unknown sandbox engine). `
98
109
  + `Valid engines: ${known}.`
99
110
  );
100
111
  }
101
112
 
102
- const adapter = ADAPTERS[engine as keyof typeof ADAPTERS];
113
+ const adapter = ADAPTERS[resolved as keyof typeof ADAPTERS];
103
114
  if (!adapter.supportedPlatforms.includes(os)) {
104
115
  const supported = enginesForPlatform(os);
105
116
  const supportedList = supported.length > 0 ? supported.join(', ') : 'none';
106
117
  throw new Error(
107
- `sandbox: "sandbox.engine" value "${engine}" is not supported on ${os}. `
118
+ `sandbox: "sandbox.engine" value "${resolved}" is not supported on ${os}. `
108
119
  + `Supported engines on ${os}: ${supportedList}.`
109
120
  );
110
121
  }
111
122
 
112
- return engine;
123
+ return resolved;
113
124
  }
114
125
 
115
126
  export function detectEngine(
@@ -15,9 +15,11 @@ Commands:
15
15
  rebuild [--quiet] [--refresh]
16
16
  Rebuild the sandbox image (--refresh pulls base + tools)
17
17
  refresh Sync host Claude Code credentials to all sandbox copies
18
- rm <branch> | --unbound | --purge
19
- Remove one sandbox, all sandboxes not bound to an
20
- active task (--unbound), or tear down everything (--purge)
18
+ rm <branch | TASK-id> | --unbound | --purge
19
+ Remove one sandbox; use a full TASK-id for task-bound
20
+ cleanup and a branch for branch-only cleanup. --unbound
21
+ removes completed task-bound and branch-only sandboxes;
22
+ active, blocked, and archive tasks are protected.
21
23
  start [--recreate] <branch | TASK-id | N>
22
24
  Verify or recover an existing sandbox container;
23
25
  optionally replace only the container on failure
@@ -36,6 +36,11 @@ export function removeWorktreeDir(
36
36
  throw new Error(`Worktree permit target mismatch: ${dir}`);
37
37
  }
38
38
  verifyWorktreePermit(permit);
39
+ if (permit.snapshot.source === 'recovered') {
40
+ removeManagedDir(worktreeBase, dir);
41
+ runSafeFn('git', ['-C', repoRoot, 'worktree', 'prune']);
42
+ return;
43
+ }
39
44
  try {
40
45
  runFn('git', ['-C', repoRoot, 'worktree', 'remove', dir, '--force']);
41
46
  } catch (error) {
@@ -6,6 +6,7 @@ import {
6
6
  worktreeDirCandidates
7
7
  } from './constants.ts';
8
8
  import type { SandboxConfig } from './config.ts';
9
+ import { sandboxWorkspaceViewStatePaths } from './workspace-view.ts';
9
10
 
10
11
  export type SandboxBindMountDeclaration = {
11
12
  hostPaths: string[];
@@ -25,11 +26,25 @@ export function sandboxCoreBindMounts(
25
26
  workspaceViewRoot: string;
26
27
  controlDir: string;
27
28
  controlStatusDir: string;
29
+ runtimeDir?: string;
28
30
  taskSource?: string;
29
31
  taskId?: string;
30
32
  }
31
33
  ): SandboxBindMountDeclaration[] {
32
34
  const taskBound = Boolean(overrides.taskSource && overrides.taskId);
35
+ const workspaceMounts = sandboxWorkspaceViewStatePaths(overrides.workspaceViewRoot).map(
36
+ ({ state, hostPath }) => taskBound && state === 'active'
37
+ ? {
38
+ hostPaths: [path.join(hostPath, '.short-ids.json')],
39
+ containerPath: '/workspace/.agents/workspace/active/.short-ids.json',
40
+ readOnly: true
41
+ }
42
+ : {
43
+ hostPaths: [hostPath],
44
+ containerPath: path.posix.join('/workspace/.agents/workspace', state),
45
+ readOnly: true
46
+ }
47
+ );
33
48
  const mounts: SandboxBindMountDeclaration[] = [
34
49
  {
35
50
  hostPaths: overrides.worktree
@@ -38,11 +53,7 @@ export function sandboxCoreBindMounts(
38
53
  containerPath: '/workspace',
39
54
  readOnly: false
40
55
  },
41
- {
42
- hostPaths: [overrides.workspaceViewRoot],
43
- containerPath: '/workspace/.agents/workspace',
44
- readOnly: true
45
- },
56
+ ...workspaceMounts,
46
57
  ];
47
58
  if (taskBound) {
48
59
  mounts.push({
@@ -79,5 +90,12 @@ export function sandboxCoreBindMounts(
79
90
  containerPath: '/run/agent-infra/control',
80
91
  readOnly: false
81
92
  });
93
+ if (taskBound && overrides.runtimeDir) {
94
+ mounts.push({
95
+ hostPaths: [overrides.runtimeDir],
96
+ containerPath: '/run/agent-infra/runtime',
97
+ readOnly: false
98
+ });
99
+ }
82
100
  return mounts;
83
101
  }
@@ -49,7 +49,7 @@ import {
49
49
  startSandboxContainer,
50
50
  type SandboxRow
51
51
  } from './commands/list-running.ts';
52
- import { getProcessStartTime, processIdentityMatches, removePidFileIfMatches } from '../server/process-state.ts';
52
+ import { getProcessIdentityState, getProcessStartTime, removePidFileIfMatches } from '../server/process-state.ts';
53
53
  import {
54
54
  acquireSandboxControlBrokerStartup,
55
55
  garbageCollectSandboxControlRoot,
@@ -93,6 +93,9 @@ export type SandboxRecoverySnapshot = {
93
93
  actualBranch: string | null;
94
94
  expectedWorkspace: SandboxWorkspaceIdentity;
95
95
  actualWorkspace: SandboxContainerWorkspaceIdentity;
96
+ workspaceTopology?: 'legacy-parent' | 'per-state' | 'unknown';
97
+ taskView?: { path: string; readable: boolean };
98
+ runtimeStoreOk?: boolean;
96
99
  runtimeCapabilityOk?: boolean;
97
100
  unexpectedCapabilityMounts?: string[];
98
101
  mounts: Array<{
@@ -251,7 +254,9 @@ export async function startSandboxControlBroker(repoRoot: string, manifestPath:
251
254
  if (broker === null || broker.version !== 3 || owner === null) {
252
255
  throw new Error('SANDBOX_CONTROL_BROKER_VERSION_INVALID: rebuild the sandbox container before starting a new broker');
253
256
  }
254
- const live = owner !== null && processIdentityMatches({ pid: owner.pid, startTime: owner.startTime });
257
+ const ownerState = getProcessIdentityState({ pid: owner.pid, startTime: owner.startTime });
258
+ if (ownerState === 'unknown') throw new Error('SANDBOX_CONTROL_BROKER_OWNER_UNAVAILABLE');
259
+ const live = ownerState === 'alive';
255
260
  if (
256
261
  live
257
262
  && owner !== null
@@ -349,6 +354,14 @@ async function ensureSandboxControlBroker(params: {
349
354
  token?: unknown;
350
355
  generation?: unknown;
351
356
  };
357
+ const validIdentity = typeof broker.pid === 'number'
358
+ && Number.isSafeInteger(broker.pid)
359
+ && typeof broker.startTime === 'number'
360
+ && Number.isSafeInteger(broker.startTime);
361
+ const ownerState = validIdentity
362
+ ? getProcessIdentityState({ pid: broker.pid as number, startTime: broker.startTime as number })
363
+ : 'dead';
364
+ if (ownerState === 'unknown') throw new Error('SANDBOX_CONTROL_BROKER_OWNER_UNAVAILABLE');
352
365
  if (
353
366
  broker.version === 3
354
367
  && typeof broker.pid === 'number'
@@ -359,7 +372,7 @@ async function ensureSandboxControlBroker(params: {
359
372
  && broker.brokerId.length > 0
360
373
  && broker.token === validatedManifest.token
361
374
  && broker.generation === validatedManifest.generation
362
- && processIdentityMatches({ pid: broker.pid, startTime: broker.startTime })
375
+ && ownerState === 'alive'
363
376
  ) {
364
377
  try {
365
378
  const status = readSandboxControlStatus(control.statusDir);
@@ -379,7 +392,8 @@ async function ensureSandboxControlBroker(params: {
379
392
  });
380
393
  return;
381
394
  }
382
- } catch {
395
+ } catch (error) {
396
+ if (error instanceof Error && error.message === 'SANDBOX_CONTROL_BROKER_OWNER_UNAVAILABLE') throw error;
383
397
  // A missing, stale, or malformed broker record is replaced below.
384
398
  }
385
399
  await startSandboxControlBroker(params.config.repoRoot, control.manifestPath);
@@ -416,6 +430,30 @@ export function classifySandboxRecovery(snapshot: SandboxRecoverySnapshot): Sand
416
430
  message: 'Container runtime capability signature does not match the current sandbox plan.'
417
431
  });
418
432
  }
433
+ if (snapshot.workspaceTopology === 'legacy-parent') {
434
+ findings.push({
435
+ repairKind: 'hard-failure',
436
+ code: 'SANDBOX_WORKSPACE_TOPOLOGY_MISMATCH',
437
+ message: 'Sandbox uses the legacy workspace parent-mount topology; recreate the container explicitly to migrate it.',
438
+ path: '/workspace/.agents/workspace'
439
+ });
440
+ }
441
+ if (snapshot.taskView && !snapshot.taskView.readable) {
442
+ findings.push({
443
+ repairKind: 'hard-failure',
444
+ code: 'SANDBOX_TASK_VIEW_UNREADABLE',
445
+ message: `Bound task view is not readable at ${snapshot.taskView.path}.`,
446
+ path: snapshot.taskView.path
447
+ });
448
+ }
449
+ if (snapshot.runtimeStoreOk === false) {
450
+ findings.push({
451
+ repairKind: 'hard-failure',
452
+ code: 'SANDBOX_RUNTIME_STORE_UNAVAILABLE',
453
+ message: 'Task-bound runtime store is not writable and readable by devuser.',
454
+ path: '/run/agent-infra/runtime'
455
+ });
456
+ }
419
457
  for (const mountPath of snapshot.unexpectedCapabilityMounts ?? []) {
420
458
  findings.push({
421
459
  repairKind: 'hard-failure',
@@ -425,6 +463,10 @@ export function classifySandboxRecovery(snapshot: SandboxRecoverySnapshot): Sand
425
463
  }
426
464
 
427
465
  for (const mount of snapshot.mounts) {
466
+ if (
467
+ snapshot.workspaceTopology === 'legacy-parent'
468
+ && mount.path.startsWith('/workspace/.agents/workspace')
469
+ ) continue;
428
470
  if (
429
471
  mount.actualType !== mount.expectedType
430
472
  || !mount.sourceMatches
@@ -517,6 +559,20 @@ function inspectContainer(
517
559
  return parsed[0] as DockerInspection;
518
560
  }
519
561
 
562
+ function workspaceTopology(mountsByDestination: ReadonlyMap<string, DockerMount>): SandboxRecoverySnapshot['workspaceTopology'] {
563
+ if (mountsByDestination.has('/workspace/.agents/workspace')) return 'legacy-parent';
564
+ const perState = [
565
+ '/workspace/.agents/workspace/active',
566
+ '/workspace/.agents/workspace/active/.short-ids.json',
567
+ '/workspace/.agents/workspace/completed',
568
+ '/workspace/.agents/workspace/blocked',
569
+ '/workspace/.agents/workspace/archive'
570
+ ];
571
+ return perState.some((destination) => mountsByDestination.has(destination))
572
+ ? 'per-state'
573
+ : 'unknown';
574
+ }
575
+
520
576
  function probe(
521
577
  engine: string,
522
578
  container: string,
@@ -596,6 +652,7 @@ function expectedMounts(params: {
596
652
  controlStatusDir: control.statusDir,
597
653
  ...(params.workspace.mode === 'task-bound'
598
654
  ? {
655
+ runtimeDir: control.runtimeDir,
599
656
  taskSource: assertSandboxTaskSource(config.repoRoot, params.workspace.taskId),
600
657
  taskId: params.workspace.taskId
601
658
  }
@@ -729,6 +786,10 @@ export function collectSandboxRecoverySnapshot(params: {
729
786
  const capabilityPlan = createSandboxCapabilityPlan(params.config);
730
787
  const workspace = params.workspace ?? { mode: 'branch-only' as const };
731
788
  const tools = [...capabilityPlan.tools];
789
+ const topology = workspaceTopology(mountsByDestination);
790
+ const taskViewPath = workspace.mode === 'task-bound'
791
+ ? `/workspace/.agents/workspace/active/${workspace.taskId}/task.md`
792
+ : null;
732
793
  const seeds = declaredTmpfsSeedEntries(tools).map((seed) => {
733
794
  const mounted = mountsByDestination.get(seed.stagingPath)?.Type === 'bind';
734
795
  return {
@@ -759,6 +820,15 @@ export function collectSandboxRecoverySnapshot(params: {
759
820
  runOkFn
760
821
  )
761
822
  }));
823
+ const runtimeStoreOk = workspace.mode === 'task-bound'
824
+ ? probe(
825
+ params.engine,
826
+ params.container,
827
+ 'probe="$1/.agent-infra-runtime-$$"; trap \'rm -f -- "$probe"\' EXIT; printf runtime-probe > "$probe" && test "$(cat -- "$probe")" = runtime-probe',
828
+ ['/run/agent-infra/runtime'],
829
+ runOkFn
830
+ )
831
+ : undefined;
762
832
  const labels = inspection.Config?.Labels ?? {};
763
833
  const branchLabel = labels[sandboxBranchLabel(params.config)];
764
834
  const containerWorkspace = parseSandboxWorkspaceIdentity(labels, {
@@ -820,6 +890,20 @@ export function collectSandboxRecoverySnapshot(params: {
820
890
  actualBranch: typeof branchLabel === 'string' ? branchLabel : null,
821
891
  expectedWorkspace: workspace,
822
892
  actualWorkspace: containerWorkspace,
893
+ workspaceTopology: topology,
894
+ ...(taskViewPath === null ? {} : {
895
+ taskView: {
896
+ path: taskViewPath,
897
+ readable: probe(
898
+ params.engine,
899
+ params.container,
900
+ 'test -r "$1"',
901
+ [taskViewPath],
902
+ runOkFn
903
+ )
904
+ }
905
+ }),
906
+ ...(runtimeStoreOk === undefined ? {} : { runtimeStoreOk }),
823
907
  identityOk: typeof inspection.Id === 'string' && inspection.Id.length > 0
824
908
  && branchLabel === params.branch
825
909
  && sameSandboxWorkspaceIdentity(containerWorkspace, workspace),
@@ -996,11 +1080,13 @@ function assess(params: {
996
1080
  function canRetryFreshReadiness(findings: SandboxRecoveryFinding[]): boolean {
997
1081
  return findings.length > 0 && findings.every((finding) =>
998
1082
  finding.repairKind === 'hard-failure'
1083
+ && finding.code !== 'SANDBOX_WORKSPACE_TOPOLOGY_MISMATCH'
999
1084
  && finding.path !== undefined
1000
1085
  && (
1001
1086
  finding.path.startsWith('/workspace/.agents/workspace')
1002
1087
  || finding.path === '/home/devuser/.host-shell-config'
1003
1088
  || finding.path === '/home/devuser/.bash_aliases'
1089
+ || finding.path === '/run/agent-infra/runtime'
1004
1090
  )
1005
1091
  );
1006
1092
  }
@@ -1236,14 +1322,9 @@ export async function ensureSandboxReady(params: EnsureSandboxReadyParams): Prom
1236
1322
  : `Sandbox recovery failed in place. Replacing only the container. ${dataBoundary}`;
1237
1323
  warnings.push(warning);
1238
1324
  (params.writeWarning ?? ((message) => process.stderr.write(`${message}\n`)))(warning);
1239
- const runVerboseFn = deps?.runVerbose ?? runVerboseEngine;
1240
1325
  let replacementResult: SandboxReadyResult | null = null;
1241
1326
  let replacementFailure: unknown = null;
1242
1327
  try {
1243
- if (params.row.running) {
1244
- runVerboseFn(params.engine, 'docker', ['stop', params.row.name]);
1245
- }
1246
- runVerboseFn(params.engine, 'docker', ['rm', params.row.name]);
1247
1328
  await params.recreate(params.branch);
1248
1329
 
1249
1330
  const rows = (deps?.fetchRows ?? fetchSandboxRows)(