@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
|
@@ -69,13 +69,26 @@ import {
|
|
|
69
69
|
prepareTmpfsMounts,
|
|
70
70
|
startSandboxControlBroker
|
|
71
71
|
} from '../recovery.ts';
|
|
72
|
+
import {
|
|
73
|
+
acquireSandboxControlReplacement,
|
|
74
|
+
assertSandboxControlCutoverSnapshot,
|
|
75
|
+
beginSandboxControlReplacement,
|
|
76
|
+
captureSandboxControlCutoverSnapshot,
|
|
77
|
+
commitSandboxControlReplacement,
|
|
78
|
+
quiesceSandboxControlRoot,
|
|
79
|
+
recoverSandboxControlReplacement,
|
|
80
|
+
readSandboxControlManifestForTransition
|
|
81
|
+
} from '../control/lifecycle.ts';
|
|
82
|
+
import { inspectSandboxControlContainer } from '../control/container-identity.ts';
|
|
72
83
|
import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.ts';
|
|
73
84
|
import { sandboxCoreBindMounts } from '../mounts.ts';
|
|
74
85
|
import {
|
|
75
86
|
assertSandboxTaskSource,
|
|
87
|
+
finalizeSandboxControlManifest,
|
|
76
88
|
materializeSandboxControl,
|
|
77
89
|
materializeSandboxWorkspaceView,
|
|
78
|
-
prepareSandboxWorkspaceMountTargets
|
|
90
|
+
prepareSandboxWorkspaceMountTargets,
|
|
91
|
+
sandboxControlPaths,
|
|
79
92
|
} from '../workspace-view.ts';
|
|
80
93
|
import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.ts';
|
|
81
94
|
import { validateSelinuxDisableEnv } from '../engines/selinux.ts';
|
|
@@ -1035,6 +1048,12 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1035
1048
|
}
|
|
1036
1049
|
const effectiveResolvedTools = resolvedTools;
|
|
1037
1050
|
const container = containerName(effectiveConfig, branch);
|
|
1051
|
+
const controlPaths = sandboxControlPaths({
|
|
1052
|
+
base: effectiveConfig.controlBase,
|
|
1053
|
+
project: effectiveConfig.project,
|
|
1054
|
+
container,
|
|
1055
|
+
identity: target.workspace
|
|
1056
|
+
});
|
|
1038
1057
|
const worktree = worktreeCandidates.find((candidate) => fs.existsSync(candidate)) ?? worktreeCandidates[0] ?? '';
|
|
1039
1058
|
const shareCommon = shareCommonDir(effectiveConfig);
|
|
1040
1059
|
const shareBranch = shareBranchDir(effectiveConfig, branch);
|
|
@@ -1223,6 +1242,22 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1223
1242
|
{
|
|
1224
1243
|
title: `Starting container '${container}'`,
|
|
1225
1244
|
task: async (message: (text: string) => void) => {
|
|
1245
|
+
const hadControlRootBeforeAcquire = fs.existsSync(controlPaths.root);
|
|
1246
|
+
const replacementLease = acquireSandboxControlReplacement(controlPaths.root);
|
|
1247
|
+
let replacementLeaseHeld = true;
|
|
1248
|
+
let replacementCutover: ReturnType<typeof beginSandboxControlReplacement> | null = null;
|
|
1249
|
+
try {
|
|
1250
|
+
const recoveryResult = await recoverSandboxControlReplacement(controlPaths.root, replacementLease);
|
|
1251
|
+
const hadExistingControlRoot = hadControlRootBeforeAcquire || recoveryResult === 'restored';
|
|
1252
|
+
const previousManifest = fs.existsSync(controlPaths.manifestPath)
|
|
1253
|
+
? readSandboxControlManifestForTransition(controlPaths.manifestPath)
|
|
1254
|
+
: null;
|
|
1255
|
+
if (hadExistingControlRoot) {
|
|
1256
|
+
await quiesceSandboxControlRoot(controlPaths.root);
|
|
1257
|
+
}
|
|
1258
|
+
const previousCutoverSnapshot = hadExistingControlRoot
|
|
1259
|
+
? captureSandboxControlCutoverSnapshot(controlPaths.root)
|
|
1260
|
+
: null;
|
|
1226
1261
|
const existing = runSafeEngine(engine, 'docker', ['ps', '-a', '--format', '{{.Names}}']).split('\n').filter(Boolean);
|
|
1227
1262
|
const matchedContainers = containerNameCandidates(effectiveConfig, branch)
|
|
1228
1263
|
.filter((name) => existing.includes(name));
|
|
@@ -1265,6 +1300,14 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1265
1300
|
runSafeEngine(engine, 'docker', ['rm', name]);
|
|
1266
1301
|
}
|
|
1267
1302
|
}
|
|
1303
|
+
if (previousManifest) {
|
|
1304
|
+
const oldContainer = await inspectSandboxControlContainer(previousManifest);
|
|
1305
|
+
if (oldContainer.state !== 'absent') {
|
|
1306
|
+
throw new Error(oldContainer.state === 'unknown'
|
|
1307
|
+
? `SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${oldContainer.reason}`
|
|
1308
|
+
: 'SANDBOX_CONTROL_CONTAINER_STILL_EXISTS');
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1268
1311
|
|
|
1269
1312
|
const aliasesFile = ensureSandboxAliasesFile(
|
|
1270
1313
|
effectiveConfig.home,
|
|
@@ -1342,6 +1385,18 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1342
1385
|
identity: target.workspace
|
|
1343
1386
|
});
|
|
1344
1387
|
prepareSandboxWorkspaceMountTargets(worktree);
|
|
1388
|
+
if (previousCutoverSnapshot) {
|
|
1389
|
+
assertSandboxControlCutoverSnapshot(previousCutoverSnapshot);
|
|
1390
|
+
if (previousManifest) {
|
|
1391
|
+
const finalOldContainer = await inspectSandboxControlContainer(previousManifest);
|
|
1392
|
+
if (finalOldContainer.state !== 'absent') {
|
|
1393
|
+
throw new Error(finalOldContainer.state === 'unknown'
|
|
1394
|
+
? `SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${finalOldContainer.reason}`
|
|
1395
|
+
: 'SANDBOX_CONTROL_CONTAINER_REAPPEARED');
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
replacementCutover = beginSandboxControlReplacement(controlPaths.root, replacementLease);
|
|
1399
|
+
}
|
|
1345
1400
|
const control = materializeSandboxControl({
|
|
1346
1401
|
base: effectiveConfig.controlBase,
|
|
1347
1402
|
repoRoot: effectiveConfig.repoRoot,
|
|
@@ -1349,7 +1404,9 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1349
1404
|
project: effectiveConfig.project,
|
|
1350
1405
|
container,
|
|
1351
1406
|
branch,
|
|
1352
|
-
identity: target.workspace
|
|
1407
|
+
identity: target.workspace,
|
|
1408
|
+
engine,
|
|
1409
|
+
replacementLease
|
|
1353
1410
|
});
|
|
1354
1411
|
hostShellConfig = prepareHostShellConfig({
|
|
1355
1412
|
home: effectiveConfig.home,
|
|
@@ -1363,6 +1420,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1363
1420
|
workspaceViewRoot: workspaceView.root,
|
|
1364
1421
|
controlDir: control.channelDir,
|
|
1365
1422
|
controlStatusDir: control.statusDir,
|
|
1423
|
+
...(target.workspace.mode === 'task-bound' ? { runtimeDir: control.runtimeDir } : {}),
|
|
1366
1424
|
...(target.workspace.mode === 'task-bound'
|
|
1367
1425
|
? {
|
|
1368
1426
|
taskSource: assertSandboxTaskSource(
|
|
@@ -1422,9 +1480,12 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1422
1480
|
const hostTz = detectHostTimezone();
|
|
1423
1481
|
const tzFlags = hostTz ? ['-e', `TZ=${hostTz}`] : [];
|
|
1424
1482
|
|
|
1425
|
-
|
|
1483
|
+
let createdContainerRef: string | null = null;
|
|
1484
|
+
try {
|
|
1485
|
+
const dockerRunId = runEngineTaskCommand(engine, 'docker', [
|
|
1426
1486
|
'run',
|
|
1427
1487
|
'-d',
|
|
1488
|
+
'--init',
|
|
1428
1489
|
'--name',
|
|
1429
1490
|
container,
|
|
1430
1491
|
'--hostname',
|
|
@@ -1451,6 +1512,9 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1451
1512
|
'AGENT_INFRA_CONTROL_DIR=/run/agent-infra/control',
|
|
1452
1513
|
'-e',
|
|
1453
1514
|
'AGENT_INFRA_CONTROL_STATUS_DIR=/run/agent-infra/control-status',
|
|
1515
|
+
...(target.workspace.mode === 'task-bound'
|
|
1516
|
+
? ['-e', 'AGENT_INFRA_RUNTIME_DIR=/run/agent-infra/runtime']
|
|
1517
|
+
: []),
|
|
1454
1518
|
'--label',
|
|
1455
1519
|
`${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
|
|
1456
1520
|
...coreVolumes,
|
|
@@ -1471,9 +1535,66 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1471
1535
|
'-w',
|
|
1472
1536
|
'/workspace',
|
|
1473
1537
|
effectiveConfig.imageName
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1538
|
+
]);
|
|
1539
|
+
createdContainerRef = dockerRunId.trim();
|
|
1540
|
+
const containerId = runEngineTaskCommand(engine, 'docker', [
|
|
1541
|
+
'inspect', '--format', '{{.Id}}', dockerRunId.trim()
|
|
1542
|
+
]).trim();
|
|
1543
|
+
if (!containerId) throw new Error('SANDBOX_CONTROL_CONTAINER_ID_INVALID');
|
|
1544
|
+
const rawContainerLabels = runEngineTaskCommand(engine, 'docker', [
|
|
1545
|
+
'inspect', '--format', '{{json .Config.Labels}}', containerId
|
|
1546
|
+
]).trim();
|
|
1547
|
+
let inspectedLabels: unknown;
|
|
1548
|
+
try {
|
|
1549
|
+
inspectedLabels = JSON.parse(rawContainerLabels);
|
|
1550
|
+
} catch {
|
|
1551
|
+
throw new Error('SANDBOX_CONTROL_CONTAINER_LABELS_INVALID');
|
|
1552
|
+
}
|
|
1553
|
+
if (!inspectedLabels || typeof inspectedLabels !== 'object' || Array.isArray(inspectedLabels)) {
|
|
1554
|
+
throw new Error('SANDBOX_CONTROL_CONTAINER_LABELS_INVALID');
|
|
1555
|
+
}
|
|
1556
|
+
const expectedLabels: Record<string, string> = {
|
|
1557
|
+
[sandboxLabel(effectiveConfig)]: '',
|
|
1558
|
+
[sandboxBranchLabel(effectiveConfig)]: branch,
|
|
1559
|
+
[sandboxWorkspaceModeLabel(effectiveConfig)]: target.workspace.mode,
|
|
1560
|
+
[sandboxRuntimeCapabilityLabel(effectiveConfig)]: capabilityPlan.runtimeSignature,
|
|
1561
|
+
...(target.workspace.mode === 'task-bound'
|
|
1562
|
+
? { [sandboxTaskIdLabel(effectiveConfig)]: target.workspace.taskId }
|
|
1563
|
+
: {})
|
|
1564
|
+
};
|
|
1565
|
+
const labels = inspectedLabels as Record<string, unknown>;
|
|
1566
|
+
for (const [key, expected] of Object.entries(expectedLabels)) {
|
|
1567
|
+
if (labels[key] !== expected) throw new Error('SANDBOX_CONTROL_CONTAINER_IDENTITY_MISMATCH');
|
|
1568
|
+
}
|
|
1569
|
+
finalizeSandboxControlManifest(control, {
|
|
1570
|
+
engine,
|
|
1571
|
+
id: containerId,
|
|
1572
|
+
labels: expectedLabels
|
|
1573
|
+
});
|
|
1574
|
+
replacementLease.clearQuiescing();
|
|
1575
|
+
await startSandboxControlBroker(effectiveConfig.repoRoot, control.manifestPath);
|
|
1576
|
+
if (replacementCutover) {
|
|
1577
|
+
commitSandboxControlReplacement(control.root, replacementLease);
|
|
1578
|
+
replacementCutover = null;
|
|
1579
|
+
}
|
|
1580
|
+
createdTmpfsSeedPlan = tmpfsSeedPlan;
|
|
1581
|
+
} catch (error) {
|
|
1582
|
+
if (createdContainerRef) {
|
|
1583
|
+
runSafeEngine(engine, 'docker', ['stop', createdContainerRef]);
|
|
1584
|
+
runSafeEngine(engine, 'docker', ['rm', createdContainerRef]);
|
|
1585
|
+
}
|
|
1586
|
+
if (!hadExistingControlRoot) removeDirRecursive(control.root);
|
|
1587
|
+
if (replacementCutover) {
|
|
1588
|
+
try {
|
|
1589
|
+
await recoverSandboxControlReplacement(control.root, replacementLease);
|
|
1590
|
+
replacementCutover = null;
|
|
1591
|
+
} catch {
|
|
1592
|
+
// Preserve the cutover state for the next explicit recreate to recover.
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
removeDirRecursive(workspaceView.root);
|
|
1596
|
+
throw error;
|
|
1597
|
+
}
|
|
1477
1598
|
} finally {
|
|
1478
1599
|
envFile.cleanup();
|
|
1479
1600
|
}
|
|
@@ -1566,7 +1687,19 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1566
1687
|
);
|
|
1567
1688
|
}
|
|
1568
1689
|
|
|
1690
|
+
replacementLease.release();
|
|
1691
|
+
replacementLeaseHeld = false;
|
|
1569
1692
|
return 'Container started';
|
|
1693
|
+
} catch (error) {
|
|
1694
|
+
if (replacementLeaseHeld) {
|
|
1695
|
+
try {
|
|
1696
|
+
replacementLease.release();
|
|
1697
|
+
} catch {
|
|
1698
|
+
// Preserve the original cutover failure and its evidence.
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
throw error;
|
|
1702
|
+
}
|
|
1570
1703
|
}
|
|
1571
1704
|
}
|
|
1572
1705
|
]);
|
|
@@ -20,7 +20,8 @@ import { assertManagedPath, removeManagedDir, removeWorktreeDir } from '../manag
|
|
|
20
20
|
import { runEngine, runOk, runOkEngine, runSafe, runSafeEngine } from '../shell.ts';
|
|
21
21
|
import { resolveSandboxTarget, type SandboxWorkspaceIdentity } from '../workspace-identity.ts';
|
|
22
22
|
import { sandboxControlPaths, sandboxWorkspaceViewPaths } from '../workspace-view.ts';
|
|
23
|
-
import {
|
|
23
|
+
import { removeSandboxControlRoot, readSandboxControlManifest } from '../control/lifecycle.ts';
|
|
24
|
+
import { inspectSandboxControlContainer } from '../control/container-identity.ts';
|
|
24
25
|
import { toolConfigDirCandidates, toolProjectDirCandidates } from '../tools.ts';
|
|
25
26
|
import { createSandboxCapabilityPlan } from '../agent-client-reconciler.ts';
|
|
26
27
|
import type { SandboxTool } from '../tools.ts';
|
|
@@ -172,9 +173,40 @@ function removeEmptyManagedParent(base: string, directory: string): void {
|
|
|
172
173
|
assertRemoved(parent, 'Empty sandbox container directory');
|
|
173
174
|
}
|
|
174
175
|
|
|
175
|
-
async function
|
|
176
|
+
async function removeExactSandboxContainer(
|
|
177
|
+
engine: string,
|
|
178
|
+
manifest: ReturnType<typeof readSandboxControlManifest>,
|
|
179
|
+
timeoutMs: number
|
|
180
|
+
): Promise<void> {
|
|
181
|
+
const deadlineAt = Date.now() + timeoutMs;
|
|
182
|
+
const remaining = (): number => Math.max(1, deadlineAt - Date.now());
|
|
183
|
+
const inspect = () => inspectSandboxControlContainer(manifest, { timeoutMs: remaining() });
|
|
184
|
+
const before = await inspect();
|
|
185
|
+
if (before.state === 'unknown') throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${before.reason}`);
|
|
186
|
+
if (before.state === 'absent') return;
|
|
187
|
+
if (before.running && !runOkEngine(engine, 'docker', ['stop', manifest.containerIdentity.id], { timeout: remaining() })) {
|
|
188
|
+
const afterStop = await inspect();
|
|
189
|
+
if (afterStop.state !== 'absent' && afterStop.state !== 'found') {
|
|
190
|
+
throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${afterStop.reason}`);
|
|
191
|
+
}
|
|
192
|
+
if (afterStop.state === 'found' && afterStop.running) {
|
|
193
|
+
throw new Error(`Failed to stop sandbox container: ${manifest.containerIdentity.id}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const afterStop = await inspect();
|
|
197
|
+
if (afterStop.state === 'unknown') throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${afterStop.reason}`);
|
|
198
|
+
if (afterStop.state === 'found' && !runOkEngine(engine, 'docker', ['rm', manifest.containerIdentity.id], { timeout: remaining() })) {
|
|
199
|
+
const afterRemove = await inspect();
|
|
200
|
+
if (afterRemove.state !== 'absent') {
|
|
201
|
+
throw new Error(`Failed to remove sandbox container: ${manifest.containerIdentity.id}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function removeProjectControlRoots(config: SandboxConfig, engine: string): Promise<Set<string>> {
|
|
207
|
+
const containers = new Set<string>();
|
|
176
208
|
const projectRoot = path.join(config.controlBase, config.project);
|
|
177
|
-
if (!fs.existsSync(projectRoot)) return;
|
|
209
|
+
if (!fs.existsSync(projectRoot)) return containers;
|
|
178
210
|
assertManagedPath(config.controlBase, projectRoot);
|
|
179
211
|
const projectStat = fs.lstatSync(projectRoot);
|
|
180
212
|
if (!projectStat.isDirectory() || projectStat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
@@ -183,9 +215,18 @@ async function quiesceProjectControlRoots(config: SandboxConfig): Promise<void>
|
|
|
183
215
|
const containerRoot = path.join(projectRoot, container.name);
|
|
184
216
|
for (const identity of fs.readdirSync(containerRoot, { withFileTypes: true })) {
|
|
185
217
|
if (!identity.isDirectory() || identity.isSymbolicLink()) continue;
|
|
186
|
-
|
|
218
|
+
const root = path.join(containerRoot, identity.name);
|
|
219
|
+
const manifestPath = path.join(root, 'manifest.json');
|
|
220
|
+
if (!fs.existsSync(manifestPath)) throw new Error(`SANDBOX_CONTROL_TARGET_EVIDENCE_MISSING: ${root}`);
|
|
221
|
+
const manifest = readSandboxControlManifest(manifestPath);
|
|
222
|
+
containers.add(manifest.container);
|
|
223
|
+
await removeSandboxControlRoot(root, {
|
|
224
|
+
inspectContainer: (timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }),
|
|
225
|
+
removeContainer: (timeoutMs) => removeExactSandboxContainer(engine, manifest, timeoutMs)
|
|
226
|
+
});
|
|
187
227
|
}
|
|
188
228
|
}
|
|
229
|
+
return containers;
|
|
189
230
|
}
|
|
190
231
|
|
|
191
232
|
function inspectionBlockers(inspections: readonly WorktreeInspection[]): WorktreeInspection[] {
|
|
@@ -277,23 +318,30 @@ async function rmOne(
|
|
|
277
318
|
return;
|
|
278
319
|
}
|
|
279
320
|
|
|
321
|
+
const coordinatedContainers = new Set<string>();
|
|
280
322
|
for (const root of controlRoots.filter((candidate) => fs.existsSync(candidate))) {
|
|
281
323
|
assertLegacyCandidateEvidence(root, config.controlBase, config, effectiveBranch, matchedContainers);
|
|
282
324
|
assertControlRootMatchesTarget(root, effectiveBranch, workspace);
|
|
283
|
-
|
|
325
|
+
const manifest = readSandboxControlManifest(path.join(root, 'manifest.json'));
|
|
326
|
+
coordinatedContainers.add(manifest.container);
|
|
327
|
+
await removeSandboxControlRoot(root, {
|
|
328
|
+
inspectContainer: (timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }),
|
|
329
|
+
removeContainer: (timeoutMs) => removeExactSandboxContainer(engine, manifest, timeoutMs)
|
|
330
|
+
});
|
|
284
331
|
}
|
|
285
332
|
|
|
286
|
-
|
|
333
|
+
const uncoordinatedContainers = matchedContainers.filter((name) => !coordinatedContainers.has(name));
|
|
334
|
+
if (uncoordinatedContainers.length > 0) {
|
|
287
335
|
const spinner = p.spinner();
|
|
288
|
-
spinner.start(`Stopping container(s): ${
|
|
289
|
-
for (const name of
|
|
336
|
+
spinner.start(`Stopping container(s): ${uncoordinatedContainers.join(', ')}`);
|
|
337
|
+
for (const name of uncoordinatedContainers) {
|
|
290
338
|
if (!runOkEngine(engine, 'docker', ['stop', name])) throw new Error(`Failed to stop sandbox container: ${name}`);
|
|
291
339
|
if (!runOkEngine(engine, 'docker', ['rm', name])) throw new Error(`Failed to remove sandbox container: ${name}`);
|
|
292
340
|
}
|
|
293
341
|
const remaining = runEngine(engine, 'docker', ['ps', '-a', '--format', '{{.Names}}']).split('\n').filter(Boolean);
|
|
294
|
-
const leftovers =
|
|
342
|
+
const leftovers = uncoordinatedContainers.filter((name) => remaining.includes(name));
|
|
295
343
|
if (leftovers.length > 0) throw new Error(`Sandbox container(s) still exist after removal: ${leftovers.join(', ')}`);
|
|
296
|
-
spinner.stop(pc.green(`Removed container(s): ${
|
|
344
|
+
spinner.stop(pc.green(`Removed container(s): ${uncoordinatedContainers.join(', ')}`));
|
|
297
345
|
} else {
|
|
298
346
|
p.log.warn(`No sandbox container found for '${branch}'`);
|
|
299
347
|
}
|
|
@@ -400,7 +448,7 @@ async function rmPurge(
|
|
|
400
448
|
}
|
|
401
449
|
const permits = cleanPermits(inspections);
|
|
402
450
|
|
|
403
|
-
await
|
|
451
|
+
const coordinatedContainers = await removeProjectControlRoots(config, engine);
|
|
404
452
|
|
|
405
453
|
const containers = runEngine(engine, 'docker', [
|
|
406
454
|
'ps',
|
|
@@ -410,17 +458,19 @@ async function rmPurge(
|
|
|
410
458
|
'--format',
|
|
411
459
|
'{{.Names}}'
|
|
412
460
|
]);
|
|
413
|
-
|
|
461
|
+
const uncoordinatedContainers = containers.split('\n').filter((name) => name && !coordinatedContainers.has(name));
|
|
462
|
+
if (uncoordinatedContainers.length > 0) {
|
|
414
463
|
const spinner = p.spinner();
|
|
415
464
|
spinner.start('Stopping project sandbox containers...');
|
|
416
|
-
for (const name of
|
|
465
|
+
for (const name of uncoordinatedContainers) {
|
|
417
466
|
if (!runOkEngine(engine, 'docker', ['stop', name])) throw new Error(`Failed to stop sandbox container: ${name}`);
|
|
418
467
|
if (!runOkEngine(engine, 'docker', ['rm', name])) throw new Error(`Failed to remove sandbox container: ${name}`);
|
|
419
468
|
}
|
|
420
469
|
const remaining = runEngine(engine, 'docker', [
|
|
421
470
|
'ps', '-a', '--filter', `label=${sandboxLabel(config)}`, '--format', '{{.Names}}'
|
|
422
471
|
]);
|
|
423
|
-
|
|
472
|
+
const leftovers = remaining.split('\n').filter((name) => name && !coordinatedContainers.has(name));
|
|
473
|
+
if (leftovers.length > 0) throw new Error(`Project sandbox container(s) still exist after removal: ${leftovers.join(', ')}`);
|
|
424
474
|
spinner.stop(pc.green('Project sandbox containers removed'));
|
|
425
475
|
} else {
|
|
426
476
|
p.log.warn('No project sandbox containers found');
|
|
@@ -17,9 +17,9 @@ const USAGE = `Usage: ai sandbox start [--recreate] <branch | TASK-id | N>
|
|
|
17
17
|
|
|
18
18
|
Start an existing sandbox container that has stopped (for example after the
|
|
19
19
|
Docker daemon was restarted or replaced). The container must already exist:
|
|
20
|
-
if none is found, run 'ai sandbox create <branch>' first.
|
|
21
|
-
already running is checked without modifying healthy runtime
|
|
22
|
-
|
|
20
|
+
if none is found, run 'ai sandbox create <branch>' first. Without --recreate, a
|
|
21
|
+
container that is already running is checked without modifying healthy runtime
|
|
22
|
+
state. --recreate replaces the existing container even when it is healthy.`;
|
|
23
23
|
|
|
24
24
|
export function parseStartArgs(args: string[]): { target: string; recreate: boolean; help: boolean } {
|
|
25
25
|
let recreate = false;
|
|
@@ -83,6 +83,7 @@ export async function start(args: string[]): Promise<void> {
|
|
|
83
83
|
workspace: target.workspace,
|
|
84
84
|
row: found,
|
|
85
85
|
allowRecreate: parsed.recreate,
|
|
86
|
+
forceRecreate: parsed.recreate,
|
|
86
87
|
recreate: async () => {
|
|
87
88
|
const { create } = await import('./create.ts');
|
|
88
89
|
await create([target.requestedRef, '--no-refresh']);
|
package/lib/sandbox/config.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { normalizeAgentClients } from '../agent-clients/config.ts';
|
|
|
7
7
|
import { isAgentClientId } from '../agent-clients/types.ts';
|
|
8
8
|
import type { AgentClientState } from '../agent-clients/types.ts';
|
|
9
9
|
import { validateSandboxEngine } from './engine.ts';
|
|
10
|
+
import type { SandboxEngineInput } from './engine.ts';
|
|
10
11
|
import { hostJoin } from './engines/wsl2-paths.ts';
|
|
11
12
|
import { findRuntimeEngineMismatches } from './runtime-engines.ts';
|
|
12
13
|
import { parseCustomTools } from './tools.ts';
|
|
@@ -29,7 +30,7 @@ type PlatformFn = typeof platform;
|
|
|
29
30
|
type WriteStderr = (chunk: string) => unknown;
|
|
30
31
|
|
|
31
32
|
type SandboxConfigInput = {
|
|
32
|
-
engine?:
|
|
33
|
+
engine?: SandboxEngineInput;
|
|
33
34
|
runtimes?: string[];
|
|
34
35
|
tools?: string[];
|
|
35
36
|
customTools?: unknown;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { SandboxControlManifest, SandboxControlManifestLike } from './protocol.ts';
|
|
2
|
+
import { commandForEngine, runEngine, runProbe } from '../shell.ts';
|
|
3
|
+
|
|
4
|
+
export type ContainerObservation =
|
|
5
|
+
| Readonly<{ state: 'found'; id: string; running: boolean; labels: Readonly<Record<string, string>> }>
|
|
6
|
+
| Readonly<{ state: 'absent'; id: string }>
|
|
7
|
+
| Readonly<{ state: 'unknown'; reason: string }>;
|
|
8
|
+
|
|
9
|
+
export type ContainerInspection = Readonly<{
|
|
10
|
+
id?: unknown;
|
|
11
|
+
running?: unknown;
|
|
12
|
+
labels?: unknown;
|
|
13
|
+
}>;
|
|
14
|
+
|
|
15
|
+
function isAuthoritativeNotFound(error: unknown): boolean {
|
|
16
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
17
|
+
return /(?:no such (?:container|object)|container .* not found|cannot find container)/i.test(message);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function classifySandboxContainerInspection(
|
|
21
|
+
expected: SandboxControlManifest['containerIdentity'],
|
|
22
|
+
inspection: ContainerInspection | { notFound: true } | { error: unknown }
|
|
23
|
+
): ContainerObservation {
|
|
24
|
+
if ('notFound' in inspection) return { state: 'absent', id: expected.id };
|
|
25
|
+
if ('error' in inspection) {
|
|
26
|
+
return isAuthoritativeNotFound(inspection.error)
|
|
27
|
+
? { state: 'absent', id: expected.id }
|
|
28
|
+
: { state: 'unknown', reason: `container inspection failed: ${inspection.error instanceof Error ? inspection.error.message : String(inspection.error)}` };
|
|
29
|
+
}
|
|
30
|
+
const response = inspection as ContainerInspection;
|
|
31
|
+
if (typeof response.id !== 'string' || response.id !== expected.id
|
|
32
|
+
|| typeof response.running !== 'boolean' || !response.labels
|
|
33
|
+
|| typeof response.labels !== 'object' || Array.isArray(response.labels)) {
|
|
34
|
+
return { state: 'unknown', reason: 'container inspection response is incomplete' };
|
|
35
|
+
}
|
|
36
|
+
const labels = response.labels as Record<string, unknown>;
|
|
37
|
+
for (const [key, value] of Object.entries(expected.labels)) {
|
|
38
|
+
if (labels[key] !== value) return { state: 'unknown', reason: 'container labels do not match the manifest' };
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
state: 'found',
|
|
42
|
+
id: response.id,
|
|
43
|
+
running: response.running,
|
|
44
|
+
labels: Object.fromEntries(
|
|
45
|
+
Object.entries(labels).flatMap(([key, value]) => typeof value === 'string' ? [[key, value]] : [])
|
|
46
|
+
)
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function inspectSandboxControlContainer(
|
|
51
|
+
manifest: SandboxControlManifestLike,
|
|
52
|
+
options: { run?: typeof runEngine; probe?: typeof runProbe; timeoutMs?: number } = {}
|
|
53
|
+
): Promise<ContainerObservation> {
|
|
54
|
+
try {
|
|
55
|
+
const args = ['inspect', '--format', '{{json .}}', manifest.containerIdentity.id];
|
|
56
|
+
let raw: string;
|
|
57
|
+
if (options.run) {
|
|
58
|
+
raw = options.run(manifest.engine, 'docker', args, options.timeoutMs === undefined ? {} : { timeout: options.timeoutMs });
|
|
59
|
+
} else {
|
|
60
|
+
const command = commandForEngine(manifest.engine, 'docker', args);
|
|
61
|
+
const result = (options.probe ?? runProbe)(
|
|
62
|
+
command.cmd,
|
|
63
|
+
command.args,
|
|
64
|
+
options.timeoutMs === undefined ? {} : { timeout: options.timeoutMs }
|
|
65
|
+
);
|
|
66
|
+
if (result.status !== 0) {
|
|
67
|
+
const stderr = typeof result.stderr === 'string' ? result.stderr : result.stderr?.toString('utf8') ?? '';
|
|
68
|
+
return classifySandboxContainerInspection(manifest.containerIdentity, {
|
|
69
|
+
error: new Error(stderr || `container inspection exited with ${result.status ?? 'unknown'}`)
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
raw = typeof result.stdout === 'string' ? result.stdout : result.stdout?.toString('utf8') ?? '';
|
|
73
|
+
}
|
|
74
|
+
const parsed = JSON.parse(raw) as {
|
|
75
|
+
Id?: unknown;
|
|
76
|
+
State?: { Running?: unknown };
|
|
77
|
+
Config?: { Labels?: unknown };
|
|
78
|
+
} | Array<{
|
|
79
|
+
Id?: unknown;
|
|
80
|
+
State?: { Running?: unknown };
|
|
81
|
+
Config?: { Labels?: unknown };
|
|
82
|
+
}>;
|
|
83
|
+
const value = Array.isArray(parsed) && parsed.length === 1 ? parsed[0] : parsed;
|
|
84
|
+
if (!value || Array.isArray(value)) return { state: 'unknown', reason: 'container inspection response is incomplete' };
|
|
85
|
+
return classifySandboxContainerInspection(manifest.containerIdentity, {
|
|
86
|
+
id: value.Id,
|
|
87
|
+
running: value.State?.Running,
|
|
88
|
+
labels: value.Config?.Labels
|
|
89
|
+
});
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return classifySandboxContainerInspection(manifest.containerIdentity, { error });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -15,9 +15,9 @@ export type SandboxControlExecutionResult = {
|
|
|
15
15
|
|
|
16
16
|
export type PreparedSandboxControlExecution = {
|
|
17
17
|
execution: SandboxControlExecution;
|
|
18
|
-
start(): void;
|
|
18
|
+
start(canWrite?: () => boolean): void;
|
|
19
19
|
completion: Promise<SandboxControlExecutionResult>;
|
|
20
|
-
terminate(): boolean;
|
|
20
|
+
terminate(updateState?: boolean): boolean;
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
function safeEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
@@ -30,11 +30,11 @@ export function nodeEntryArgs(entry: string, args: string[]): string[] {
|
|
|
30
30
|
: [entry, ...args];
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
async function waitForStartTime(pid: number, timeoutMs = 2_000): Promise<
|
|
33
|
+
async function waitForStartTime(pid: number, timeoutMs = 2_000): Promise<number> {
|
|
34
34
|
const deadline = Date.now() + timeoutMs;
|
|
35
35
|
while (Date.now() < deadline) {
|
|
36
36
|
const startTime = getProcessStartTime(pid);
|
|
37
|
-
if (startTime) return startTime;
|
|
37
|
+
if (startTime !== null) return startTime;
|
|
38
38
|
await new Promise<void>((resolve) => setTimeout(resolve, 10));
|
|
39
39
|
}
|
|
40
40
|
throw new Error('SANDBOX_CONTROL_EXECUTOR_IDENTITY_UNAVAILABLE');
|
|
@@ -57,7 +57,11 @@ export async function prepareSandboxControlExecution(params: {
|
|
|
57
57
|
cwd: params.manifest.repoRoot,
|
|
58
58
|
detached: process.platform !== 'win32',
|
|
59
59
|
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
|
60
|
-
env: {
|
|
60
|
+
env: {
|
|
61
|
+
...safeEnv(process.env),
|
|
62
|
+
AGENT_INFRA_EXECUTOR_MANIFEST: params.manifestPath,
|
|
63
|
+
AGENT_INFRA_RUNTIME_DIR: params.manifest.runtimeDir
|
|
64
|
+
}
|
|
61
65
|
}
|
|
62
66
|
);
|
|
63
67
|
if (typeof child.pid !== 'number') throw new Error('SANDBOX_CONTROL_EXECUTOR_SPAWN_FAILED');
|
|
@@ -73,7 +77,7 @@ export async function prepareSandboxControlExecution(params: {
|
|
|
73
77
|
});
|
|
74
78
|
const startTime = await waitForStartTime(child.pid);
|
|
75
79
|
const execution: SandboxControlExecution = {
|
|
76
|
-
version:
|
|
80
|
+
version: 2,
|
|
77
81
|
generation: params.manifest.generation,
|
|
78
82
|
requestId: params.request.id,
|
|
79
83
|
nonce,
|
|
@@ -88,16 +92,20 @@ export async function prepareSandboxControlExecution(params: {
|
|
|
88
92
|
atomicWriteJson(executionPath(params.manifest, params.request.id), execution);
|
|
89
93
|
return {
|
|
90
94
|
execution,
|
|
91
|
-
start() {
|
|
95
|
+
start(canWrite = () => true) {
|
|
92
96
|
if (!child.connected) throw new Error('SANDBOX_CONTROL_EXECUTOR_GATE_CLOSED');
|
|
97
|
+
if (!canWrite()) throw new Error('SANDBOX_CONTROL_OWNER_LOST');
|
|
93
98
|
atomicWriteJson(executionPath(params.manifest, params.request.id), { ...execution, phase: 'running', updatedAt: Date.now() });
|
|
99
|
+
if (!canWrite()) throw new Error('SANDBOX_CONTROL_OWNER_LOST');
|
|
94
100
|
child.send({ version: 1, nonce });
|
|
95
101
|
},
|
|
96
102
|
completion,
|
|
97
|
-
terminate() {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
terminate(updateState = true) {
|
|
104
|
+
if (updateState) {
|
|
105
|
+
atomicWriteJson(executionPath(params.manifest, params.request.id), {
|
|
106
|
+
...execution, phase: 'terminating', updatedAt: Date.now()
|
|
107
|
+
});
|
|
108
|
+
}
|
|
101
109
|
return terminateSandboxControlExecution(execution);
|
|
102
110
|
}
|
|
103
111
|
};
|
|
@@ -138,7 +146,11 @@ function executeRequest(manifest: SandboxControlManifest, request: SandboxContro
|
|
|
138
146
|
const result = spawnSync(
|
|
139
147
|
process.execPath,
|
|
140
148
|
nodeEntryArgs(process.argv[1]!, [request.family, ...boundArgs]),
|
|
141
|
-
{
|
|
149
|
+
{
|
|
150
|
+
cwd: manifest.repoRoot,
|
|
151
|
+
encoding: 'utf8',
|
|
152
|
+
env: { ...safeEnv(process.env), AGENT_INFRA_RUNTIME_DIR: manifest.runtimeDir }
|
|
153
|
+
}
|
|
142
154
|
);
|
|
143
155
|
return { exitCode: result.status ?? 1, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
|
|
144
156
|
}
|