@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
|
@@ -17,9 +17,11 @@ import { parseSandboxWorkspaceIdentity, resolveSandboxTarget, sameSandboxWorkspa
|
|
|
17
17
|
import { createSandboxCapabilityPlan, runSandboxHooks, runBoundedSandboxHookCommand } from '../agent-client-reconciler.js';
|
|
18
18
|
import { resolveTools, tmpfsSeedStagingPath, tmpfsSeedTargetPath, toolConfigDirCandidates } from '../tools.js';
|
|
19
19
|
import { assertFreshSandboxReady, hydrateTmpfsSeedEntries, prepareTmpfsMounts, startSandboxControlBroker } from '../recovery.js';
|
|
20
|
+
import { acquireSandboxControlReplacement, assertSandboxControlCutoverSnapshot, beginSandboxControlReplacement, captureSandboxControlCutoverSnapshot, commitSandboxControlReplacement, quiesceSandboxControlRoot, recoverSandboxControlReplacement, readSandboxControlManifestForTransition } from '../control/lifecycle.js';
|
|
21
|
+
import { inspectSandboxControlContainer } from '../control/container-identity.js';
|
|
20
22
|
import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.js';
|
|
21
23
|
import { sandboxCoreBindMounts } from '../mounts.js';
|
|
22
|
-
import { assertSandboxTaskSource, materializeSandboxControl, materializeSandboxWorkspaceView, prepareSandboxWorkspaceMountTargets } from '../workspace-view.js';
|
|
24
|
+
import { assertSandboxTaskSource, finalizeSandboxControlManifest, materializeSandboxControl, materializeSandboxWorkspaceView, prepareSandboxWorkspaceMountTargets, sandboxControlPaths, } from '../workspace-view.js';
|
|
23
25
|
import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.js';
|
|
24
26
|
import { validateSelinuxDisableEnv } from '../engines/selinux.js';
|
|
25
27
|
import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.js';
|
|
@@ -702,6 +704,12 @@ export async function create(args) {
|
|
|
702
704
|
}
|
|
703
705
|
const effectiveResolvedTools = resolvedTools;
|
|
704
706
|
const container = containerName(effectiveConfig, branch);
|
|
707
|
+
const controlPaths = sandboxControlPaths({
|
|
708
|
+
base: effectiveConfig.controlBase,
|
|
709
|
+
project: effectiveConfig.project,
|
|
710
|
+
container,
|
|
711
|
+
identity: target.workspace
|
|
712
|
+
});
|
|
705
713
|
const worktree = worktreeCandidates.find((candidate) => fs.existsSync(candidate)) ?? worktreeCandidates[0] ?? '';
|
|
706
714
|
const shareCommon = shareCommonDir(effectiveConfig);
|
|
707
715
|
const shareBranch = shareBranchDir(effectiveConfig, branch);
|
|
@@ -863,292 +871,414 @@ export async function create(args) {
|
|
|
863
871
|
{
|
|
864
872
|
title: `Starting container '${container}'`,
|
|
865
873
|
task: async (message) => {
|
|
866
|
-
const
|
|
867
|
-
const
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
874
|
+
const hadControlRootBeforeAcquire = fs.existsSync(controlPaths.root);
|
|
875
|
+
const replacementLease = acquireSandboxControlReplacement(controlPaths.root);
|
|
876
|
+
let replacementLeaseHeld = true;
|
|
877
|
+
let replacementCutover = null;
|
|
878
|
+
try {
|
|
879
|
+
const recoveryResult = await recoverSandboxControlReplacement(controlPaths.root, replacementLease);
|
|
880
|
+
const hadExistingControlRoot = hadControlRootBeforeAcquire || recoveryResult === 'restored';
|
|
881
|
+
const previousManifest = fs.existsSync(controlPaths.manifestPath)
|
|
882
|
+
? readSandboxControlManifestForTransition(controlPaths.manifestPath)
|
|
883
|
+
: null;
|
|
884
|
+
if (hadExistingControlRoot) {
|
|
885
|
+
await quiesceSandboxControlRoot(controlPaths.root);
|
|
886
|
+
}
|
|
887
|
+
const previousCutoverSnapshot = hadExistingControlRoot
|
|
888
|
+
? captureSandboxControlCutoverSnapshot(controlPaths.root)
|
|
889
|
+
: null;
|
|
890
|
+
const existing = runSafeEngine(engine, 'docker', ['ps', '-a', '--format', '{{.Names}}']).split('\n').filter(Boolean);
|
|
891
|
+
const matchedContainers = containerNameCandidates(effectiveConfig, branch)
|
|
892
|
+
.filter((name) => existing.includes(name));
|
|
893
|
+
if (matchedContainers.length > 0) {
|
|
894
|
+
for (const name of matchedContainers) {
|
|
895
|
+
const rawLabels = runSafeEngine(engine, 'docker', [
|
|
896
|
+
'inspect', '--format', '{{json .Config.Labels}}', name
|
|
897
|
+
]).trim();
|
|
898
|
+
let labels = {};
|
|
899
|
+
try {
|
|
900
|
+
labels = JSON.parse(rawLabels);
|
|
901
|
+
}
|
|
902
|
+
catch {
|
|
903
|
+
labels = {};
|
|
904
|
+
}
|
|
905
|
+
const existingIdentity = parseSandboxWorkspaceIdentity(labels, {
|
|
906
|
+
mode: sandboxWorkspaceModeLabel(effectiveConfig),
|
|
907
|
+
taskId: sandboxTaskIdLabel(effectiveConfig)
|
|
908
|
+
});
|
|
909
|
+
if (!sameSandboxWorkspaceIdentity(existingIdentity, target.workspace)) {
|
|
910
|
+
const existingDescription = existingIdentity.mode === 'task-bound'
|
|
911
|
+
? `task-bound:${existingIdentity.taskId}`
|
|
912
|
+
: existingIdentity.mode;
|
|
913
|
+
const requestedDescription = target.workspace.mode === 'task-bound'
|
|
914
|
+
? `task-bound:${target.workspace.taskId}`
|
|
915
|
+
: target.workspace.mode;
|
|
916
|
+
const recoveryTarget = target.workspace.mode === 'task-bound'
|
|
917
|
+
? target.workspace.taskId
|
|
918
|
+
: branch;
|
|
919
|
+
throw new Error(`SANDBOX_WORKSPACE_IDENTITY_CONFLICT: container '${name}' is ${existingDescription}, `
|
|
920
|
+
+ `but this request is ${requestedDescription}. `
|
|
921
|
+
+ `Run 'ai sandbox start --recreate ${recoveryTarget}' on the host, then retry.`);
|
|
922
|
+
}
|
|
877
923
|
}
|
|
878
|
-
|
|
879
|
-
|
|
924
|
+
message('Removing old container instance...');
|
|
925
|
+
for (const name of matchedContainers) {
|
|
926
|
+
runSafeEngine(engine, 'docker', ['stop', name]);
|
|
927
|
+
runSafeEngine(engine, 'docker', ['rm', name]);
|
|
880
928
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
: existingIdentity.mode;
|
|
889
|
-
const requestedDescription = target.workspace.mode === 'task-bound'
|
|
890
|
-
? `task-bound:${target.workspace.taskId}`
|
|
891
|
-
: target.workspace.mode;
|
|
892
|
-
const recoveryTarget = target.workspace.mode === 'task-bound'
|
|
893
|
-
? target.workspace.taskId
|
|
894
|
-
: branch;
|
|
895
|
-
throw new Error(`SANDBOX_WORKSPACE_IDENTITY_CONFLICT: container '${name}' is ${existingDescription}, `
|
|
896
|
-
+ `but this request is ${requestedDescription}. `
|
|
897
|
-
+ `Run 'ai sandbox start --recreate ${recoveryTarget}' on the host, then retry.`);
|
|
929
|
+
}
|
|
930
|
+
if (previousManifest) {
|
|
931
|
+
const oldContainer = await inspectSandboxControlContainer(previousManifest);
|
|
932
|
+
if (oldContainer.state !== 'absent') {
|
|
933
|
+
throw new Error(oldContainer.state === 'unknown'
|
|
934
|
+
? `SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${oldContainer.reason}`
|
|
935
|
+
: 'SANDBOX_CONTROL_CONTAINER_STILL_EXISTS');
|
|
898
936
|
}
|
|
899
937
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
runSafeEngine(engine, 'docker', ['rm', name]);
|
|
938
|
+
const aliasesFile = ensureSandboxAliasesFile(effectiveConfig.home, capabilityPlan.aliases);
|
|
939
|
+
if (aliasesFile.created) {
|
|
940
|
+
message(`Created default sandbox aliases at ${aliasesFile.path}`);
|
|
904
941
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
const readmeResults = ensureSandboxDiscoveryReadmes(effectiveConfig, branch);
|
|
911
|
-
for (const { created, path: readmePath } of readmeResults) {
|
|
912
|
-
if (created) {
|
|
913
|
-
message(`Created discovery README at ${readmePath}`);
|
|
942
|
+
const readmeResults = ensureSandboxDiscoveryReadmes(effectiveConfig, branch);
|
|
943
|
+
for (const { created, path: readmePath } of readmeResults) {
|
|
944
|
+
if (created) {
|
|
945
|
+
message(`Created discovery README at ${readmePath}`);
|
|
946
|
+
}
|
|
914
947
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
:
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
additionalEntries: proxyEntries
|
|
931
|
-
});
|
|
932
|
-
let hostShellConfig;
|
|
933
|
-
let tmpfsSeedPlan = [];
|
|
934
|
-
try {
|
|
935
|
-
const beforeCreateResults = await runSandboxHooks({
|
|
936
|
-
hooks: capabilityPlan.hooksByPhase['before-container-create'],
|
|
937
|
-
phase: 'before-container-create',
|
|
938
|
-
context: {
|
|
939
|
-
config: effectiveConfig,
|
|
940
|
-
plan: capabilityPlan,
|
|
941
|
-
create: {
|
|
942
|
-
hostHome: effectiveConfig.home,
|
|
943
|
-
hostEnv: { ...process.env },
|
|
944
|
-
project: effectiveConfig.project,
|
|
945
|
-
resolvedTools: effectiveResolvedTools
|
|
946
|
-
}
|
|
947
|
-
},
|
|
948
|
-
runCommand: runBoundedSandboxHookCommand
|
|
948
|
+
const gitconfigPath = path.join(effectiveConfig.home, '.gitconfig');
|
|
949
|
+
const gitconfigContent = fs.existsSync(gitconfigPath)
|
|
950
|
+
? fs.readFileSync(gitconfigPath, 'utf8')
|
|
951
|
+
: '';
|
|
952
|
+
const needsGpg = detectGpgConfig(gitconfigContent);
|
|
953
|
+
const hasHostGpgKeys = needsGpg && hostHasGpgKeys(effectiveConfig.home);
|
|
954
|
+
const signingKey = needsGpg
|
|
955
|
+
? getGitSigningKey({ repoPath: worktree, home: effectiveConfig.home })
|
|
956
|
+
: null;
|
|
957
|
+
const cachedGpg = needsGpg
|
|
958
|
+
? readGpgCache(effectiveConfig.home, effectiveConfig.project, undefined, signingKey)
|
|
959
|
+
: null;
|
|
960
|
+
const proxyEntries = values['inherit-proxy'] ? collectHostProxyEntries(process.env) : [];
|
|
961
|
+
const envFile = buildContainerEnvFile(effectiveResolvedTools, engine, undefined, {
|
|
962
|
+
additionalEntries: proxyEntries
|
|
949
963
|
});
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
964
|
+
let hostShellConfig;
|
|
965
|
+
let tmpfsSeedPlan = [];
|
|
966
|
+
try {
|
|
967
|
+
const beforeCreateResults = await runSandboxHooks({
|
|
968
|
+
hooks: capabilityPlan.hooksByPhase['before-container-create'],
|
|
969
|
+
phase: 'before-container-create',
|
|
970
|
+
context: {
|
|
971
|
+
config: effectiveConfig,
|
|
972
|
+
plan: capabilityPlan,
|
|
973
|
+
create: {
|
|
974
|
+
hostHome: effectiveConfig.home,
|
|
975
|
+
hostEnv: { ...process.env },
|
|
976
|
+
project: effectiveConfig.project,
|
|
977
|
+
resolvedTools: effectiveResolvedTools
|
|
978
|
+
}
|
|
979
|
+
},
|
|
980
|
+
runCommand: runBoundedSandboxHookCommand
|
|
981
|
+
});
|
|
982
|
+
const beforeCreateFailure = beforeCreateResults.find((result) => result.status === 'fatal');
|
|
983
|
+
if (beforeCreateFailure) {
|
|
984
|
+
throw new Error(beforeCreateFailure.message
|
|
985
|
+
?? `Sandbox hook '${beforeCreateFailure.hookId}' failed.`);
|
|
986
|
+
}
|
|
987
|
+
const toolVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) => tool.tmpfs ? [] : ['-v', volumeArg(engine, dir, tool.containerMount)]);
|
|
988
|
+
const tmpfsArgs = effectiveResolvedTools.flatMap(({ tool }) => tool.tmpfs ? buildTmpfsRunArgs(tool.containerMount, tool.tmpfs) : []);
|
|
989
|
+
const workspaceView = materializeSandboxWorkspaceView({
|
|
990
|
+
base: effectiveConfig.workspaceViewBase,
|
|
991
|
+
project: effectiveConfig.project,
|
|
992
|
+
container,
|
|
993
|
+
identity: target.workspace
|
|
994
|
+
});
|
|
995
|
+
prepareSandboxWorkspaceMountTargets(worktree);
|
|
996
|
+
if (previousCutoverSnapshot) {
|
|
997
|
+
assertSandboxControlCutoverSnapshot(previousCutoverSnapshot);
|
|
998
|
+
if (previousManifest) {
|
|
999
|
+
const finalOldContainer = await inspectSandboxControlContainer(previousManifest);
|
|
1000
|
+
if (finalOldContainer.state !== 'absent') {
|
|
1001
|
+
throw new Error(finalOldContainer.state === 'unknown'
|
|
1002
|
+
? `SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${finalOldContainer.reason}`
|
|
1003
|
+
: 'SANDBOX_CONTROL_CONTAINER_REAPPEARED');
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
replacementCutover = beginSandboxControlReplacement(controlPaths.root, replacementLease);
|
|
1007
|
+
}
|
|
1008
|
+
const control = materializeSandboxControl({
|
|
1009
|
+
base: effectiveConfig.controlBase,
|
|
1010
|
+
repoRoot: effectiveConfig.repoRoot,
|
|
1011
|
+
worktreeRoot: worktree,
|
|
1012
|
+
project: effectiveConfig.project,
|
|
1013
|
+
container,
|
|
1014
|
+
branch,
|
|
1015
|
+
identity: target.workspace,
|
|
1016
|
+
engine,
|
|
1017
|
+
replacementLease
|
|
1018
|
+
});
|
|
1019
|
+
hostShellConfig = prepareHostShellConfig({
|
|
1020
|
+
home: effectiveConfig.home,
|
|
1021
|
+
project: effectiveConfig.project,
|
|
1022
|
+
branch,
|
|
1023
|
+
repoRoot: effectiveConfig.repoRoot
|
|
1024
|
+
});
|
|
1025
|
+
const coreVolumes = sandboxCoreBindMounts(effectiveConfig, branch, {
|
|
1026
|
+
worktree,
|
|
1027
|
+
shellConfigHostDir: hostShellConfig.hostDir,
|
|
1028
|
+
workspaceViewRoot: workspaceView.root,
|
|
1029
|
+
controlDir: control.channelDir,
|
|
1030
|
+
controlStatusDir: control.statusDir,
|
|
1031
|
+
...(target.workspace.mode === 'task-bound' ? { runtimeDir: control.runtimeDir } : {}),
|
|
1032
|
+
...(target.workspace.mode === 'task-bound'
|
|
1033
|
+
? {
|
|
1034
|
+
taskSource: assertSandboxTaskSource(effectiveConfig.repoRoot, target.workspace.taskId),
|
|
1035
|
+
taskId: target.workspace.taskId
|
|
1036
|
+
}
|
|
1037
|
+
: {})
|
|
1038
|
+
}).flatMap(({ hostPaths, containerPath, readOnly }) => [
|
|
1039
|
+
'-v',
|
|
1040
|
+
volumeArg(engine, hostPaths[0], containerPath, readOnly ? ':ro' : '')
|
|
1041
|
+
]);
|
|
1042
|
+
// Mount each declared seed read-only at an isolated staging path.
|
|
1043
|
+
// After docker run mounts the empty tmpfs, the container's default
|
|
1044
|
+
// user copies these entries into it so the runtime targets are normal,
|
|
1045
|
+
// writable files rather than nested mount points. The allowlist keeps
|
|
1046
|
+
// stale runtime files (logs_2.sqlite, sessions, etc.) on the host from
|
|
1047
|
+
// being exposed or written through.
|
|
1048
|
+
tmpfsSeedPlan = effectiveResolvedTools.flatMap(({ tool, dir }) => (tool.tmpfs?.seed ?? []).flatMap((entry, index) => {
|
|
1049
|
+
const targetPath = tmpfsSeedTargetPath(tool.containerMount, entry);
|
|
1050
|
+
const hostPath = path.join(dir, entry);
|
|
1051
|
+
if (!fs.existsSync(hostPath)) {
|
|
1052
|
+
return [];
|
|
1053
|
+
}
|
|
1054
|
+
const stagingPath = tmpfsSeedStagingPath(tool.id, index);
|
|
1055
|
+
return [{
|
|
1056
|
+
toolId: tool.id,
|
|
1057
|
+
containerMount: tool.containerMount,
|
|
1058
|
+
stagingPath,
|
|
1059
|
+
targetPath,
|
|
1060
|
+
volumeArgs: ['-v', volumeArg(engine, hostPath, stagingPath, ':ro')]
|
|
1061
|
+
}];
|
|
1062
|
+
}));
|
|
1063
|
+
const liveMountVolumes = effectiveResolvedTools.flatMap(({ tool }) => (tool.hostLiveMounts ?? [])
|
|
1064
|
+
.filter(({ hostPath }) => fs.existsSync(hostPath))
|
|
1065
|
+
.flatMap(({ hostPath, containerSubpath }) => [
|
|
1066
|
+
'-v',
|
|
1067
|
+
volumeArg(engine, hostPath, path.posix.join(tool.containerMount, containerSubpath))
|
|
1068
|
+
]));
|
|
1069
|
+
fs.mkdirSync(shareCommon, { recursive: true });
|
|
1070
|
+
fs.mkdirSync(shareBranch, { recursive: true });
|
|
1071
|
+
fs.mkdirSync(clipboardHostDir(effectiveConfig.home), { recursive: true, mode: 0o700 });
|
|
1072
|
+
const dotfilesSnapshot = materializeDotfiles(effectiveConfig.dotfilesDir, dotfilesCacheDir(effectiveConfig.home, effectiveConfig.project));
|
|
1073
|
+
const dotfilesMount = dotfilesSnapshot
|
|
1074
|
+
? buildDotfilesVolumeArgs(engine, dotfilesSnapshot.cacheDir)
|
|
1075
|
+
: [];
|
|
1076
|
+
const hostTz = detectHostTimezone();
|
|
1077
|
+
const tzFlags = hostTz ? ['-e', `TZ=${hostTz}`] : [];
|
|
1078
|
+
let createdContainerRef = null;
|
|
1079
|
+
try {
|
|
1080
|
+
const dockerRunId = runEngineTaskCommand(engine, 'docker', [
|
|
1081
|
+
'run',
|
|
1082
|
+
'-d',
|
|
1083
|
+
'--init',
|
|
1084
|
+
'--name',
|
|
1085
|
+
container,
|
|
1086
|
+
'--hostname',
|
|
1087
|
+
`${effectiveConfig.project}-sandbox`,
|
|
1088
|
+
'--label',
|
|
1089
|
+
sandboxLabel(effectiveConfig),
|
|
1090
|
+
'--label',
|
|
1091
|
+
`${sandboxBranchLabel(effectiveConfig)}=${branch}`,
|
|
1092
|
+
'--label',
|
|
1093
|
+
`${sandboxWorkspaceModeLabel(effectiveConfig)}=${target.workspace.mode}`,
|
|
1094
|
+
...(target.workspace.mode === 'task-bound'
|
|
1095
|
+
? [
|
|
1096
|
+
'--label',
|
|
1097
|
+
`${sandboxTaskIdLabel(effectiveConfig)}=${target.workspace.taskId}`,
|
|
1098
|
+
'-e',
|
|
1099
|
+
`AGENT_INFRA_TASK_ID=${target.workspace.taskId}`
|
|
1100
|
+
]
|
|
1101
|
+
: []),
|
|
1102
|
+
'-e',
|
|
1103
|
+
`AGENT_INFRA_CONTROL_TOKEN=${control.token}`,
|
|
1104
|
+
'-e',
|
|
1105
|
+
`AGENT_INFRA_CONTROL_GENERATION=${control.generation}`,
|
|
1106
|
+
'-e',
|
|
1107
|
+
'AGENT_INFRA_CONTROL_DIR=/run/agent-infra/control',
|
|
1108
|
+
'-e',
|
|
1109
|
+
'AGENT_INFRA_CONTROL_STATUS_DIR=/run/agent-infra/control-status',
|
|
1110
|
+
...(target.workspace.mode === 'task-bound'
|
|
1111
|
+
? ['-e', 'AGENT_INFRA_RUNTIME_DIR=/run/agent-infra/runtime']
|
|
1112
|
+
: []),
|
|
1113
|
+
'--label',
|
|
1114
|
+
`${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
|
|
1115
|
+
...coreVolumes,
|
|
1116
|
+
...buildClipboardVolumeArgs(engine, effectiveConfig.home),
|
|
1117
|
+
'-v',
|
|
1118
|
+
volumeArg(engine, path.join(effectiveConfig.repoRoot, '.git'), `${toEnginePath(engine, effectiveConfig.repoRoot)}/.git`),
|
|
1119
|
+
...dotfilesMount,
|
|
1120
|
+
...toolVolumes,
|
|
1121
|
+
...tmpfsArgs,
|
|
1122
|
+
...tmpfsSeedPlan.flatMap(({ volumeArgs }) => volumeArgs),
|
|
1123
|
+
...liveMountVolumes,
|
|
1124
|
+
...envFile.dockerArgs,
|
|
1125
|
+
...tzFlags,
|
|
1126
|
+
'-w',
|
|
1127
|
+
'/workspace',
|
|
1128
|
+
effectiveConfig.imageName
|
|
1129
|
+
]);
|
|
1130
|
+
createdContainerRef = dockerRunId.trim();
|
|
1131
|
+
const containerId = runEngineTaskCommand(engine, 'docker', [
|
|
1132
|
+
'inspect', '--format', '{{.Id}}', dockerRunId.trim()
|
|
1133
|
+
]).trim();
|
|
1134
|
+
if (!containerId)
|
|
1135
|
+
throw new Error('SANDBOX_CONTROL_CONTAINER_ID_INVALID');
|
|
1136
|
+
const rawContainerLabels = runEngineTaskCommand(engine, 'docker', [
|
|
1137
|
+
'inspect', '--format', '{{json .Config.Labels}}', containerId
|
|
1138
|
+
]).trim();
|
|
1139
|
+
let inspectedLabels;
|
|
1140
|
+
try {
|
|
1141
|
+
inspectedLabels = JSON.parse(rawContainerLabels);
|
|
1142
|
+
}
|
|
1143
|
+
catch {
|
|
1144
|
+
throw new Error('SANDBOX_CONTROL_CONTAINER_LABELS_INVALID');
|
|
1145
|
+
}
|
|
1146
|
+
if (!inspectedLabels || typeof inspectedLabels !== 'object' || Array.isArray(inspectedLabels)) {
|
|
1147
|
+
throw new Error('SANDBOX_CONTROL_CONTAINER_LABELS_INVALID');
|
|
1148
|
+
}
|
|
1149
|
+
const expectedLabels = {
|
|
1150
|
+
[sandboxLabel(effectiveConfig)]: '',
|
|
1151
|
+
[sandboxBranchLabel(effectiveConfig)]: branch,
|
|
1152
|
+
[sandboxWorkspaceModeLabel(effectiveConfig)]: target.workspace.mode,
|
|
1153
|
+
[sandboxRuntimeCapabilityLabel(effectiveConfig)]: capabilityPlan.runtimeSignature,
|
|
1154
|
+
...(target.workspace.mode === 'task-bound'
|
|
1155
|
+
? { [sandboxTaskIdLabel(effectiveConfig)]: target.workspace.taskId }
|
|
1156
|
+
: {})
|
|
1157
|
+
};
|
|
1158
|
+
const labels = inspectedLabels;
|
|
1159
|
+
for (const [key, expected] of Object.entries(expectedLabels)) {
|
|
1160
|
+
if (labels[key] !== expected)
|
|
1161
|
+
throw new Error('SANDBOX_CONTROL_CONTAINER_IDENTITY_MISMATCH');
|
|
1162
|
+
}
|
|
1163
|
+
finalizeSandboxControlManifest(control, {
|
|
1164
|
+
engine,
|
|
1165
|
+
id: containerId,
|
|
1166
|
+
labels: expectedLabels
|
|
1167
|
+
});
|
|
1168
|
+
replacementLease.clearQuiescing();
|
|
1169
|
+
await startSandboxControlBroker(effectiveConfig.repoRoot, control.manifestPath);
|
|
1170
|
+
if (replacementCutover) {
|
|
1171
|
+
commitSandboxControlReplacement(control.root, replacementLease);
|
|
1172
|
+
replacementCutover = null;
|
|
1173
|
+
}
|
|
1174
|
+
createdTmpfsSeedPlan = tmpfsSeedPlan;
|
|
1175
|
+
}
|
|
1176
|
+
catch (error) {
|
|
1177
|
+
if (createdContainerRef) {
|
|
1178
|
+
runSafeEngine(engine, 'docker', ['stop', createdContainerRef]);
|
|
1179
|
+
runSafeEngine(engine, 'docker', ['rm', createdContainerRef]);
|
|
1180
|
+
}
|
|
1181
|
+
if (!hadExistingControlRoot)
|
|
1182
|
+
removeDirRecursive(control.root);
|
|
1183
|
+
if (replacementCutover) {
|
|
1184
|
+
try {
|
|
1185
|
+
await recoverSandboxControlReplacement(control.root, replacementLease);
|
|
1186
|
+
replacementCutover = null;
|
|
1187
|
+
}
|
|
1188
|
+
catch {
|
|
1189
|
+
// Preserve the cutover state for the next explicit recreate to recover.
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
removeDirRecursive(workspaceView.root);
|
|
1193
|
+
throw error;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
finally {
|
|
1197
|
+
envFile.cleanup();
|
|
954
1198
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
const workspaceView = materializeSandboxWorkspaceView({
|
|
958
|
-
base: effectiveConfig.workspaceViewBase,
|
|
959
|
-
project: effectiveConfig.project,
|
|
1199
|
+
prepareTmpfsMounts({
|
|
1200
|
+
engine,
|
|
960
1201
|
container,
|
|
961
|
-
|
|
1202
|
+
mountPaths: effectiveResolvedTools
|
|
1203
|
+
.filter(({ tool }) => tool.tmpfs)
|
|
1204
|
+
.map(({ tool }) => tool.containerMount)
|
|
962
1205
|
});
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
base: effectiveConfig.controlBase,
|
|
966
|
-
repoRoot: effectiveConfig.repoRoot,
|
|
967
|
-
worktreeRoot: worktree,
|
|
968
|
-
project: effectiveConfig.project,
|
|
1206
|
+
hydrateTmpfsSeedEntries({
|
|
1207
|
+
engine,
|
|
969
1208
|
container,
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
});
|
|
973
|
-
hostShellConfig = prepareHostShellConfig({
|
|
974
|
-
home: effectiveConfig.home,
|
|
975
|
-
project: effectiveConfig.project,
|
|
976
|
-
branch,
|
|
977
|
-
repoRoot: effectiveConfig.repoRoot
|
|
1209
|
+
entries: tmpfsSeedPlan,
|
|
1210
|
+
replace: true
|
|
978
1211
|
});
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1212
|
+
// Belt-and-suspenders: re-create the four shell-config symlinks at
|
|
1213
|
+
// runtime so users with a custom `sandbox.dockerfile` (which won't
|
|
1214
|
+
// include the ai-tools.dockerfile symlink fragment) still get
|
|
1215
|
+
// ~/.gitconfig and friends pointing into the host bind-mount.
|
|
1216
|
+
// `ln -sf` is idempotent for the default image.
|
|
1217
|
+
ensureShellConfigSymlinks(engine, container);
|
|
1218
|
+
if (needsGpg) {
|
|
1219
|
+
message(cachedGpg
|
|
1220
|
+
? 'Syncing GPG keys from cache...'
|
|
1221
|
+
: hasHostGpgKeys
|
|
1222
|
+
? 'Syncing GPG keys (you may be prompted for your passphrase)...'
|
|
1223
|
+
: 'Checking GPG cache before falling back to stripped git config...');
|
|
1224
|
+
try {
|
|
1225
|
+
if (syncGpgKeys(container, effectiveConfig.home, effectiveConfig.project, undefined, undefined, {
|
|
1226
|
+
cachedOverride: cachedGpg,
|
|
1227
|
+
repoPath: worktree,
|
|
1228
|
+
signingKey,
|
|
1229
|
+
dockerExecFn: (cmd, args, opts) => execEngine(engine, cmd, args, opts),
|
|
1230
|
+
dockerRunSafeFn: (cmd, args, opts) => runSafeEngine(engine, cmd, args, opts)
|
|
1231
|
+
})) {
|
|
1232
|
+
writeSanitizedGitconfig({
|
|
1233
|
+
home: effectiveConfig.home,
|
|
1234
|
+
hostConfigDir: hostShellConfig.hostDir,
|
|
1235
|
+
stripGpg: false,
|
|
1236
|
+
repoRoot: effectiveConfig.repoRoot
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
else {
|
|
1240
|
+
message(hasHostGpgKeys
|
|
1241
|
+
? 'GPG key sync failed; using stripped git config fallback...'
|
|
1242
|
+
: 'Host GPG keys unavailable; using stripped git config fallback...');
|
|
989
1243
|
}
|
|
990
|
-
: {})
|
|
991
|
-
}).flatMap(({ hostPaths, containerPath, readOnly }) => [
|
|
992
|
-
'-v',
|
|
993
|
-
volumeArg(engine, hostPaths[0], containerPath, readOnly ? ':ro' : '')
|
|
994
|
-
]);
|
|
995
|
-
// Mount each declared seed read-only at an isolated staging path.
|
|
996
|
-
// After docker run mounts the empty tmpfs, the container's default
|
|
997
|
-
// user copies these entries into it so the runtime targets are normal,
|
|
998
|
-
// writable files rather than nested mount points. The allowlist keeps
|
|
999
|
-
// stale runtime files (logs_2.sqlite, sessions, etc.) on the host from
|
|
1000
|
-
// being exposed or written through.
|
|
1001
|
-
tmpfsSeedPlan = effectiveResolvedTools.flatMap(({ tool, dir }) => (tool.tmpfs?.seed ?? []).flatMap((entry, index) => {
|
|
1002
|
-
const targetPath = tmpfsSeedTargetPath(tool.containerMount, entry);
|
|
1003
|
-
const hostPath = path.join(dir, entry);
|
|
1004
|
-
if (!fs.existsSync(hostPath)) {
|
|
1005
|
-
return [];
|
|
1006
|
-
}
|
|
1007
|
-
const stagingPath = tmpfsSeedStagingPath(tool.id, index);
|
|
1008
|
-
return [{
|
|
1009
|
-
toolId: tool.id,
|
|
1010
|
-
containerMount: tool.containerMount,
|
|
1011
|
-
stagingPath,
|
|
1012
|
-
targetPath,
|
|
1013
|
-
volumeArgs: ['-v', volumeArg(engine, hostPath, stagingPath, ':ro')]
|
|
1014
|
-
}];
|
|
1015
|
-
}));
|
|
1016
|
-
const liveMountVolumes = effectiveResolvedTools.flatMap(({ tool }) => (tool.hostLiveMounts ?? [])
|
|
1017
|
-
.filter(({ hostPath }) => fs.existsSync(hostPath))
|
|
1018
|
-
.flatMap(({ hostPath, containerSubpath }) => [
|
|
1019
|
-
'-v',
|
|
1020
|
-
volumeArg(engine, hostPath, path.posix.join(tool.containerMount, containerSubpath))
|
|
1021
|
-
]));
|
|
1022
|
-
fs.mkdirSync(shareCommon, { recursive: true });
|
|
1023
|
-
fs.mkdirSync(shareBranch, { recursive: true });
|
|
1024
|
-
fs.mkdirSync(clipboardHostDir(effectiveConfig.home), { recursive: true, mode: 0o700 });
|
|
1025
|
-
const dotfilesSnapshot = materializeDotfiles(effectiveConfig.dotfilesDir, dotfilesCacheDir(effectiveConfig.home, effectiveConfig.project));
|
|
1026
|
-
const dotfilesMount = dotfilesSnapshot
|
|
1027
|
-
? buildDotfilesVolumeArgs(engine, dotfilesSnapshot.cacheDir)
|
|
1028
|
-
: [];
|
|
1029
|
-
const hostTz = detectHostTimezone();
|
|
1030
|
-
const tzFlags = hostTz ? ['-e', `TZ=${hostTz}`] : [];
|
|
1031
|
-
runEngineTaskCommand(engine, 'docker', [
|
|
1032
|
-
'run',
|
|
1033
|
-
'-d',
|
|
1034
|
-
'--name',
|
|
1035
|
-
container,
|
|
1036
|
-
'--hostname',
|
|
1037
|
-
`${effectiveConfig.project}-sandbox`,
|
|
1038
|
-
'--label',
|
|
1039
|
-
sandboxLabel(effectiveConfig),
|
|
1040
|
-
'--label',
|
|
1041
|
-
`${sandboxBranchLabel(effectiveConfig)}=${branch}`,
|
|
1042
|
-
'--label',
|
|
1043
|
-
`${sandboxWorkspaceModeLabel(effectiveConfig)}=${target.workspace.mode}`,
|
|
1044
|
-
...(target.workspace.mode === 'task-bound'
|
|
1045
|
-
? [
|
|
1046
|
-
'--label',
|
|
1047
|
-
`${sandboxTaskIdLabel(effectiveConfig)}=${target.workspace.taskId}`,
|
|
1048
|
-
'-e',
|
|
1049
|
-
`AGENT_INFRA_TASK_ID=${target.workspace.taskId}`
|
|
1050
|
-
]
|
|
1051
|
-
: []),
|
|
1052
|
-
'-e',
|
|
1053
|
-
`AGENT_INFRA_CONTROL_TOKEN=${control.token}`,
|
|
1054
|
-
'-e',
|
|
1055
|
-
`AGENT_INFRA_CONTROL_GENERATION=${control.generation}`,
|
|
1056
|
-
'-e',
|
|
1057
|
-
'AGENT_INFRA_CONTROL_DIR=/run/agent-infra/control',
|
|
1058
|
-
'-e',
|
|
1059
|
-
'AGENT_INFRA_CONTROL_STATUS_DIR=/run/agent-infra/control-status',
|
|
1060
|
-
'--label',
|
|
1061
|
-
`${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
|
|
1062
|
-
...coreVolumes,
|
|
1063
|
-
...buildClipboardVolumeArgs(engine, effectiveConfig.home),
|
|
1064
|
-
'-v',
|
|
1065
|
-
volumeArg(engine, path.join(effectiveConfig.repoRoot, '.git'), `${toEnginePath(engine, effectiveConfig.repoRoot)}/.git`),
|
|
1066
|
-
...dotfilesMount,
|
|
1067
|
-
...toolVolumes,
|
|
1068
|
-
...tmpfsArgs,
|
|
1069
|
-
...tmpfsSeedPlan.flatMap(({ volumeArgs }) => volumeArgs),
|
|
1070
|
-
...liveMountVolumes,
|
|
1071
|
-
...envFile.dockerArgs,
|
|
1072
|
-
...tzFlags,
|
|
1073
|
-
'-w',
|
|
1074
|
-
'/workspace',
|
|
1075
|
-
effectiveConfig.imageName
|
|
1076
|
-
]);
|
|
1077
|
-
await startSandboxControlBroker(effectiveConfig.repoRoot, control.manifestPath);
|
|
1078
|
-
createdTmpfsSeedPlan = tmpfsSeedPlan;
|
|
1079
|
-
}
|
|
1080
|
-
finally {
|
|
1081
|
-
envFile.cleanup();
|
|
1082
|
-
}
|
|
1083
|
-
prepareTmpfsMounts({
|
|
1084
|
-
engine,
|
|
1085
|
-
container,
|
|
1086
|
-
mountPaths: effectiveResolvedTools
|
|
1087
|
-
.filter(({ tool }) => tool.tmpfs)
|
|
1088
|
-
.map(({ tool }) => tool.containerMount)
|
|
1089
|
-
});
|
|
1090
|
-
hydrateTmpfsSeedEntries({
|
|
1091
|
-
engine,
|
|
1092
|
-
container,
|
|
1093
|
-
entries: tmpfsSeedPlan,
|
|
1094
|
-
replace: true
|
|
1095
|
-
});
|
|
1096
|
-
// Belt-and-suspenders: re-create the four shell-config symlinks at
|
|
1097
|
-
// runtime so users with a custom `sandbox.dockerfile` (which won't
|
|
1098
|
-
// include the ai-tools.dockerfile symlink fragment) still get
|
|
1099
|
-
// ~/.gitconfig and friends pointing into the host bind-mount.
|
|
1100
|
-
// `ln -sf` is idempotent for the default image.
|
|
1101
|
-
ensureShellConfigSymlinks(engine, container);
|
|
1102
|
-
if (needsGpg) {
|
|
1103
|
-
message(cachedGpg
|
|
1104
|
-
? 'Syncing GPG keys from cache...'
|
|
1105
|
-
: hasHostGpgKeys
|
|
1106
|
-
? 'Syncing GPG keys (you may be prompted for your passphrase)...'
|
|
1107
|
-
: 'Checking GPG cache before falling back to stripped git config...');
|
|
1108
|
-
try {
|
|
1109
|
-
if (syncGpgKeys(container, effectiveConfig.home, effectiveConfig.project, undefined, undefined, {
|
|
1110
|
-
cachedOverride: cachedGpg,
|
|
1111
|
-
repoPath: worktree,
|
|
1112
|
-
signingKey,
|
|
1113
|
-
dockerExecFn: (cmd, args, opts) => execEngine(engine, cmd, args, opts),
|
|
1114
|
-
dockerRunSafeFn: (cmd, args, opts) => runSafeEngine(engine, cmd, args, opts)
|
|
1115
|
-
})) {
|
|
1116
|
-
writeSanitizedGitconfig({
|
|
1117
|
-
home: effectiveConfig.home,
|
|
1118
|
-
hostConfigDir: hostShellConfig.hostDir,
|
|
1119
|
-
stripGpg: false,
|
|
1120
|
-
repoRoot: effectiveConfig.repoRoot
|
|
1121
|
-
});
|
|
1122
1244
|
}
|
|
1123
|
-
|
|
1245
|
+
catch {
|
|
1124
1246
|
message(hasHostGpgKeys
|
|
1125
1247
|
? 'GPG key sync failed; using stripped git config fallback...'
|
|
1126
1248
|
: 'Host GPG keys unavailable; using stripped git config fallback...');
|
|
1127
1249
|
}
|
|
1128
1250
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1251
|
+
for (const { tool } of effectiveResolvedTools) {
|
|
1252
|
+
for (const command of tool.postSetupCmds ?? []) {
|
|
1253
|
+
runSafeEngine(engine, 'docker', ['exec', container, 'bash', '-lc', command]);
|
|
1254
|
+
}
|
|
1133
1255
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1256
|
+
const afterStartResults = await runSandboxHooks({
|
|
1257
|
+
hooks: capabilityPlan.hooksByPhase['after-container-start'],
|
|
1258
|
+
phase: 'after-container-start',
|
|
1259
|
+
context: { config: effectiveConfig, plan: capabilityPlan },
|
|
1260
|
+
runCommand: runBoundedSandboxHookCommand
|
|
1261
|
+
});
|
|
1262
|
+
const afterStartFailure = afterStartResults.find((result) => result.status === 'fatal');
|
|
1263
|
+
if (afterStartFailure) {
|
|
1264
|
+
throw new Error(afterStartFailure.message
|
|
1265
|
+
?? `Sandbox hook '${afterStartFailure.hookId}' failed.`);
|
|
1138
1266
|
}
|
|
1267
|
+
replacementLease.release();
|
|
1268
|
+
replacementLeaseHeld = false;
|
|
1269
|
+
return 'Container started';
|
|
1139
1270
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1271
|
+
catch (error) {
|
|
1272
|
+
if (replacementLeaseHeld) {
|
|
1273
|
+
try {
|
|
1274
|
+
replacementLease.release();
|
|
1275
|
+
}
|
|
1276
|
+
catch {
|
|
1277
|
+
// Preserve the original cutover failure and its evidence.
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
throw error;
|
|
1150
1281
|
}
|
|
1151
|
-
return 'Container started';
|
|
1152
1282
|
}
|
|
1153
1283
|
}
|
|
1154
1284
|
]);
|