@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,17 +1,433 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
|
-
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
5
6
|
import { buildProcessTreeStopCommand } from '../../server/process-control.js';
|
|
6
7
|
import { getProcessIdentityState, getProcessStartTime } from '../../server/process-state.js';
|
|
7
8
|
import { DEFAULT_SANDBOX_CONTROL_TIMING } from './protocol.js';
|
|
8
9
|
import { inspectSandboxControlContainer } from './container-identity.js';
|
|
10
|
+
import { acquireSandboxResourceLock, resolveSandboxLockNamespace } from './native-file-lock.js';
|
|
11
|
+
import { isSandboxAuthorityEvidence } from '../engines/authority.js';
|
|
9
12
|
import { parseSandboxControlStatus, readExecution, terminateSandboxControlExecution } from './state.js';
|
|
10
13
|
const DEFAULT_QUIESCE_TIMEOUT_MS = DEFAULT_SANDBOX_CONTROL_TIMING.quiesceDeadlineMs;
|
|
11
14
|
const QUIESCING_FILE = 'quiescing.json';
|
|
12
15
|
const BROKER_STARTING_FILE = 'broker-starting.json';
|
|
13
16
|
const REPLACEMENT_FILE = 'replacement.json';
|
|
14
17
|
const REPLACEMENT_STATE_SUFFIX = '.replacement-state.json';
|
|
18
|
+
const REMOVAL_JOURNAL_ROOT = path.join('.agent-infra', 'sandbox-removal-journal');
|
|
19
|
+
export const SANDBOX_REMOVAL_JOURNAL_PHASES = [
|
|
20
|
+
'prepared', 'target-committed', 'container-removal', 'container-absent',
|
|
21
|
+
'carrier-finalizing', 'carrier-removed', 'workspace-finalizing', 'workspace-removed',
|
|
22
|
+
'branch-finalizing', 'branch-removed', 'tool-finalizing', 'tool-removed',
|
|
23
|
+
'shell-finalizing', 'shell-removed', 'share-finalizing', 'share-removed', 'completed'
|
|
24
|
+
];
|
|
25
|
+
function removalCarrierDigest(engine, containerId, generation) {
|
|
26
|
+
return createHash('sha256').update(`${engine}\0${containerId}\0${generation}`).digest('hex');
|
|
27
|
+
}
|
|
28
|
+
function assertPrivateDirectory(directory) {
|
|
29
|
+
const home = path.resolve(os.homedir());
|
|
30
|
+
const resolved = path.resolve(directory);
|
|
31
|
+
const relative = path.relative(home, resolved);
|
|
32
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
33
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
|
|
34
|
+
}
|
|
35
|
+
let current = home;
|
|
36
|
+
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
37
|
+
current = path.join(current, segment);
|
|
38
|
+
const stat = fs.lstatSync(current);
|
|
39
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
40
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
|
|
41
|
+
}
|
|
42
|
+
if (process.platform !== 'win32' && typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
|
|
43
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function removalJournalDomain(lockDomain, create = true) {
|
|
48
|
+
const namespace = resolveSandboxLockNamespace('sandbox-removal-journal', { lockDomain });
|
|
49
|
+
const root = path.join(path.dirname(namespace.lockRoot), REMOVAL_JOURNAL_ROOT.split(path.sep).at(-1));
|
|
50
|
+
if (create)
|
|
51
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
52
|
+
assertPrivateDirectory(root);
|
|
53
|
+
const domain = path.join(root, lockDomain);
|
|
54
|
+
if (create)
|
|
55
|
+
fs.mkdirSync(domain, { recursive: true, mode: 0o700 });
|
|
56
|
+
assertPrivateDirectory(domain);
|
|
57
|
+
return domain;
|
|
58
|
+
}
|
|
59
|
+
function removalJournalPath(manifest) {
|
|
60
|
+
return path.join(removalJournalDomain(manifest.authorityEvidence.lockDomain), `${removalCarrierDigest(manifest.engine, manifest.containerIdentity.id, manifest.generation)}.json`);
|
|
61
|
+
}
|
|
62
|
+
function removalJournalPathForRecord(record) {
|
|
63
|
+
return path.join(removalJournalDomain(record.lockDomain, false), `${record.carrierIdentityDigest}.json`);
|
|
64
|
+
}
|
|
65
|
+
function isRecord(value) {
|
|
66
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
67
|
+
}
|
|
68
|
+
function hasExactKeys(value, keys) {
|
|
69
|
+
const actual = Object.keys(value).sort();
|
|
70
|
+
return actual.length === keys.length && actual.every((key, index) => key === keys[index]);
|
|
71
|
+
}
|
|
72
|
+
function isRemovalPermitCommit(value) {
|
|
73
|
+
if (!isRecord(value) || typeof value.path !== 'string' || !path.isAbsolute(value.path)
|
|
74
|
+
|| (value.mode !== 'clean' && value.mode !== 'discard') || !isRecord(value.snapshot)
|
|
75
|
+
|| typeof value.snapshot.worktree !== 'string' || !path.isAbsolute(value.snapshot.worktree)
|
|
76
|
+
|| typeof value.snapshot.branch !== 'string' || typeof value.snapshot.head !== 'string'
|
|
77
|
+
|| typeof value.snapshot.identity !== 'string' || !Array.isArray(value.snapshot.changes)
|
|
78
|
+
|| value.snapshot.changes.some((change) => !isRecord(change)
|
|
79
|
+
|| typeof change.indexStatus !== 'string' || typeof change.worktreeStatus !== 'string'
|
|
80
|
+
|| typeof change.path !== 'string'
|
|
81
|
+
|| (change.originalPath !== undefined && typeof change.originalPath !== 'string'))
|
|
82
|
+
|| (value.snapshot.source !== undefined
|
|
83
|
+
&& value.snapshot.source !== 'registered' && value.snapshot.source !== 'recovered'))
|
|
84
|
+
return false;
|
|
85
|
+
if (value.snapshot.recovery === undefined)
|
|
86
|
+
return true;
|
|
87
|
+
const recovery = value.snapshot.recovery;
|
|
88
|
+
return isRecord(recovery)
|
|
89
|
+
&& typeof recovery.repoRoot === 'string' && path.isAbsolute(recovery.repoRoot)
|
|
90
|
+
&& typeof recovery.worktreeBase === 'string' && path.isAbsolute(recovery.worktreeBase)
|
|
91
|
+
&& typeof recovery.branch === 'string'
|
|
92
|
+
&& (recovery.identitySource === 'branch-only' || recovery.identitySource === 'task-bound')
|
|
93
|
+
&& (recovery.taskId === null || typeof recovery.taskId === 'string');
|
|
94
|
+
}
|
|
95
|
+
function parseRemovalJournal(raw) {
|
|
96
|
+
let value;
|
|
97
|
+
try {
|
|
98
|
+
value = JSON.parse(raw);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
|
|
102
|
+
}
|
|
103
|
+
if (!isRecord(value) || !hasExactKeys(value, [
|
|
104
|
+
'authorityFingerprint', 'carrierIdentityDigest', 'containerId', 'engine', 'expectedOldJournalRevision',
|
|
105
|
+
'generation', 'handoffId', 'lockDomain', 'newOperation', 'oldOperation', 'operation', 'owner', 'phase',
|
|
106
|
+
'recordedAt', 'revision', 'target', 'transitionId', 'version'
|
|
107
|
+
]) || !isRecord(value.owner) || !hasExactKeys(value.owner, ['leaseNonce', 'pid', 'startTime'])
|
|
108
|
+
|| !isRecord(value.target) || !hasExactKeys(value.target, [
|
|
109
|
+
'branch', 'controlRoot', 'permitDigest', 'permits', 'project', 'removeBranch', 'removeShare',
|
|
110
|
+
'removeWorktree', 'sharePath', 'shellPaths', 'targetDigest', 'toolPaths', 'workspaceViewPaths',
|
|
111
|
+
'worktreePaths'
|
|
112
|
+
]) || value.version !== 2 || value.operation !== 'sandbox-rm'
|
|
113
|
+
|| value.oldOperation !== 'sandbox-rm' || value.newOperation !== 'sandbox-rm'
|
|
114
|
+
|| typeof value.handoffId !== 'string' || value.handoffId.length === 0
|
|
115
|
+
|| typeof value.transitionId !== 'string' || value.transitionId.length === 0
|
|
116
|
+
|| typeof value.engine !== 'string' || value.engine.length === 0
|
|
117
|
+
|| typeof value.containerId !== 'string' || value.containerId.length === 0
|
|
118
|
+
|| typeof value.generation !== 'string' || value.generation.length === 0
|
|
119
|
+
|| typeof value.authorityFingerprint !== 'string' || !/^[a-f0-9]{64}$/u.test(value.authorityFingerprint)
|
|
120
|
+
|| typeof value.carrierIdentityDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(value.carrierIdentityDigest)
|
|
121
|
+
|| typeof value.lockDomain !== 'string' || !/^[a-f0-9]{64}$/u.test(value.lockDomain)
|
|
122
|
+
|| !Number.isSafeInteger(value.owner.pid) || value.owner.pid <= 0
|
|
123
|
+
|| !Number.isSafeInteger(value.owner.startTime) || typeof value.owner.leaseNonce !== 'string'
|
|
124
|
+
|| value.owner.leaseNonce.length === 0
|
|
125
|
+
|| !SANDBOX_REMOVAL_JOURNAL_PHASES.includes(value.phase)
|
|
126
|
+
|| !(value.expectedOldJournalRevision === null
|
|
127
|
+
|| (Number.isSafeInteger(value.expectedOldJournalRevision) && value.expectedOldJournalRevision >= 0))
|
|
128
|
+
|| !Number.isSafeInteger(value.revision) || value.revision <= 0
|
|
129
|
+
|| typeof value.target.branch !== 'string' || value.target.branch.length === 0
|
|
130
|
+
|| typeof value.target.project !== 'string' || value.target.project.length === 0
|
|
131
|
+
|| typeof value.target.controlRoot !== 'string' || path.isAbsolute(value.target.controlRoot) === false
|
|
132
|
+
|| typeof value.target.targetDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(value.target.targetDigest)
|
|
133
|
+
|| typeof value.target.permitDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(value.target.permitDigest)
|
|
134
|
+
|| typeof value.target.removeWorktree !== 'boolean'
|
|
135
|
+
|| typeof value.target.removeBranch !== 'boolean'
|
|
136
|
+
|| typeof value.target.removeShare !== 'boolean'
|
|
137
|
+
|| typeof value.target.sharePath !== 'string' || !path.isAbsolute(value.target.sharePath)
|
|
138
|
+
|| !Array.isArray(value.target.worktreePaths) || value.target.worktreePaths.some((entry) => typeof entry !== 'string' || !path.isAbsolute(entry))
|
|
139
|
+
|| !Array.isArray(value.target.workspaceViewPaths) || value.target.workspaceViewPaths.some((entry) => typeof entry !== 'string' || !path.isAbsolute(entry))
|
|
140
|
+
|| !Array.isArray(value.target.toolPaths) || value.target.toolPaths.some((entry) => typeof entry !== 'string' || !path.isAbsolute(entry))
|
|
141
|
+
|| !Array.isArray(value.target.shellPaths) || value.target.shellPaths.some((entry) => typeof entry !== 'string' || !path.isAbsolute(entry))
|
|
142
|
+
|| !Array.isArray(value.target.permits)
|
|
143
|
+
|| value.target.permits.some((permit) => !isRemovalPermitCommit(permit))
|
|
144
|
+
|| !Number.isSafeInteger(value.recordedAt)) {
|
|
145
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
|
|
146
|
+
}
|
|
147
|
+
const expected = value.expectedOldJournalRevision;
|
|
148
|
+
const revision = value.revision;
|
|
149
|
+
if ((expected === null && revision !== 1) || (expected !== null && revision !== expected + 1)) {
|
|
150
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
version: 2,
|
|
154
|
+
operation: 'sandbox-rm',
|
|
155
|
+
oldOperation: 'sandbox-rm',
|
|
156
|
+
newOperation: 'sandbox-rm',
|
|
157
|
+
handoffId: value.handoffId,
|
|
158
|
+
transitionId: value.transitionId,
|
|
159
|
+
engine: value.engine,
|
|
160
|
+
containerId: value.containerId,
|
|
161
|
+
generation: value.generation,
|
|
162
|
+
authorityFingerprint: value.authorityFingerprint,
|
|
163
|
+
carrierIdentityDigest: value.carrierIdentityDigest,
|
|
164
|
+
lockDomain: value.lockDomain,
|
|
165
|
+
owner: {
|
|
166
|
+
pid: value.owner.pid,
|
|
167
|
+
startTime: value.owner.startTime,
|
|
168
|
+
leaseNonce: value.owner.leaseNonce
|
|
169
|
+
},
|
|
170
|
+
phase: value.phase,
|
|
171
|
+
expectedOldJournalRevision: expected,
|
|
172
|
+
revision,
|
|
173
|
+
target: {
|
|
174
|
+
branch: value.target.branch,
|
|
175
|
+
project: value.target.project,
|
|
176
|
+
controlRoot: value.target.controlRoot,
|
|
177
|
+
targetDigest: value.target.targetDigest,
|
|
178
|
+
permitDigest: value.target.permitDigest,
|
|
179
|
+
removeWorktree: value.target.removeWorktree,
|
|
180
|
+
removeBranch: value.target.removeBranch,
|
|
181
|
+
removeShare: value.target.removeShare,
|
|
182
|
+
worktreePaths: value.target.worktreePaths,
|
|
183
|
+
workspaceViewPaths: value.target.workspaceViewPaths,
|
|
184
|
+
toolPaths: value.target.toolPaths,
|
|
185
|
+
shellPaths: value.target.shellPaths,
|
|
186
|
+
sharePath: value.target.sharePath,
|
|
187
|
+
permits: value.target.permits
|
|
188
|
+
},
|
|
189
|
+
recordedAt: value.recordedAt
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function readRemovalJournalAt(target) {
|
|
193
|
+
if (!fs.existsSync(target))
|
|
194
|
+
return null;
|
|
195
|
+
const stat = fs.lstatSync(target);
|
|
196
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
197
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
|
|
198
|
+
return parseRemovalJournal(fs.readFileSync(target, 'utf8'));
|
|
199
|
+
}
|
|
200
|
+
export function readSandboxRemovalJournal(manifest) {
|
|
201
|
+
return readRemovalJournalAt(removalJournalPath(manifest));
|
|
202
|
+
}
|
|
203
|
+
export function listSandboxRemovalJournals(filter = {}) {
|
|
204
|
+
const namespace = resolveSandboxLockNamespace('sandbox-removal-journal');
|
|
205
|
+
const root = path.join(path.dirname(namespace.lockRoot), REMOVAL_JOURNAL_ROOT.split(path.sep).at(-1));
|
|
206
|
+
if (!fs.existsSync(root))
|
|
207
|
+
return [];
|
|
208
|
+
assertPrivateDirectory(root);
|
|
209
|
+
const journals = [];
|
|
210
|
+
for (const domainEntry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
211
|
+
if (!domainEntry.isDirectory() || domainEntry.isSymbolicLink()) {
|
|
212
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
|
|
213
|
+
}
|
|
214
|
+
const domain = path.join(root, domainEntry.name);
|
|
215
|
+
assertPrivateDirectory(domain);
|
|
216
|
+
for (const entry of fs.readdirSync(domain, { withFileTypes: true })) {
|
|
217
|
+
if (!entry.isFile() || entry.isSymbolicLink() || !entry.name.endsWith('.json')) {
|
|
218
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
|
|
219
|
+
}
|
|
220
|
+
const record = readRemovalJournalAt(path.join(domain, entry.name));
|
|
221
|
+
if (!record || record.lockDomain !== domainEntry.name
|
|
222
|
+
|| `${record.carrierIdentityDigest}.json` !== entry.name) {
|
|
223
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
|
|
224
|
+
}
|
|
225
|
+
if ((!filter.branch || record.target.branch === filter.branch)
|
|
226
|
+
&& (!filter.project || record.target.project === filter.project)
|
|
227
|
+
&& (!filter.targetDigest || record.target.targetDigest === filter.targetDigest)) {
|
|
228
|
+
journals.push(record);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return journals;
|
|
233
|
+
}
|
|
234
|
+
export function clearSandboxRemovalJournalRecord(record) {
|
|
235
|
+
const target = removalJournalPathForRecord(record);
|
|
236
|
+
if (fs.existsSync(target)) {
|
|
237
|
+
const current = readRemovalJournalAt(target);
|
|
238
|
+
if (!current || current.revision !== record.revision || current.handoffId !== record.handoffId) {
|
|
239
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_REVISION_MISMATCH');
|
|
240
|
+
}
|
|
241
|
+
fs.rmSync(target, { force: true });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
export function clearSandboxRemovalJournal(manifest) {
|
|
245
|
+
const target = removalJournalPath(manifest);
|
|
246
|
+
if (fs.existsSync(target))
|
|
247
|
+
fs.rmSync(target, { force: true });
|
|
248
|
+
}
|
|
249
|
+
function defaultRemovalTarget(manifest) {
|
|
250
|
+
const targetDigest = createHash('sha256')
|
|
251
|
+
.update(`${manifest.project}\0${manifest.branch}\0${manifest.container}\0${manifest.channelDir}`)
|
|
252
|
+
.digest('hex');
|
|
253
|
+
return {
|
|
254
|
+
branch: manifest.branch,
|
|
255
|
+
project: manifest.project,
|
|
256
|
+
controlRoot: path.dirname(manifest.channelDir),
|
|
257
|
+
targetDigest,
|
|
258
|
+
permitDigest: createHash('sha256').update('none').digest('hex'),
|
|
259
|
+
removeWorktree: false,
|
|
260
|
+
removeBranch: false,
|
|
261
|
+
removeShare: false,
|
|
262
|
+
worktreePaths: [],
|
|
263
|
+
workspaceViewPaths: [],
|
|
264
|
+
toolPaths: [],
|
|
265
|
+
shellPaths: [],
|
|
266
|
+
sharePath: path.join(path.dirname(manifest.channelDir), 'share'),
|
|
267
|
+
permits: []
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function sameRemovalTarget(left, right) {
|
|
271
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
272
|
+
}
|
|
273
|
+
function phaseIndex(phase) {
|
|
274
|
+
return SANDBOX_REMOVAL_JOURNAL_PHASES.indexOf(phase);
|
|
275
|
+
}
|
|
276
|
+
export function sandboxRemovalPhaseIndex(phase) {
|
|
277
|
+
return phaseIndex(phase);
|
|
278
|
+
}
|
|
279
|
+
function assertNextRemovalPhase(current, next) {
|
|
280
|
+
const currentIndex = phaseIndex(current);
|
|
281
|
+
const nextIndex = phaseIndex(next);
|
|
282
|
+
if (nextIndex === currentIndex)
|
|
283
|
+
return false;
|
|
284
|
+
if (current === 'target-committed' && next === 'container-absent')
|
|
285
|
+
return true;
|
|
286
|
+
if (nextIndex !== currentIndex + 1) {
|
|
287
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_PHASE_TRANSITION_INVALID');
|
|
288
|
+
}
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
function assertRemovalJournalLock(record, resourceLock) {
|
|
292
|
+
const namespace = resolveSandboxLockNamespace(`${record.engine}:${record.containerId}`, {
|
|
293
|
+
lockDomain: record.lockDomain
|
|
294
|
+
});
|
|
295
|
+
if (resourceLock.lockDomain !== record.lockDomain || resourceLock.path !== namespace.lockPath) {
|
|
296
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_LOCK_MISMATCH');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function writeRemovalJournalAt(target, base, phase, expectedOldJournalRevision) {
|
|
300
|
+
const existing = readRemovalJournalAt(target);
|
|
301
|
+
if (expectedOldJournalRevision === null
|
|
302
|
+
? existing !== null
|
|
303
|
+
: existing?.revision !== expectedOldJournalRevision) {
|
|
304
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_REVISION_MISMATCH');
|
|
305
|
+
}
|
|
306
|
+
const record = {
|
|
307
|
+
...base,
|
|
308
|
+
phase,
|
|
309
|
+
expectedOldJournalRevision,
|
|
310
|
+
revision: expectedOldJournalRevision === null ? 1 : expectedOldJournalRevision + 1,
|
|
311
|
+
recordedAt: Date.now()
|
|
312
|
+
};
|
|
313
|
+
const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
314
|
+
try {
|
|
315
|
+
fs.writeFileSync(temporary, `${JSON.stringify(record)}\n`, { mode: 0o600, flag: 'wx' });
|
|
316
|
+
if (expectedOldJournalRevision === null && fs.existsSync(target)) {
|
|
317
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_REVISION_MISMATCH');
|
|
318
|
+
}
|
|
319
|
+
fs.renameSync(temporary, target);
|
|
320
|
+
}
|
|
321
|
+
finally {
|
|
322
|
+
fs.rmSync(temporary, { force: true });
|
|
323
|
+
}
|
|
324
|
+
return record;
|
|
325
|
+
}
|
|
326
|
+
function writeRemovalJournal(manifest, base, phase, expectedOldJournalRevision) {
|
|
327
|
+
return writeRemovalJournalAt(removalJournalPath(manifest), base, phase, expectedOldJournalRevision);
|
|
328
|
+
}
|
|
329
|
+
function startRemovalJournal(manifest, options) {
|
|
330
|
+
const target = removalJournalPath(manifest);
|
|
331
|
+
const existing = readRemovalJournalAt(target);
|
|
332
|
+
if (existing && (existing.engine !== manifest.engine || existing.containerId !== manifest.containerIdentity.id
|
|
333
|
+
|| existing.generation !== manifest.generation
|
|
334
|
+
|| existing.authorityFingerprint !== manifest.authorityEvidence.authorityFingerprint
|
|
335
|
+
|| existing.lockDomain !== manifest.authorityEvidence.lockDomain)) {
|
|
336
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_IDENTITY_MISMATCH');
|
|
337
|
+
}
|
|
338
|
+
const targetCommit = options.target ?? existing?.target ?? defaultRemovalTarget(manifest);
|
|
339
|
+
if (existing && options.target && !sameRemovalTarget(existing.target, options.target)) {
|
|
340
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH');
|
|
341
|
+
}
|
|
342
|
+
if (existing) {
|
|
343
|
+
const state = options.identityProbe(existing.owner);
|
|
344
|
+
if (state === 'alive')
|
|
345
|
+
throw new Error('SANDBOX_CONTROL_REMOVE_RETRY_IN_PROGRESS');
|
|
346
|
+
if (state === 'unknown')
|
|
347
|
+
throw new Error('SANDBOX_CONTROL_REMOVE_OWNER_UNKNOWN');
|
|
348
|
+
}
|
|
349
|
+
const startTime = getProcessStartTime(process.pid);
|
|
350
|
+
if (startTime === null)
|
|
351
|
+
throw new Error('SANDBOX_CONTROL_REMOVE_OWNER_UNAVAILABLE');
|
|
352
|
+
const base = {
|
|
353
|
+
version: 2,
|
|
354
|
+
operation: 'sandbox-rm',
|
|
355
|
+
oldOperation: 'sandbox-rm',
|
|
356
|
+
newOperation: 'sandbox-rm',
|
|
357
|
+
handoffId: randomUUID(),
|
|
358
|
+
transitionId: randomUUID(),
|
|
359
|
+
engine: manifest.engine,
|
|
360
|
+
containerId: manifest.containerIdentity.id,
|
|
361
|
+
generation: manifest.generation,
|
|
362
|
+
authorityFingerprint: manifest.authorityEvidence.authorityFingerprint,
|
|
363
|
+
carrierIdentityDigest: removalCarrierDigest(manifest.engine, manifest.containerIdentity.id, manifest.generation),
|
|
364
|
+
lockDomain: manifest.authorityEvidence.lockDomain,
|
|
365
|
+
owner: { pid: process.pid, startTime, leaseNonce: randomUUID() },
|
|
366
|
+
target: targetCommit
|
|
367
|
+
};
|
|
368
|
+
const initialPhase = existing?.phase ?? 'prepared';
|
|
369
|
+
let record = writeRemovalJournal(manifest, base, initialPhase, existing?.revision ?? null);
|
|
370
|
+
return {
|
|
371
|
+
get record() { return record; },
|
|
372
|
+
write(phase) {
|
|
373
|
+
if (!assertNextRemovalPhase(record.phase, phase))
|
|
374
|
+
return record;
|
|
375
|
+
record = writeRemovalJournal(manifest, base, phase, record.revision);
|
|
376
|
+
return record;
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
export function claimSandboxRemovalJournal(journal, options = {}) {
|
|
381
|
+
const target = removalJournalPathForRecord(journal);
|
|
382
|
+
const resourceLock = options.resourceLock
|
|
383
|
+
?? acquireSandboxResourceLock(`${journal.engine}:${journal.containerId}`, { lockDomain: journal.lockDomain });
|
|
384
|
+
const ownedLock = options.resourceLock ? null : resourceLock;
|
|
385
|
+
try {
|
|
386
|
+
assertRemovalJournalLock(journal, resourceLock);
|
|
387
|
+
const current = readRemovalJournalAt(target);
|
|
388
|
+
if (!current || current.revision !== journal.revision || current.handoffId !== journal.handoffId) {
|
|
389
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_REVISION_MISMATCH');
|
|
390
|
+
}
|
|
391
|
+
const state = (options.identityProbe ?? getProcessIdentityState)(current.owner);
|
|
392
|
+
if (state === 'alive')
|
|
393
|
+
throw new Error('SANDBOX_CONTROL_REMOVE_RETRY_IN_PROGRESS');
|
|
394
|
+
if (state === 'unknown')
|
|
395
|
+
throw new Error('SANDBOX_CONTROL_REMOVE_OWNER_UNKNOWN');
|
|
396
|
+
const startTime = getProcessStartTime(process.pid);
|
|
397
|
+
if (startTime === null)
|
|
398
|
+
throw new Error('SANDBOX_CONTROL_REMOVE_OWNER_UNAVAILABLE');
|
|
399
|
+
const base = {
|
|
400
|
+
...current,
|
|
401
|
+
handoffId: randomUUID(),
|
|
402
|
+
transitionId: randomUUID(),
|
|
403
|
+
owner: { pid: process.pid, startTime, leaseNonce: randomUUID() }
|
|
404
|
+
};
|
|
405
|
+
return writeRemovalJournalAt(target, base, current.phase, current.revision);
|
|
406
|
+
}
|
|
407
|
+
finally {
|
|
408
|
+
ownedLock?.release();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
export function advanceSandboxRemovalJournalPhase(journal, phase, options = {}) {
|
|
412
|
+
const target = removalJournalPathForRecord(journal);
|
|
413
|
+
const resourceLock = options.resourceLock
|
|
414
|
+
?? acquireSandboxResourceLock(`${journal.engine}:${journal.containerId}`, { lockDomain: journal.lockDomain });
|
|
415
|
+
const ownedLock = options.resourceLock ? null : resourceLock;
|
|
416
|
+
try {
|
|
417
|
+
assertRemovalJournalLock(journal, resourceLock);
|
|
418
|
+
const current = readRemovalJournalAt(target);
|
|
419
|
+
if (!current || current.revision !== journal.revision || current.handoffId !== journal.handoffId) {
|
|
420
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_REVISION_MISMATCH');
|
|
421
|
+
}
|
|
422
|
+
if (!assertNextRemovalPhase(current.phase, phase))
|
|
423
|
+
return current;
|
|
424
|
+
const { phase: _phase, expectedOldJournalRevision: _expected, revision: _revision, recordedAt: _recordedAt, ...base } = current;
|
|
425
|
+
return writeRemovalJournalAt(target, base, phase, current.revision);
|
|
426
|
+
}
|
|
427
|
+
finally {
|
|
428
|
+
ownedLock?.release();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
15
431
|
async function awaitWithDeadline(operation, deadlineAt, errorCode) {
|
|
16
432
|
const remaining = deadlineAt - Date.now();
|
|
17
433
|
if (remaining <= 0)
|
|
@@ -62,6 +478,25 @@ function markSandboxControlRootQuiescing(root) {
|
|
|
62
478
|
throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
63
479
|
}
|
|
64
480
|
}
|
|
481
|
+
function recordSandboxRemovalPending(root, manifest, phase, journal) {
|
|
482
|
+
const pendingPath = path.join(root, 'removal-pending.json');
|
|
483
|
+
const temporary = `${pendingPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
484
|
+
try {
|
|
485
|
+
fs.writeFileSync(temporary, `${JSON.stringify({
|
|
486
|
+
version: 1,
|
|
487
|
+
phase,
|
|
488
|
+
containerId: manifest.containerIdentity.id,
|
|
489
|
+
generation: manifest.generation,
|
|
490
|
+
authorityFingerprint: manifest.authorityEvidence.authorityFingerprint,
|
|
491
|
+
recordedAt: Date.now()
|
|
492
|
+
})}\n`, { mode: 0o600, flag: 'wx' });
|
|
493
|
+
fs.renameSync(temporary, pendingPath);
|
|
494
|
+
}
|
|
495
|
+
finally {
|
|
496
|
+
fs.rmSync(temporary, { force: true });
|
|
497
|
+
}
|
|
498
|
+
journal.write(phase === 'container-verification' ? 'container-absent' : 'container-removal');
|
|
499
|
+
}
|
|
65
500
|
function readStartupOwner(filePath) {
|
|
66
501
|
if (!fs.existsSync(filePath))
|
|
67
502
|
return null;
|
|
@@ -446,9 +881,10 @@ function readSandboxControlManifestValue(manifestPath) {
|
|
|
446
881
|
const expectedKeys = [
|
|
447
882
|
'branch', 'channelDir', 'container', 'containerIdentity', 'engine', 'generation',
|
|
448
883
|
'mode', 'processingDir', 'project', 'publicStatusDir', 'repoRoot', 'runtimeDir',
|
|
449
|
-
'taskId', 'token', 'worktreeRoot'
|
|
884
|
+
'taskId', 'token', 'worktreeRoot', 'authorityEvidence'
|
|
450
885
|
];
|
|
451
|
-
|
|
886
|
+
const actualKeys = Object.keys(manifest).sort().join(',');
|
|
887
|
+
if (actualKeys !== expectedKeys.sort().join(',')) {
|
|
452
888
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
453
889
|
}
|
|
454
890
|
if (typeof candidate.containerIdentity !== 'object' || candidate.containerIdentity === null
|
|
@@ -471,7 +907,8 @@ function readSandboxControlManifestValue(manifestPath) {
|
|
|
471
907
|
|| typeof candidate.token !== 'string'
|
|
472
908
|
|| typeof candidate.generation !== 'string'
|
|
473
909
|
|| (candidate.mode === 'task-bound' && (typeof candidate.taskId !== 'string' || candidate.taskId.length === 0))
|
|
474
|
-
|| (candidate.mode === 'branch-only' && candidate.taskId !== null)
|
|
910
|
+
|| (candidate.mode === 'branch-only' && candidate.taskId !== null)
|
|
911
|
+
|| !isSandboxAuthorityEvidence(candidate.authorityEvidence))
|
|
475
912
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
476
913
|
const root = path.dirname(path.resolve(manifestPath));
|
|
477
914
|
if (path.resolve(candidate.channelDir) !== path.join(root, 'channel')
|
|
@@ -492,6 +929,7 @@ function readSandboxControlManifestValue(manifestPath) {
|
|
|
492
929
|
id: containerIdentity.id,
|
|
493
930
|
labels: { ...containerIdentity.labels }
|
|
494
931
|
},
|
|
932
|
+
authorityEvidence: candidate.authorityEvidence,
|
|
495
933
|
branch: candidate.branch,
|
|
496
934
|
mode: candidate.mode,
|
|
497
935
|
taskId: candidate.taskId,
|
|
@@ -761,142 +1199,198 @@ export async function removeSandboxControlRoot(root, options) {
|
|
|
761
1199
|
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
762
1200
|
throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
763
1201
|
const manifestPath = path.join(resolvedRoot, 'manifest.json');
|
|
1202
|
+
const manifestRawBeforeLock = fs.readFileSync(manifestPath, 'utf8');
|
|
764
1203
|
const manifest = readSandboxControlManifest(manifestPath);
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
const deadlineAt = Date.now() + timeoutMs;
|
|
770
|
-
const forceAt = forceDeadline(deadlineAt, timeoutMs);
|
|
771
|
-
const remaining = () => Math.max(0, deadlineAt - Date.now());
|
|
772
|
-
const observation = await awaitWithDeadline(() => inspectContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
773
|
-
if (observation.state === 'unknown')
|
|
774
|
-
throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${observation.reason}`);
|
|
775
|
-
if (options.requireAbsent && observation.state !== 'absent') {
|
|
776
|
-
throw new Error('SANDBOX_CONTROL_CONTAINER_REAPPEARED');
|
|
777
|
-
}
|
|
778
|
-
markSandboxControlRootQuiescing(resolvedRoot);
|
|
779
|
-
await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()), identityProbe);
|
|
780
|
-
const brokerPath = path.join(resolvedRoot, 'broker.json');
|
|
781
|
-
const statusPath = path.join(resolvedRoot, 'public', 'status.json');
|
|
782
|
-
const broker = readBrokerOwner(brokerPath);
|
|
783
|
-
const status = readStatusOwner(statusPath);
|
|
784
|
-
if (broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
|
|
785
|
-
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
786
|
-
}
|
|
787
|
-
if (status && status.generation !== manifest.generation) {
|
|
788
|
-
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
789
|
-
}
|
|
790
|
-
if (broker && status && !sameOwner(broker, status.broker)
|
|
791
|
-
&& (ownerLive(broker, identityProbe) || ownerLive(status.broker, identityProbe))) {
|
|
792
|
-
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
793
|
-
}
|
|
794
|
-
const platform = options.platform ?? process.platform;
|
|
795
|
-
const owner = broker && ownerLive(broker, identityProbe) ? broker
|
|
796
|
-
: status && ownerLive(status.broker, identityProbe) ? status.broker : null;
|
|
797
|
-
const selfOwner = options.selfOwner;
|
|
798
|
-
const selfOwned = Boolean(selfOwner && broker && sameOwner(broker, selfOwner));
|
|
799
|
-
if (selfOwner && (!broker || !selfOwned || broker.token !== manifest.token || broker.generation !== manifest.generation)) {
|
|
800
|
-
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
801
|
-
}
|
|
802
|
-
let executions = readExecutions(manifest);
|
|
803
|
-
if (owner && !selfOwned)
|
|
804
|
-
signalOwner(owner, platform, false);
|
|
805
|
-
for (const execution of executions) {
|
|
806
|
-
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
|
|
807
|
-
}
|
|
808
|
-
const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
|
|
809
|
-
while (Date.now() < softDeadlineAt) {
|
|
810
|
-
if ((!owner || selfOwned || !ownerLive(owner, identityProbe))
|
|
811
|
-
&& !executions.some((execution) => executionAlive(execution, platform, identityProbe)))
|
|
812
|
-
break;
|
|
813
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
814
|
-
}
|
|
815
|
-
if (observation.state === 'found') {
|
|
816
|
-
if (Date.now() >= deadlineAt)
|
|
817
|
-
throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
818
|
-
await awaitWithDeadline(() => options.removeContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
819
|
-
const afterRemoval = await awaitWithDeadline(() => inspectContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
820
|
-
if (afterRemoval.state !== 'absent') {
|
|
821
|
-
throw new Error(afterRemoval.state === 'unknown'
|
|
822
|
-
? `SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${afterRemoval.reason}`
|
|
823
|
-
: 'SANDBOX_CONTROL_CONTAINER_STILL_EXISTS');
|
|
1204
|
+
const resourceLock = options.resourceLock ?? acquireSandboxResourceLock(`${manifest.engine}:${manifest.containerIdentity.id}`, { lockDomain: manifest.authorityEvidence.lockDomain });
|
|
1205
|
+
try {
|
|
1206
|
+
if (fs.readFileSync(manifestPath, 'utf8') !== manifestRawBeforeLock) {
|
|
1207
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_CHANGED');
|
|
824
1208
|
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
for (const execution of executions) {
|
|
849
|
-
if (!terminateSandboxControlExecution(execution, {
|
|
850
|
-
platform, timeoutMs: remaining(), deadlineAt, forceAt: Date.now(), identityProbe
|
|
851
|
-
})) {
|
|
852
|
-
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
|
|
1209
|
+
const inspectContainer = options.inspectContainer
|
|
1210
|
+
?? ((timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }));
|
|
1211
|
+
const timeoutMs = options.timeoutMs ?? options.timing?.quiesceDeadlineMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
|
|
1212
|
+
const identityProbe = options.identityProbe ?? getProcessIdentityState;
|
|
1213
|
+
const deadlineAt = Date.now() + timeoutMs;
|
|
1214
|
+
const forceAt = forceDeadline(deadlineAt, timeoutMs);
|
|
1215
|
+
const remaining = () => Math.max(0, deadlineAt - Date.now());
|
|
1216
|
+
const journal = startRemovalJournal(manifest, {
|
|
1217
|
+
target: options.removalTarget,
|
|
1218
|
+
identityProbe
|
|
1219
|
+
});
|
|
1220
|
+
if (journal.record.phase === 'carrier-finalizing') {
|
|
1221
|
+
if (fs.readFileSync(manifestPath, 'utf8') !== manifestRawBeforeLock
|
|
1222
|
+
|| !isSandboxControlRootQuiescing(resolvedRoot)) {
|
|
1223
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_CHANGED');
|
|
1224
|
+
}
|
|
1225
|
+
fs.rmSync(resolvedRoot, { recursive: true, force: true });
|
|
1226
|
+
if (fs.existsSync(resolvedRoot))
|
|
1227
|
+
throw new Error('SANDBOX_CONTROL_TARGET_MISMATCH');
|
|
1228
|
+
journal.write('carrier-removed');
|
|
1229
|
+
if (!options.retainRemovalJournal)
|
|
1230
|
+
clearSandboxRemovalJournal(manifest);
|
|
1231
|
+
return;
|
|
853
1232
|
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
if (remaining() <= 0)
|
|
857
|
-
throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
858
|
-
signalOwner(currentBroker, platform, true);
|
|
859
|
-
if (remaining() <= 0 || !await waitForExit(currentBroker, remaining(), deadlineAt, identityProbe)) {
|
|
860
|
-
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
1233
|
+
if (sandboxRemovalPhaseIndex(journal.record.phase) >= sandboxRemovalPhaseIndex('carrier-removed')) {
|
|
1234
|
+
throw new Error('SANDBOX_CONTROL_TARGET_MISMATCH');
|
|
861
1235
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
1236
|
+
if (journal.record.phase === 'prepared')
|
|
1237
|
+
journal.write('target-committed');
|
|
1238
|
+
const observation = await awaitWithDeadline(() => inspectContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
1239
|
+
if (observation.state === 'unknown')
|
|
1240
|
+
throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${observation.reason}`);
|
|
1241
|
+
journal.write(observation.state === 'absent' ? 'container-absent' : 'container-removal');
|
|
1242
|
+
if (options.requireAbsent && observation.state !== 'absent') {
|
|
1243
|
+
throw new Error('SANDBOX_CONTROL_CONTAINER_REAPPEARED');
|
|
1244
|
+
}
|
|
1245
|
+
markSandboxControlRootQuiescing(resolvedRoot);
|
|
1246
|
+
await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()), identityProbe);
|
|
1247
|
+
const brokerPath = path.join(resolvedRoot, 'broker.json');
|
|
1248
|
+
const statusPath = path.join(resolvedRoot, 'public', 'status.json');
|
|
1249
|
+
const broker = readBrokerOwner(brokerPath);
|
|
1250
|
+
const status = readStatusOwner(statusPath);
|
|
1251
|
+
if (broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
|
|
871
1252
|
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
872
|
-
const finalSelfOwner = Boolean(selfOwner && sameOwner(finalBroker, selfOwner)
|
|
873
|
-
&& finalBroker.token === manifest.token && finalBroker.generation === manifest.generation);
|
|
874
|
-
if (ownerLive(finalBroker, identityProbe) && !finalSelfOwner) {
|
|
875
|
-
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
876
1253
|
}
|
|
877
|
-
if (
|
|
878
|
-
throw new Error('
|
|
1254
|
+
if (status && status.generation !== manifest.generation) {
|
|
1255
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
879
1256
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
const finalStatusRaw = fs.existsSync(statusPath) ? fs.readFileSync(statusPath, 'utf8') : null;
|
|
883
|
-
if (finalStatusRaw !== null) {
|
|
884
|
-
const finalStatus = readStatusOwner(statusPath);
|
|
885
|
-
if (!finalStatus)
|
|
1257
|
+
if (broker && status && !sameOwner(broker, status.broker)
|
|
1258
|
+
&& (ownerLive(broker, identityProbe) || ownerLive(status.broker, identityProbe))) {
|
|
886
1259
|
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
887
|
-
if (ownerLive(finalStatus.broker, identityProbe)
|
|
888
|
-
&& !(selfOwner && sameOwner(finalStatus.broker, selfOwner))) {
|
|
889
|
-
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
890
1260
|
}
|
|
891
|
-
|
|
1261
|
+
const platform = options.platform ?? process.platform;
|
|
1262
|
+
const owner = broker && ownerLive(broker, identityProbe) ? broker
|
|
1263
|
+
: status && ownerLive(status.broker, identityProbe) ? status.broker : null;
|
|
1264
|
+
const selfOwner = options.selfOwner;
|
|
1265
|
+
const selfOwned = Boolean(selfOwner && broker && sameOwner(broker, selfOwner));
|
|
1266
|
+
if (selfOwner && (!broker || !selfOwned || broker.token !== manifest.token || broker.generation !== manifest.generation)) {
|
|
892
1267
|
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
893
1268
|
}
|
|
894
|
-
|
|
1269
|
+
let executions = readExecutions(manifest);
|
|
1270
|
+
if (owner && !selfOwned)
|
|
1271
|
+
signalOwner(owner, platform, false);
|
|
1272
|
+
for (const execution of executions) {
|
|
1273
|
+
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
|
|
1274
|
+
}
|
|
1275
|
+
const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
|
|
1276
|
+
while (Date.now() < softDeadlineAt) {
|
|
1277
|
+
if ((!owner || selfOwned || !ownerLive(owner, identityProbe))
|
|
1278
|
+
&& !executions.some((execution) => executionAlive(execution, platform, identityProbe)))
|
|
1279
|
+
break;
|
|
1280
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
1281
|
+
}
|
|
1282
|
+
if (observation.state === 'found') {
|
|
1283
|
+
if (Date.now() >= deadlineAt) {
|
|
1284
|
+
recordSandboxRemovalPending(resolvedRoot, manifest, 'container-removal', journal);
|
|
1285
|
+
throw new Error('SANDBOX_CONTROL_REMOVE_PENDING');
|
|
1286
|
+
}
|
|
1287
|
+
let afterRemoval;
|
|
1288
|
+
try {
|
|
1289
|
+
await awaitWithDeadline(() => options.removeContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
1290
|
+
afterRemoval = await awaitWithDeadline(() => inspectContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
1291
|
+
}
|
|
1292
|
+
catch (error) {
|
|
1293
|
+
if (error instanceof Error && error.message === 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED') {
|
|
1294
|
+
recordSandboxRemovalPending(resolvedRoot, manifest, 'container-removal', journal);
|
|
1295
|
+
throw new Error('SANDBOX_CONTROL_REMOVE_PENDING');
|
|
1296
|
+
}
|
|
1297
|
+
throw error;
|
|
1298
|
+
}
|
|
1299
|
+
if (afterRemoval.state !== 'absent') {
|
|
1300
|
+
throw new Error(afterRemoval.state === 'unknown'
|
|
1301
|
+
? `SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${afterRemoval.reason}`
|
|
1302
|
+
: 'SANDBOX_CONTROL_CONTAINER_STILL_EXISTS');
|
|
1303
|
+
}
|
|
1304
|
+
journal.write('container-absent');
|
|
1305
|
+
}
|
|
1306
|
+
const currentManifestRaw = fs.readFileSync(manifestPath, 'utf8');
|
|
1307
|
+
const currentManifest = readSandboxControlManifest(manifestPath);
|
|
1308
|
+
if (currentManifestRaw !== manifestRawBeforeLock
|
|
1309
|
+
|| currentManifest.engine !== manifest.engine
|
|
1310
|
+
|| currentManifest.containerIdentity.id !== manifest.containerIdentity.id
|
|
1311
|
+
|| currentManifest.authorityEvidence.lockDomain !== manifest.authorityEvidence.lockDomain
|
|
1312
|
+
|| currentManifest.token !== manifest.token || currentManifest.generation !== manifest.generation
|
|
1313
|
+
|| !isSandboxControlRootQuiescing(resolvedRoot)) {
|
|
1314
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_CHANGED');
|
|
1315
|
+
}
|
|
1316
|
+
const currentBroker = readBrokerOwner(brokerPath);
|
|
1317
|
+
if (selfOwned && (!currentBroker || !selfOwner || !sameOwner(selfOwner, currentBroker)
|
|
1318
|
+
|| currentBroker.token !== manifest.token || currentBroker.generation !== manifest.generation)) {
|
|
1319
|
+
throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
|
|
1320
|
+
}
|
|
1321
|
+
if (owner && currentBroker && !sameOwner(owner, currentBroker)) {
|
|
1322
|
+
throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
|
|
1323
|
+
}
|
|
1324
|
+
if (!owner && currentBroker && ownerLive(currentBroker, identityProbe)) {
|
|
1325
|
+
throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
|
|
1326
|
+
}
|
|
1327
|
+
if (owner && ownerLive(owner, identityProbe) && !currentBroker) {
|
|
1328
|
+
throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
|
|
1329
|
+
}
|
|
1330
|
+
executions = mergeExecutions(executions, readExecutions(manifest));
|
|
1331
|
+
if (Date.now() >= deadlineAt)
|
|
1332
|
+
throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
1333
|
+
for (const execution of executions) {
|
|
1334
|
+
if (!terminateSandboxControlExecution(execution, {
|
|
1335
|
+
platform, timeoutMs: remaining(), deadlineAt, forceAt: Date.now(), identityProbe
|
|
1336
|
+
})) {
|
|
1337
|
+
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
if (owner && currentBroker && ownerLive(currentBroker, identityProbe) && !selfOwned) {
|
|
1341
|
+
if (remaining() <= 0)
|
|
1342
|
+
throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
1343
|
+
signalOwner(currentBroker, platform, true);
|
|
1344
|
+
if (remaining() <= 0 || !await waitForExit(currentBroker, remaining(), deadlineAt, identityProbe)) {
|
|
1345
|
+
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
const finalExecutions = mergeExecutions(executions, readExecutions(manifest));
|
|
1349
|
+
if (finalExecutions.some((execution) => executionAlive(execution, platform, identityProbe))) {
|
|
1350
|
+
throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
1351
|
+
}
|
|
1352
|
+
const finalBrokerRaw = fs.existsSync(brokerPath) ? fs.readFileSync(brokerPath, 'utf8') : null;
|
|
1353
|
+
if (finalBrokerRaw !== null) {
|
|
1354
|
+
const finalBroker = readBrokerOwner(brokerPath);
|
|
1355
|
+
if (!finalBroker)
|
|
1356
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
1357
|
+
const finalSelfOwner = Boolean(selfOwner && sameOwner(finalBroker, selfOwner)
|
|
1358
|
+
&& finalBroker.token === manifest.token && finalBroker.generation === manifest.generation);
|
|
1359
|
+
if (ownerLive(finalBroker, identityProbe) && !finalSelfOwner) {
|
|
1360
|
+
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
1361
|
+
}
|
|
1362
|
+
if (fs.readFileSync(brokerPath, 'utf8') !== finalBrokerRaw) {
|
|
1363
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
1364
|
+
}
|
|
1365
|
+
fs.unlinkSync(brokerPath);
|
|
1366
|
+
}
|
|
1367
|
+
const finalStatusRaw = fs.existsSync(statusPath) ? fs.readFileSync(statusPath, 'utf8') : null;
|
|
1368
|
+
if (finalStatusRaw !== null) {
|
|
1369
|
+
const finalStatus = readStatusOwner(statusPath);
|
|
1370
|
+
if (!finalStatus)
|
|
1371
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
1372
|
+
if (ownerLive(finalStatus.broker, identityProbe)
|
|
1373
|
+
&& !(selfOwner && sameOwner(finalStatus.broker, selfOwner))) {
|
|
1374
|
+
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
1375
|
+
}
|
|
1376
|
+
if (fs.readFileSync(statusPath, 'utf8') !== finalStatusRaw) {
|
|
1377
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
1378
|
+
}
|
|
1379
|
+
fs.unlinkSync(statusPath);
|
|
1380
|
+
}
|
|
1381
|
+
if (fs.existsSync(brokerPath) || fs.existsSync(statusPath)) {
|
|
1382
|
+
throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_REMAINS');
|
|
1383
|
+
}
|
|
1384
|
+
journal.write('carrier-finalizing');
|
|
1385
|
+
fs.rmSync(resolvedRoot, { recursive: true, force: true });
|
|
1386
|
+
journal.write('carrier-removed');
|
|
1387
|
+
if (!options.retainRemovalJournal)
|
|
1388
|
+
clearSandboxRemovalJournal(manifest);
|
|
895
1389
|
}
|
|
896
|
-
|
|
897
|
-
|
|
1390
|
+
finally {
|
|
1391
|
+
if (!options.resourceLock)
|
|
1392
|
+
resourceLock.release();
|
|
898
1393
|
}
|
|
899
|
-
fs.rmSync(resolvedRoot, { recursive: true, force: true });
|
|
900
1394
|
}
|
|
901
1395
|
export async function garbageCollectSandboxControlRoot(root, options = {}) {
|
|
902
1396
|
await removeSandboxControlRoot(path.resolve(root), {
|