@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
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/mounts.ts
CHANGED
|
@@ -26,6 +26,7 @@ export function sandboxCoreBindMounts(
|
|
|
26
26
|
workspaceViewRoot: string;
|
|
27
27
|
controlDir: string;
|
|
28
28
|
controlStatusDir: string;
|
|
29
|
+
runtimeDir?: string;
|
|
29
30
|
taskSource?: string;
|
|
30
31
|
taskId?: string;
|
|
31
32
|
}
|
|
@@ -89,5 +90,12 @@ export function sandboxCoreBindMounts(
|
|
|
89
90
|
containerPath: '/run/agent-infra/control',
|
|
90
91
|
readOnly: false
|
|
91
92
|
});
|
|
93
|
+
if (taskBound && overrides.runtimeDir) {
|
|
94
|
+
mounts.push({
|
|
95
|
+
hostPaths: [overrides.runtimeDir],
|
|
96
|
+
containerPath: '/run/agent-infra/runtime',
|
|
97
|
+
readOnly: false
|
|
98
|
+
});
|
|
99
|
+
}
|
|
92
100
|
return mounts;
|
|
93
101
|
}
|
package/lib/sandbox/recovery.ts
CHANGED
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
type SandboxWorkspaceIdentity
|
|
36
36
|
} from './workspace-identity.ts';
|
|
37
37
|
import { inspectWorktree, type WorktreeSnapshot } from './worktree-safety.ts';
|
|
38
|
-
import { runEngine, runOkEngine, runVerboseEngine } from './shell.ts';
|
|
38
|
+
import { commandForEngine, runEngine, runOkEngine, runProbe, runVerboseEngine } from './shell.ts';
|
|
39
39
|
import {
|
|
40
40
|
declaredTmpfsSeedEntries,
|
|
41
41
|
resolveTools,
|
|
@@ -49,15 +49,19 @@ 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,
|
|
56
|
+
isSandboxControlRootQuiescing,
|
|
57
|
+
readSandboxControlManifest,
|
|
58
|
+
type BrokerOwner
|
|
56
59
|
} from './control/lifecycle.ts';
|
|
57
60
|
import {
|
|
58
61
|
appendSandboxControlAudit,
|
|
59
62
|
readSandboxControlStatus
|
|
60
63
|
} from './control/state.ts';
|
|
64
|
+
import { inspectSandboxControlContainer } from './control/container-identity.ts';
|
|
61
65
|
import {
|
|
62
66
|
SANDBOX_CONTROL_FUTURE_SKEW_MS,
|
|
63
67
|
SANDBOX_CONTROL_STATUS_STALE_MS,
|
|
@@ -89,6 +93,9 @@ export type SandboxRecoverySnapshot = {
|
|
|
89
93
|
actualBranch: string | null;
|
|
90
94
|
expectedWorkspace: SandboxWorkspaceIdentity;
|
|
91
95
|
actualWorkspace: SandboxContainerWorkspaceIdentity;
|
|
96
|
+
workspaceTopology?: 'legacy-parent' | 'per-state' | 'unknown';
|
|
97
|
+
taskView?: { path: string; readable: boolean };
|
|
98
|
+
runtimeStoreOk?: boolean;
|
|
92
99
|
runtimeCapabilityOk?: boolean;
|
|
93
100
|
unexpectedCapabilityMounts?: string[];
|
|
94
101
|
mounts: Array<{
|
|
@@ -142,8 +149,27 @@ type RecoveryCommandDeps = {
|
|
|
142
149
|
start?: typeof startSandboxContainer;
|
|
143
150
|
fetchRows?: typeof fetchSandboxRows;
|
|
144
151
|
ensureControlBroker?: () => void | Promise<void>;
|
|
152
|
+
probe?: typeof runProbe;
|
|
145
153
|
};
|
|
146
154
|
|
|
155
|
+
export function worktreeProbeForEngine(
|
|
156
|
+
engine: string,
|
|
157
|
+
probe: typeof runProbe = runProbe
|
|
158
|
+
): typeof runProbe {
|
|
159
|
+
return (cmd, args, opts = {}) => {
|
|
160
|
+
const engineArgs = [...args];
|
|
161
|
+
if (engine === 'wsl2' && cmd === 'git') {
|
|
162
|
+
for (let index = 0; index < engineArgs.length - 1; index += 1) {
|
|
163
|
+
if (engineArgs[index] === '-C') {
|
|
164
|
+
engineArgs[index + 1] = toEnginePath(engine, engineArgs[index + 1]!);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const command = commandForEngine(engine, cmd, engineArgs);
|
|
169
|
+
return probe(command.cmd, command.args, opts);
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
147
173
|
type EnsureSandboxReadyParams = {
|
|
148
174
|
config: SandboxConfig;
|
|
149
175
|
engine: string;
|
|
@@ -151,6 +177,7 @@ type EnsureSandboxReadyParams = {
|
|
|
151
177
|
workspace?: SandboxWorkspaceIdentity;
|
|
152
178
|
row: SandboxRow;
|
|
153
179
|
allowRecreate?: boolean;
|
|
180
|
+
forceRecreate?: boolean;
|
|
154
181
|
recreate?: (branch: string) => Promise<void>;
|
|
155
182
|
writeWarning?: (message: string) => void;
|
|
156
183
|
deps?: RecoveryCommandDeps;
|
|
@@ -168,7 +195,7 @@ async function waitForSandboxControlBrokerStatus(params: {
|
|
|
168
195
|
statusDir: string;
|
|
169
196
|
generation: string;
|
|
170
197
|
pid: number;
|
|
171
|
-
startTime?:
|
|
198
|
+
startTime?: number;
|
|
172
199
|
}, timeoutMs = 5_000): Promise<void> {
|
|
173
200
|
const deadline = Date.now() + timeoutMs;
|
|
174
201
|
while (Date.now() < deadline) {
|
|
@@ -191,7 +218,7 @@ export async function startSandboxControlBroker(repoRoot: string, manifestPath:
|
|
|
191
218
|
const directory = path.dirname(fileURLToPath(import.meta.url));
|
|
192
219
|
const extension = path.extname(fileURLToPath(import.meta.url));
|
|
193
220
|
const internalCli = path.resolve(directory, '..', '..', 'bin', `internal-cli${extension}`);
|
|
194
|
-
const manifest =
|
|
221
|
+
const manifest = readSandboxControlManifest(manifestPath);
|
|
195
222
|
const root = path.dirname(manifestPath);
|
|
196
223
|
if (isSandboxControlRootQuiescing(root)) throw new Error('SANDBOX_CONTROL_QUIESCING');
|
|
197
224
|
const brokerPath = path.join(root, 'broker.json');
|
|
@@ -207,6 +234,7 @@ export async function startSandboxControlBroker(repoRoot: string, manifestPath:
|
|
|
207
234
|
version?: unknown;
|
|
208
235
|
pid?: unknown;
|
|
209
236
|
startTime?: unknown;
|
|
237
|
+
brokerId?: unknown;
|
|
210
238
|
token?: unknown;
|
|
211
239
|
generation?: unknown;
|
|
212
240
|
};
|
|
@@ -216,15 +244,24 @@ export async function startSandboxControlBroker(repoRoot: string, manifestPath:
|
|
|
216
244
|
} catch {
|
|
217
245
|
// Malformed owner records are stale and replaced below.
|
|
218
246
|
}
|
|
219
|
-
const owner = broker !== null &&
|
|
220
|
-
|
|
247
|
+
const owner = broker !== null && broker.version === 3 && typeof broker.pid === 'number'
|
|
248
|
+
&& Number.isSafeInteger(broker.pid) && broker.pid > 0 && typeof broker.startTime === 'number'
|
|
249
|
+
&& Number.isSafeInteger(broker.startTime) && typeof broker.brokerId === 'string'
|
|
250
|
+
&& broker.brokerId.length > 0 && typeof broker.token === 'string'
|
|
251
|
+
&& typeof broker.generation === 'string'
|
|
252
|
+
? broker as BrokerOwner
|
|
221
253
|
: null;
|
|
222
|
-
|
|
254
|
+
if (broker === null || broker.version !== 3 || owner === null) {
|
|
255
|
+
throw new Error('SANDBOX_CONTROL_BROKER_VERSION_INVALID: rebuild the sandbox container before starting a new broker');
|
|
256
|
+
}
|
|
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';
|
|
223
260
|
if (
|
|
224
261
|
live
|
|
225
262
|
&& owner !== null
|
|
226
263
|
&& broker !== null
|
|
227
|
-
&& broker.version ===
|
|
264
|
+
&& broker.version === 3
|
|
228
265
|
&& broker.token === manifest.token
|
|
229
266
|
&& broker.generation === manifest.generation
|
|
230
267
|
) {
|
|
@@ -237,27 +274,6 @@ export async function startSandboxControlBroker(repoRoot: string, manifestPath:
|
|
|
237
274
|
});
|
|
238
275
|
return;
|
|
239
276
|
}
|
|
240
|
-
if (live && owner) {
|
|
241
|
-
const deadline = Date.now() + 5_000;
|
|
242
|
-
while (Date.now() < deadline) {
|
|
243
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
244
|
-
try {
|
|
245
|
-
if (fs.readFileSync(brokerPath, 'utf8') !== brokerSnapshot) {
|
|
246
|
-
return startSandboxControlBroker(repoRoot, manifestPath);
|
|
247
|
-
}
|
|
248
|
-
} catch (error) {
|
|
249
|
-
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
250
|
-
return startSandboxControlBroker(repoRoot, manifestPath);
|
|
251
|
-
}
|
|
252
|
-
throw error;
|
|
253
|
-
}
|
|
254
|
-
if (!processIdentityMatches(owner)) {
|
|
255
|
-
removePidFileIfMatches(brokerPath, brokerSnapshot);
|
|
256
|
-
return startSandboxControlBroker(repoRoot, manifestPath);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
throw new Error('SANDBOX_CONTROL_BROKER_OWNER_ACTIVE');
|
|
260
|
-
}
|
|
261
277
|
}
|
|
262
278
|
const startupStartTime = getProcessStartTime(process.pid);
|
|
263
279
|
if (!startupStartTime) throw new Error('SANDBOX_CONTROL_BROKER_IDENTITY_UNAVAILABLE');
|
|
@@ -317,9 +333,16 @@ async function ensureSandboxControlBroker(params: {
|
|
|
317
333
|
identity: params.workspace
|
|
318
334
|
});
|
|
319
335
|
if (!fs.existsSync(control.manifestPath)) return;
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
336
|
+
const validatedManifest = readSandboxControlManifest(control.manifestPath);
|
|
337
|
+
const containerObservation = await inspectSandboxControlContainer(validatedManifest);
|
|
338
|
+
if (containerObservation.state === 'unknown') {
|
|
339
|
+
throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${containerObservation.reason}`);
|
|
340
|
+
}
|
|
341
|
+
if (containerObservation.state === 'absent') {
|
|
342
|
+
await garbageCollectSandboxControlRoot(control.root, {
|
|
343
|
+
inspectContainer: (timeoutMs) => inspectSandboxControlContainer(validatedManifest, { timeoutMs })
|
|
344
|
+
});
|
|
345
|
+
throw new Error('SANDBOX_CONTROL_CONTAINER_ABSENT: recreate the sandbox container before broker recovery');
|
|
323
346
|
}
|
|
324
347
|
const brokerPath = path.join(control.root, 'broker.json');
|
|
325
348
|
try {
|
|
@@ -327,20 +350,33 @@ async function ensureSandboxControlBroker(params: {
|
|
|
327
350
|
version?: unknown;
|
|
328
351
|
pid?: unknown;
|
|
329
352
|
startTime?: unknown;
|
|
353
|
+
brokerId?: unknown;
|
|
330
354
|
token?: unknown;
|
|
331
355
|
generation?: unknown;
|
|
332
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');
|
|
333
365
|
if (
|
|
334
|
-
broker.version ===
|
|
366
|
+
broker.version === 3
|
|
335
367
|
&& typeof broker.pid === 'number'
|
|
336
|
-
&&
|
|
337
|
-
&& broker.
|
|
338
|
-
&& broker.
|
|
339
|
-
&&
|
|
368
|
+
&& Number.isSafeInteger(broker.pid)
|
|
369
|
+
&& typeof broker.startTime === 'number'
|
|
370
|
+
&& Number.isSafeInteger(broker.startTime)
|
|
371
|
+
&& typeof broker.brokerId === 'string'
|
|
372
|
+
&& broker.brokerId.length > 0
|
|
373
|
+
&& broker.token === validatedManifest.token
|
|
374
|
+
&& broker.generation === validatedManifest.generation
|
|
375
|
+
&& ownerState === 'alive'
|
|
340
376
|
) {
|
|
341
377
|
try {
|
|
342
378
|
const status = readSandboxControlStatus(control.statusDir);
|
|
343
|
-
if (status.generation ===
|
|
379
|
+
if (status.generation === validatedManifest.generation && status.broker.pid === broker.pid
|
|
344
380
|
&& status.broker.startTime === broker.startTime
|
|
345
381
|
&& Date.now() - status.updatedAt <= SANDBOX_CONTROL_STATUS_STALE_MS
|
|
346
382
|
&& status.updatedAt <= Date.now() + SANDBOX_CONTROL_FUTURE_SKEW_MS) return;
|
|
@@ -350,13 +386,14 @@ async function ensureSandboxControlBroker(params: {
|
|
|
350
386
|
await waitForSandboxControlBrokerStatus({
|
|
351
387
|
root: control.root,
|
|
352
388
|
statusDir: control.statusDir,
|
|
353
|
-
generation:
|
|
389
|
+
generation: validatedManifest.generation,
|
|
354
390
|
pid: broker.pid,
|
|
355
391
|
startTime: broker.startTime
|
|
356
392
|
});
|
|
357
393
|
return;
|
|
358
394
|
}
|
|
359
|
-
} catch {
|
|
395
|
+
} catch (error) {
|
|
396
|
+
if (error instanceof Error && error.message === 'SANDBOX_CONTROL_BROKER_OWNER_UNAVAILABLE') throw error;
|
|
360
397
|
// A missing, stale, or malformed broker record is replaced below.
|
|
361
398
|
}
|
|
362
399
|
await startSandboxControlBroker(params.config.repoRoot, control.manifestPath);
|
|
@@ -393,6 +430,30 @@ export function classifySandboxRecovery(snapshot: SandboxRecoverySnapshot): Sand
|
|
|
393
430
|
message: 'Container runtime capability signature does not match the current sandbox plan.'
|
|
394
431
|
});
|
|
395
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
|
+
}
|
|
396
457
|
for (const mountPath of snapshot.unexpectedCapabilityMounts ?? []) {
|
|
397
458
|
findings.push({
|
|
398
459
|
repairKind: 'hard-failure',
|
|
@@ -402,6 +463,10 @@ export function classifySandboxRecovery(snapshot: SandboxRecoverySnapshot): Sand
|
|
|
402
463
|
}
|
|
403
464
|
|
|
404
465
|
for (const mount of snapshot.mounts) {
|
|
466
|
+
if (
|
|
467
|
+
snapshot.workspaceTopology === 'legacy-parent'
|
|
468
|
+
&& mount.path.startsWith('/workspace/.agents/workspace')
|
|
469
|
+
) continue;
|
|
405
470
|
if (
|
|
406
471
|
mount.actualType !== mount.expectedType
|
|
407
472
|
|| !mount.sourceMatches
|
|
@@ -494,6 +559,20 @@ function inspectContainer(
|
|
|
494
559
|
return parsed[0] as DockerInspection;
|
|
495
560
|
}
|
|
496
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
|
+
|
|
497
576
|
function probe(
|
|
498
577
|
engine: string,
|
|
499
578
|
container: string,
|
|
@@ -573,6 +652,7 @@ function expectedMounts(params: {
|
|
|
573
652
|
controlStatusDir: control.statusDir,
|
|
574
653
|
...(params.workspace.mode === 'task-bound'
|
|
575
654
|
? {
|
|
655
|
+
runtimeDir: control.runtimeDir,
|
|
576
656
|
taskSource: assertSandboxTaskSource(config.repoRoot, params.workspace.taskId),
|
|
577
657
|
taskId: params.workspace.taskId
|
|
578
658
|
}
|
|
@@ -706,6 +786,10 @@ export function collectSandboxRecoverySnapshot(params: {
|
|
|
706
786
|
const capabilityPlan = createSandboxCapabilityPlan(params.config);
|
|
707
787
|
const workspace = params.workspace ?? { mode: 'branch-only' as const };
|
|
708
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;
|
|
709
793
|
const seeds = declaredTmpfsSeedEntries(tools).map((seed) => {
|
|
710
794
|
const mounted = mountsByDestination.get(seed.stagingPath)?.Type === 'bind';
|
|
711
795
|
return {
|
|
@@ -736,6 +820,15 @@ export function collectSandboxRecoverySnapshot(params: {
|
|
|
736
820
|
runOkFn
|
|
737
821
|
)
|
|
738
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;
|
|
739
832
|
const labels = inspection.Config?.Labels ?? {};
|
|
740
833
|
const branchLabel = labels[sandboxBranchLabel(params.config)];
|
|
741
834
|
const containerWorkspace = parseSandboxWorkspaceIdentity(labels, {
|
|
@@ -797,6 +890,20 @@ export function collectSandboxRecoverySnapshot(params: {
|
|
|
797
890
|
actualBranch: typeof branchLabel === 'string' ? branchLabel : null,
|
|
798
891
|
expectedWorkspace: workspace,
|
|
799
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 }),
|
|
800
907
|
identityOk: typeof inspection.Id === 'string' && inspection.Id.length > 0
|
|
801
908
|
&& branchLabel === params.branch
|
|
802
909
|
&& sameSandboxWorkspaceIdentity(containerWorkspace, workspace),
|
|
@@ -973,11 +1080,13 @@ function assess(params: {
|
|
|
973
1080
|
function canRetryFreshReadiness(findings: SandboxRecoveryFinding[]): boolean {
|
|
974
1081
|
return findings.length > 0 && findings.every((finding) =>
|
|
975
1082
|
finding.repairKind === 'hard-failure'
|
|
1083
|
+
&& finding.code !== 'SANDBOX_WORKSPACE_TOPOLOGY_MISMATCH'
|
|
976
1084
|
&& finding.path !== undefined
|
|
977
1085
|
&& (
|
|
978
1086
|
finding.path.startsWith('/workspace/.agents/workspace')
|
|
979
1087
|
|| finding.path === '/home/devuser/.host-shell-config'
|
|
980
1088
|
|| finding.path === '/home/devuser/.bash_aliases'
|
|
1089
|
+
|| finding.path === '/run/agent-infra/runtime'
|
|
981
1090
|
)
|
|
982
1091
|
);
|
|
983
1092
|
}
|
|
@@ -1048,6 +1157,9 @@ export async function ensureSandboxReady(params: EnsureSandboxReadyParams): Prom
|
|
|
1048
1157
|
workspace: params.workspace ?? { mode: 'branch-only' }
|
|
1049
1158
|
});
|
|
1050
1159
|
}
|
|
1160
|
+
if (params.forceRecreate) {
|
|
1161
|
+
throw new Error('Explicit container recreation requested.');
|
|
1162
|
+
}
|
|
1051
1163
|
if (!params.row.running) {
|
|
1052
1164
|
startFn(params.engine, params.row.name);
|
|
1053
1165
|
const initial = assess({
|
|
@@ -1192,7 +1304,8 @@ export async function ensureSandboxReady(params: EnsureSandboxReadyParams): Prom
|
|
|
1192
1304
|
`SANDBOX_RECOVERY_WORKTREE_SNAPSHOT_INVALID: expected exactly one existing worktree for '${params.branch}', found ${existingWorktrees.length}. ${remediation}`
|
|
1193
1305
|
);
|
|
1194
1306
|
}
|
|
1195
|
-
const
|
|
1307
|
+
const worktreeProbe = worktreeProbeForEngine(params.engine, deps?.probe);
|
|
1308
|
+
const beforeInspection = inspectWorktree(existingWorktrees[0]!, { probe: worktreeProbe });
|
|
1196
1309
|
if (beforeInspection.status === 'failed') {
|
|
1197
1310
|
throw new Error(`SANDBOX_RECOVERY_WORKTREE_SNAPSHOT_INVALID: ${beforeInspection.message}`);
|
|
1198
1311
|
}
|
|
@@ -1204,17 +1317,14 @@ export async function ensureSandboxReady(params: EnsureSandboxReadyParams): Prom
|
|
|
1204
1317
|
);
|
|
1205
1318
|
}
|
|
1206
1319
|
|
|
1207
|
-
const warning =
|
|
1320
|
+
const warning = params.forceRecreate
|
|
1321
|
+
? `Explicit sandbox recreation requested. Replacing only the container. ${dataBoundary}`
|
|
1322
|
+
: `Sandbox recovery failed in place. Replacing only the container. ${dataBoundary}`;
|
|
1208
1323
|
warnings.push(warning);
|
|
1209
1324
|
(params.writeWarning ?? ((message) => process.stderr.write(`${message}\n`)))(warning);
|
|
1210
|
-
const runVerboseFn = deps?.runVerbose ?? runVerboseEngine;
|
|
1211
1325
|
let replacementResult: SandboxReadyResult | null = null;
|
|
1212
1326
|
let replacementFailure: unknown = null;
|
|
1213
1327
|
try {
|
|
1214
|
-
if (params.row.running) {
|
|
1215
|
-
runVerboseFn(params.engine, 'docker', ['stop', params.row.name]);
|
|
1216
|
-
}
|
|
1217
|
-
runVerboseFn(params.engine, 'docker', ['rm', params.row.name]);
|
|
1218
1328
|
await params.recreate(params.branch);
|
|
1219
1329
|
|
|
1220
1330
|
const rows = (deps?.fetchRows ?? fetchSandboxRows)(
|
|
@@ -1245,7 +1355,7 @@ export async function ensureSandboxReady(params: EnsureSandboxReadyParams): Prom
|
|
|
1245
1355
|
replacementFailure = error;
|
|
1246
1356
|
}
|
|
1247
1357
|
|
|
1248
|
-
const afterInspection = inspectWorktree(before.worktree);
|
|
1358
|
+
const afterInspection = inspectWorktree(before.worktree, { probe: worktreeProbe });
|
|
1249
1359
|
if (afterInspection.status === 'failed' || afterInspection.snapshot.identity !== before.identity) {
|
|
1250
1360
|
const detail = afterInspection.status === 'failed'
|
|
1251
1361
|
? afterInspection.message
|
|
@@ -47,8 +47,10 @@ export function sandboxWorkspaceViewPaths(params: Readonly<{
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export type SandboxControlSetup = Readonly<{
|
|
50
|
+
root: string;
|
|
50
51
|
channelDir: string;
|
|
51
52
|
statusDir: string;
|
|
53
|
+
runtimeDir: string;
|
|
52
54
|
manifestPath: string;
|
|
53
55
|
token: string;
|
|
54
56
|
generation: string;
|
|
@@ -59,7 +61,7 @@ export function sandboxControlPaths(params: Readonly<{
|
|
|
59
61
|
project: string;
|
|
60
62
|
container: string;
|
|
61
63
|
identity: SandboxWorkspaceIdentity;
|
|
62
|
-
}>): Readonly<{ root: string; channelDir: string; statusDir: string; processingDir: string; manifestPath: string }> {
|
|
64
|
+
}>): Readonly<{ root: string; channelDir: string; statusDir: string; processingDir: string; runtimeDir: string; manifestPath: string }> {
|
|
63
65
|
const identityKey = params.identity.mode === 'task-bound'
|
|
64
66
|
? `task-bound:${params.identity.taskId}`
|
|
65
67
|
: 'branch-only';
|
|
@@ -70,6 +72,7 @@ export function sandboxControlPaths(params: Readonly<{
|
|
|
70
72
|
channelDir: path.join(root, 'channel'),
|
|
71
73
|
statusDir: path.join(root, 'public'),
|
|
72
74
|
processingDir: path.join(root, 'processing'),
|
|
75
|
+
runtimeDir: path.join(root, 'runtime'),
|
|
73
76
|
manifestPath: path.join(root, 'manifest.json')
|
|
74
77
|
};
|
|
75
78
|
}
|
|
@@ -84,7 +87,11 @@ function assertSafeDirectory(directory: string, expectedBase: string): void {
|
|
|
84
87
|
}
|
|
85
88
|
}
|
|
86
89
|
|
|
87
|
-
function assertSafeMountTargetPath(
|
|
90
|
+
function assertSafeMountTargetPath(
|
|
91
|
+
target: string,
|
|
92
|
+
worktreeRoot: string,
|
|
93
|
+
canonicalWorktreeRoot: string
|
|
94
|
+
): void {
|
|
88
95
|
const relative = path.relative(worktreeRoot, target);
|
|
89
96
|
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
90
97
|
throw new Error(`Sandbox workspace mount target escapes its worktree: ${target}`);
|
|
@@ -106,6 +113,21 @@ function assertSafeMountTargetPath(target: string, worktreeRoot: string, canonic
|
|
|
106
113
|
}
|
|
107
114
|
}
|
|
108
115
|
|
|
116
|
+
export function assertSandboxTaskSource(repoRoot: string, taskId: string): string {
|
|
117
|
+
if (!/^TASK-\d{8}-\d{6}$/.test(taskId)) throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
118
|
+
const activeRoot = fs.realpathSync.native(path.join(repoRoot, '.agents', 'workspace', 'active'));
|
|
119
|
+
const source = path.join(activeRoot, taskId);
|
|
120
|
+
const stat = fs.lstatSync(source);
|
|
121
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
122
|
+
throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${source} must be a real directory`);
|
|
123
|
+
}
|
|
124
|
+
const canonical = fs.realpathSync.native(source);
|
|
125
|
+
if (path.dirname(canonical) !== activeRoot) {
|
|
126
|
+
throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${source} escapes the active workspace`);
|
|
127
|
+
}
|
|
128
|
+
return canonical;
|
|
129
|
+
}
|
|
130
|
+
|
|
109
131
|
export function prepareSandboxWorkspaceMountTargets(worktreeRoot: string): void {
|
|
110
132
|
const resolvedWorktreeRoot = path.resolve(worktreeRoot);
|
|
111
133
|
const canonicalWorktreeRoot = fs.realpathSync.native(resolvedWorktreeRoot);
|
|
@@ -118,25 +140,12 @@ export function prepareSandboxWorkspaceMountTargets(worktreeRoot: string): void
|
|
|
118
140
|
fs.mkdirSync(workspaceRoot, { recursive: true, mode: 0o700 });
|
|
119
141
|
for (const { hostPath } of statePaths) {
|
|
120
142
|
fs.mkdirSync(hostPath, { recursive: true, mode: 0o700 });
|
|
143
|
+
fs.chmodSync(hostPath, 0o700);
|
|
121
144
|
}
|
|
145
|
+
fs.chmodSync(workspaceRoot, 0o700);
|
|
122
146
|
fs.closeSync(fs.openSync(registryTarget, 'a', 0o600));
|
|
123
147
|
}
|
|
124
148
|
|
|
125
|
-
export function assertSandboxTaskSource(repoRoot: string, taskId: string): string {
|
|
126
|
-
if (!/^TASK-\d{8}-\d{6}$/.test(taskId)) throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
127
|
-
const activeRoot = fs.realpathSync.native(path.join(repoRoot, '.agents', 'workspace', 'active'));
|
|
128
|
-
const source = path.join(activeRoot, taskId);
|
|
129
|
-
const stat = fs.lstatSync(source);
|
|
130
|
-
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
131
|
-
throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${source} must be a real directory`);
|
|
132
|
-
}
|
|
133
|
-
const canonical = fs.realpathSync.native(source);
|
|
134
|
-
if (path.dirname(canonical) !== activeRoot) {
|
|
135
|
-
throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${source} escapes the active workspace`);
|
|
136
|
-
}
|
|
137
|
-
return canonical;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
149
|
export function materializeSandboxWorkspaceView(params: Readonly<{
|
|
141
150
|
base: string;
|
|
142
151
|
project: string;
|
|
@@ -180,17 +189,38 @@ export function materializeSandboxControl(params: Readonly<{
|
|
|
180
189
|
container: string;
|
|
181
190
|
branch: string;
|
|
182
191
|
identity: SandboxWorkspaceIdentity;
|
|
192
|
+
engine?: string;
|
|
193
|
+
replacementLease?: Readonly<{
|
|
194
|
+
root: string;
|
|
195
|
+
assertOwned(): void;
|
|
196
|
+
}>;
|
|
183
197
|
}>): SandboxControlSetup {
|
|
184
|
-
const { root, channelDir, statusDir, processingDir, manifestPath } = sandboxControlPaths(params);
|
|
198
|
+
const { root, channelDir, statusDir, processingDir, runtimeDir, manifestPath } = sandboxControlPaths(params);
|
|
185
199
|
assertSafeDirectory(root, path.resolve(params.base));
|
|
200
|
+
if (fs.existsSync(root)) {
|
|
201
|
+
if (!params.replacementLease || path.resolve(params.replacementLease.root) !== root) {
|
|
202
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_REQUIRED');
|
|
203
|
+
}
|
|
204
|
+
params.replacementLease.assertOwned();
|
|
205
|
+
}
|
|
186
206
|
const consumedDir = path.join(root, 'consumed');
|
|
187
207
|
assertSafeDirectory(consumedDir, root);
|
|
188
208
|
fs.rmSync(consumedDir, { recursive: true, force: true });
|
|
189
209
|
fs.mkdirSync(consumedDir, { recursive: true, mode: 0o700 });
|
|
190
|
-
for (const directory of [statusDir, processingDir]) {
|
|
210
|
+
for (const directory of [statusDir, processingDir, runtimeDir]) {
|
|
191
211
|
assertSafeDirectory(directory, root);
|
|
212
|
+
if (directory === runtimeDir) fs.rmSync(directory, { recursive: true, force: true });
|
|
192
213
|
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
193
214
|
}
|
|
215
|
+
for (const directory of [
|
|
216
|
+
path.join(runtimeDir, 'clients'),
|
|
217
|
+
path.join(runtimeDir, 'clients', 'codex', 'capabilities'),
|
|
218
|
+
path.join(runtimeDir, 'clients', 'codex', 'lifecycle')
|
|
219
|
+
]) {
|
|
220
|
+
assertSafeDirectory(directory, runtimeDir);
|
|
221
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
222
|
+
fs.chmodSync(directory, 0o700);
|
|
223
|
+
}
|
|
194
224
|
for (const queue of ['requests', 'responses']) {
|
|
195
225
|
const directory = path.join(channelDir, queue);
|
|
196
226
|
assertSafeDirectory(directory, root);
|
|
@@ -201,11 +231,13 @@ export function materializeSandboxControl(params: Readonly<{
|
|
|
201
231
|
const generation = randomBytes(16).toString('hex');
|
|
202
232
|
const repoRoot = fs.realpathSync.native(params.repoRoot);
|
|
203
233
|
const manifest: SandboxControlManifest = {
|
|
204
|
-
version:
|
|
234
|
+
version: 5,
|
|
235
|
+
engine: params.engine ?? 'docker',
|
|
205
236
|
repoRoot,
|
|
206
237
|
worktreeRoot: fs.realpathSync.native(params.worktreeRoot),
|
|
207
238
|
project: params.project,
|
|
208
239
|
container: params.container,
|
|
240
|
+
containerIdentity: { id: '', labels: {} },
|
|
209
241
|
branch: params.branch,
|
|
210
242
|
mode: params.identity.mode,
|
|
211
243
|
taskId: params.identity.mode === 'task-bound' ? params.identity.taskId : null,
|
|
@@ -213,8 +245,27 @@ export function materializeSandboxControl(params: Readonly<{
|
|
|
213
245
|
generation,
|
|
214
246
|
channelDir,
|
|
215
247
|
publicStatusDir: statusDir,
|
|
216
|
-
processingDir
|
|
248
|
+
processingDir,
|
|
249
|
+
runtimeDir
|
|
217
250
|
};
|
|
218
251
|
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest)}\n`, { mode: 0o600 });
|
|
219
|
-
return { channelDir, statusDir, manifestPath, token, generation };
|
|
252
|
+
return { root, channelDir, statusDir, runtimeDir, manifestPath, token, generation };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function finalizeSandboxControlManifest(
|
|
256
|
+
setup: SandboxControlSetup,
|
|
257
|
+
identity: Readonly<{ engine: string; id: string; labels: Readonly<Record<string, string>> }>
|
|
258
|
+
): void {
|
|
259
|
+
if (!identity.engine || !identity.id) throw new Error('SANDBOX_CONTROL_CONTAINER_ID_INVALID');
|
|
260
|
+
const manifest = JSON.parse(fs.readFileSync(setup.manifestPath, 'utf8')) as Record<string, unknown>;
|
|
261
|
+
manifest.version = 5;
|
|
262
|
+
manifest.engine = identity.engine;
|
|
263
|
+
manifest.containerIdentity = { id: identity.id, labels: { ...identity.labels } };
|
|
264
|
+
const temporary = `${setup.manifestPath}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
|
|
265
|
+
try {
|
|
266
|
+
fs.writeFileSync(temporary, `${JSON.stringify(manifest)}\n`, { mode: 0o600, flag: 'wx' });
|
|
267
|
+
fs.renameSync(temporary, setup.manifestPath);
|
|
268
|
+
} finally {
|
|
269
|
+
fs.rmSync(temporary, { force: true });
|
|
270
|
+
}
|
|
220
271
|
}
|