@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.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/bin/internal-cli.js +11 -1
- package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +60 -22
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +175 -11
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +147 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +86 -69
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
- package/dist/lib/decide.js +68 -14
- package/dist/lib/git/workflow.js +10 -1
- package/dist/lib/init.js +1 -1
- package/dist/lib/internal/codex-lifecycle.js +19 -5
- package/dist/lib/internal/codex-sandbox-controller.js +6 -13
- package/dist/lib/internal/git-workflow.js +5 -7
- package/dist/lib/internal/platform-pr.js +20 -7
- package/dist/lib/internal/release-workflow.js +3 -4
- package/dist/lib/internal/sandbox-control.js +79 -2
- package/dist/lib/internal/task-activity.js +57 -7
- package/dist/lib/internal/task-event.js +57 -4
- package/dist/lib/internal/task-finalization.js +54 -0
- package/dist/lib/internal/task-ledger.js +59 -5
- package/dist/lib/internal/task-lifecycle.js +59 -8
- package/dist/lib/internal/task-orchestration.js +161 -288
- package/dist/lib/internal/task-override.js +131 -0
- package/dist/lib/internal/task-review.js +64 -6
- package/dist/lib/internal/task-verify.js +65 -3
- package/dist/lib/internal/task-warning.js +57 -4
- package/dist/lib/platform/pr-summary.js +53 -13
- package/dist/lib/platform/pull-requests.js +205 -41
- package/dist/lib/runtime/agent-runtime.js +56 -0
- package/dist/lib/sandbox/commands/create.js +389 -311
- package/dist/lib/sandbox/commands/rm.js +316 -55
- package/dist/lib/sandbox/control/client.js +190 -6
- package/dist/lib/sandbox/control/container-identity.js +1 -1
- package/dist/lib/sandbox/control/controller-registration.js +273 -0
- package/dist/lib/sandbox/control/executor.js +145 -5
- package/dist/lib/sandbox/control/lifecycle.js +395 -73
- package/dist/lib/sandbox/control/protocol.js +67 -6
- package/dist/lib/sandbox/control/state.js +27 -11
- package/dist/lib/sandbox/engine.js +14 -7
- package/dist/lib/sandbox/index.js +5 -3
- package/dist/lib/sandbox/managed-fs.js +5 -0
- package/dist/lib/sandbox/mounts.js +20 -5
- package/dist/lib/sandbox/recovery.js +78 -10
- package/dist/lib/sandbox/task-resolver.js +50 -6
- package/dist/lib/sandbox/workspace-identity.js +27 -0
- package/dist/lib/sandbox/workspace-view.js +62 -40
- package/dist/lib/sandbox/worktree-safety.js +215 -2
- package/dist/lib/server/process-state.js +51 -1
- package/dist/lib/task/activity-intent.js +4 -1
- package/dist/lib/task/codex-orchestration.js +158 -33
- package/dist/lib/task/commands/status.js +59 -1
- package/dist/lib/task/commit-operation.js +335 -0
- package/dist/lib/task/commit-policy.js +18 -0
- package/dist/lib/task/decision-intents.js +4 -2
- package/dist/lib/task/delegation-receipts.js +279 -17
- package/dist/lib/task/events.js +9 -5
- package/dist/lib/task/finalization.js +567 -0
- package/dist/lib/task/guard-override.js +31 -0
- package/dist/lib/task/human-override.js +1240 -0
- package/dist/lib/task/ledger-intents.js +6 -3
- package/dist/lib/task/lifecycle.js +69 -14
- package/dist/lib/task/operation-outcome.js +33 -0
- package/dist/lib/task/orchestration.js +199 -719
- package/dist/lib/task/review-finalization.js +3 -2
- package/dist/lib/task/short-id.js +59 -1
- package/dist/lib/task/task-execution-lock.js +6 -1
- package/dist/lib/task/verification-engine.js +138 -59
- package/dist/lib/task/verification.js +15 -2
- package/dist/lib/task/workflow-warning-intents.js +54 -3
- package/dist/lib/task/write.js +2 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/claude-code.ts +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +63 -20
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +278 -13
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +112 -93
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
- package/lib/decide.ts +59 -14
- package/lib/git/workflow.ts +11 -2
- package/lib/init.ts +1 -1
- package/lib/internal/codex-lifecycle.ts +18 -5
- package/lib/internal/codex-sandbox-controller.ts +6 -12
- package/lib/internal/git-workflow.ts +5 -6
- package/lib/internal/platform-pr.ts +14 -7
- package/lib/internal/release-workflow.ts +3 -4
- package/lib/internal/sandbox-control.ts +92 -2
- package/lib/internal/task-activity.ts +46 -6
- package/lib/internal/task-event.ts +53 -4
- package/lib/internal/task-finalization.ts +68 -0
- package/lib/internal/task-ledger.ts +48 -4
- package/lib/internal/task-lifecycle.ts +56 -12
- package/lib/internal/task-orchestration.ts +32 -107
- package/lib/internal/task-override.ts +138 -0
- package/lib/internal/task-review.ts +51 -6
- package/lib/internal/task-verify.ts +52 -3
- package/lib/internal/task-warning.ts +44 -3
- package/lib/platform/pr-summary.ts +65 -14
- package/lib/platform/pull-requests.ts +211 -41
- package/lib/runtime/agent-runtime.ts +76 -0
- package/lib/sandbox/commands/create.ts +88 -4
- package/lib/sandbox/commands/rm.ts +413 -61
- package/lib/sandbox/config.ts +2 -1
- package/lib/sandbox/control/client.ts +255 -6
- package/lib/sandbox/control/container-identity.ts +3 -3
- package/lib/sandbox/control/controller-registration.ts +347 -0
- package/lib/sandbox/control/executor.ts +172 -6
- package/lib/sandbox/control/lifecycle.ts +465 -77
- package/lib/sandbox/control/protocol.ts +85 -9
- package/lib/sandbox/control/state.ts +35 -18
- package/lib/sandbox/engine.ts +20 -9
- package/lib/sandbox/index.ts +5 -3
- package/lib/sandbox/managed-fs.ts +5 -0
- package/lib/sandbox/mounts.ts +23 -5
- package/lib/sandbox/recovery.ts +90 -9
- package/lib/sandbox/task-resolver.ts +58 -7
- package/lib/sandbox/workspace-identity.ts +50 -2
- package/lib/sandbox/workspace-view.ts +73 -43
- package/lib/sandbox/worktree-safety.ts +236 -2
- package/lib/server/process-state.ts +48 -1
- package/lib/task/activity-intent.ts +4 -2
- package/lib/task/codex-orchestration.ts +167 -33
- package/lib/task/commands/status.ts +78 -1
- package/lib/task/commit-operation.ts +430 -0
- package/lib/task/commit-policy.ts +30 -0
- package/lib/task/decision-intents.ts +4 -2
- package/lib/task/delegation-receipts.ts +304 -19
- package/lib/task/events.ts +10 -5
- package/lib/task/finalization.ts +719 -0
- package/lib/task/guard-override.ts +50 -0
- package/lib/task/human-override.ts +1503 -0
- package/lib/task/ledger-intents.ts +6 -3
- package/lib/task/lifecycle.ts +71 -14
- package/lib/task/operation-outcome.ts +48 -0
- package/lib/task/orchestration.ts +239 -922
- package/lib/task/review-finalization.ts +6 -2
- package/lib/task/short-id.ts +65 -1
- package/lib/task/task-execution-lock.ts +10 -1
- package/lib/task/verification-engine.ts +145 -64
- package/lib/task/verification.ts +18 -3
- package/lib/task/workflow-warning-intents.ts +61 -4
- package/lib/task/write.ts +4 -1
- package/package.json +2 -2
- package/templates/.agents/QUICKSTART.en.md +2 -2
- package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
- package/templates/.agents/hooks/lifecycle-delegation.js +9 -24
- package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -3
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -3
- package/templates/.agents/rules/task-management.en.md +2 -2
- package/templates/.agents/rules/task-management.zh-CN.md +2 -2
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
- package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/commit/SKILL.en.md +41 -125
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
- package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
- package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
- package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
- package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -1
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
- package/templates/.codex/hooks.json +4 -4
- package/dist/lib/task/commit-finalization.js +0 -305
- package/dist/lib/task/commit-intent.js +0 -165
- package/lib/task/commit-finalization.ts +0 -419
- 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
|
|
31
|
-
|
|
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:
|
|
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
|
|
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 !==
|
|
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
|
-
|
|
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'
|
|
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 {
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
177
|
+
return 'alive';
|
|
171
178
|
} catch (error) {
|
|
172
|
-
|
|
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
|
|
186
|
+
const treeState = (): 'alive' | 'dead' | 'unknown' => {
|
|
177
187
|
if (platform === 'win32' || !processGroupId) {
|
|
178
|
-
return
|
|
188
|
+
return (options.identityProbe ?? getProcessIdentityState)({
|
|
189
|
+
pid: execution.child.pid, startTime: execution.child.startTime
|
|
190
|
+
});
|
|
179
191
|
}
|
|
180
|
-
return
|
|
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);
|
package/lib/sandbox/engine.ts
CHANGED
|
@@ -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?:
|
|
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:
|
|
96
|
+
engine: SandboxEngineInput,
|
|
87
97
|
{ platformFn = platform }: Pick<EngineDependencies, 'platformFn'> = {}
|
|
88
98
|
): string | null {
|
|
89
|
-
|
|
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
|
-
|
|
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 "${
|
|
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[
|
|
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 "${
|
|
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
|
|
123
|
+
return resolved;
|
|
113
124
|
}
|
|
114
125
|
|
|
115
126
|
export function detectEngine(
|
package/lib/sandbox/index.ts
CHANGED
|
@@ -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
|
|
20
|
-
|
|
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) {
|
package/lib/sandbox/mounts.ts
CHANGED
|
@@ -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
|
}
|
package/lib/sandbox/recovery.ts
CHANGED
|
@@ -49,7 +49,7 @@ import {
|
|
|
49
49
|
startSandboxContainer,
|
|
50
50
|
type SandboxRow
|
|
51
51
|
} from './commands/list-running.ts';
|
|
52
|
-
import {
|
|
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
|
|
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
|
-
&&
|
|
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)(
|