@fitlab-ai/agent-infra 0.9.5 → 0.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/internal-cli.js +8 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +171 -20
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +68 -8
- package/dist/lib/agent-clients/adapters/codex.js +1 -1
- package/dist/lib/defaults.json +10 -2
- package/dist/lib/git/workflow.js +30 -4
- package/dist/lib/internal/codex-lifecycle.js +169 -3
- package/dist/lib/internal/codex-sandbox-controller.js +106 -0
- package/dist/lib/internal/release-workflow.js +108 -11
- package/dist/lib/internal/sandbox-control.js +43 -6
- package/dist/lib/internal/task-create.js +12 -4
- package/dist/lib/internal/task-orchestration.js +40 -7
- package/dist/lib/platform/context.js +6 -4
- package/dist/lib/platform/github-client.js +55 -2
- package/dist/lib/platform/issue-comments.js +2 -1
- package/dist/lib/process-data/checkpoint.js +319 -0
- package/dist/lib/process-data/index.js +128 -60
- package/dist/lib/process-data/materialize.js +84 -0
- package/dist/lib/process-data/normalize.js +34 -1
- package/dist/lib/process-data/reconcile.js +55 -1
- package/dist/lib/process-data/sources.js +422 -1
- package/dist/lib/process-data/store.js +341 -11
- package/dist/lib/release/workflow.js +29 -21
- package/dist/lib/sandbox/commands/create.js +111 -60
- package/dist/lib/sandbox/commands/prune.js +40 -16
- package/dist/lib/sandbox/commands/rm.js +372 -94
- package/dist/lib/sandbox/commands/start.js +4 -3
- package/dist/lib/sandbox/control/client.js +138 -22
- package/dist/lib/sandbox/control/container-identity.js +64 -0
- package/dist/lib/sandbox/control/executor.js +142 -0
- package/dist/lib/sandbox/control/lifecycle.js +557 -0
- package/dist/lib/sandbox/control/protocol.js +69 -26
- package/dist/lib/sandbox/control/server.js +395 -99
- package/dist/lib/sandbox/control/state.js +191 -0
- package/dist/lib/sandbox/control/timing.js +8 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/managed-fs.js +14 -2
- package/dist/lib/sandbox/mounts.js +22 -19
- package/dist/lib/sandbox/recovery.js +284 -50
- package/dist/lib/sandbox/workspace-view.js +90 -10
- package/dist/lib/sandbox/worktree-safety.js +196 -0
- package/dist/lib/server/daemon.js +1 -1
- package/dist/lib/server/process-control.js +17 -1
- package/dist/lib/server/process-state.js +57 -20
- package/dist/lib/task/artifact-lifecycle.js +87 -15
- package/dist/lib/task/artifact-receipts.js +128 -0
- package/dist/lib/task/codex-orchestration.js +236 -0
- package/dist/lib/task/commands/status.js +3 -0
- package/dist/lib/task/commands/validate.js +289 -0
- package/dist/lib/task/commit-finalization.js +36 -9
- package/dist/lib/task/create.js +10 -2
- package/dist/lib/task/delegation-receipts.js +109 -2
- package/dist/lib/task/events.js +149 -6
- package/dist/lib/task/finalization-artifacts.js +2 -1
- package/dist/lib/task/implementation-inputs.js +1 -7
- package/dist/lib/task/index.js +9 -0
- package/dist/lib/task/ledger.js +13 -1
- package/dist/lib/task/orchestration.js +365 -15
- package/dist/lib/task/sections.js +5 -1
- package/dist/lib/task/task-execution-lock.js +5 -4
- package/dist/lib/task/verification-engine.js +78 -14
- package/dist/lib/task/verification.js +1 -0
- package/dist/lifecycle-build-manifest.json +6 -0
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +202 -18
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +83 -6
- package/lib/agent-clients/adapters/codex.ts +1 -1
- package/lib/defaults.json +10 -2
- package/lib/git/workflow.ts +25 -3
- package/lib/internal/codex-lifecycle.ts +175 -4
- package/lib/internal/codex-sandbox-controller.ts +109 -0
- package/lib/internal/release-workflow.ts +107 -11
- package/lib/internal/sandbox-control.ts +38 -6
- package/lib/internal/task-create.ts +16 -4
- package/lib/internal/task-orchestration.ts +34 -7
- package/lib/platform/context.ts +8 -5
- package/lib/platform/github-client.ts +61 -3
- package/lib/platform/issue-comments.ts +2 -1
- package/lib/process-data/checkpoint.ts +335 -0
- package/lib/process-data/index.ts +112 -60
- package/lib/process-data/materialize.ts +83 -0
- package/lib/process-data/normalize.ts +29 -1
- package/lib/process-data/reconcile.ts +64 -2
- package/lib/process-data/sources.ts +449 -2
- package/lib/process-data/store.ts +304 -11
- package/lib/process-data/types.ts +124 -2
- package/lib/release/workflow.ts +69 -20
- package/lib/sandbox/commands/create.ts +70 -25
- package/lib/sandbox/commands/prune.ts +53 -15
- package/lib/sandbox/commands/rm.ts +423 -108
- package/lib/sandbox/commands/start.ts +4 -3
- package/lib/sandbox/control/client.ts +152 -28
- package/lib/sandbox/control/container-identity.ts +93 -0
- package/lib/sandbox/control/executor.ts +167 -0
- package/lib/sandbox/control/lifecycle.ts +573 -0
- package/lib/sandbox/control/protocol.ts +121 -68
- package/lib/sandbox/control/server.ts +398 -109
- package/lib/sandbox/control/state.ts +213 -0
- package/lib/sandbox/control/timing.ts +12 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/managed-fs.ts +17 -2
- package/lib/sandbox/mounts.ts +16 -8
- package/lib/sandbox/recovery.ts +335 -65
- package/lib/sandbox/workspace-view.ts +102 -11
- package/lib/sandbox/worktree-safety.ts +234 -0
- package/lib/server/daemon.ts +1 -1
- package/lib/server/process-control.ts +24 -3
- package/lib/server/process-state.ts +82 -27
- package/lib/task/artifact-lifecycle.ts +78 -14
- package/lib/task/artifact-receipts.ts +161 -0
- package/lib/task/codex-orchestration.ts +319 -0
- package/lib/task/commands/status.ts +2 -0
- package/lib/task/commands/validate.ts +282 -0
- package/lib/task/commit-finalization.ts +52 -9
- package/lib/task/create.ts +11 -2
- package/lib/task/delegation-receipts.ts +208 -7
- package/lib/task/events.ts +131 -5
- package/lib/task/finalization-artifacts.ts +2 -1
- package/lib/task/implementation-inputs.ts +1 -5
- package/lib/task/index.ts +8 -0
- package/lib/task/ledger.ts +16 -1
- package/lib/task/orchestration.ts +552 -18
- package/lib/task/sections.ts +6 -2
- package/lib/task/task-execution-lock.ts +11 -9
- package/lib/task/verification-engine.ts +93 -24
- package/lib/task/verification.ts +2 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +7 -0
- package/templates/.agents/README.zh-CN.md +5 -0
- package/templates/.agents/hooks/lifecycle-delegation.js +72 -12
- package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
- package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
- package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.en.md +3 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +25 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +25 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +69 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +69 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -3
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -3
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
- package/templates/.claude/commands/run-manual-validation.en.md +9 -0
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
- package/templates/.codex/hooks.json +8 -8
- package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
- package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
- package/templates/.agents/workspace/README.en.md +0 -26
- package/templates/.agents/workspace/README.zh-CN.md +0 -26
package/lib/sandbox/recovery.ts
CHANGED
|
@@ -2,13 +2,15 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { nodeEntryArgs } from './control/executor.ts';
|
|
5
6
|
import {
|
|
6
7
|
containerNameCandidates,
|
|
7
8
|
sandboxBranchLabel,
|
|
8
9
|
sandboxLabel,
|
|
9
10
|
sandboxRuntimeCapabilityLabel,
|
|
10
11
|
sandboxTaskIdLabel,
|
|
11
|
-
sandboxWorkspaceModeLabel
|
|
12
|
+
sandboxWorkspaceModeLabel,
|
|
13
|
+
worktreeDirCandidates
|
|
12
14
|
} from './constants.ts';
|
|
13
15
|
import { isAgentClientId } from '../agent-clients/types.ts';
|
|
14
16
|
import {
|
|
@@ -29,9 +31,11 @@ import {
|
|
|
29
31
|
import {
|
|
30
32
|
parseSandboxWorkspaceIdentity,
|
|
31
33
|
sameSandboxWorkspaceIdentity,
|
|
34
|
+
type SandboxContainerWorkspaceIdentity,
|
|
32
35
|
type SandboxWorkspaceIdentity
|
|
33
36
|
} from './workspace-identity.ts';
|
|
34
|
-
import {
|
|
37
|
+
import { inspectWorktree, type WorktreeSnapshot } from './worktree-safety.ts';
|
|
38
|
+
import { commandForEngine, runEngine, runOkEngine, runProbe, runVerboseEngine } from './shell.ts';
|
|
35
39
|
import {
|
|
36
40
|
declaredTmpfsSeedEntries,
|
|
37
41
|
resolveTools,
|
|
@@ -45,10 +49,28 @@ import {
|
|
|
45
49
|
startSandboxContainer,
|
|
46
50
|
type SandboxRow
|
|
47
51
|
} from './commands/list-running.ts';
|
|
48
|
-
import { processIdentityMatches } from '../server/process-state.ts';
|
|
52
|
+
import { getProcessStartTime, processIdentityMatches, removePidFileIfMatches } from '../server/process-state.ts';
|
|
53
|
+
import {
|
|
54
|
+
acquireSandboxControlBrokerStartup,
|
|
55
|
+
garbageCollectSandboxControlRoot,
|
|
56
|
+
isSandboxControlRootQuiescing,
|
|
57
|
+
readSandboxControlManifest,
|
|
58
|
+
type BrokerOwner
|
|
59
|
+
} from './control/lifecycle.ts';
|
|
60
|
+
import {
|
|
61
|
+
appendSandboxControlAudit,
|
|
62
|
+
readSandboxControlStatus
|
|
63
|
+
} from './control/state.ts';
|
|
64
|
+
import { inspectSandboxControlContainer } from './control/container-identity.ts';
|
|
65
|
+
import {
|
|
66
|
+
SANDBOX_CONTROL_FUTURE_SKEW_MS,
|
|
67
|
+
SANDBOX_CONTROL_STATUS_STALE_MS,
|
|
68
|
+
type SandboxControlManifest
|
|
69
|
+
} from './control/protocol.ts';
|
|
49
70
|
|
|
50
71
|
export type SandboxRecoveryFinding = {
|
|
51
72
|
repairKind: 'permissions' | 'missing-seed' | 'builtin-link' | 'hard-failure';
|
|
73
|
+
code?: string;
|
|
52
74
|
message: string;
|
|
53
75
|
path?: string;
|
|
54
76
|
seed?: TmpfsSeedEntry;
|
|
@@ -66,6 +88,11 @@ type SandboxAgentClientCheckSnapshot = {
|
|
|
66
88
|
|
|
67
89
|
export type SandboxRecoverySnapshot = {
|
|
68
90
|
identityOk: boolean;
|
|
91
|
+
containerIdValid: boolean;
|
|
92
|
+
expectedBranch: string;
|
|
93
|
+
actualBranch: string | null;
|
|
94
|
+
expectedWorkspace: SandboxWorkspaceIdentity;
|
|
95
|
+
actualWorkspace: SandboxContainerWorkspaceIdentity;
|
|
69
96
|
runtimeCapabilityOk?: boolean;
|
|
70
97
|
unexpectedCapabilityMounts?: string[];
|
|
71
98
|
mounts: Array<{
|
|
@@ -118,8 +145,28 @@ type RecoveryCommandDeps = {
|
|
|
118
145
|
runVerbose?: typeof runVerboseEngine;
|
|
119
146
|
start?: typeof startSandboxContainer;
|
|
120
147
|
fetchRows?: typeof fetchSandboxRows;
|
|
148
|
+
ensureControlBroker?: () => void | Promise<void>;
|
|
149
|
+
probe?: typeof runProbe;
|
|
121
150
|
};
|
|
122
151
|
|
|
152
|
+
export function worktreeProbeForEngine(
|
|
153
|
+
engine: string,
|
|
154
|
+
probe: typeof runProbe = runProbe
|
|
155
|
+
): typeof runProbe {
|
|
156
|
+
return (cmd, args, opts = {}) => {
|
|
157
|
+
const engineArgs = [...args];
|
|
158
|
+
if (engine === 'wsl2' && cmd === 'git') {
|
|
159
|
+
for (let index = 0; index < engineArgs.length - 1; index += 1) {
|
|
160
|
+
if (engineArgs[index] === '-C') {
|
|
161
|
+
engineArgs[index + 1] = toEnginePath(engine, engineArgs[index + 1]!);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const command = commandForEngine(engine, cmd, engineArgs);
|
|
166
|
+
return probe(command.cmd, command.args, opts);
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
123
170
|
type EnsureSandboxReadyParams = {
|
|
124
171
|
config: SandboxConfig;
|
|
125
172
|
engine: string;
|
|
@@ -127,6 +174,7 @@ type EnsureSandboxReadyParams = {
|
|
|
127
174
|
workspace?: SandboxWorkspaceIdentity;
|
|
128
175
|
row: SandboxRow;
|
|
129
176
|
allowRecreate?: boolean;
|
|
177
|
+
forceRecreate?: boolean;
|
|
130
178
|
recreate?: (branch: string) => Promise<void>;
|
|
131
179
|
writeWarning?: (message: string) => void;
|
|
132
180
|
deps?: RecoveryCommandDeps;
|
|
@@ -139,11 +187,140 @@ type ExpectedMount = {
|
|
|
139
187
|
expectedRW: boolean;
|
|
140
188
|
};
|
|
141
189
|
|
|
142
|
-
function
|
|
190
|
+
async function waitForSandboxControlBrokerStatus(params: {
|
|
191
|
+
root: string;
|
|
192
|
+
statusDir: string;
|
|
193
|
+
generation: string;
|
|
194
|
+
pid: number;
|
|
195
|
+
startTime?: number;
|
|
196
|
+
}, timeoutMs = 5_000): Promise<void> {
|
|
197
|
+
const deadline = Date.now() + timeoutMs;
|
|
198
|
+
while (Date.now() < deadline) {
|
|
199
|
+
if (isSandboxControlRootQuiescing(params.root)) throw new Error('SANDBOX_CONTROL_QUIESCING');
|
|
200
|
+
try {
|
|
201
|
+
const status = readSandboxControlStatus(params.statusDir);
|
|
202
|
+
if (status.generation === params.generation && status.broker.pid === params.pid
|
|
203
|
+
&& (!params.startTime || status.broker.startTime === params.startTime)
|
|
204
|
+
&& Date.now() - status.updatedAt <= SANDBOX_CONTROL_STATUS_STALE_MS
|
|
205
|
+
&& status.updatedAt <= Date.now() + SANDBOX_CONTROL_FUTURE_SKEW_MS) return;
|
|
206
|
+
} catch {
|
|
207
|
+
// The broker may still be publishing its initial atomic status.
|
|
208
|
+
}
|
|
209
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
210
|
+
}
|
|
211
|
+
throw new Error('SANDBOX_CONTROL_BROKER_START_TIMEOUT');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function startSandboxControlBroker(repoRoot: string, manifestPath: string): Promise<void> {
|
|
215
|
+
const directory = path.dirname(fileURLToPath(import.meta.url));
|
|
216
|
+
const extension = path.extname(fileURLToPath(import.meta.url));
|
|
217
|
+
const internalCli = path.resolve(directory, '..', '..', 'bin', `internal-cli${extension}`);
|
|
218
|
+
const manifest = readSandboxControlManifest(manifestPath);
|
|
219
|
+
const root = path.dirname(manifestPath);
|
|
220
|
+
if (isSandboxControlRootQuiescing(root)) throw new Error('SANDBOX_CONTROL_QUIESCING');
|
|
221
|
+
const brokerPath = path.join(root, 'broker.json');
|
|
222
|
+
let brokerSnapshot: string | null = null;
|
|
223
|
+
let replacedBrokerRecord = false;
|
|
224
|
+
try {
|
|
225
|
+
brokerSnapshot = fs.readFileSync(brokerPath, 'utf8');
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
228
|
+
}
|
|
229
|
+
if (brokerSnapshot !== null) {
|
|
230
|
+
type ExistingBrokerRecord = {
|
|
231
|
+
version?: unknown;
|
|
232
|
+
pid?: unknown;
|
|
233
|
+
startTime?: unknown;
|
|
234
|
+
brokerId?: unknown;
|
|
235
|
+
token?: unknown;
|
|
236
|
+
generation?: unknown;
|
|
237
|
+
};
|
|
238
|
+
let broker: ExistingBrokerRecord | null = null;
|
|
239
|
+
try {
|
|
240
|
+
broker = JSON.parse(brokerSnapshot) as ExistingBrokerRecord;
|
|
241
|
+
} catch {
|
|
242
|
+
// Malformed owner records are stale and replaced below.
|
|
243
|
+
}
|
|
244
|
+
const owner = broker !== null && broker.version === 3 && typeof broker.pid === 'number'
|
|
245
|
+
&& Number.isSafeInteger(broker.pid) && broker.pid > 0 && typeof broker.startTime === 'number'
|
|
246
|
+
&& Number.isSafeInteger(broker.startTime) && typeof broker.brokerId === 'string'
|
|
247
|
+
&& broker.brokerId.length > 0 && typeof broker.token === 'string'
|
|
248
|
+
&& typeof broker.generation === 'string'
|
|
249
|
+
? broker as BrokerOwner
|
|
250
|
+
: null;
|
|
251
|
+
if (broker === null || broker.version !== 3 || owner === null) {
|
|
252
|
+
throw new Error('SANDBOX_CONTROL_BROKER_VERSION_INVALID: rebuild the sandbox container before starting a new broker');
|
|
253
|
+
}
|
|
254
|
+
const live = owner !== null && processIdentityMatches({ pid: owner.pid, startTime: owner.startTime });
|
|
255
|
+
if (
|
|
256
|
+
live
|
|
257
|
+
&& owner !== null
|
|
258
|
+
&& broker !== null
|
|
259
|
+
&& broker.version === 3
|
|
260
|
+
&& broker.token === manifest.token
|
|
261
|
+
&& broker.generation === manifest.generation
|
|
262
|
+
) {
|
|
263
|
+
await waitForSandboxControlBrokerStatus({
|
|
264
|
+
root,
|
|
265
|
+
statusDir: manifest.publicStatusDir,
|
|
266
|
+
generation: manifest.generation,
|
|
267
|
+
pid: owner.pid,
|
|
268
|
+
startTime: owner.startTime
|
|
269
|
+
});
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const startupStartTime = getProcessStartTime(process.pid);
|
|
274
|
+
if (!startupStartTime) throw new Error('SANDBOX_CONTROL_BROKER_IDENTITY_UNAVAILABLE');
|
|
275
|
+
const releaseStartup = await acquireSandboxControlBrokerStartup(root, {
|
|
276
|
+
pid: process.pid,
|
|
277
|
+
startTime: startupStartTime
|
|
278
|
+
});
|
|
279
|
+
let child: ReturnType<typeof spawn>;
|
|
280
|
+
try {
|
|
281
|
+
const currentBroker = fs.existsSync(brokerPath) ? fs.readFileSync(brokerPath, 'utf8') : null;
|
|
282
|
+
if (currentBroker !== brokerSnapshot) return startSandboxControlBroker(repoRoot, manifestPath);
|
|
283
|
+
if (brokerSnapshot !== null) {
|
|
284
|
+
replacedBrokerRecord = removePidFileIfMatches(brokerPath, brokerSnapshot);
|
|
285
|
+
if (!replacedBrokerRecord && fs.existsSync(brokerPath)) {
|
|
286
|
+
throw new Error('SANDBOX_CONTROL_BROKER_OWNER_TRANSITION');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (replacedBrokerRecord) {
|
|
290
|
+
appendSandboxControlAudit(manifest, 'broker-observed-crash');
|
|
291
|
+
appendSandboxControlAudit(manifest, 'broker-restart');
|
|
292
|
+
}
|
|
293
|
+
child = spawn(
|
|
294
|
+
process.execPath,
|
|
295
|
+
nodeEntryArgs(internalCli, ['sandbox-control', 'serve', '--manifest', manifestPath]),
|
|
296
|
+
{ cwd: repoRoot, detached: true, stdio: 'ignore' }
|
|
297
|
+
);
|
|
298
|
+
} finally {
|
|
299
|
+
releaseStartup();
|
|
300
|
+
}
|
|
301
|
+
let spawnError: Error | null = null;
|
|
302
|
+
child.once('error', (error) => { spawnError = error; });
|
|
303
|
+
child.unref();
|
|
304
|
+
if (!child.pid) throw new Error('SANDBOX_CONTROL_BROKER_START_FAILED');
|
|
305
|
+
try {
|
|
306
|
+
await waitForSandboxControlBrokerStatus({
|
|
307
|
+
root,
|
|
308
|
+
statusDir: manifest.publicStatusDir,
|
|
309
|
+
generation: manifest.generation,
|
|
310
|
+
pid: child.pid
|
|
311
|
+
});
|
|
312
|
+
} catch (error) {
|
|
313
|
+
child.kill('SIGTERM');
|
|
314
|
+
if (spawnError) throw spawnError;
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function ensureSandboxControlBroker(params: {
|
|
143
320
|
config: SandboxConfig;
|
|
144
321
|
container: string;
|
|
145
322
|
workspace: SandboxWorkspaceIdentity;
|
|
146
|
-
}): void {
|
|
323
|
+
}): Promise<void> {
|
|
147
324
|
const control = sandboxControlPaths({
|
|
148
325
|
base: params.config.controlBase ?? path.join(params.config.home, '.agent-infra', 'sandbox-control'),
|
|
149
326
|
project: params.config.project,
|
|
@@ -151,40 +328,61 @@ function ensureSandboxControlBroker(params: {
|
|
|
151
328
|
identity: params.workspace
|
|
152
329
|
});
|
|
153
330
|
if (!fs.existsSync(control.manifestPath)) return;
|
|
154
|
-
const
|
|
331
|
+
const validatedManifest = readSandboxControlManifest(control.manifestPath);
|
|
332
|
+
const containerObservation = await inspectSandboxControlContainer(validatedManifest);
|
|
333
|
+
if (containerObservation.state === 'unknown') {
|
|
334
|
+
throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${containerObservation.reason}`);
|
|
335
|
+
}
|
|
336
|
+
if (containerObservation.state === 'absent') {
|
|
337
|
+
await garbageCollectSandboxControlRoot(control.root, {
|
|
338
|
+
inspectContainer: (timeoutMs) => inspectSandboxControlContainer(validatedManifest, { timeoutMs })
|
|
339
|
+
});
|
|
340
|
+
throw new Error('SANDBOX_CONTROL_CONTAINER_ABSENT: recreate the sandbox container before broker recovery');
|
|
341
|
+
}
|
|
155
342
|
const brokerPath = path.join(control.root, 'broker.json');
|
|
156
343
|
try {
|
|
157
344
|
const broker = JSON.parse(fs.readFileSync(brokerPath, 'utf8')) as {
|
|
158
345
|
version?: unknown;
|
|
159
346
|
pid?: unknown;
|
|
160
347
|
startTime?: unknown;
|
|
348
|
+
brokerId?: unknown;
|
|
161
349
|
token?: unknown;
|
|
350
|
+
generation?: unknown;
|
|
162
351
|
};
|
|
163
352
|
if (
|
|
164
|
-
broker.version ===
|
|
353
|
+
broker.version === 3
|
|
165
354
|
&& typeof broker.pid === 'number'
|
|
166
|
-
&&
|
|
167
|
-
&& broker.
|
|
168
|
-
&&
|
|
169
|
-
|
|
355
|
+
&& Number.isSafeInteger(broker.pid)
|
|
356
|
+
&& typeof broker.startTime === 'number'
|
|
357
|
+
&& Number.isSafeInteger(broker.startTime)
|
|
358
|
+
&& typeof broker.brokerId === 'string'
|
|
359
|
+
&& broker.brokerId.length > 0
|
|
360
|
+
&& broker.token === validatedManifest.token
|
|
361
|
+
&& broker.generation === validatedManifest.generation
|
|
362
|
+
&& processIdentityMatches({ pid: broker.pid, startTime: broker.startTime })
|
|
363
|
+
) {
|
|
364
|
+
try {
|
|
365
|
+
const status = readSandboxControlStatus(control.statusDir);
|
|
366
|
+
if (status.generation === validatedManifest.generation && status.broker.pid === broker.pid
|
|
367
|
+
&& status.broker.startTime === broker.startTime
|
|
368
|
+
&& Date.now() - status.updatedAt <= SANDBOX_CONTROL_STATUS_STALE_MS
|
|
369
|
+
&& status.updatedAt <= Date.now() + SANDBOX_CONTROL_FUTURE_SKEW_MS) return;
|
|
370
|
+
} catch {
|
|
371
|
+
// A verified live owner may still be publishing its initial status.
|
|
372
|
+
}
|
|
373
|
+
await waitForSandboxControlBrokerStatus({
|
|
374
|
+
root: control.root,
|
|
375
|
+
statusDir: control.statusDir,
|
|
376
|
+
generation: validatedManifest.generation,
|
|
377
|
+
pid: broker.pid,
|
|
378
|
+
startTime: broker.startTime
|
|
379
|
+
});
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
170
382
|
} catch {
|
|
171
383
|
// A missing, stale, or malformed broker record is replaced below.
|
|
172
384
|
}
|
|
173
|
-
|
|
174
|
-
const directory = path.dirname(fileURLToPath(import.meta.url));
|
|
175
|
-
const extension = path.extname(fileURLToPath(import.meta.url));
|
|
176
|
-
const internalCli = path.resolve(directory, '..', '..', 'bin', `internal-cli${extension}`);
|
|
177
|
-
const child = spawn(
|
|
178
|
-
process.execPath,
|
|
179
|
-
[internalCli, 'sandbox-control', 'serve', '--manifest', control.manifestPath],
|
|
180
|
-
{ cwd: params.config.repoRoot, detached: true, stdio: 'ignore' }
|
|
181
|
-
);
|
|
182
|
-
child.once('error', () => {
|
|
183
|
-
// Readiness remains fail-closed; a later control request reports the
|
|
184
|
-
// unavailable broker without turning this detached spawn into an
|
|
185
|
-
// unhandled process error.
|
|
186
|
-
});
|
|
187
|
-
child.unref();
|
|
385
|
+
await startSandboxControlBroker(params.config.repoRoot, control.manifestPath);
|
|
188
386
|
}
|
|
189
387
|
|
|
190
388
|
function findingKey(finding: SandboxRecoveryFinding): string {
|
|
@@ -194,9 +392,22 @@ function findingKey(finding: SandboxRecoveryFinding): string {
|
|
|
194
392
|
export function classifySandboxRecovery(snapshot: SandboxRecoverySnapshot): SandboxRecoveryFinding[] {
|
|
195
393
|
const findings: SandboxRecoveryFinding[] = [];
|
|
196
394
|
if (!snapshot.identityOk) {
|
|
395
|
+
const branchOnly = snapshot.containerIdValid
|
|
396
|
+
&& snapshot.actualBranch === snapshot.expectedBranch
|
|
397
|
+
&& snapshot.expectedWorkspace.mode === 'task-bound'
|
|
398
|
+
&& snapshot.actualWorkspace.mode === 'branch-only';
|
|
399
|
+
const expectedIdentity = snapshot.expectedWorkspace.mode === 'task-bound'
|
|
400
|
+
? `task-bound:${snapshot.expectedWorkspace.taskId}`
|
|
401
|
+
: snapshot.expectedWorkspace.mode;
|
|
402
|
+
const actualIdentity = snapshot.actualWorkspace.mode === 'task-bound'
|
|
403
|
+
? `task-bound:${snapshot.actualWorkspace.taskId}`
|
|
404
|
+
: snapshot.actualWorkspace.mode;
|
|
197
405
|
findings.push({
|
|
198
406
|
repairKind: 'hard-failure',
|
|
199
|
-
|
|
407
|
+
code: branchOnly ? 'SANDBOX_CONTROL_BRANCH_ONLY' : 'SANDBOX_WORKSPACE_IDENTITY_CONFLICT',
|
|
408
|
+
message: branchOnly
|
|
409
|
+
? `Container is branch-only but the requested sandbox is ${expectedIdentity}.`
|
|
410
|
+
: `Container identity ${actualIdentity} on branch ${snapshot.actualBranch ?? 'unknown'} does not match requested ${expectedIdentity} on branch ${snapshot.expectedBranch}.`
|
|
200
411
|
});
|
|
201
412
|
}
|
|
202
413
|
if (snapshot.runtimeCapabilityOk === false) {
|
|
@@ -382,6 +593,7 @@ function expectedMounts(params: {
|
|
|
382
593
|
const core = sandboxCoreBindMounts(config, branch, {
|
|
383
594
|
workspaceViewRoot: view.root,
|
|
384
595
|
controlDir: control.channelDir,
|
|
596
|
+
controlStatusDir: control.statusDir,
|
|
385
597
|
...(params.workspace.mode === 'task-bound'
|
|
386
598
|
? {
|
|
387
599
|
taskSource: assertSandboxTaskSource(config.repoRoot, params.workspace.taskId),
|
|
@@ -603,8 +815,12 @@ export function collectSandboxRecoverySnapshot(params: {
|
|
|
603
815
|
});
|
|
604
816
|
|
|
605
817
|
return {
|
|
606
|
-
|
|
607
|
-
|
|
818
|
+
containerIdValid: typeof inspection.Id === 'string' && inspection.Id.length > 0,
|
|
819
|
+
expectedBranch: params.branch,
|
|
820
|
+
actualBranch: typeof branchLabel === 'string' ? branchLabel : null,
|
|
821
|
+
expectedWorkspace: workspace,
|
|
822
|
+
actualWorkspace: containerWorkspace,
|
|
823
|
+
identityOk: typeof inspection.Id === 'string' && inspection.Id.length > 0
|
|
608
824
|
&& branchLabel === params.branch
|
|
609
825
|
&& sameSandboxWorkspaceIdentity(containerWorkspace, workspace),
|
|
610
826
|
runtimeCapabilityOk: enforceCanonicalPlan
|
|
@@ -762,7 +978,7 @@ function repairFindings(params: {
|
|
|
762
978
|
}
|
|
763
979
|
|
|
764
980
|
function describeFindings(findings: SandboxRecoveryFinding[]): string {
|
|
765
|
-
return findings.map((finding) => finding.message).join(' ');
|
|
981
|
+
return findings.map((finding) => finding.code ? `${finding.code}: ${finding.message}` : finding.message).join(' ');
|
|
766
982
|
}
|
|
767
983
|
|
|
768
984
|
function assess(params: {
|
|
@@ -845,13 +1061,19 @@ export async function ensureSandboxReady(params: EnsureSandboxReadyParams): Prom
|
|
|
845
1061
|
const startFn = deps?.start ?? startSandboxContainer;
|
|
846
1062
|
const warnings: string[] = [];
|
|
847
1063
|
let failure: Error | null = null;
|
|
848
|
-
ensureSandboxControlBroker({
|
|
849
|
-
config: params.config,
|
|
850
|
-
container: params.row.name,
|
|
851
|
-
workspace: params.workspace ?? { mode: 'branch-only' }
|
|
852
|
-
});
|
|
853
|
-
|
|
854
1064
|
try {
|
|
1065
|
+
if (deps?.ensureControlBroker) {
|
|
1066
|
+
await deps.ensureControlBroker();
|
|
1067
|
+
} else {
|
|
1068
|
+
await ensureSandboxControlBroker({
|
|
1069
|
+
config: params.config,
|
|
1070
|
+
container: params.row.name,
|
|
1071
|
+
workspace: params.workspace ?? { mode: 'branch-only' }
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
if (params.forceRecreate) {
|
|
1075
|
+
throw new Error('Explicit container recreation requested.');
|
|
1076
|
+
}
|
|
855
1077
|
if (!params.row.running) {
|
|
856
1078
|
startFn(params.engine, params.row.name);
|
|
857
1079
|
const initial = assess({
|
|
@@ -975,42 +1197,90 @@ export async function ensureSandboxReady(params: EnsureSandboxReadyParams): Prom
|
|
|
975
1197
|
|
|
976
1198
|
const dataBoundary =
|
|
977
1199
|
'The existing container writable layer, ordinary /tmp data, processes, tmux sessions, and RAM state may be lost by replacement; worktree and host-managed sandbox data are preserved.';
|
|
1200
|
+
const recoveryTarget = params.workspace?.mode === 'task-bound'
|
|
1201
|
+
? params.workspace.taskId
|
|
1202
|
+
: params.branch;
|
|
978
1203
|
if (!params.allowRecreate || !params.recreate) {
|
|
979
|
-
throw new Error(
|
|
1204
|
+
throw new Error([
|
|
1205
|
+
`Sandbox recovery failed. Run 'ai sandbox start --recreate ${recoveryTarget}' on the host to authorize container-only replacement.`,
|
|
1206
|
+
dataBoundary,
|
|
1207
|
+
`Details: ${failure.message}`
|
|
1208
|
+
].join('\n'));
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
const existingWorktrees = worktreeDirCandidates(params.config, params.branch).filter((candidate) => fs.existsSync(candidate));
|
|
1212
|
+
if (existingWorktrees.length !== 1) {
|
|
1213
|
+
const remediation = existingWorktrees.length === 0
|
|
1214
|
+
? `Restore the worktree for '${params.branch}' before recovering the container.`
|
|
1215
|
+
: `Found ${existingWorktrees.map((candidate) => JSON.stringify(candidate)).join(', ')}; `
|
|
1216
|
+
+ 'keep the worktree in use, remove the stale directory, then retry.';
|
|
1217
|
+
throw new Error(
|
|
1218
|
+
`SANDBOX_RECOVERY_WORKTREE_SNAPSHOT_INVALID: expected exactly one existing worktree for '${params.branch}', found ${existingWorktrees.length}. ${remediation}`
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
const worktreeProbe = worktreeProbeForEngine(params.engine, deps?.probe);
|
|
1222
|
+
const beforeInspection = inspectWorktree(existingWorktrees[0]!, { probe: worktreeProbe });
|
|
1223
|
+
if (beforeInspection.status === 'failed') {
|
|
1224
|
+
throw new Error(`SANDBOX_RECOVERY_WORKTREE_SNAPSHOT_INVALID: ${beforeInspection.message}`);
|
|
1225
|
+
}
|
|
1226
|
+
const before: WorktreeSnapshot = beforeInspection.snapshot;
|
|
1227
|
+
if (before.branch !== params.branch) {
|
|
1228
|
+
throw new Error(
|
|
1229
|
+
`SANDBOX_RECOVERY_WORKTREE_SNAPSHOT_INVALID: expected branch '${params.branch}', found '${before.branch}'. `
|
|
1230
|
+
+ `Run 'git -C ${JSON.stringify(before.worktree)} checkout ${params.branch}' to restore the expected branch, then retry.`
|
|
1231
|
+
);
|
|
980
1232
|
}
|
|
981
1233
|
|
|
982
|
-
const warning =
|
|
1234
|
+
const warning = params.forceRecreate
|
|
1235
|
+
? `Explicit sandbox recreation requested. Replacing only the container. ${dataBoundary}`
|
|
1236
|
+
: `Sandbox recovery failed in place. Replacing only the container. ${dataBoundary}`;
|
|
983
1237
|
warnings.push(warning);
|
|
984
1238
|
(params.writeWarning ?? ((message) => process.stderr.write(`${message}\n`)))(warning);
|
|
985
1239
|
const runVerboseFn = deps?.runVerbose ?? runVerboseEngine;
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1240
|
+
let replacementResult: SandboxReadyResult | null = null;
|
|
1241
|
+
let replacementFailure: unknown = null;
|
|
1242
|
+
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
|
+
await params.recreate(params.branch);
|
|
991
1248
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1249
|
+
const rows = (deps?.fetchRows ?? fetchSandboxRows)(
|
|
1250
|
+
params.engine,
|
|
1251
|
+
sandboxLabel(params.config),
|
|
1252
|
+
sandboxBranchLabel(params.config)
|
|
1253
|
+
);
|
|
1254
|
+
const replacement = selectSandboxContainer(
|
|
1255
|
+
[...rows.running, ...rows.nonRunning],
|
|
1256
|
+
containerNameCandidates(params.config, params.branch)
|
|
1257
|
+
);
|
|
1258
|
+
if (!replacement?.running) {
|
|
1259
|
+
throw new Error('Replacement sandbox container was not found in a running state.');
|
|
1260
|
+
}
|
|
1261
|
+
const final = assess({
|
|
1262
|
+
config: params.config,
|
|
1263
|
+
engine: params.engine,
|
|
1264
|
+
branch: params.branch,
|
|
1265
|
+
workspace: params.workspace,
|
|
1266
|
+
container: replacement.name,
|
|
1267
|
+
deps
|
|
1268
|
+
});
|
|
1269
|
+
if (final.findings.length > 0) {
|
|
1270
|
+
throw new Error(`Replacement sandbox readiness check failed: ${describeFindings(final.findings)}`);
|
|
1271
|
+
}
|
|
1272
|
+
replacementResult = { container: replacement.name, path: 'recreated', warnings };
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
replacementFailure = error;
|
|
1003
1275
|
}
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
});
|
|
1012
|
-
if (final.findings.length > 0) {
|
|
1013
|
-
throw new Error(`Replacement sandbox readiness check failed: ${describeFindings(final.findings)}`);
|
|
1276
|
+
|
|
1277
|
+
const afterInspection = inspectWorktree(before.worktree, { probe: worktreeProbe });
|
|
1278
|
+
if (afterInspection.status === 'failed' || afterInspection.snapshot.identity !== before.identity) {
|
|
1279
|
+
const detail = afterInspection.status === 'failed'
|
|
1280
|
+
? afterInspection.message
|
|
1281
|
+
: `snapshot changed from ${before.identity} to ${afterInspection.snapshot.identity}`;
|
|
1282
|
+
throw new Error(`SANDBOX_RECOVERY_WORKTREE_CHANGED: ${detail}`, replacementFailure === null ? undefined : { cause: replacementFailure });
|
|
1014
1283
|
}
|
|
1015
|
-
|
|
1284
|
+
if (replacementFailure !== null) throw replacementFailure;
|
|
1285
|
+
return replacementResult!;
|
|
1016
1286
|
}
|