@fitlab-ai/agent-infra 0.9.13 → 0.9.14
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/bin/cli.ts +26 -9
- package/dist/bin/cli.js +173 -157
- package/dist/bin/internal-cli.js +42 -21
- package/dist/lib/agent-client.js +5 -0
- package/dist/lib/internal/agent-client.js +5 -2
- package/dist/lib/internal/cli-route-inventory.js +152 -0
- package/dist/lib/internal/codex-lifecycle.js +9 -6
- package/dist/lib/internal/codex-sandbox-controller.js +8 -2
- package/dist/lib/internal/git-workflow.js +10 -6
- package/dist/lib/internal/platform-checks.js +16 -4
- package/dist/lib/internal/platform-comment.js +16 -4
- package/dist/lib/internal/platform-context.js +4 -1
- package/dist/lib/internal/platform-issue.js +12 -4
- package/dist/lib/internal/platform-metadata.js +88 -0
- package/dist/lib/internal/platform-pr-review.js +16 -3
- package/dist/lib/internal/platform-pr.js +24 -10
- package/dist/lib/internal/platform-release-notes.js +6 -3
- package/dist/lib/internal/platform-security.js +93 -0
- package/dist/lib/internal/pr-review-grade.js +14 -3
- package/dist/lib/internal/release-workflow.js +19 -5
- package/dist/lib/internal/sandbox-control.js +22 -5
- package/dist/lib/internal/task-activity.js +13 -4
- package/dist/lib/internal/task-artifact.js +186 -27
- package/dist/lib/internal/task-context.js +4 -1
- package/dist/lib/internal/task-create.js +48 -15
- package/dist/lib/internal/task-delivery.js +4 -1
- package/dist/lib/internal/task-event.js +4 -1
- package/dist/lib/internal/task-finalization.js +4 -1
- package/dist/lib/internal/task-invalidation.js +4 -1
- package/dist/lib/internal/task-ledger.js +12 -1
- package/dist/lib/internal/task-lifecycle.js +4 -1
- package/dist/lib/internal/task-operation-registry.js +421 -0
- package/dist/lib/internal/task-orchestration.js +12 -0
- package/dist/lib/internal/task-override.js +10 -3
- package/dist/lib/internal/task-qualification.js +90 -0
- package/dist/lib/internal/task-review.js +4 -1
- package/dist/lib/internal/task-short-id.js +11 -1
- package/dist/lib/internal/task-snapshot.js +4 -1
- package/dist/lib/internal/task-validate.js +7 -2
- package/dist/lib/internal/task-verify.js +5 -1
- package/dist/lib/internal/task-warning.js +8 -1
- package/dist/lib/platform/comment-safety.js +226 -0
- package/dist/lib/platform/github-provider.js +233 -6
- package/dist/lib/platform/issue-comments.js +146 -34
- package/dist/lib/platform/pr-change-report.js +6 -12
- package/dist/lib/platform/pr-summary.js +28 -7
- package/dist/lib/platform/provider-contract.d.ts +67 -1
- package/dist/lib/platform/provider-contract.js +6 -2
- package/dist/lib/platform/provider-validation.js +76 -3
- package/dist/lib/platform/pull-requests.js +115 -170
- package/dist/lib/platform/repository-metadata.js +377 -0
- package/dist/lib/platform/security-alerts.js +155 -0
- package/dist/lib/platform/verification-sync.js +33 -26
- package/dist/lib/process-data/index.js +7 -0
- package/dist/lib/run/index.js +5 -0
- package/dist/lib/sandbox/commands/control-status.js +22 -0
- package/dist/lib/sandbox/commands/create.js +32 -6
- package/dist/lib/sandbox/commands/ls.js +5 -0
- package/dist/lib/sandbox/commands/rm.js +827 -120
- package/dist/lib/sandbox/commands/show.js +16 -0
- package/dist/lib/sandbox/control/client.js +83 -9
- package/dist/lib/sandbox/control/completed-reentry.js +85 -0
- package/dist/lib/sandbox/control/container-identity.js +28 -31
- package/dist/lib/sandbox/control/executor.js +149 -14
- package/dist/lib/sandbox/control/lifecycle.js +621 -127
- package/dist/lib/sandbox/control/native-file-lock.js +177 -0
- package/dist/lib/sandbox/control/server.js +305 -35
- package/dist/lib/sandbox/control/state.js +5 -3
- package/dist/lib/sandbox/control/task-view.js +183 -0
- package/dist/lib/sandbox/engines/authority.js +289 -0
- package/dist/lib/sandbox/engines/docker-exact-discovery.js +97 -0
- package/dist/lib/sandbox/index.js +5 -0
- package/dist/lib/sandbox/recovery.js +21 -1
- package/dist/lib/sandbox/workspace-view.js +2 -1
- package/dist/lib/sandbox/worktree-safety.js +4 -1
- package/dist/lib/server/index.js +8 -0
- package/dist/lib/task/artifact-lifecycle.js +42 -13
- package/dist/lib/task/artifact-operations.js +520 -0
- package/dist/lib/task/artifact-repair-intent.js +59 -0
- package/dist/lib/task/artifact-schema.js +135 -0
- package/dist/lib/task/capabilities.js +72 -1
- package/dist/lib/task/control-authority.js +4 -0
- package/dist/lib/task/create-service.js +103 -4
- package/dist/lib/task/create.js +25 -0
- package/dist/lib/task/events.js +186 -67
- package/dist/lib/task/index.js +11 -3
- package/dist/lib/task/local-artifact-finalization.js +75 -150
- package/dist/lib/task/orchestration.js +192 -52
- package/dist/lib/task/qualification-audit.js +490 -0
- package/dist/lib/task/qualification-confirmation.js +109 -0
- package/dist/lib/task/qualification-intents.js +148 -0
- package/dist/lib/task/review-artifacts.js +22 -23
- package/dist/lib/task/review-finalization.js +121 -4
- package/dist/lib/task/sections.js +6 -4
- package/dist/lib/task/verification-config.js +11 -0
- package/dist/lib/task/verification-engine.js +54 -4
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-client.ts +9 -0
- package/lib/internal/agent-client.ts +4 -2
- package/lib/internal/cli-route-inventory.ts +140 -0
- package/lib/internal/codex-lifecycle.ts +8 -6
- package/lib/internal/codex-sandbox-controller.ts +7 -2
- package/lib/internal/git-workflow.ts +9 -6
- package/lib/internal/platform-checks.ts +12 -4
- package/lib/internal/platform-comment.ts +12 -4
- package/lib/internal/platform-context.ts +3 -1
- package/lib/internal/platform-issue.ts +11 -4
- package/lib/internal/platform-metadata.ts +70 -0
- package/lib/internal/platform-pr-review.ts +14 -3
- package/lib/internal/platform-pr.ts +23 -10
- package/lib/internal/platform-release-notes.ts +5 -3
- package/lib/internal/platform-security.ts +83 -0
- package/lib/internal/pr-review-grade.ts +10 -3
- package/lib/internal/release-workflow.ts +17 -5
- package/lib/internal/sandbox-control.ts +19 -5
- package/lib/internal/task-activity.ts +12 -4
- package/lib/internal/task-artifact.ts +164 -25
- package/lib/internal/task-context.ts +3 -1
- package/lib/internal/task-create.ts +76 -15
- package/lib/internal/task-delivery.ts +3 -1
- package/lib/internal/task-event.ts +3 -1
- package/lib/internal/task-finalization.ts +3 -1
- package/lib/internal/task-invalidation.ts +3 -1
- package/lib/internal/task-ledger.ts +11 -1
- package/lib/internal/task-lifecycle.ts +3 -1
- package/lib/internal/task-operation-registry.ts +482 -0
- package/lib/internal/task-orchestration.ts +12 -0
- package/lib/internal/task-override.ts +9 -3
- package/lib/internal/task-qualification.ts +61 -0
- package/lib/internal/task-review.ts +3 -1
- package/lib/internal/task-short-id.ts +10 -1
- package/lib/internal/task-snapshot.ts +3 -1
- package/lib/internal/task-validate.ts +6 -2
- package/lib/internal/task-verify.ts +4 -1
- package/lib/internal/task-warning.ts +7 -1
- package/lib/platform/comment-safety.ts +250 -0
- package/lib/platform/github-provider.ts +229 -7
- package/lib/platform/issue-comments.ts +160 -35
- package/lib/platform/pr-change-report.ts +6 -12
- package/lib/platform/pr-summary.ts +27 -6
- package/lib/platform/provider-contract.ts +78 -3
- package/lib/platform/provider-validation.ts +85 -3
- package/lib/platform/pull-requests.ts +126 -178
- package/lib/platform/repository-metadata.ts +453 -0
- package/lib/platform/security-alerts.ts +191 -0
- package/lib/platform/verification-sync.ts +32 -28
- package/lib/process-data/index.ts +7 -0
- package/lib/run/index.ts +5 -0
- package/lib/sandbox/commands/control-status.ts +26 -0
- package/lib/sandbox/commands/create.ts +46 -6
- package/lib/sandbox/commands/ls.ts +7 -0
- package/lib/sandbox/commands/rm.ts +1000 -87
- package/lib/sandbox/commands/show.ts +16 -0
- package/lib/sandbox/control/client.ts +103 -9
- package/lib/sandbox/control/completed-reentry.ts +98 -0
- package/lib/sandbox/control/container-identity.ts +27 -43
- package/lib/sandbox/control/executor.ts +177 -12
- package/lib/sandbox/control/lifecycle.ts +629 -17
- package/lib/sandbox/control/native-file-lock.ts +209 -0
- package/lib/sandbox/control/protocol.ts +5 -2
- package/lib/sandbox/control/server.ts +319 -37
- package/lib/sandbox/control/state.ts +6 -3
- package/lib/sandbox/control/task-view.ts +240 -0
- package/lib/sandbox/engines/authority.ts +365 -0
- package/lib/sandbox/engines/docker-exact-discovery.ts +132 -0
- package/lib/sandbox/index.ts +7 -0
- package/lib/sandbox/recovery.ts +21 -1
- package/lib/sandbox/workspace-view.ts +9 -3
- package/lib/sandbox/worktree-safety.ts +4 -1
- package/lib/server/index.ts +10 -0
- package/lib/task/artifact-lifecycle.ts +38 -16
- package/lib/task/artifact-operations.ts +626 -0
- package/lib/task/artifact-repair-intent.ts +71 -0
- package/lib/task/artifact-schema.ts +192 -0
- package/lib/task/capabilities.ts +70 -1
- package/lib/task/control-authority.ts +11 -1
- package/lib/task/create-service.ts +131 -6
- package/lib/task/create.ts +27 -0
- package/lib/task/events.ts +161 -65
- package/lib/task/index.ts +16 -3
- package/lib/task/local-artifact-finalization.ts +87 -218
- package/lib/task/orchestration.ts +172 -49
- package/lib/task/qualification-audit.ts +599 -0
- package/lib/task/qualification-confirmation.ts +117 -0
- package/lib/task/qualification-intents.ts +150 -0
- package/lib/task/review-artifacts.ts +21 -22
- package/lib/task/review-finalization.ts +133 -4
- package/lib/task/sections.ts +6 -4
- package/lib/task/verification-config.ts +12 -0
- package/lib/task/verification-engine.ts +55 -4
- package/package.json +6 -1
- package/templates/.agents/rules/README.en.md +2 -0
- package/templates/.agents/rules/README.zh-CN.md +2 -0
- package/templates/.agents/rules/decision-qualification.en.md +22 -0
- package/templates/.agents/rules/decision-qualification.zh-CN.md +22 -0
- package/templates/.agents/rules/evidence-reporting.en.md +28 -0
- package/templates/.agents/rules/evidence-reporting.zh-CN.md +28 -0
- package/templates/.agents/rules/label-milestone-setup.en.md +7 -7
- package/templates/.agents/rules/label-milestone-setup.zh-CN.md +7 -7
- package/templates/.agents/rules/local-artifact-repair.en.md +5 -1
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +5 -1
- package/templates/.agents/rules/security-alerts.en.md +8 -8
- package/templates/.agents/rules/security-alerts.zh-CN.md +8 -8
- package/templates/.agents/skills/analyze-task/SKILL.en.md +15 -2
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +19 -2
- package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -12
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -12
- package/templates/.agents/skills/close-codescan/SKILL.en.md +2 -2
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +2 -2
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/SKILL.en.md +14 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +14 -1
- package/templates/.agents/skills/code-task/config/verify.en.json +5 -14
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +5 -14
- package/templates/.agents/skills/code-task/reference/code-rules.en.md +5 -4
- package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +5 -4
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/fix-mode.en.md +3 -1
- package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +3 -1
- package/templates/.agents/skills/code-task/reference/report-template.en.md +42 -15
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +39 -12
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +5 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/complete-task/SKILL.en.md +9 -22
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +9 -22
- package/templates/.agents/skills/create-task/SKILL.en.md +8 -0
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +7 -0
- package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/init-labels/SKILL.en.md +8 -8
- package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +8 -8
- package/templates/.agents/skills/init-milestones/SKILL.en.md +9 -11
- package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +9 -11
- package/templates/.agents/skills/plan-task/SKILL.en.md +14 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +16 -1
- package/templates/.agents/skills/plan-task/config/verify.en.json +4 -14
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +4 -14
- package/templates/.agents/skills/review-analysis/SKILL.en.md +14 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +14 -1
- package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -18
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -18
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +50 -23
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +45 -18
- package/templates/.agents/skills/review-code/SKILL.en.md +14 -1
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +14 -1
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -23
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -23
- package/templates/.agents/skills/review-code/reference/report-template.en.md +49 -22
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +45 -18
- package/templates/.agents/skills/review-plan/SKILL.en.md +14 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +14 -1
- package/templates/.agents/skills/review-plan/config/verify.en.json +1 -18
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -18
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +49 -22
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +45 -18
- package/templates/.agents/skills/review-pr/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -3
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +5 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +3 -1
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +3 -1
- package/templates/.agents/skills/test/SKILL.en.md +13 -2
- package/templates/.agents/skills/test/SKILL.zh-CN.md +13 -2
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +14 -0
- package/templates/.agents/templates/task.en.md +6 -0
- package/templates/.agents/templates/task.zh-CN.md +6 -0
- package/templates/.agents/scripts/security-alerts.github.sh +0 -151
- package/templates/.agents/scripts/security-alerts.sh +0 -31
- package/templates/.agents/skills/init-labels/scripts/init-labels.github.sh +0 -146
- package/templates/.agents/skills/init-labels/scripts/init-labels.sh +0 -32
- package/templates/.agents/skills/init-milestones/scripts/init-milestones.github.sh +0 -294
- package/templates/.agents/skills/init-milestones/scripts/init-milestones.sh +0 -6
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
3
4
|
import { parseArgs } from 'node:util';
|
|
4
5
|
import * as p from '@clack/prompts';
|
|
5
6
|
import pc from 'picocolors';
|
|
@@ -8,11 +9,13 @@ import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, san
|
|
|
8
9
|
import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, stopManagedVm } from '../engine.js';
|
|
9
10
|
import { pruneSandboxDanglingImages } from '../image-prune.js';
|
|
10
11
|
import { assertManagedPath, removeManagedDir, removeWorktreeDir } from '../managed-fs.js';
|
|
11
|
-
import { runEngine, runOk, runOkEngine, runSafe, runSafeEngine } from '../shell.js';
|
|
12
|
+
import { run, runEngine, runOk, runOkEngine, runSafe, runSafeEngine } from '../shell.js';
|
|
12
13
|
import { parseSandboxWorkspaceIdentity, resolveSandboxCleanupTarget, sameSandboxWorkspaceIdentity } from '../workspace-identity.js';
|
|
13
14
|
import { sandboxControlPaths, sandboxWorkspaceViewPaths } from '../workspace-view.js';
|
|
14
|
-
import { removeSandboxControlRoot, readSandboxControlManifest } from '../control/lifecycle.js';
|
|
15
|
+
import { SANDBOX_REMOVAL_JOURNAL_PHASES, advanceSandboxRemovalJournalPhase, claimSandboxRemovalJournal, clearSandboxRemovalJournalRecord, listSandboxRemovalJournals, removeSandboxControlRoot, readSandboxControlManifest, sandboxRemovalPhaseIndex } from '../control/lifecycle.js';
|
|
15
16
|
import { inspectSandboxControlContainer } from '../control/container-identity.js';
|
|
17
|
+
import { commandForSandboxAuthority } from '../engines/authority.js';
|
|
18
|
+
import { acquireSandboxResourceLock } from '../control/native-file-lock.js';
|
|
16
19
|
import { toolConfigDirCandidates, toolProjectDirCandidates } from '../tools.js';
|
|
17
20
|
import { createSandboxCapabilityPlan } from '../agent-client-reconciler.js';
|
|
18
21
|
import { fetchSandboxRows } from './list-running.js';
|
|
@@ -29,6 +32,518 @@ function isMissingPathError(error) {
|
|
|
29
32
|
return Boolean(error && typeof error === 'object' && 'code' in error
|
|
30
33
|
&& error.code === 'ENOENT');
|
|
31
34
|
}
|
|
35
|
+
function digestCleanupValue(value) {
|
|
36
|
+
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
37
|
+
}
|
|
38
|
+
function removalTargetDigest(config, target) {
|
|
39
|
+
return digestCleanupValue({
|
|
40
|
+
project: config.project,
|
|
41
|
+
branch: target.effectiveBranch,
|
|
42
|
+
workspace: target.workspace,
|
|
43
|
+
controlRoots: target.controlRoots.map((candidate) => path.resolve(candidate)),
|
|
44
|
+
workspaceViewRoots: target.workspaceViewRoots.map((candidate) => path.resolve(candidate)),
|
|
45
|
+
managedPathCandidates: (target.managedPathCandidates ?? []).map((candidate) => path.resolve(candidate)).sort()
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function removalPermitDigest(permits) {
|
|
49
|
+
return digestCleanupValue([...permits.entries()]
|
|
50
|
+
.map(([worktree, permit]) => [path.resolve(worktree), permit.mode, permit.snapshot.identity])
|
|
51
|
+
.sort(([left], [right]) => String(left).localeCompare(String(right))));
|
|
52
|
+
}
|
|
53
|
+
function removalPermitCommits(permits) {
|
|
54
|
+
return [...permits.entries()]
|
|
55
|
+
.map(([worktree, permit]) => ({
|
|
56
|
+
path: path.resolve(worktree),
|
|
57
|
+
mode: permit.mode,
|
|
58
|
+
snapshot: permit.snapshot
|
|
59
|
+
}))
|
|
60
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
61
|
+
}
|
|
62
|
+
function removalTargetCommit(config, target, permits, removeWorktree, removeBranch, removeShare) {
|
|
63
|
+
return {
|
|
64
|
+
branch: target.effectiveBranch,
|
|
65
|
+
project: config.project,
|
|
66
|
+
controlRoot: path.resolve(target.controlRoots[0] ?? path.join(config.controlBase, config.project)),
|
|
67
|
+
targetDigest: removalTargetDigest(config, target),
|
|
68
|
+
permitDigest: removalPermitDigest(permits),
|
|
69
|
+
removeWorktree,
|
|
70
|
+
removeBranch,
|
|
71
|
+
removeShare,
|
|
72
|
+
worktreePaths: target.existingWorktrees.map((candidate) => path.resolve(candidate)).sort(),
|
|
73
|
+
workspaceViewPaths: target.workspaceViewRoots.map((candidate) => path.resolve(candidate)).sort(),
|
|
74
|
+
toolPaths: target.toolCandidates.flatMap(({ candidates }) => candidates.map((candidate) => path.resolve(candidate))).sort(),
|
|
75
|
+
shellPaths: shellConfigDirCandidates(config, target.effectiveBranch).map((candidate) => path.resolve(candidate)).sort(),
|
|
76
|
+
sharePath: path.resolve(shareBranchDir(config, target.effectiveBranch)),
|
|
77
|
+
permits: removalPermitCommits(permits)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function assertRemovalSelectionMatches(expected, actual) {
|
|
81
|
+
const normalize = (value) => ({
|
|
82
|
+
...value,
|
|
83
|
+
controlRoot: '',
|
|
84
|
+
permits: value.permits
|
|
85
|
+
});
|
|
86
|
+
if (JSON.stringify(normalize(expected)) !== JSON.stringify(normalize(actual))) {
|
|
87
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function advanceRemovalJournals(project, target, targetDigest, phase, resourceLocks) {
|
|
91
|
+
const targetIndex = sandboxRemovalPhaseIndex(phase);
|
|
92
|
+
return listSandboxRemovalJournals({
|
|
93
|
+
branch: target.effectiveBranch,
|
|
94
|
+
project,
|
|
95
|
+
targetDigest
|
|
96
|
+
}).map((journal) => {
|
|
97
|
+
let current = journal;
|
|
98
|
+
while (sandboxRemovalPhaseIndex(current.phase) < targetIndex) {
|
|
99
|
+
if (current.phase === 'target-committed' && phase === 'container-absent') {
|
|
100
|
+
const lock = resourceLocks.get(current.target.controlRoot);
|
|
101
|
+
if (!lock)
|
|
102
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_LOCK_MISMATCH');
|
|
103
|
+
current = advanceSandboxRemovalJournalPhase(current, phase, { resourceLock: lock });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const next = SANDBOX_REMOVAL_JOURNAL_PHASES[sandboxRemovalPhaseIndex(current.phase) + 1];
|
|
107
|
+
if (!next)
|
|
108
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_PHASE_TRANSITION_INVALID');
|
|
109
|
+
const lock = resourceLocks.get(current.target.controlRoot);
|
|
110
|
+
if (!lock)
|
|
111
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_LOCK_MISMATCH');
|
|
112
|
+
current = advanceSandboxRemovalJournalPhase(current, next, { resourceLock: lock });
|
|
113
|
+
}
|
|
114
|
+
return current;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function prepareRemovalAction(project, target, targetDigest, startPhase, completedPhase, resourceLocks) {
|
|
118
|
+
const journals = listSandboxRemovalJournals({
|
|
119
|
+
branch: target.effectiveBranch,
|
|
120
|
+
project,
|
|
121
|
+
targetDigest
|
|
122
|
+
});
|
|
123
|
+
const startIndex = sandboxRemovalPhaseIndex(startPhase);
|
|
124
|
+
const completedIndex = sandboxRemovalPhaseIndex(completedPhase);
|
|
125
|
+
const shouldRun = journals.length === 0
|
|
126
|
+
|| journals.some((journal) => sandboxRemovalPhaseIndex(journal.phase) < completedIndex);
|
|
127
|
+
const recovering = journals.some((journal) => {
|
|
128
|
+
const index = sandboxRemovalPhaseIndex(journal.phase);
|
|
129
|
+
return index >= startIndex && index < completedIndex;
|
|
130
|
+
});
|
|
131
|
+
advanceRemovalJournals(project, target, targetDigest, startPhase, resourceLocks);
|
|
132
|
+
return { run: shouldRun, recovering };
|
|
133
|
+
}
|
|
134
|
+
function assertRemovalPathsAbsent(paths) {
|
|
135
|
+
const remaining = paths.filter((candidate) => fs.existsSync(candidate));
|
|
136
|
+
if (remaining.length > 0) {
|
|
137
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${remaining[0]}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function assertBranchRemovalIdentity(config, branch, permits) {
|
|
141
|
+
if (!runOk('git', ['-C', config.repoRoot, 'show-ref', '--verify', `refs/heads/${branch}`]))
|
|
142
|
+
return;
|
|
143
|
+
const expectedHeads = new Set([...permits.values()]
|
|
144
|
+
.filter((permit) => permit.snapshot.branch === branch)
|
|
145
|
+
.map((permit) => permit.snapshot.head));
|
|
146
|
+
if (expectedHeads.size === 0) {
|
|
147
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
|
|
148
|
+
}
|
|
149
|
+
const actualHead = runSafe('git', ['-C', config.repoRoot, 'rev-parse', `refs/heads/${branch}`]);
|
|
150
|
+
if (!expectedHeads.has(actualHead)) {
|
|
151
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function removalTombstonePath(targetDigest, kind, source) {
|
|
155
|
+
const sourceDigest = createHash('sha256')
|
|
156
|
+
.update(`${kind}\0${path.resolve(source)}`)
|
|
157
|
+
.digest('hex')
|
|
158
|
+
.slice(0, 24);
|
|
159
|
+
return path.join(path.dirname(path.resolve(source)), `.agent-infra-removal-${targetDigest.slice(0, 16)}-${kind}-${sourceDigest}`);
|
|
160
|
+
}
|
|
161
|
+
const REMOVAL_TOMBSTONE_MARKER = '.agent-infra-removal-ownership.json';
|
|
162
|
+
const REMOVAL_TOMBSTONE_PAYLOAD = 'payload';
|
|
163
|
+
function removalSourceClaimPath(source, ownership) {
|
|
164
|
+
const claimDigest = createHash('sha256')
|
|
165
|
+
.update(JSON.stringify(ownership))
|
|
166
|
+
.digest('hex')
|
|
167
|
+
.slice(0, 24);
|
|
168
|
+
return path.join(source, `.agent-infra-removal-source-${claimDigest}.json`);
|
|
169
|
+
}
|
|
170
|
+
function removalSourceIdentity(source) {
|
|
171
|
+
const stat = fs.lstatSync(source, { bigint: true });
|
|
172
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.dev === 0n || stat.ino === 0n) {
|
|
173
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
|
|
174
|
+
}
|
|
175
|
+
return { dev: String(stat.dev), ino: String(stat.ino) };
|
|
176
|
+
}
|
|
177
|
+
function sameRemovalSourceIdentity(left, right) {
|
|
178
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
179
|
+
}
|
|
180
|
+
function removalSourceClaimMatches(source, ownership, identity) {
|
|
181
|
+
try {
|
|
182
|
+
const claimPath = removalSourceClaimPath(source, ownership);
|
|
183
|
+
const claimStat = fs.lstatSync(claimPath);
|
|
184
|
+
if (!claimStat.isFile() || claimStat.isSymbolicLink())
|
|
185
|
+
return false;
|
|
186
|
+
const record = JSON.parse(fs.readFileSync(claimPath, 'utf8'));
|
|
187
|
+
return record.version === ownership.version
|
|
188
|
+
&& record.targetDigest === ownership.targetDigest
|
|
189
|
+
&& record.permitDigest === ownership.permitDigest
|
|
190
|
+
&& record.kind === ownership.kind
|
|
191
|
+
&& typeof record.source === 'string'
|
|
192
|
+
&& path.resolve(record.source) === path.resolve(ownership.source)
|
|
193
|
+
&& record.sourceIdentity?.dev === identity.dev
|
|
194
|
+
&& record.sourceIdentity?.ino === identity.ino;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function isOwnedRemovalSourceClaim(source, ownership) {
|
|
201
|
+
try {
|
|
202
|
+
const claimPath = removalSourceClaimPath(source, ownership);
|
|
203
|
+
const claimStat = fs.lstatSync(claimPath);
|
|
204
|
+
if (!claimStat.isFile() || claimStat.isSymbolicLink())
|
|
205
|
+
return false;
|
|
206
|
+
const record = JSON.parse(fs.readFileSync(claimPath, 'utf8'));
|
|
207
|
+
return record.version === ownership.version
|
|
208
|
+
&& record.targetDigest === ownership.targetDigest
|
|
209
|
+
&& record.permitDigest === ownership.permitDigest
|
|
210
|
+
&& record.kind === ownership.kind
|
|
211
|
+
&& typeof record.source === 'string'
|
|
212
|
+
&& path.resolve(record.source) === path.resolve(ownership.source);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function removeOwnedSourceClaim(source, ownership) {
|
|
219
|
+
try {
|
|
220
|
+
const claimPath = removalSourceClaimPath(source, ownership);
|
|
221
|
+
if (isOwnedRemovalSourceClaim(source, ownership))
|
|
222
|
+
fs.unlinkSync(claimPath);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// Preserve the source when the claim cannot be revalidated.
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function readRemovalTombstoneRecord(root, tombstone) {
|
|
229
|
+
assertManagedPath(root, tombstone);
|
|
230
|
+
try {
|
|
231
|
+
const tombstoneStat = fs.lstatSync(tombstone);
|
|
232
|
+
const markerStat = fs.lstatSync(removalTombstoneMarker(tombstone));
|
|
233
|
+
if (!tombstoneStat.isDirectory() || tombstoneStat.isSymbolicLink()
|
|
234
|
+
|| !markerStat.isFile() || markerStat.isSymbolicLink())
|
|
235
|
+
return null;
|
|
236
|
+
const record = JSON.parse(fs.readFileSync(removalTombstoneMarker(tombstone), 'utf8'));
|
|
237
|
+
if (record.version !== 1
|
|
238
|
+
|| typeof record.targetDigest !== 'string'
|
|
239
|
+
|| typeof record.permitDigest !== 'string'
|
|
240
|
+
|| typeof record.kind !== 'string'
|
|
241
|
+
|| typeof record.source !== 'string'
|
|
242
|
+
|| record.sourceIdentity?.dev === undefined
|
|
243
|
+
|| record.sourceIdentity.ino === undefined)
|
|
244
|
+
return null;
|
|
245
|
+
return record;
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function removalTombstonePayload(tombstone) {
|
|
252
|
+
return path.join(tombstone, REMOVAL_TOMBSTONE_PAYLOAD);
|
|
253
|
+
}
|
|
254
|
+
function removalTombstoneMarker(tombstone) {
|
|
255
|
+
return path.join(tombstone, REMOVAL_TOMBSTONE_MARKER);
|
|
256
|
+
}
|
|
257
|
+
function isOwnedRemovalTombstone(root, tombstone, ownership) {
|
|
258
|
+
const record = readRemovalTombstoneRecord(root, tombstone);
|
|
259
|
+
return record !== null
|
|
260
|
+
&& record.version === 1
|
|
261
|
+
&& record.targetDigest === ownership.targetDigest
|
|
262
|
+
&& record.permitDigest === ownership.permitDigest
|
|
263
|
+
&& record.kind === ownership.kind
|
|
264
|
+
&& typeof record.source === 'string'
|
|
265
|
+
&& path.resolve(record.source) === path.resolve(ownership.source);
|
|
266
|
+
}
|
|
267
|
+
function isOwnedRemovalPayload(root, tombstone, ownership) {
|
|
268
|
+
const record = readRemovalTombstoneRecord(root, tombstone);
|
|
269
|
+
if (!record || record.targetDigest !== ownership.targetDigest
|
|
270
|
+
|| record.permitDigest !== ownership.permitDigest
|
|
271
|
+
|| record.kind !== ownership.kind
|
|
272
|
+
|| path.resolve(record.source) !== path.resolve(ownership.source))
|
|
273
|
+
return false;
|
|
274
|
+
try {
|
|
275
|
+
const payload = removalTombstonePayload(tombstone);
|
|
276
|
+
const payloadIdentity = removalSourceIdentity(payload);
|
|
277
|
+
return sameRemovalSourceIdentity(payloadIdentity, record.sourceIdentity)
|
|
278
|
+
&& removalSourceClaimMatches(payload, ownership, record.sourceIdentity);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function assertOwnedRemovalTombstone(root, tombstone, ownership) {
|
|
285
|
+
if (!isOwnedRemovalTombstone(root, tombstone, ownership)) {
|
|
286
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${ownership.source}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function shouldStageManagedRemoval(source, tombstone, recovering) {
|
|
290
|
+
return fs.existsSync(source) || (recovering && fs.existsSync(tombstone));
|
|
291
|
+
}
|
|
292
|
+
function stageManagedRemoval(root, source, tombstone, recovering, ownership) {
|
|
293
|
+
assertManagedPath(root, source);
|
|
294
|
+
assertManagedPath(root, tombstone);
|
|
295
|
+
const sourceExists = fs.existsSync(source);
|
|
296
|
+
const tombstoneExists = fs.existsSync(tombstone);
|
|
297
|
+
if (tombstoneExists) {
|
|
298
|
+
assertOwnedRemovalTombstone(root, tombstone, ownership);
|
|
299
|
+
if (sourceExists) {
|
|
300
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
|
|
301
|
+
}
|
|
302
|
+
if (recovering) {
|
|
303
|
+
if (!isOwnedRemovalPayload(root, tombstone, ownership)) {
|
|
304
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
|
|
305
|
+
}
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
|
|
309
|
+
}
|
|
310
|
+
if (recovering) {
|
|
311
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_ACTION_OUTCOME_UNKNOWN: ${source}`);
|
|
312
|
+
}
|
|
313
|
+
if (!sourceExists)
|
|
314
|
+
return;
|
|
315
|
+
// The operator authorizes cleanup of this managed target after execution stops.
|
|
316
|
+
// Path checks detect conflicts; they do not provide an atomic source-instance move.
|
|
317
|
+
const expectedIdentity = removalSourceIdentity(source);
|
|
318
|
+
const claimPath = removalSourceClaimPath(source, ownership);
|
|
319
|
+
try {
|
|
320
|
+
fs.writeFileSync(claimPath, `${JSON.stringify({ ...ownership, sourceIdentity: expectedIdentity })}\n`, {
|
|
321
|
+
encoding: 'utf8',
|
|
322
|
+
mode: 0o600,
|
|
323
|
+
flag: 'wx'
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
|
|
328
|
+
}
|
|
329
|
+
if (!sameRemovalSourceIdentity(expectedIdentity, removalSourceIdentity(source))) {
|
|
330
|
+
removeOwnedSourceClaim(source, ownership);
|
|
331
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
|
|
332
|
+
}
|
|
333
|
+
let moved = false;
|
|
334
|
+
try {
|
|
335
|
+
fs.mkdirSync(tombstone, { mode: 0o700 });
|
|
336
|
+
fs.writeFileSync(removalTombstoneMarker(tombstone), `${JSON.stringify({ ...ownership, sourceIdentity: expectedIdentity })}\n`, {
|
|
337
|
+
encoding: 'utf8',
|
|
338
|
+
mode: 0o600,
|
|
339
|
+
flag: 'wx'
|
|
340
|
+
});
|
|
341
|
+
fs.renameSync(source, removalTombstonePayload(tombstone));
|
|
342
|
+
moved = true;
|
|
343
|
+
if (!isOwnedRemovalPayload(root, tombstone, ownership)) {
|
|
344
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
if (!moved)
|
|
349
|
+
removeOwnedSourceClaim(source, ownership);
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function cleanupManagedTombstones(root, targetDigest, permitDigest, kind, sources) {
|
|
354
|
+
for (const source of sources) {
|
|
355
|
+
const tombstone = removalTombstonePath(targetDigest, kind, source);
|
|
356
|
+
if (!fs.existsSync(tombstone))
|
|
357
|
+
continue;
|
|
358
|
+
const ownership = { version: 1, targetDigest, permitDigest, kind, source };
|
|
359
|
+
if (!isOwnedRemovalTombstone(root, tombstone, ownership))
|
|
360
|
+
continue;
|
|
361
|
+
if (!isOwnedRemovalPayload(root, tombstone, ownership)) {
|
|
362
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
|
|
363
|
+
}
|
|
364
|
+
removeManagedDir(root, tombstone);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function branchRemovalTombstoneRef(targetDigest) {
|
|
368
|
+
return `refs/agent-infra/sandbox-removal/${targetDigest}/branch`;
|
|
369
|
+
}
|
|
370
|
+
function branchPermitHead(branch, permits) {
|
|
371
|
+
const heads = new Set([...permits.values()]
|
|
372
|
+
.filter((permit) => permit.snapshot.branch === branch)
|
|
373
|
+
.map((permit) => permit.snapshot.head));
|
|
374
|
+
return heads.size === 1 ? [...heads][0] : null;
|
|
375
|
+
}
|
|
376
|
+
function stageBranchRemoval(config, branch, targetDigest, permits, recovering) {
|
|
377
|
+
const expectedHead = branchPermitHead(branch, permits);
|
|
378
|
+
if (!expectedHead)
|
|
379
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
|
|
380
|
+
const branchName = `refs/heads/${branch}`;
|
|
381
|
+
const tombstone = branchRemovalTombstoneRef(targetDigest);
|
|
382
|
+
const branchExists = runOk('git', ['-C', config.repoRoot, 'show-ref', '--verify', branchName]);
|
|
383
|
+
const tombstoneExists = runOk('git', ['-C', config.repoRoot, 'show-ref', '--verify', tombstone]);
|
|
384
|
+
if (branchExists && tombstoneExists) {
|
|
385
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
|
|
386
|
+
}
|
|
387
|
+
if (recovering) {
|
|
388
|
+
if (branchExists && !tombstoneExists) {
|
|
389
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_ACTION_OUTCOME_UNKNOWN: ${branch}`);
|
|
390
|
+
}
|
|
391
|
+
if (tombstoneExists && runSafe('git', ['-C', config.repoRoot, 'rev-parse', tombstone]) !== expectedHead) {
|
|
392
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
|
|
393
|
+
}
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (!branchExists)
|
|
397
|
+
return;
|
|
398
|
+
assertBranchRemovalIdentity(config, branch, permits);
|
|
399
|
+
const actualHead = runSafe('git', ['-C', config.repoRoot, 'rev-parse', branchName]);
|
|
400
|
+
if (!runOk('git', ['-C', config.repoRoot, 'update-ref', tombstone, actualHead, '0'.repeat(40)])) {
|
|
401
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
|
|
402
|
+
}
|
|
403
|
+
if (!runOk('git', ['-C', config.repoRoot, 'update-ref', '-d', branchName, actualHead])) {
|
|
404
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function cleanupBranchTombstone(config, branch, targetDigest, permits) {
|
|
408
|
+
const tombstone = branchRemovalTombstoneRef(targetDigest);
|
|
409
|
+
if (!runOk('git', ['-C', config.repoRoot, 'show-ref', '--verify', tombstone]))
|
|
410
|
+
return;
|
|
411
|
+
const expectedHead = branchPermitHead(branch, permits);
|
|
412
|
+
if (!expectedHead || runSafe('git', ['-C', config.repoRoot, 'rev-parse', tombstone]) !== expectedHead) {
|
|
413
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
|
|
414
|
+
}
|
|
415
|
+
if (!runOk('git', ['-C', config.repoRoot, 'update-ref', '-d', tombstone, expectedHead])) {
|
|
416
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function worktreeRegistration(config, worktree) {
|
|
420
|
+
const lines = runSafe('git', ['-C', config.repoRoot, 'worktree', 'list', '--porcelain']).split('\n');
|
|
421
|
+
let current = null;
|
|
422
|
+
const flush = () => {
|
|
423
|
+
if (!current || path.resolve(current.path) !== path.resolve(worktree))
|
|
424
|
+
return null;
|
|
425
|
+
return { head: current.head, branch: current.branch };
|
|
426
|
+
};
|
|
427
|
+
for (const line of lines) {
|
|
428
|
+
if (line.startsWith('worktree ')) {
|
|
429
|
+
const previous = flush();
|
|
430
|
+
if (previous)
|
|
431
|
+
return previous;
|
|
432
|
+
current = { path: line.slice('worktree '.length), head: '', branch: null };
|
|
433
|
+
}
|
|
434
|
+
else if (current && line.startsWith('HEAD ')) {
|
|
435
|
+
current.head = line.slice('HEAD '.length);
|
|
436
|
+
}
|
|
437
|
+
else if (current && line.startsWith('branch ')) {
|
|
438
|
+
current.branch = line.slice('branch '.length);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return flush();
|
|
442
|
+
}
|
|
443
|
+
function assertWorktreeRegistrationIdentity(config, worktree, permit) {
|
|
444
|
+
const registration = worktreeRegistration(config, worktree);
|
|
445
|
+
if (!registration)
|
|
446
|
+
return;
|
|
447
|
+
if (registration.head !== permit.snapshot.head
|
|
448
|
+
|| registration.branch !== `refs/heads/${permit.snapshot.branch}`) {
|
|
449
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${worktree}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function pruneWorktreeRegistration(config, worktree) {
|
|
453
|
+
runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
|
|
454
|
+
if (worktreeRegistration(config, worktree)) {
|
|
455
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_ACTION_OUTCOME_UNKNOWN: ${worktree}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function stageWorktreeRemoval(config, worktree, permit, tombstone, recovering, ownership) {
|
|
459
|
+
assertManagedPath(config.worktreeBase, worktree);
|
|
460
|
+
assertManagedPath(config.worktreeBase, tombstone);
|
|
461
|
+
const worktreeExists = fs.existsSync(worktree);
|
|
462
|
+
const tombstoneExists = fs.existsSync(tombstone);
|
|
463
|
+
if (worktreeExists || !recovering)
|
|
464
|
+
verifyWorktreePermit(permit);
|
|
465
|
+
if (recovering) {
|
|
466
|
+
if (worktreeExists || !tombstoneExists) {
|
|
467
|
+
if (worktreeExists && tombstoneExists) {
|
|
468
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${worktree}`);
|
|
469
|
+
}
|
|
470
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_ACTION_OUTCOME_UNKNOWN: ${worktree}`);
|
|
471
|
+
}
|
|
472
|
+
assertOwnedRemovalTombstone(config.worktreeBase, tombstone, ownership);
|
|
473
|
+
assertWorktreeRegistrationIdentity(config, worktree, permit);
|
|
474
|
+
pruneWorktreeRegistration(config, worktree);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (!worktreeExists)
|
|
478
|
+
return;
|
|
479
|
+
stageManagedRemoval(config.worktreeBase, worktree, tombstone, false, ownership);
|
|
480
|
+
pruneWorktreeRegistration(config, worktree);
|
|
481
|
+
}
|
|
482
|
+
function cleanupCompletedRemovalArtifacts(config, target, committedTarget, targetDigest) {
|
|
483
|
+
const journals = listSandboxRemovalJournals({
|
|
484
|
+
branch: target.effectiveBranch,
|
|
485
|
+
project: config.project,
|
|
486
|
+
targetDigest
|
|
487
|
+
});
|
|
488
|
+
if (journals.length === 0)
|
|
489
|
+
return;
|
|
490
|
+
const completed = (phase) => journals.every((journal) => (sandboxRemovalPhaseIndex(journal.phase) >= sandboxRemovalPhaseIndex(phase)));
|
|
491
|
+
if (completed('workspace-removed')) {
|
|
492
|
+
cleanupManagedTombstones(config.workspaceViewBase, targetDigest, committedTarget.permitDigest, 'workspace', target.workspaceViewRoots);
|
|
493
|
+
cleanupManagedTombstones(path.join(config.controlBase, config.project), targetDigest, committedTarget.permitDigest, 'workspace', target.controlRoots);
|
|
494
|
+
cleanupManagedTombstones(config.worktreeBase, targetDigest, committedTarget.permitDigest, 'worktree', committedTarget.worktreePaths);
|
|
495
|
+
}
|
|
496
|
+
if (completed('branch-removed')) {
|
|
497
|
+
cleanupBranchTombstone(config, target.effectiveBranch, targetDigest, new Map(committedTarget.permits.map((permit) => [path.resolve(permit.path), {
|
|
498
|
+
mode: permit.mode,
|
|
499
|
+
snapshot: permit.snapshot
|
|
500
|
+
}])));
|
|
501
|
+
}
|
|
502
|
+
if (completed('tool-removed')) {
|
|
503
|
+
cleanupManagedTombstones(config.home, targetDigest, committedTarget.permitDigest, 'tool', committedTarget.toolPaths);
|
|
504
|
+
}
|
|
505
|
+
if (completed('shell-removed')) {
|
|
506
|
+
cleanupManagedTombstones(config.shellConfigBase, targetDigest, committedTarget.permitDigest, 'shell', committedTarget.shellPaths);
|
|
507
|
+
}
|
|
508
|
+
if (completed('share-removed')) {
|
|
509
|
+
cleanupManagedTombstones(config.shareBase, targetDigest, committedTarget.permitDigest, 'share', [committedTarget.sharePath]);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function removalActionPaths(target, config, phase) {
|
|
513
|
+
if (phase === 'workspace-removed') {
|
|
514
|
+
return [...target.workspaceViewRoots, ...target.controlRoots];
|
|
515
|
+
}
|
|
516
|
+
if (phase === 'tool-removed') {
|
|
517
|
+
return target.toolCandidates.flatMap(({ candidates }) => candidates);
|
|
518
|
+
}
|
|
519
|
+
if (phase === 'shell-removed') {
|
|
520
|
+
return shellConfigDirCandidates(config, target.effectiveBranch);
|
|
521
|
+
}
|
|
522
|
+
if (phase === 'share-removed') {
|
|
523
|
+
return [shareBranchDir(config, target.effectiveBranch)];
|
|
524
|
+
}
|
|
525
|
+
return [];
|
|
526
|
+
}
|
|
527
|
+
function permitsFromJournal(journals, target, expectedTargetDigest) {
|
|
528
|
+
const permits = new Map();
|
|
529
|
+
for (const journal of journals) {
|
|
530
|
+
if (journal.target.targetDigest !== expectedTargetDigest)
|
|
531
|
+
continue;
|
|
532
|
+
if (journal.target.branch !== target.effectiveBranch) {
|
|
533
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH');
|
|
534
|
+
}
|
|
535
|
+
for (const entry of journal.target.permits) {
|
|
536
|
+
if (path.resolve(entry.path) !== path.resolve(entry.snapshot.worktree)) {
|
|
537
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_PERMIT_MISMATCH');
|
|
538
|
+
}
|
|
539
|
+
permits.set(path.resolve(entry.path), {
|
|
540
|
+
mode: entry.mode,
|
|
541
|
+
snapshot: entry.snapshot
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return permits;
|
|
546
|
+
}
|
|
32
547
|
export function sandboxManagedPathKey(candidate, platform = process.platform, resolveExistingPath = (resolved) => fs.realpathSync.native(resolved)) {
|
|
33
548
|
const resolved = path.resolve(candidate);
|
|
34
549
|
let identity = resolved;
|
|
@@ -297,12 +812,19 @@ async function removeExactSandboxContainer(engine, manifest, timeoutMs) {
|
|
|
297
812
|
const deadlineAt = Date.now() + timeoutMs;
|
|
298
813
|
const remaining = () => Math.max(1, deadlineAt - Date.now());
|
|
299
814
|
const inspect = () => inspectSandboxControlContainer(manifest, { timeoutMs: remaining() });
|
|
815
|
+
const runAuthorityCommand = (args) => {
|
|
816
|
+
const command = manifest.authorityEvidence
|
|
817
|
+
? commandForSandboxAuthority(manifest.authorityEvidence, 'docker', args)
|
|
818
|
+
: null;
|
|
819
|
+
return command ? runOk(command.cmd, command.args, { timeout: remaining() })
|
|
820
|
+
: runOkEngine(engine, 'docker', args, { timeout: remaining() });
|
|
821
|
+
};
|
|
300
822
|
const before = await inspect();
|
|
301
823
|
if (before.state === 'unknown')
|
|
302
824
|
throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${before.reason}`);
|
|
303
825
|
if (before.state === 'absent')
|
|
304
826
|
return;
|
|
305
|
-
if (before.running && !
|
|
827
|
+
if (before.running && !runAuthorityCommand(['stop', '--timeout', '1', manifest.containerIdentity.id])) {
|
|
306
828
|
const afterStop = await inspect();
|
|
307
829
|
if (afterStop.state !== 'absent' && afterStop.state !== 'found') {
|
|
308
830
|
throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${afterStop.reason}`);
|
|
@@ -314,7 +836,7 @@ async function removeExactSandboxContainer(engine, manifest, timeoutMs) {
|
|
|
314
836
|
const afterStop = await inspect();
|
|
315
837
|
if (afterStop.state === 'unknown')
|
|
316
838
|
throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${afterStop.reason}`);
|
|
317
|
-
if (afterStop.state === 'found' && !
|
|
839
|
+
if (afterStop.state === 'found' && !runAuthorityCommand(['rm', manifest.containerIdentity.id])) {
|
|
318
840
|
const afterRemove = await inspect();
|
|
319
841
|
if (afterRemove.state !== 'absent') {
|
|
320
842
|
throw new Error(`Failed to remove sandbox container: ${manifest.containerIdentity.id}`);
|
|
@@ -389,7 +911,7 @@ async function authorizeWorktrees(worktrees, { allowDirtyDiscard, assumeYes }, {
|
|
|
389
911
|
}
|
|
390
912
|
p.log.warn(formatWorktreeSnapshot(inspection.snapshot));
|
|
391
913
|
const confirmed = await confirm({
|
|
392
|
-
message: 'Discard
|
|
914
|
+
message: 'Discard this worktree and all its uncommitted changes, including later changes?',
|
|
393
915
|
initialValue: false
|
|
394
916
|
});
|
|
395
917
|
if (isCancel(confirmed) || !confirmed)
|
|
@@ -408,146 +930,331 @@ async function rmOne(config, tools, branch, options = {}) {
|
|
|
408
930
|
if (!options.quiet) {
|
|
409
931
|
p.intro(pc.cyan(`Removing sandbox for ${branch}`));
|
|
410
932
|
}
|
|
933
|
+
const targetDigest = removalTargetDigest(config, target);
|
|
934
|
+
const existingJournals = listSandboxRemovalJournals({
|
|
935
|
+
branch: effectiveBranch,
|
|
936
|
+
project: config.project,
|
|
937
|
+
targetDigest
|
|
938
|
+
});
|
|
939
|
+
const persistedTarget = existingJournals[0]?.target;
|
|
940
|
+
for (const journal of existingJournals) {
|
|
941
|
+
if (!persistedTarget)
|
|
942
|
+
break;
|
|
943
|
+
assertRemovalSelectionMatches(persistedTarget, journal.target);
|
|
944
|
+
}
|
|
411
945
|
const recovery = options.permits
|
|
412
946
|
? new Map()
|
|
413
947
|
: recoveryContexts(config, target, existingWorktrees);
|
|
948
|
+
const journalPermits = options.permits ? new Map()
|
|
949
|
+
: permitsFromJournal(existingJournals, target, targetDigest);
|
|
414
950
|
const permits = options.permits
|
|
415
951
|
? new Map(options.permits)
|
|
416
|
-
:
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
952
|
+
: journalPermits.size > 0
|
|
953
|
+
? journalPermits
|
|
954
|
+
: await authorizeWorktrees(existingWorktrees, {
|
|
955
|
+
allowDirtyDiscard: options.allowDirtyDiscard ?? true,
|
|
956
|
+
assumeYes: Boolean(options.assumeYes)
|
|
957
|
+
}, {
|
|
958
|
+
...options.prompt,
|
|
959
|
+
interactive: options.interactive ?? Boolean(process.stdin.isTTY),
|
|
960
|
+
recovery
|
|
961
|
+
});
|
|
424
962
|
for (const worktree of existingWorktrees) {
|
|
425
963
|
const permit = permits.get(path.resolve(worktree));
|
|
426
964
|
if (!permit)
|
|
427
965
|
throw new Error(`Missing worktree removal permit: ${worktree}`);
|
|
428
|
-
|
|
966
|
+
const worktreeTombstone = removalTombstonePath(targetDigest, 'worktree', worktree);
|
|
967
|
+
const workspacePhasePending = existingJournals.some((journal) => (sandboxRemovalPhaseIndex(journal.phase) < sandboxRemovalPhaseIndex('workspace-removed')));
|
|
968
|
+
if (fs.existsSync(worktree)
|
|
969
|
+
|| (workspacePhasePending
|
|
970
|
+
&& !isOwnedRemovalTombstone(config.worktreeBase, worktreeTombstone, {
|
|
971
|
+
version: 1,
|
|
972
|
+
targetDigest,
|
|
973
|
+
permitDigest: removalPermitDigest(permits),
|
|
974
|
+
kind: 'worktree',
|
|
975
|
+
source: worktree
|
|
976
|
+
}))) {
|
|
977
|
+
verifyWorktreePermit(permit);
|
|
978
|
+
}
|
|
429
979
|
}
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
980
|
+
const recoveredRemovalChoice = persistedTarget;
|
|
981
|
+
const shouldRemoveWorktree = recoveredRemovalChoice
|
|
982
|
+
? recoveredRemovalChoice.removeWorktree
|
|
983
|
+
: existingWorktrees.length === 0
|
|
984
|
+
? false
|
|
985
|
+
: options.assumeYes
|
|
986
|
+
? true
|
|
987
|
+
: await confirm({
|
|
988
|
+
message: `Remove worktree(s): ${existingWorktrees.join(', ')}?`,
|
|
989
|
+
initialValue: true
|
|
990
|
+
});
|
|
438
991
|
if (isCancel(shouldRemoveWorktree)) {
|
|
439
992
|
p.outro('Cancelled');
|
|
440
993
|
return;
|
|
441
994
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
});
|
|
456
|
-
removeEmptyManagedParent(path.join(config.controlBase, config.project), root);
|
|
995
|
+
const shouldDeleteBranch = recoveredRemovalChoice
|
|
996
|
+
? recoveredRemovalChoice.removeBranch
|
|
997
|
+
: shouldRemoveWorktree && existingWorktrees.length > 0
|
|
998
|
+
? options.assumeYes
|
|
999
|
+
? true
|
|
1000
|
+
: await confirm({
|
|
1001
|
+
message: `Also delete local branch '${effectiveBranch}'?`,
|
|
1002
|
+
initialValue: true
|
|
1003
|
+
})
|
|
1004
|
+
: false;
|
|
1005
|
+
if (isCancel(shouldDeleteBranch)) {
|
|
1006
|
+
p.outro('Cancelled');
|
|
1007
|
+
return;
|
|
457
1008
|
}
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
1009
|
+
const sharePath = path.resolve(shareBranchDir(config, effectiveBranch));
|
|
1010
|
+
const shouldRemoveShare = recoveredRemovalChoice
|
|
1011
|
+
? recoveredRemovalChoice.removeShare
|
|
1012
|
+
: fs.existsSync(sharePath)
|
|
1013
|
+
? options.assumeYes
|
|
1014
|
+
? true
|
|
1015
|
+
: await confirm({
|
|
1016
|
+
message: `Remove share dir for branch '${effectiveBranch}' (${sharePath})?`,
|
|
1017
|
+
initialValue: true
|
|
1018
|
+
})
|
|
1019
|
+
: false;
|
|
1020
|
+
if (isCancel(shouldRemoveShare)) {
|
|
1021
|
+
p.outro('Cancelled');
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
const committedTarget = persistedTarget
|
|
1025
|
+
? {
|
|
1026
|
+
...persistedTarget,
|
|
1027
|
+
permitDigest: removalPermitDigest(permits),
|
|
1028
|
+
permits: removalPermitCommits(permits)
|
|
462
1029
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
1030
|
+
: removalTargetCommit(config, target, permits, Boolean(shouldRemoveWorktree), Boolean(shouldDeleteBranch), Boolean(shouldRemoveShare));
|
|
1031
|
+
if (persistedTarget)
|
|
1032
|
+
assertRemovalSelectionMatches(persistedTarget, committedTarget);
|
|
1033
|
+
const coordinatedManifests = controlRoots
|
|
1034
|
+
.filter((candidate) => fs.existsSync(candidate))
|
|
1035
|
+
.flatMap((root) => {
|
|
1036
|
+
const manifestPath = path.join(root, 'manifest.json');
|
|
1037
|
+
return fs.existsSync(manifestPath)
|
|
1038
|
+
? [[root, readSandboxControlManifest(manifestPath), fs.readFileSync(manifestPath, 'utf8')]]
|
|
1039
|
+
: [];
|
|
1040
|
+
});
|
|
1041
|
+
const resourceLocks = new Map();
|
|
1042
|
+
const recoveredContainerNames = new Set();
|
|
1043
|
+
try {
|
|
1044
|
+
for (const [root, manifest] of [...coordinatedManifests].sort(([left], [right]) => left.localeCompare(right))) {
|
|
1045
|
+
resourceLocks.set(root, acquireSandboxResourceLock(`${manifest.engine}:${manifest.containerIdentity.id}`, { lockDomain: manifest.authorityEvidence.lockDomain }));
|
|
470
1046
|
}
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
[workspaceViewRoots, path.join(config.workspaceViewBase, config.project), 'Workspace view'],
|
|
482
|
-
[controlRoots, path.join(config.controlBase, config.project), 'Control channel']
|
|
483
|
-
]) {
|
|
484
|
-
for (const directory of roots.filter((candidate) => fs.existsSync(candidate))) {
|
|
485
|
-
assertLegacyCandidateEvidence(directory, base === path.join(config.controlBase, config.project)
|
|
486
|
-
? config.controlBase : config.workspaceViewBase, config, effectiveBranch, matchedContainers);
|
|
487
|
-
removeManagedDir(base, directory);
|
|
488
|
-
assertRemoved(directory, label);
|
|
489
|
-
removeEmptyManagedParent(base, directory);
|
|
490
|
-
if (!options.quiet)
|
|
491
|
-
p.log.success(`${label} removed: ${directory}`);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
if (shouldRemoveWorktree) {
|
|
495
|
-
for (const worktree of existingWorktrees) {
|
|
496
|
-
const permit = permits.get(path.resolve(worktree));
|
|
497
|
-
if (!permit)
|
|
498
|
-
throw new Error(`Missing worktree removal permit: ${worktree}`);
|
|
499
|
-
removeWorktreeDir(config.repoRoot, config.worktreeBase, worktree, permit, {
|
|
500
|
-
allowRegisteredPathFallback: engine === ENGINES.WSL2
|
|
1047
|
+
for (const journal of existingJournals.filter((candidate) => !fs.existsSync(candidate.target.controlRoot))) {
|
|
1048
|
+
if (!target.controlRoots.some((root) => path.resolve(root) === path.resolve(journal.target.controlRoot))) {
|
|
1049
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH');
|
|
1050
|
+
}
|
|
1051
|
+
assertRemovalSelectionMatches(committedTarget, journal.target);
|
|
1052
|
+
if (journal.target.permitDigest !== removalPermitDigest(permits)) {
|
|
1053
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_PERMIT_MISMATCH');
|
|
1054
|
+
}
|
|
1055
|
+
const lock = acquireSandboxResourceLock(`${journal.engine}:${journal.containerId}`, {
|
|
1056
|
+
lockDomain: journal.lockDomain
|
|
501
1057
|
});
|
|
502
|
-
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
1058
|
+
resourceLocks.set(journal.target.controlRoot, lock);
|
|
1059
|
+
const current = listSandboxRemovalJournals({
|
|
1060
|
+
branch: journal.target.branch,
|
|
1061
|
+
project: journal.target.project,
|
|
1062
|
+
targetDigest: journal.target.targetDigest
|
|
1063
|
+
}).find((candidate) => candidate.carrierIdentityDigest === journal.carrierIdentityDigest);
|
|
1064
|
+
if (!current || current.revision !== journal.revision || current.handoffId !== journal.handoffId) {
|
|
1065
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_REVISION_MISMATCH');
|
|
1066
|
+
}
|
|
1067
|
+
const claimed = claimSandboxRemovalJournal(current, { resourceLock: lock });
|
|
1068
|
+
if (!['carrier-finalizing', 'carrier-removed', 'workspace-finalizing', 'workspace-removed',
|
|
1069
|
+
'branch-finalizing', 'branch-removed', 'tool-finalizing', 'tool-removed',
|
|
1070
|
+
'shell-finalizing', 'shell-removed', 'share-finalizing', 'share-removed', 'completed']
|
|
1071
|
+
.includes(claimed.phase)) {
|
|
1072
|
+
throw new Error('SANDBOX_CONTROL_REMOVE_RECOVERY_PENDING');
|
|
1073
|
+
}
|
|
1074
|
+
const recovered = claimed.phase === 'carrier-finalizing'
|
|
1075
|
+
? advanceSandboxRemovalJournalPhase(claimed, 'carrier-removed', { resourceLock: lock })
|
|
1076
|
+
: claimed;
|
|
1077
|
+
recoveredContainerNames.add(controlRootContainer(config, journal.target.controlRoot));
|
|
1078
|
+
}
|
|
1079
|
+
cleanupCompletedRemovalArtifacts(config, target, committedTarget, targetDigest);
|
|
1080
|
+
preflightRmTarget(config, target);
|
|
1081
|
+
const coordinatedContainers = new Set(recoveredContainerNames);
|
|
1082
|
+
for (const root of controlRoots.filter((candidate) => fs.existsSync(candidate))) {
|
|
1083
|
+
assertLegacyCandidateEvidence(root, config.controlBase, config, effectiveBranch, matchedContainers);
|
|
1084
|
+
assertControlRootMatchesTarget(root, effectiveBranch, workspace);
|
|
1085
|
+
const manifestPath = path.join(root, 'manifest.json');
|
|
1086
|
+
if (!fs.existsSync(manifestPath))
|
|
1087
|
+
continue;
|
|
1088
|
+
const currentRaw = fs.readFileSync(manifestPath, 'utf8');
|
|
1089
|
+
const manifest = readSandboxControlManifest(manifestPath);
|
|
1090
|
+
const initial = coordinatedManifests.find(([candidate]) => candidate === root);
|
|
1091
|
+
if (!initial || currentRaw !== initial[2])
|
|
1092
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_CHANGED');
|
|
1093
|
+
coordinatedContainers.add(manifest.container);
|
|
1094
|
+
await removeSandboxControlRoot(root, {
|
|
1095
|
+
inspectContainer: (timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }),
|
|
1096
|
+
removeContainer: (timeoutMs) => removeExactSandboxContainer(engine, manifest, timeoutMs),
|
|
1097
|
+
resourceLock: resourceLocks.get(root),
|
|
1098
|
+
retainRemovalJournal: true,
|
|
1099
|
+
removalTarget: { ...committedTarget, controlRoot: path.resolve(root) }
|
|
513
1100
|
});
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
1101
|
+
removeEmptyManagedParent(path.join(config.controlBase, config.project), root);
|
|
1102
|
+
}
|
|
1103
|
+
if ([...recoveredContainerNames].some((name) => matchedContainers.includes(name))) {
|
|
1104
|
+
throw new Error('SANDBOX_CONTROL_CONTAINER_REAPPEARED');
|
|
1105
|
+
}
|
|
1106
|
+
const uncoordinatedContainers = matchedContainers.filter((name) => !coordinatedContainers.has(name));
|
|
1107
|
+
if (uncoordinatedContainers.length > 0) {
|
|
1108
|
+
if (workspace.mode === 'task-bound') {
|
|
1109
|
+
throw new Error(`SANDBOX_CONTROL_TARGET_EVIDENCE_MISSING: container '${uncoordinatedContainers[0]}'`);
|
|
517
1110
|
}
|
|
518
|
-
|
|
519
|
-
|
|
1111
|
+
const spinner = p.spinner();
|
|
1112
|
+
spinner.start(`Stopping container(s): ${uncoordinatedContainers.join(', ')}`);
|
|
1113
|
+
for (const name of uncoordinatedContainers) {
|
|
1114
|
+
if (!runOkEngine(engine, 'docker', ['stop', name]))
|
|
1115
|
+
throw new Error(`Failed to stop sandbox container: ${name}`);
|
|
1116
|
+
if (!runOkEngine(engine, 'docker', ['rm', name]))
|
|
1117
|
+
throw new Error(`Failed to remove sandbox container: ${name}`);
|
|
520
1118
|
}
|
|
1119
|
+
const remaining = runEngine(engine, 'docker', ['ps', '-a', '--format', '{{.Names}}']).split('\n').filter(Boolean);
|
|
1120
|
+
const leftovers = uncoordinatedContainers.filter((name) => remaining.includes(name));
|
|
1121
|
+
if (leftovers.length > 0)
|
|
1122
|
+
throw new Error(`Sandbox container(s) still exist after removal: ${leftovers.join(', ')}`);
|
|
1123
|
+
spinner.stop(pc.green(`Removed container(s): ${uncoordinatedContainers.join(', ')}`));
|
|
521
1124
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
for (const dir of candidates.filter((candidate) => fs.existsSync(candidate))) {
|
|
525
|
-
removeManagedDir(tool.sandboxBase, dir);
|
|
526
|
-
assertRemoved(dir, `${tool.name} state`);
|
|
527
|
-
p.log.success(`${tool.name} state removed: ${dir}`);
|
|
1125
|
+
else {
|
|
1126
|
+
p.log.warn(`No sandbox container found for '${branch}'`);
|
|
528
1127
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
1128
|
+
const runWorkspaceCleanup = prepareRemovalAction(config.project, target, targetDigest, 'workspace-finalizing', 'workspace-removed', resourceLocks);
|
|
1129
|
+
if (runWorkspaceCleanup.run) {
|
|
1130
|
+
for (const [roots, base, label] of [
|
|
1131
|
+
[workspaceViewRoots, path.join(config.workspaceViewBase, config.project), 'Workspace view'],
|
|
1132
|
+
[controlRoots, path.join(config.controlBase, config.project), 'Control channel']
|
|
1133
|
+
]) {
|
|
1134
|
+
for (const directory of roots.filter((candidate) => shouldStageManagedRemoval(candidate, removalTombstonePath(targetDigest, 'workspace', candidate), runWorkspaceCleanup.recovering))) {
|
|
1135
|
+
assertLegacyCandidateEvidence(directory, base === path.join(config.controlBase, config.project)
|
|
1136
|
+
? config.controlBase : config.workspaceViewBase, config, effectiveBranch, matchedContainers);
|
|
1137
|
+
stageManagedRemoval(base, directory, removalTombstonePath(targetDigest, 'workspace', directory), runWorkspaceCleanup.recovering, {
|
|
1138
|
+
version: 1,
|
|
1139
|
+
targetDigest,
|
|
1140
|
+
permitDigest: committedTarget.permitDigest,
|
|
1141
|
+
kind: 'workspace',
|
|
1142
|
+
source: directory
|
|
1143
|
+
});
|
|
1144
|
+
if (!options.quiet)
|
|
1145
|
+
p.log.success(`${label} removed: ${directory}`);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
if (shouldRemoveWorktree) {
|
|
1149
|
+
for (const worktree of existingWorktrees) {
|
|
1150
|
+
const permit = permits.get(path.resolve(worktree));
|
|
1151
|
+
if (!permit)
|
|
1152
|
+
throw new Error(`Missing worktree removal permit: ${worktree}`);
|
|
1153
|
+
stageWorktreeRemoval(config, worktree, permit, removalTombstonePath(targetDigest, 'worktree', worktree), runWorkspaceCleanup.recovering, {
|
|
1154
|
+
version: 1,
|
|
1155
|
+
targetDigest,
|
|
1156
|
+
permitDigest: committedTarget.permitDigest,
|
|
1157
|
+
kind: 'worktree',
|
|
1158
|
+
source: worktree
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
else {
|
|
1164
|
+
assertRemovalPathsAbsent([
|
|
1165
|
+
...removalActionPaths(target, config, 'workspace-removed'),
|
|
1166
|
+
...(shouldRemoveWorktree ? existingWorktrees : [])
|
|
1167
|
+
]);
|
|
1168
|
+
}
|
|
1169
|
+
advanceRemovalJournals(config.project, target, targetDigest, 'workspace-removed', resourceLocks);
|
|
1170
|
+
cleanupCompletedRemovalArtifacts(config, target, committedTarget, targetDigest);
|
|
1171
|
+
for (const [roots, base] of [
|
|
1172
|
+
[workspaceViewRoots, path.join(config.workspaceViewBase, config.project)],
|
|
1173
|
+
[controlRoots, path.join(config.controlBase, config.project)]
|
|
1174
|
+
]) {
|
|
1175
|
+
for (const directory of roots) {
|
|
1176
|
+
if (!fs.existsSync(directory))
|
|
1177
|
+
removeEmptyManagedParent(base, directory);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
const runBranchCleanup = prepareRemovalAction(config.project, target, targetDigest, 'branch-finalizing', 'branch-removed', resourceLocks);
|
|
1181
|
+
if (runBranchCleanup.run) {
|
|
1182
|
+
if (shouldRemoveWorktree && shouldDeleteBranch) {
|
|
1183
|
+
stageBranchRemoval(config, effectiveBranch, targetDigest, permits, runBranchCleanup.recovering);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
else if (shouldRemoveWorktree && shouldDeleteBranch
|
|
1187
|
+
&& runOk('git', ['-C', config.repoRoot, 'show-ref', '--verify', `refs/heads/${effectiveBranch}`])) {
|
|
1188
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${effectiveBranch}`);
|
|
1189
|
+
}
|
|
1190
|
+
advanceRemovalJournals(config.project, target, targetDigest, 'branch-removed', resourceLocks);
|
|
1191
|
+
cleanupCompletedRemovalArtifacts(config, target, committedTarget, targetDigest);
|
|
1192
|
+
const runToolCleanup = prepareRemovalAction(config.project, target, targetDigest, 'tool-finalizing', 'tool-removed', resourceLocks);
|
|
1193
|
+
if (runToolCleanup.run) {
|
|
1194
|
+
for (const { tool, candidates } of toolCandidates) {
|
|
1195
|
+
for (const dir of candidates.filter((candidate) => shouldStageManagedRemoval(candidate, removalTombstonePath(targetDigest, 'tool', candidate), runToolCleanup.recovering))) {
|
|
1196
|
+
stageManagedRemoval(tool.sandboxBase, dir, removalTombstonePath(targetDigest, 'tool', dir), runToolCleanup.recovering, {
|
|
1197
|
+
version: 1,
|
|
1198
|
+
targetDigest,
|
|
1199
|
+
permitDigest: committedTarget.permitDigest,
|
|
1200
|
+
kind: 'tool',
|
|
1201
|
+
source: dir
|
|
1202
|
+
});
|
|
1203
|
+
p.log.success(`${tool.name} state removed: ${dir}`);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
else {
|
|
1208
|
+
assertRemovalPathsAbsent(removalActionPaths(target, config, 'tool-removed'));
|
|
1209
|
+
}
|
|
1210
|
+
advanceRemovalJournals(config.project, target, targetDigest, 'tool-removed', resourceLocks);
|
|
1211
|
+
cleanupCompletedRemovalArtifacts(config, target, committedTarget, targetDigest);
|
|
1212
|
+
const runShellCleanup = prepareRemovalAction(config.project, target, targetDigest, 'shell-finalizing', 'shell-removed', resourceLocks);
|
|
1213
|
+
if (runShellCleanup.run) {
|
|
1214
|
+
for (const dir of shellConfigDirCandidates(config, effectiveBranch).filter((candidate) => shouldStageManagedRemoval(candidate, removalTombstonePath(targetDigest, 'shell', candidate), runShellCleanup.recovering))) {
|
|
1215
|
+
stageManagedRemoval(config.shellConfigBase, dir, removalTombstonePath(targetDigest, 'shell', dir), runShellCleanup.recovering, {
|
|
1216
|
+
version: 1,
|
|
1217
|
+
targetDigest,
|
|
1218
|
+
permitDigest: committedTarget.permitDigest,
|
|
1219
|
+
kind: 'shell',
|
|
1220
|
+
source: dir
|
|
1221
|
+
});
|
|
1222
|
+
p.log.success(`Shell config removed: ${dir}`);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
else {
|
|
1226
|
+
assertRemovalPathsAbsent(removalActionPaths(target, config, 'shell-removed'));
|
|
1227
|
+
}
|
|
1228
|
+
advanceRemovalJournals(config.project, target, targetDigest, 'shell-removed', resourceLocks);
|
|
1229
|
+
cleanupCompletedRemovalArtifacts(config, target, committedTarget, targetDigest);
|
|
1230
|
+
const runShareCleanup = prepareRemovalAction(config.project, target, targetDigest, 'share-finalizing', 'share-removed', resourceLocks);
|
|
1231
|
+
if (runShareCleanup.run) {
|
|
1232
|
+
if (shouldRemoveShare && shouldStageManagedRemoval(sharePath, removalTombstonePath(targetDigest, 'share', sharePath), runShareCleanup.recovering)) {
|
|
1233
|
+
stageManagedRemoval(config.shareBase, sharePath, removalTombstonePath(targetDigest, 'share', sharePath), runShareCleanup.recovering, {
|
|
1234
|
+
version: 1,
|
|
1235
|
+
targetDigest,
|
|
1236
|
+
permitDigest: committedTarget.permitDigest,
|
|
1237
|
+
kind: 'share',
|
|
1238
|
+
source: sharePath
|
|
1239
|
+
});
|
|
1240
|
+
p.log.success(`Share dir removed: ${sharePath}`);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
else if (shouldRemoveShare) {
|
|
1244
|
+
assertRemovalPathsAbsent(removalActionPaths(target, config, 'share-removed'));
|
|
1245
|
+
}
|
|
1246
|
+
advanceRemovalJournals(config.project, target, targetDigest, 'share-removed', resourceLocks);
|
|
1247
|
+
cleanupCompletedRemovalArtifacts(config, target, committedTarget, targetDigest);
|
|
1248
|
+
const completedJournals = advanceRemovalJournals(config.project, target, targetDigest, 'completed', resourceLocks);
|
|
1249
|
+
for (const journal of completedJournals)
|
|
1250
|
+
clearSandboxRemovalJournalRecord(journal);
|
|
1251
|
+
if (!options.quiet) {
|
|
1252
|
+
p.outro(pc.green('Sandbox removed'));
|
|
547
1253
|
}
|
|
548
1254
|
}
|
|
549
|
-
|
|
550
|
-
|
|
1255
|
+
finally {
|
|
1256
|
+
for (const lock of [...resourceLocks.values()].reverse())
|
|
1257
|
+
lock.release();
|
|
551
1258
|
}
|
|
552
1259
|
}
|
|
553
1260
|
async function rmPurge(config, tools, prompt = {}) {
|