@fitlab-ai/agent-infra 0.9.7 → 0.9.9
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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/bin/internal-cli.js +11 -1
- package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +60 -22
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +175 -11
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +147 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +86 -69
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
- package/dist/lib/decide.js +68 -14
- package/dist/lib/git/workflow.js +10 -1
- package/dist/lib/init.js +1 -1
- package/dist/lib/internal/codex-lifecycle.js +19 -5
- package/dist/lib/internal/codex-sandbox-controller.js +6 -13
- package/dist/lib/internal/git-workflow.js +5 -7
- package/dist/lib/internal/platform-pr.js +20 -7
- package/dist/lib/internal/release-workflow.js +3 -4
- package/dist/lib/internal/sandbox-control.js +79 -2
- package/dist/lib/internal/task-activity.js +57 -7
- package/dist/lib/internal/task-event.js +57 -4
- package/dist/lib/internal/task-finalization.js +54 -0
- 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 +161 -288
- 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/pr-summary.js +53 -13
- package/dist/lib/platform/pull-requests.js +205 -41
- package/dist/lib/runtime/agent-runtime.js +56 -0
- package/dist/lib/sandbox/commands/create.js +389 -311
- package/dist/lib/sandbox/commands/rm.js +316 -55
- package/dist/lib/sandbox/control/client.js +190 -6
- package/dist/lib/sandbox/control/container-identity.js +1 -1
- package/dist/lib/sandbox/control/controller-registration.js +273 -0
- package/dist/lib/sandbox/control/executor.js +145 -5
- package/dist/lib/sandbox/control/lifecycle.js +395 -73
- package/dist/lib/sandbox/control/protocol.js +67 -6
- package/dist/lib/sandbox/control/state.js +27 -11
- package/dist/lib/sandbox/engine.js +14 -7
- package/dist/lib/sandbox/index.js +5 -3
- package/dist/lib/sandbox/managed-fs.js +5 -0
- package/dist/lib/sandbox/mounts.js +20 -5
- package/dist/lib/sandbox/recovery.js +78 -10
- package/dist/lib/sandbox/task-resolver.js +50 -6
- package/dist/lib/sandbox/workspace-identity.js +27 -0
- package/dist/lib/sandbox/workspace-view.js +62 -40
- package/dist/lib/sandbox/worktree-safety.js +215 -2
- package/dist/lib/server/process-state.js +51 -1
- package/dist/lib/task/activity-intent.js +4 -1
- package/dist/lib/task/codex-orchestration.js +158 -33
- package/dist/lib/task/commands/status.js +59 -1
- package/dist/lib/task/commit-operation.js +335 -0
- package/dist/lib/task/commit-policy.js +18 -0
- package/dist/lib/task/decision-intents.js +4 -2
- package/dist/lib/task/delegation-receipts.js +279 -17
- package/dist/lib/task/events.js +9 -5
- package/dist/lib/task/finalization.js +567 -0
- 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/lifecycle.js +69 -14
- package/dist/lib/task/operation-outcome.js +33 -0
- package/dist/lib/task/orchestration.js +199 -719
- package/dist/lib/task/review-finalization.js +3 -2
- package/dist/lib/task/short-id.js +59 -1
- package/dist/lib/task/task-execution-lock.js +6 -1
- package/dist/lib/task/verification-engine.js +138 -59
- package/dist/lib/task/verification.js +15 -2
- package/dist/lib/task/workflow-warning-intents.js +54 -3
- package/dist/lib/task/write.js +2 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/claude-code.ts +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +63 -20
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +278 -13
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +112 -93
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
- package/lib/decide.ts +59 -14
- package/lib/git/workflow.ts +11 -2
- package/lib/init.ts +1 -1
- package/lib/internal/codex-lifecycle.ts +18 -5
- package/lib/internal/codex-sandbox-controller.ts +6 -12
- package/lib/internal/git-workflow.ts +5 -6
- package/lib/internal/platform-pr.ts +14 -7
- package/lib/internal/release-workflow.ts +3 -4
- package/lib/internal/sandbox-control.ts +92 -2
- package/lib/internal/task-activity.ts +46 -6
- package/lib/internal/task-event.ts +53 -4
- package/lib/internal/task-finalization.ts +68 -0
- package/lib/internal/task-ledger.ts +48 -4
- package/lib/internal/task-lifecycle.ts +56 -12
- package/lib/internal/task-orchestration.ts +32 -107
- 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/pr-summary.ts +65 -14
- package/lib/platform/pull-requests.ts +211 -41
- package/lib/runtime/agent-runtime.ts +76 -0
- package/lib/sandbox/commands/create.ts +88 -4
- package/lib/sandbox/commands/rm.ts +413 -61
- package/lib/sandbox/config.ts +2 -1
- package/lib/sandbox/control/client.ts +255 -6
- package/lib/sandbox/control/container-identity.ts +3 -3
- package/lib/sandbox/control/controller-registration.ts +347 -0
- package/lib/sandbox/control/executor.ts +172 -6
- package/lib/sandbox/control/lifecycle.ts +465 -77
- package/lib/sandbox/control/protocol.ts +85 -9
- package/lib/sandbox/control/state.ts +35 -18
- package/lib/sandbox/engine.ts +20 -9
- package/lib/sandbox/index.ts +5 -3
- package/lib/sandbox/managed-fs.ts +5 -0
- package/lib/sandbox/mounts.ts +23 -5
- package/lib/sandbox/recovery.ts +90 -9
- package/lib/sandbox/task-resolver.ts +58 -7
- package/lib/sandbox/workspace-identity.ts +50 -2
- package/lib/sandbox/workspace-view.ts +73 -43
- package/lib/sandbox/worktree-safety.ts +236 -2
- package/lib/server/process-state.ts +48 -1
- package/lib/task/activity-intent.ts +4 -2
- package/lib/task/codex-orchestration.ts +167 -33
- package/lib/task/commands/status.ts +78 -1
- package/lib/task/commit-operation.ts +430 -0
- package/lib/task/commit-policy.ts +30 -0
- package/lib/task/decision-intents.ts +4 -2
- package/lib/task/delegation-receipts.ts +304 -19
- package/lib/task/events.ts +10 -5
- package/lib/task/finalization.ts +719 -0
- 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/lifecycle.ts +71 -14
- package/lib/task/operation-outcome.ts +48 -0
- package/lib/task/orchestration.ts +239 -922
- package/lib/task/review-finalization.ts +6 -2
- package/lib/task/short-id.ts +65 -1
- package/lib/task/task-execution-lock.ts +10 -1
- package/lib/task/verification-engine.ts +145 -64
- package/lib/task/verification.ts +18 -3
- package/lib/task/workflow-warning-intents.ts +61 -4
- package/lib/task/write.ts +4 -1
- package/package.json +2 -2
- package/templates/.agents/QUICKSTART.en.md +2 -2
- package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
- package/templates/.agents/hooks/lifecycle-delegation.js +9 -24
- package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -3
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -3
- package/templates/.agents/rules/task-management.en.md +2 -2
- package/templates/.agents/rules/task-management.zh-CN.md +2 -2
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
- package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/commit/SKILL.en.md +41 -125
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
- package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
- package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
- package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
- package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -1
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
- package/templates/.codex/hooks.json +4 -4
- package/dist/lib/task/commit-finalization.js +0 -305
- package/dist/lib/task/commit-intent.js +0 -165
- package/lib/task/commit-finalization.ts +0 -419
- package/lib/task/commit-intent.ts +0 -230
|
@@ -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, finalizeSandboxControlManifest, materializeSandboxControl, materializeSandboxWorkspaceView,
|
|
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,344 +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
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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
|
+
}
|
|
880
923
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
if (!sameSandboxWorkspaceIdentity(existingIdentity, target.workspace)) {
|
|
886
|
-
const existingDescription = existingIdentity.mode === 'task-bound'
|
|
887
|
-
? `task-bound:${existingIdentity.taskId}`
|
|
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.`);
|
|
924
|
+
message('Removing old container instance...');
|
|
925
|
+
for (const name of matchedContainers) {
|
|
926
|
+
runSafeEngine(engine, 'docker', ['stop', name]);
|
|
927
|
+
runSafeEngine(engine, 'docker', ['rm', name]);
|
|
898
928
|
}
|
|
899
929
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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');
|
|
936
|
+
}
|
|
904
937
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
message(`Created default sandbox aliases at ${aliasesFile.path}`);
|
|
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}`);
|
|
938
|
+
const aliasesFile = ensureSandboxAliasesFile(effectiveConfig.home, capabilityPlan.aliases);
|
|
939
|
+
if (aliasesFile.created) {
|
|
940
|
+
message(`Created default sandbox aliases at ${aliasesFile.path}`);
|
|
914
941
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
const needsGpg = detectGpgConfig(gitconfigContent);
|
|
921
|
-
const hasHostGpgKeys = needsGpg && hostHasGpgKeys(effectiveConfig.home);
|
|
922
|
-
const signingKey = needsGpg
|
|
923
|
-
? getGitSigningKey({ repoPath: worktree, home: effectiveConfig.home })
|
|
924
|
-
: null;
|
|
925
|
-
const cachedGpg = needsGpg
|
|
926
|
-
? readGpgCache(effectiveConfig.home, effectiveConfig.project, undefined, signingKey)
|
|
927
|
-
: null;
|
|
928
|
-
const proxyEntries = values['inherit-proxy'] ? collectHostProxyEntries(process.env) : [];
|
|
929
|
-
const envFile = buildContainerEnvFile(effectiveResolvedTools, engine, undefined, {
|
|
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
|
|
949
|
-
});
|
|
950
|
-
const beforeCreateFailure = beforeCreateResults.find((result) => result.status === 'fatal');
|
|
951
|
-
if (beforeCreateFailure) {
|
|
952
|
-
throw new Error(beforeCreateFailure.message
|
|
953
|
-
?? `Sandbox hook '${beforeCreateFailure.hookId}' failed.`);
|
|
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
|
+
}
|
|
954
947
|
}
|
|
955
|
-
const
|
|
956
|
-
const
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
branch,
|
|
971
|
-
identity: target.workspace,
|
|
972
|
-
engine
|
|
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
|
|
973
963
|
});
|
|
974
|
-
hostShellConfig
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
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
|
+
}
|
|
990
1005
|
}
|
|
991
|
-
|
|
992
|
-
}).flatMap(({ hostPaths, containerPath, readOnly }) => [
|
|
993
|
-
'-v',
|
|
994
|
-
volumeArg(engine, hostPaths[0], containerPath, readOnly ? ':ro' : '')
|
|
995
|
-
]);
|
|
996
|
-
// Mount each declared seed read-only at an isolated staging path.
|
|
997
|
-
// After docker run mounts the empty tmpfs, the container's default
|
|
998
|
-
// user copies these entries into it so the runtime targets are normal,
|
|
999
|
-
// writable files rather than nested mount points. The allowlist keeps
|
|
1000
|
-
// stale runtime files (logs_2.sqlite, sessions, etc.) on the host from
|
|
1001
|
-
// being exposed or written through.
|
|
1002
|
-
tmpfsSeedPlan = effectiveResolvedTools.flatMap(({ tool, dir }) => (tool.tmpfs?.seed ?? []).flatMap((entry, index) => {
|
|
1003
|
-
const targetPath = tmpfsSeedTargetPath(tool.containerMount, entry);
|
|
1004
|
-
const hostPath = path.join(dir, entry);
|
|
1005
|
-
if (!fs.existsSync(hostPath)) {
|
|
1006
|
-
return [];
|
|
1006
|
+
replacementCutover = beginSandboxControlReplacement(controlPaths.root, replacementLease);
|
|
1007
1007
|
}
|
|
1008
|
-
const
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
targetPath,
|
|
1014
|
-
volumeArgs: ['-v', volumeArg(engine, hostPath, stagingPath, ':ro')]
|
|
1015
|
-
}];
|
|
1016
|
-
}));
|
|
1017
|
-
const liveMountVolumes = effectiveResolvedTools.flatMap(({ tool }) => (tool.hostLiveMounts ?? [])
|
|
1018
|
-
.filter(({ hostPath }) => fs.existsSync(hostPath))
|
|
1019
|
-
.flatMap(({ hostPath, containerSubpath }) => [
|
|
1020
|
-
'-v',
|
|
1021
|
-
volumeArg(engine, hostPath, path.posix.join(tool.containerMount, containerSubpath))
|
|
1022
|
-
]));
|
|
1023
|
-
fs.mkdirSync(shareCommon, { recursive: true });
|
|
1024
|
-
fs.mkdirSync(shareBranch, { recursive: true });
|
|
1025
|
-
fs.mkdirSync(clipboardHostDir(effectiveConfig.home), { recursive: true, mode: 0o700 });
|
|
1026
|
-
const dotfilesSnapshot = materializeDotfiles(effectiveConfig.dotfilesDir, dotfilesCacheDir(effectiveConfig.home, effectiveConfig.project));
|
|
1027
|
-
const dotfilesMount = dotfilesSnapshot
|
|
1028
|
-
? buildDotfilesVolumeArgs(engine, dotfilesSnapshot.cacheDir)
|
|
1029
|
-
: [];
|
|
1030
|
-
const hostTz = detectHostTimezone();
|
|
1031
|
-
const tzFlags = hostTz ? ['-e', `TZ=${hostTz}`] : [];
|
|
1032
|
-
let createdContainerRef = null;
|
|
1033
|
-
try {
|
|
1034
|
-
const dockerRunId = runEngineTaskCommand(engine, 'docker', [
|
|
1035
|
-
'run',
|
|
1036
|
-
'-d',
|
|
1037
|
-
'--init',
|
|
1038
|
-
'--name',
|
|
1008
|
+
const control = materializeSandboxControl({
|
|
1009
|
+
base: effectiveConfig.controlBase,
|
|
1010
|
+
repoRoot: effectiveConfig.repoRoot,
|
|
1011
|
+
worktreeRoot: worktree,
|
|
1012
|
+
project: effectiveConfig.project,
|
|
1039
1013
|
container,
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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 } : {}),
|
|
1048
1032
|
...(target.workspace.mode === 'task-bound'
|
|
1049
|
-
?
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
: []),
|
|
1056
|
-
'-e',
|
|
1057
|
-
`AGENT_INFRA_CONTROL_TOKEN=${control.token}`,
|
|
1058
|
-
'-e',
|
|
1059
|
-
`AGENT_INFRA_CONTROL_GENERATION=${control.generation}`,
|
|
1060
|
-
'-e',
|
|
1061
|
-
'AGENT_INFRA_CONTROL_DIR=/run/agent-infra/control',
|
|
1062
|
-
'-e',
|
|
1063
|
-
'AGENT_INFRA_CONTROL_STATUS_DIR=/run/agent-infra/control-status',
|
|
1064
|
-
'--label',
|
|
1065
|
-
`${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
|
|
1066
|
-
...coreVolumes,
|
|
1067
|
-
...buildClipboardVolumeArgs(engine, effectiveConfig.home),
|
|
1033
|
+
? {
|
|
1034
|
+
taskSource: assertSandboxTaskSource(effectiveConfig.repoRoot, target.workspace.taskId),
|
|
1035
|
+
taskId: target.workspace.taskId
|
|
1036
|
+
}
|
|
1037
|
+
: {})
|
|
1038
|
+
}).flatMap(({ hostPaths, containerPath, readOnly }) => [
|
|
1068
1039
|
'-v',
|
|
1069
|
-
volumeArg(engine,
|
|
1070
|
-
...dotfilesMount,
|
|
1071
|
-
...toolVolumes,
|
|
1072
|
-
...tmpfsArgs,
|
|
1073
|
-
...tmpfsSeedPlan.flatMap(({ volumeArgs }) => volumeArgs),
|
|
1074
|
-
...liveMountVolumes,
|
|
1075
|
-
...envFile.dockerArgs,
|
|
1076
|
-
...tzFlags,
|
|
1077
|
-
'-w',
|
|
1078
|
-
'/workspace',
|
|
1079
|
-
effectiveConfig.imageName
|
|
1040
|
+
volumeArg(engine, hostPaths[0], containerPath, readOnly ? ':ro' : '')
|
|
1080
1041
|
]);
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
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;
|
|
1091
1079
|
try {
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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;
|
|
1099
1175
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
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;
|
|
1113
1194
|
}
|
|
1114
|
-
finalizeSandboxControlManifest(control, {
|
|
1115
|
-
engine,
|
|
1116
|
-
id: containerId,
|
|
1117
|
-
labels: expectedLabels
|
|
1118
|
-
});
|
|
1119
|
-
await startSandboxControlBroker(effectiveConfig.repoRoot, control.manifestPath);
|
|
1120
|
-
createdTmpfsSeedPlan = tmpfsSeedPlan;
|
|
1121
1195
|
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
runSafeEngine(engine, 'docker', ['stop', createdContainerRef]);
|
|
1125
|
-
runSafeEngine(engine, 'docker', ['rm', createdContainerRef]);
|
|
1126
|
-
}
|
|
1127
|
-
removeDirRecursive(control.root);
|
|
1128
|
-
removeDirRecursive(workspaceView.root);
|
|
1129
|
-
throw error;
|
|
1196
|
+
finally {
|
|
1197
|
+
envFile.cleanup();
|
|
1130
1198
|
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1199
|
+
prepareTmpfsMounts({
|
|
1200
|
+
engine,
|
|
1201
|
+
container,
|
|
1202
|
+
mountPaths: effectiveResolvedTools
|
|
1203
|
+
.filter(({ tool }) => tool.tmpfs)
|
|
1204
|
+
.map(({ tool }) => tool.containerMount)
|
|
1205
|
+
});
|
|
1206
|
+
hydrateTmpfsSeedEntries({
|
|
1207
|
+
engine,
|
|
1208
|
+
container,
|
|
1209
|
+
entries: tmpfsSeedPlan,
|
|
1210
|
+
replace: true
|
|
1211
|
+
});
|
|
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...');
|
|
1243
|
+
}
|
|
1174
1244
|
}
|
|
1175
|
-
|
|
1245
|
+
catch {
|
|
1176
1246
|
message(hasHostGpgKeys
|
|
1177
1247
|
? 'GPG key sync failed; using stripped git config fallback...'
|
|
1178
1248
|
: 'Host GPG keys unavailable; using stripped git config fallback...');
|
|
1179
1249
|
}
|
|
1180
1250
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1251
|
+
for (const { tool } of effectiveResolvedTools) {
|
|
1252
|
+
for (const command of tool.postSetupCmds ?? []) {
|
|
1253
|
+
runSafeEngine(engine, 'docker', ['exec', container, 'bash', '-lc', command]);
|
|
1254
|
+
}
|
|
1185
1255
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
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.`);
|
|
1190
1266
|
}
|
|
1267
|
+
replacementLease.release();
|
|
1268
|
+
replacementLeaseHeld = false;
|
|
1269
|
+
return 'Container started';
|
|
1191
1270
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
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;
|
|
1202
1281
|
}
|
|
1203
|
-
return 'Container started';
|
|
1204
1282
|
}
|
|
1205
1283
|
}
|
|
1206
1284
|
]);
|