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