@fitlab-ai/agent-infra 0.9.13 → 0.9.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/README.zh-CN.md +10 -0
- package/bin/cli.ts +26 -9
- package/bin/internal-cli.sh +45 -0
- package/dist/bin/cli.js +173 -157
- package/dist/bin/internal-cli.js +126 -42
- package/dist/lib/agent-client.js +5 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +165 -27
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -2
- package/dist/lib/defaults.json +103 -103
- package/dist/lib/fs/durable-write.js +37 -0
- package/dist/lib/fs/file-lock.js +111 -0
- package/dist/lib/host-control/audit.js +19 -0
- package/dist/lib/host-control/client.js +142 -0
- package/dist/lib/host-control/command.js +49 -0
- package/dist/lib/host-control/path.js +160 -0
- package/dist/lib/host-control/secure-fs.js +140 -0
- package/dist/lib/host-control/server.js +159 -0
- package/dist/lib/host-control/service.js +112 -0
- package/dist/lib/init.js +11 -2
- package/dist/lib/internal/agent-client.js +5 -2
- package/dist/lib/internal/cli-route-inventory.js +154 -0
- package/dist/lib/internal/codex-lifecycle.js +15 -12
- package/dist/lib/internal/codex-sandbox-controller.js +8 -2
- package/dist/lib/internal/demo-promotion.js +322 -0
- package/dist/lib/internal/demo-transcript.js +454 -0
- package/dist/lib/internal/git-workflow.js +10 -6
- package/dist/lib/internal/host-control.js +28 -0
- package/dist/lib/internal/manual-validation.js +322 -0
- 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 +42 -14
- 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 +82 -89
- package/dist/lib/internal/sandbox-control.js +171 -7
- package/dist/lib/internal/task-activity.js +13 -4
- package/dist/lib/internal/task-artifact.js +19 -107
- package/dist/lib/internal/task-context.js +4 -1
- package/dist/lib/internal/task-create.js +48 -14
- package/dist/lib/internal/task-delivery.js +4 -1
- package/dist/lib/internal/task-event.js +38 -35
- 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 +519 -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 +10 -52
- 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 +37 -12
- package/dist/lib/internal/task-verify.js +5 -1
- package/dist/lib/internal/task-warning.js +8 -1
- package/dist/lib/owned-path.js +33 -0
- package/dist/lib/platform/check-status.js +11 -0
- package/dist/lib/platform/comment-safety.js +229 -0
- package/dist/lib/platform/completion-backfill.js +1 -1
- package/dist/lib/platform/github-data.js +241 -0
- package/dist/lib/platform/github-provider.js +248 -26
- package/dist/lib/platform/issue-comments.js +146 -34
- package/dist/lib/platform/issues.js +1 -179
- package/dist/lib/platform/pr-change-report.js +6 -12
- package/dist/lib/platform/pr-checks.js +12 -65
- package/dist/lib/platform/pr-summary.js +83 -10
- 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 +117 -275
- package/dist/lib/platform/repository-metadata.js +377 -0
- package/dist/lib/platform/security-alerts.js +155 -0
- package/dist/lib/platform/snapshots.js +2 -0
- package/dist/lib/platform/verification-sync.js +154 -162
- package/dist/lib/process-data/index.js +7 -0
- package/dist/lib/release/workflow.js +1 -1
- 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 +39 -8
- package/dist/lib/sandbox/commands/ls.js +5 -0
- package/dist/lib/sandbox/commands/rm.js +3 -807
- package/dist/lib/sandbox/commands/show.js +16 -0
- package/dist/lib/sandbox/control/audit.js +203 -0
- package/dist/lib/sandbox/control/client.js +125 -12
- 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 +233 -34
- package/dist/lib/sandbox/control/finalization-response.js +27 -0
- package/dist/lib/sandbox/control/identity-sentinel.js +107 -0
- package/dist/lib/sandbox/control/lifecycle.js +656 -128
- package/dist/lib/sandbox/control/native-file-lock.js +85 -0
- package/dist/lib/sandbox/control/protocol.js +55 -4
- package/dist/lib/sandbox/control/server.js +720 -79
- package/dist/lib/sandbox/control/state.js +63 -3
- package/dist/lib/sandbox/control/task-view.js +183 -0
- package/dist/lib/sandbox/control/task-workflow.js +74 -0
- package/dist/lib/sandbox/control/workflow-executor.js +99 -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 +7 -2
- package/dist/lib/sandbox/recovery.js +49 -5
- package/dist/lib/sandbox/removal.js +1717 -0
- package/dist/lib/sandbox/task-cleanup.js +217 -0
- package/dist/lib/sandbox/workspace-view.js +15 -15
- package/dist/lib/sandbox/worktree-safety.js +4 -1
- package/dist/lib/server/index.js +8 -0
- package/dist/lib/task/activity-intent.js +2 -1
- package/dist/lib/task/activity-log.js +30 -24
- package/dist/lib/task/artifact-command.js +101 -0
- package/dist/lib/task/artifact-lifecycle.js +71 -40
- package/dist/lib/task/artifact-name.js +36 -0
- package/dist/lib/task/artifact-operations.js +474 -0
- package/dist/lib/task/artifact-receipts.js +3 -15
- package/dist/lib/task/artifact-repair-intent.js +130 -0
- package/dist/lib/task/artifact-schema.js +127 -0
- package/dist/lib/task/capabilities.js +83 -19
- package/dist/lib/task/codex-orchestration.js +71 -25
- package/dist/lib/task/commit-identity.js +28 -0
- package/dist/lib/task/commit-operation.js +2 -16
- package/dist/lib/task/control-authority.js +375 -5
- package/dist/lib/task/control-recovery.js +128 -0
- package/dist/lib/task/create-service.js +103 -4
- package/dist/lib/task/create.js +80 -2
- package/dist/lib/task/decision-details.js +2 -46
- package/dist/lib/task/events.js +306 -76
- package/dist/lib/task/finalization-state.js +9 -0
- package/dist/lib/task/human-override.js +3 -6
- package/dist/lib/task/implementation-inputs.js +3 -3
- package/dist/lib/task/index.js +11 -3
- package/dist/lib/task/intermediate-cleanup.js +383 -0
- package/dist/lib/task/invalidation.js +18 -65
- package/dist/lib/task/ledger-intents.js +7 -22
- package/dist/lib/task/ledger.js +1 -3
- package/dist/lib/task/lifecycle.js +19 -1
- package/dist/lib/task/local-artifact-finalization.js +190 -290
- package/dist/lib/task/manual-validation-completion.js +26 -0
- package/dist/lib/task/manual-validation-evidence.js +130 -0
- package/dist/lib/task/manual-validation-receipt.js +101 -0
- package/dist/lib/task/manual-validation-shared.js +40 -0
- package/dist/lib/task/manual-validation-transaction.js +243 -0
- package/dist/lib/task/markdown.js +47 -0
- package/dist/lib/task/orchestration.js +206 -69
- package/dist/lib/task/qualification-audit.js +476 -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 +23 -40
- package/dist/lib/task/review-command.js +25 -0
- package/dist/lib/task/review-finalization.js +146 -97
- package/dist/lib/task/review-fingerprint.js +2 -2
- package/dist/lib/task/sections.js +34 -54
- package/dist/lib/task/verification-config.js +11 -0
- package/dist/lib/task/verification-engine.js +170 -327
- package/dist/lib/task/verification.js +1 -1
- package/dist/lib/task/workflow-command.js +17 -0
- package/dist/lib/task/workflow-warning-intents.js +0 -16
- package/dist/lib/task/workflow-warnings.js +28 -2
- package/dist/lib/update.js +2 -0
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-client.ts +9 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +223 -33
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -2
- package/lib/fs/durable-write.ts +29 -0
- package/lib/fs/file-lock.ts +113 -0
- package/lib/host-control/audit.ts +22 -0
- package/lib/host-control/client.ts +172 -0
- package/lib/host-control/command.ts +60 -0
- package/lib/host-control/path.ts +167 -0
- package/lib/host-control/secure-fs.ts +135 -0
- package/lib/host-control/server.ts +180 -0
- package/lib/host-control/service.ts +133 -0
- package/lib/init.ts +15 -2
- package/lib/internal/agent-client.ts +4 -2
- package/lib/internal/cli-route-inventory.ts +142 -0
- package/lib/internal/codex-lifecycle.ts +14 -12
- package/lib/internal/codex-sandbox-controller.ts +7 -2
- package/lib/internal/demo-promotion.ts +354 -0
- package/lib/internal/demo-transcript.ts +419 -0
- package/lib/internal/git-workflow.ts +9 -6
- package/lib/internal/host-control.ts +28 -0
- package/lib/internal/manual-validation.ts +306 -0
- 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 +38 -14
- 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 +91 -87
- package/lib/internal/sandbox-control.ts +181 -6
- package/lib/internal/task-activity.ts +12 -4
- package/lib/internal/task-artifact.ts +15 -79
- 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 +21 -10
- 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 +590 -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 +7 -50
- package/lib/internal/task-short-id.ts +10 -1
- package/lib/internal/task-snapshot.ts +3 -1
- package/lib/internal/task-validate.ts +32 -12
- package/lib/internal/task-verify.ts +4 -1
- package/lib/internal/task-warning.ts +7 -1
- package/lib/owned-path.ts +26 -0
- package/lib/platform/check-status.ts +9 -0
- package/lib/platform/comment-safety.ts +255 -0
- package/lib/platform/completion-backfill.ts +1 -1
- package/lib/platform/github-data.ts +322 -0
- package/lib/platform/github-provider.ts +244 -27
- package/lib/platform/issue-comments.ts +159 -35
- package/lib/platform/issues.ts +4 -245
- package/lib/platform/pr-change-report.ts +6 -12
- package/lib/platform/pr-checks.ts +13 -77
- package/lib/platform/pr-summary.ts +88 -11
- package/lib/platform/provider-contract.ts +78 -3
- package/lib/platform/provider-validation.ts +85 -3
- package/lib/platform/pull-requests.ts +129 -348
- package/lib/platform/repository-metadata.ts +453 -0
- package/lib/platform/security-alerts.ts +191 -0
- package/lib/platform/snapshots.ts +53 -0
- package/lib/platform/verification-sync.ts +171 -177
- package/lib/process-data/index.ts +7 -0
- package/lib/release/workflow.ts +3 -3
- package/lib/run/index.ts +5 -0
- package/lib/sandbox/commands/control-status.ts +26 -0
- package/lib/sandbox/commands/create.ts +53 -12
- package/lib/sandbox/commands/ls.ts +7 -0
- package/lib/sandbox/commands/rm.ts +3 -996
- package/lib/sandbox/commands/show.ts +16 -0
- package/lib/sandbox/control/audit.ts +253 -0
- package/lib/sandbox/control/client.ts +150 -13
- package/lib/sandbox/control/completed-reentry.ts +98 -0
- package/lib/sandbox/control/container-identity.ts +27 -43
- package/lib/sandbox/control/executor.ts +262 -37
- package/lib/sandbox/control/finalization-response.ts +29 -0
- package/lib/sandbox/control/identity-sentinel.ts +137 -0
- package/lib/sandbox/control/lifecycle.ts +672 -18
- package/lib/sandbox/control/native-file-lock.ts +108 -0
- package/lib/sandbox/control/protocol.ts +80 -8
- package/lib/sandbox/control/server.ts +779 -82
- package/lib/sandbox/control/state.ts +84 -3
- package/lib/sandbox/control/task-view.ts +240 -0
- package/lib/sandbox/control/task-workflow.ts +95 -0
- package/lib/sandbox/control/workflow-executor.ts +102 -0
- package/lib/sandbox/engines/authority.ts +365 -0
- package/lib/sandbox/engines/docker-exact-discovery.ts +132 -0
- package/lib/sandbox/index.ts +9 -2
- package/lib/sandbox/recovery.ts +49 -6
- package/lib/sandbox/removal.ts +2144 -0
- package/lib/sandbox/task-cleanup.ts +209 -0
- package/lib/sandbox/workspace-view.ts +26 -18
- package/lib/sandbox/worktree-safety.ts +4 -1
- package/lib/server/index.ts +10 -0
- package/lib/task/activity-intent.ts +2 -2
- package/lib/task/activity-log.ts +27 -19
- package/lib/task/artifact-command.ts +104 -0
- package/lib/task/artifact-lifecycle.ts +66 -39
- package/lib/task/artifact-name.ts +55 -0
- package/lib/task/artifact-operations.ts +580 -0
- package/lib/task/artifact-receipts.ts +3 -14
- package/lib/task/artifact-repair-intent.ts +152 -0
- package/lib/task/artifact-schema.ts +163 -0
- package/lib/task/capabilities.ts +82 -22
- package/lib/task/codex-orchestration.ts +85 -24
- package/lib/task/commit-identity.ts +31 -0
- package/lib/task/commit-operation.ts +2 -16
- package/lib/task/control-authority.ts +513 -6
- package/lib/task/control-recovery.ts +174 -0
- package/lib/task/create-service.ts +131 -6
- package/lib/task/create.ts +83 -1
- package/lib/task/decision-details.ts +3 -71
- package/lib/task/events.ts +292 -75
- package/lib/task/finalization-state.ts +11 -0
- package/lib/task/human-override.ts +3 -8
- package/lib/task/implementation-inputs.ts +3 -3
- package/lib/task/index.ts +16 -3
- package/lib/task/intermediate-cleanup.ts +455 -0
- package/lib/task/invalidation.ts +18 -62
- package/lib/task/ledger-intents.ts +7 -24
- package/lib/task/ledger.ts +1 -3
- package/lib/task/lifecycle.ts +17 -1
- package/lib/task/local-artifact-finalization.ts +223 -388
- package/lib/task/manual-validation-completion.ts +40 -0
- package/lib/task/manual-validation-evidence.ts +209 -0
- package/lib/task/manual-validation-receipt.ts +152 -0
- package/lib/task/manual-validation-shared.ts +50 -0
- package/lib/task/manual-validation-transaction.ts +304 -0
- package/lib/task/markdown.ts +70 -0
- package/lib/task/orchestration.ts +191 -68
- package/lib/task/qualification-audit.ts +588 -0
- package/lib/task/qualification-confirmation.ts +117 -0
- package/lib/task/qualification-intents.ts +150 -0
- package/lib/task/review-artifacts.ts +21 -41
- package/lib/task/review-command.ts +23 -0
- package/lib/task/review-finalization.ts +154 -153
- package/lib/task/review-fingerprint.ts +2 -2
- package/lib/task/sections.ts +33 -52
- package/lib/task/verification-config.ts +12 -0
- package/lib/task/verification-engine.ts +165 -370
- package/lib/task/verification-types.ts +17 -0
- package/lib/task/verification.ts +1 -1
- package/lib/task/workflow-command.ts +18 -0
- package/lib/task/workflow-warning-intents.ts +0 -13
- package/lib/task/workflow-warnings.ts +21 -2
- package/lib/update.ts +6 -0
- package/package.json +7 -2
- package/templates/.agents/hooks/lifecycle-delegation.js +6 -6
- 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/rules/testing-discipline.en.md +2 -0
- package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -0
- package/templates/.agents/scripts/package.json +3 -0
- 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 +37 -21
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +37 -21
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +4 -0
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +4 -0
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +16 -7
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +16 -7
- package/templates/.agents/skills/complete-task/SKILL.en.md +10 -23
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +10 -23
- 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/release/SKILL.en.md +7 -3
- package/templates/.agents/skills/release/SKILL.zh-CN.md +7 -3
- 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 +25 -4
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +25 -4
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +4 -1
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +4 -1
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +5 -1
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +5 -1
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/test/SKILL.en.md +29 -7
- package/templates/.agents/skills/test/SKILL.zh-CN.md +29 -7
- 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/.claude/commands/run-manual-validation.en.md +1 -1
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
- package/dist/lib/platform/adapters.js +0 -56
- package/lib/platform/adapters.ts +0 -197
- 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,7 +1,8 @@
|
|
|
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.ts';
|
|
6
7
|
import {
|
|
7
8
|
getProcessIdentityState,
|
|
@@ -16,6 +17,12 @@ import type {
|
|
|
16
17
|
} from './protocol.ts';
|
|
17
18
|
import { DEFAULT_SANDBOX_CONTROL_TIMING } from './protocol.ts';
|
|
18
19
|
import { inspectSandboxControlContainer, type ContainerObservation } from './container-identity.ts';
|
|
20
|
+
import {
|
|
21
|
+
acquireSandboxResourceLock,
|
|
22
|
+
resolveSandboxLockNamespace,
|
|
23
|
+
type SandboxResourceLock
|
|
24
|
+
} from './native-file-lock.ts';
|
|
25
|
+
import { isSandboxAuthorityEvidence } from '../engines/authority.ts';
|
|
19
26
|
import {
|
|
20
27
|
parseSandboxControlStatus,
|
|
21
28
|
readExecution,
|
|
@@ -30,6 +37,556 @@ const QUIESCING_FILE = 'quiescing.json';
|
|
|
30
37
|
const BROKER_STARTING_FILE = 'broker-starting.json';
|
|
31
38
|
const REPLACEMENT_FILE = 'replacement.json';
|
|
32
39
|
const REPLACEMENT_STATE_SUFFIX = '.replacement-state.json';
|
|
40
|
+
const REMOVAL_JOURNAL_ROOT = path.join('.agent-infra', 'sandbox-removal-journal');
|
|
41
|
+
|
|
42
|
+
export const SANDBOX_REMOVAL_JOURNAL_PHASES = [
|
|
43
|
+
'prepared', 'target-committed', 'container-removal', 'container-absent',
|
|
44
|
+
'carrier-finalizing', 'carrier-removed', 'workspace-finalizing', 'workspace-removed',
|
|
45
|
+
'branch-finalizing', 'branch-removed', 'tool-finalizing', 'tool-removed',
|
|
46
|
+
'shell-finalizing', 'shell-removed', 'share-finalizing', 'share-removed', 'completed'
|
|
47
|
+
] as const;
|
|
48
|
+
export type SandboxRemovalJournalPhase = typeof SANDBOX_REMOVAL_JOURNAL_PHASES[number];
|
|
49
|
+
export type SandboxRemovalTargetCommit = Readonly<{
|
|
50
|
+
branch: string;
|
|
51
|
+
project: string;
|
|
52
|
+
controlRoot: string;
|
|
53
|
+
targetDigest: string;
|
|
54
|
+
permitDigest: string;
|
|
55
|
+
removeWorktree: boolean;
|
|
56
|
+
removeBranch: boolean;
|
|
57
|
+
removeShare: boolean;
|
|
58
|
+
worktreePaths: readonly string[];
|
|
59
|
+
workspaceViewPaths: readonly string[];
|
|
60
|
+
toolPaths: readonly string[];
|
|
61
|
+
shellPaths: readonly string[];
|
|
62
|
+
sharePath: string;
|
|
63
|
+
permits: readonly Readonly<{
|
|
64
|
+
path: string;
|
|
65
|
+
mode: 'clean' | 'discard';
|
|
66
|
+
snapshot: Readonly<{
|
|
67
|
+
worktree: string;
|
|
68
|
+
branch: string;
|
|
69
|
+
head: string;
|
|
70
|
+
changes: readonly Readonly<{
|
|
71
|
+
indexStatus: string;
|
|
72
|
+
worktreeStatus: string;
|
|
73
|
+
path: string;
|
|
74
|
+
originalPath?: string;
|
|
75
|
+
}>[];
|
|
76
|
+
identity: string;
|
|
77
|
+
source?: 'registered' | 'recovered';
|
|
78
|
+
recovery?: Readonly<{
|
|
79
|
+
repoRoot: string;
|
|
80
|
+
worktreeBase: string;
|
|
81
|
+
branch: string;
|
|
82
|
+
identitySource: 'branch-only' | 'task-bound';
|
|
83
|
+
taskId: string | null;
|
|
84
|
+
}>;
|
|
85
|
+
}>;
|
|
86
|
+
}>[];
|
|
87
|
+
}>;
|
|
88
|
+
|
|
89
|
+
export type SandboxRemovalJournal = Readonly<{
|
|
90
|
+
version: 2;
|
|
91
|
+
operation: 'sandbox-rm';
|
|
92
|
+
oldOperation: 'sandbox-rm';
|
|
93
|
+
newOperation: 'sandbox-rm';
|
|
94
|
+
handoffId: string;
|
|
95
|
+
transitionId: string;
|
|
96
|
+
engine: string;
|
|
97
|
+
containerId: string;
|
|
98
|
+
generation: string;
|
|
99
|
+
authorityFingerprint: string;
|
|
100
|
+
carrierIdentityDigest: string;
|
|
101
|
+
lockDomain: string;
|
|
102
|
+
owner: ProcessIdentity & Readonly<{ leaseNonce: string }>;
|
|
103
|
+
phase: SandboxRemovalJournalPhase;
|
|
104
|
+
expectedOldJournalRevision: number | null;
|
|
105
|
+
revision: number;
|
|
106
|
+
target: SandboxRemovalTargetCommit;
|
|
107
|
+
recordedAt: number;
|
|
108
|
+
}>;
|
|
109
|
+
|
|
110
|
+
function removalCarrierDigest(engine: string, containerId: string, generation: string): string {
|
|
111
|
+
return createHash('sha256').update(`${engine}\0${containerId}\0${generation}`).digest('hex');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function assertPrivateDirectory(directory: string): void {
|
|
115
|
+
const home = path.resolve(os.homedir());
|
|
116
|
+
const resolved = path.resolve(directory);
|
|
117
|
+
const relative = path.relative(home, resolved);
|
|
118
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
119
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
|
|
120
|
+
}
|
|
121
|
+
let current = home;
|
|
122
|
+
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
123
|
+
current = path.join(current, segment);
|
|
124
|
+
const stat = fs.lstatSync(current);
|
|
125
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
126
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
|
|
127
|
+
}
|
|
128
|
+
if (process.platform !== 'win32' && typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
|
|
129
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function removalJournalDomain(lockDomain: string, create = true): string {
|
|
135
|
+
const namespace = resolveSandboxLockNamespace('sandbox-removal-journal', { lockDomain });
|
|
136
|
+
const root = path.join(path.dirname(namespace.lockRoot), REMOVAL_JOURNAL_ROOT.split(path.sep).at(-1)!);
|
|
137
|
+
if (create) fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
138
|
+
assertPrivateDirectory(root);
|
|
139
|
+
const domain = path.join(root, lockDomain);
|
|
140
|
+
if (create) fs.mkdirSync(domain, { recursive: true, mode: 0o700 });
|
|
141
|
+
assertPrivateDirectory(domain);
|
|
142
|
+
return domain;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function removalJournalPath(manifest: SandboxControlManifest): string {
|
|
146
|
+
return path.join(
|
|
147
|
+
removalJournalDomain(manifest.authorityEvidence.lockDomain),
|
|
148
|
+
`${removalCarrierDigest(manifest.engine, manifest.containerIdentity.id, manifest.generation)}.json`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function removalJournalPathForRecord(record: SandboxRemovalJournal): string {
|
|
153
|
+
return path.join(
|
|
154
|
+
removalJournalDomain(record.lockDomain, false),
|
|
155
|
+
`${record.carrierIdentityDigest}.json`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
160
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function hasExactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
164
|
+
const actual = Object.keys(value).sort();
|
|
165
|
+
return actual.length === keys.length && actual.every((key, index) => key === keys[index]);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isRemovalPermitCommit(value: unknown): boolean {
|
|
169
|
+
if (!isRecord(value) || typeof value.path !== 'string' || !path.isAbsolute(value.path)
|
|
170
|
+
|| (value.mode !== 'clean' && value.mode !== 'discard') || !isRecord(value.snapshot)
|
|
171
|
+
|| typeof value.snapshot.worktree !== 'string' || !path.isAbsolute(value.snapshot.worktree)
|
|
172
|
+
|| typeof value.snapshot.branch !== 'string' || typeof value.snapshot.head !== 'string'
|
|
173
|
+
|| typeof value.snapshot.identity !== 'string' || !Array.isArray(value.snapshot.changes)
|
|
174
|
+
|| value.snapshot.changes.some((change) => !isRecord(change)
|
|
175
|
+
|| typeof change.indexStatus !== 'string' || typeof change.worktreeStatus !== 'string'
|
|
176
|
+
|| typeof change.path !== 'string'
|
|
177
|
+
|| (change.originalPath !== undefined && typeof change.originalPath !== 'string'))
|
|
178
|
+
|| (value.snapshot.source !== undefined
|
|
179
|
+
&& value.snapshot.source !== 'registered' && value.snapshot.source !== 'recovered')) return false;
|
|
180
|
+
if (value.snapshot.recovery === undefined) return true;
|
|
181
|
+
const recovery = value.snapshot.recovery;
|
|
182
|
+
return isRecord(recovery)
|
|
183
|
+
&& typeof recovery.repoRoot === 'string' && path.isAbsolute(recovery.repoRoot)
|
|
184
|
+
&& typeof recovery.worktreeBase === 'string' && path.isAbsolute(recovery.worktreeBase)
|
|
185
|
+
&& typeof recovery.branch === 'string'
|
|
186
|
+
&& (recovery.identitySource === 'branch-only' || recovery.identitySource === 'task-bound')
|
|
187
|
+
&& (recovery.taskId === null || typeof recovery.taskId === 'string');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function parseRemovalJournal(raw: string): SandboxRemovalJournal {
|
|
191
|
+
let value: unknown;
|
|
192
|
+
try {
|
|
193
|
+
value = JSON.parse(raw);
|
|
194
|
+
} catch {
|
|
195
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
|
|
196
|
+
}
|
|
197
|
+
if (!isRecord(value) || !hasExactKeys(value, [
|
|
198
|
+
'authorityFingerprint', 'carrierIdentityDigest', 'containerId', 'engine', 'expectedOldJournalRevision',
|
|
199
|
+
'generation', 'handoffId', 'lockDomain', 'newOperation', 'oldOperation', 'operation', 'owner', 'phase',
|
|
200
|
+
'recordedAt', 'revision', 'target', 'transitionId', 'version'
|
|
201
|
+
]) || !isRecord(value.owner) || !hasExactKeys(value.owner, ['leaseNonce', 'pid', 'startTime'])
|
|
202
|
+
|| !isRecord(value.target) || !hasExactKeys(value.target, [
|
|
203
|
+
'branch', 'controlRoot', 'permitDigest', 'permits', 'project', 'removeBranch', 'removeShare',
|
|
204
|
+
'removeWorktree', 'sharePath', 'shellPaths', 'targetDigest', 'toolPaths', 'workspaceViewPaths',
|
|
205
|
+
'worktreePaths'
|
|
206
|
+
]) || value.version !== 2 || value.operation !== 'sandbox-rm'
|
|
207
|
+
|| value.oldOperation !== 'sandbox-rm' || value.newOperation !== 'sandbox-rm'
|
|
208
|
+
|| typeof value.handoffId !== 'string' || value.handoffId.length === 0
|
|
209
|
+
|| typeof value.transitionId !== 'string' || value.transitionId.length === 0
|
|
210
|
+
|| typeof value.engine !== 'string' || value.engine.length === 0
|
|
211
|
+
|| typeof value.containerId !== 'string' || value.containerId.length === 0
|
|
212
|
+
|| typeof value.generation !== 'string' || value.generation.length === 0
|
|
213
|
+
|| typeof value.authorityFingerprint !== 'string' || !/^[a-f0-9]{64}$/u.test(value.authorityFingerprint)
|
|
214
|
+
|| typeof value.carrierIdentityDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(value.carrierIdentityDigest)
|
|
215
|
+
|| typeof value.lockDomain !== 'string' || !/^[a-f0-9]{64}$/u.test(value.lockDomain)
|
|
216
|
+
|| !Number.isSafeInteger(value.owner.pid) || (value.owner.pid as number) <= 0
|
|
217
|
+
|| !Number.isSafeInteger(value.owner.startTime) || typeof value.owner.leaseNonce !== 'string'
|
|
218
|
+
|| value.owner.leaseNonce.length === 0
|
|
219
|
+
|| !SANDBOX_REMOVAL_JOURNAL_PHASES.includes(value.phase as SandboxRemovalJournalPhase)
|
|
220
|
+
|| !(value.expectedOldJournalRevision === null
|
|
221
|
+
|| (Number.isSafeInteger(value.expectedOldJournalRevision) && (value.expectedOldJournalRevision as number) >= 0))
|
|
222
|
+
|| !Number.isSafeInteger(value.revision) || (value.revision as number) <= 0
|
|
223
|
+
|| typeof value.target.branch !== 'string' || value.target.branch.length === 0
|
|
224
|
+
|| typeof value.target.project !== 'string' || value.target.project.length === 0
|
|
225
|
+
|| typeof value.target.controlRoot !== 'string' || path.isAbsolute(value.target.controlRoot) === false
|
|
226
|
+
|| typeof value.target.targetDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(value.target.targetDigest)
|
|
227
|
+
|| typeof value.target.permitDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(value.target.permitDigest)
|
|
228
|
+
|| typeof value.target.removeWorktree !== 'boolean'
|
|
229
|
+
|| typeof value.target.removeBranch !== 'boolean'
|
|
230
|
+
|| typeof value.target.removeShare !== 'boolean'
|
|
231
|
+
|| typeof value.target.sharePath !== 'string' || !path.isAbsolute(value.target.sharePath)
|
|
232
|
+
|| !Array.isArray(value.target.worktreePaths) || value.target.worktreePaths.some((entry) => typeof entry !== 'string' || !path.isAbsolute(entry))
|
|
233
|
+
|| !Array.isArray(value.target.workspaceViewPaths) || value.target.workspaceViewPaths.some((entry) => typeof entry !== 'string' || !path.isAbsolute(entry))
|
|
234
|
+
|| !Array.isArray(value.target.toolPaths) || value.target.toolPaths.some((entry) => typeof entry !== 'string' || !path.isAbsolute(entry))
|
|
235
|
+
|| !Array.isArray(value.target.shellPaths) || value.target.shellPaths.some((entry) => typeof entry !== 'string' || !path.isAbsolute(entry))
|
|
236
|
+
|| !Array.isArray(value.target.permits)
|
|
237
|
+
|| value.target.permits.some((permit) => !isRemovalPermitCommit(permit))
|
|
238
|
+
|| !Number.isSafeInteger(value.recordedAt)) {
|
|
239
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
|
|
240
|
+
}
|
|
241
|
+
const expected = value.expectedOldJournalRevision as number | null;
|
|
242
|
+
const revision = value.revision as number;
|
|
243
|
+
if ((expected === null && revision !== 1) || (expected !== null && revision !== expected + 1)) {
|
|
244
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
version: 2,
|
|
248
|
+
operation: 'sandbox-rm',
|
|
249
|
+
oldOperation: 'sandbox-rm',
|
|
250
|
+
newOperation: 'sandbox-rm',
|
|
251
|
+
handoffId: value.handoffId as string,
|
|
252
|
+
transitionId: value.transitionId as string,
|
|
253
|
+
engine: value.engine as string,
|
|
254
|
+
containerId: value.containerId as string,
|
|
255
|
+
generation: value.generation as string,
|
|
256
|
+
authorityFingerprint: value.authorityFingerprint as string,
|
|
257
|
+
carrierIdentityDigest: value.carrierIdentityDigest as string,
|
|
258
|
+
lockDomain: value.lockDomain as string,
|
|
259
|
+
owner: {
|
|
260
|
+
pid: value.owner.pid as number,
|
|
261
|
+
startTime: value.owner.startTime as number,
|
|
262
|
+
leaseNonce: value.owner.leaseNonce as string
|
|
263
|
+
},
|
|
264
|
+
phase: value.phase as SandboxRemovalJournalPhase,
|
|
265
|
+
expectedOldJournalRevision: expected,
|
|
266
|
+
revision,
|
|
267
|
+
target: {
|
|
268
|
+
branch: value.target.branch as string,
|
|
269
|
+
project: value.target.project as string,
|
|
270
|
+
controlRoot: value.target.controlRoot as string,
|
|
271
|
+
targetDigest: value.target.targetDigest as string,
|
|
272
|
+
permitDigest: value.target.permitDigest as string,
|
|
273
|
+
removeWorktree: value.target.removeWorktree as boolean,
|
|
274
|
+
removeBranch: value.target.removeBranch as boolean,
|
|
275
|
+
removeShare: value.target.removeShare as boolean,
|
|
276
|
+
worktreePaths: value.target.worktreePaths as string[],
|
|
277
|
+
workspaceViewPaths: value.target.workspaceViewPaths as string[],
|
|
278
|
+
toolPaths: value.target.toolPaths as string[],
|
|
279
|
+
shellPaths: value.target.shellPaths as string[],
|
|
280
|
+
sharePath: value.target.sharePath as string,
|
|
281
|
+
permits: value.target.permits as SandboxRemovalTargetCommit['permits']
|
|
282
|
+
},
|
|
283
|
+
recordedAt: value.recordedAt as number
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function readRemovalJournalAt(target: string): SandboxRemovalJournal | null {
|
|
288
|
+
if (!fs.existsSync(target)) return null;
|
|
289
|
+
const stat = fs.lstatSync(target);
|
|
290
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
|
|
291
|
+
return parseRemovalJournal(fs.readFileSync(target, 'utf8'));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function readSandboxRemovalJournal(manifest: SandboxControlManifest): SandboxRemovalJournal | null {
|
|
295
|
+
return readRemovalJournalAt(removalJournalPath(manifest));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function listSandboxRemovalJournals(
|
|
299
|
+
filter: Readonly<{ branch?: string; project?: string; targetDigest?: string }> = {}
|
|
300
|
+
): SandboxRemovalJournal[] {
|
|
301
|
+
const namespace = resolveSandboxLockNamespace('sandbox-removal-journal');
|
|
302
|
+
const root = path.join(path.dirname(namespace.lockRoot), REMOVAL_JOURNAL_ROOT.split(path.sep).at(-1)!);
|
|
303
|
+
if (!fs.existsSync(root)) return [];
|
|
304
|
+
assertPrivateDirectory(root);
|
|
305
|
+
const journals: SandboxRemovalJournal[] = [];
|
|
306
|
+
for (const domainEntry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
307
|
+
if (!domainEntry.isDirectory() || domainEntry.isSymbolicLink()) {
|
|
308
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
|
|
309
|
+
}
|
|
310
|
+
const domain = path.join(root, domainEntry.name);
|
|
311
|
+
assertPrivateDirectory(domain);
|
|
312
|
+
for (const entry of fs.readdirSync(domain, { withFileTypes: true })) {
|
|
313
|
+
if (!entry.isFile() || entry.isSymbolicLink() || !entry.name.endsWith('.json')) {
|
|
314
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
|
|
315
|
+
}
|
|
316
|
+
const record = readRemovalJournalAt(path.join(domain, entry.name));
|
|
317
|
+
if (!record || record.lockDomain !== domainEntry.name
|
|
318
|
+
|| `${record.carrierIdentityDigest}.json` !== entry.name) {
|
|
319
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
|
|
320
|
+
}
|
|
321
|
+
if ((!filter.branch || record.target.branch === filter.branch)
|
|
322
|
+
&& (!filter.project || record.target.project === filter.project)
|
|
323
|
+
&& (!filter.targetDigest || record.target.targetDigest === filter.targetDigest)) {
|
|
324
|
+
journals.push(record);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return journals;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function clearSandboxRemovalJournalRecord(record: SandboxRemovalJournal): void {
|
|
332
|
+
const target = removalJournalPathForRecord(record);
|
|
333
|
+
if (fs.existsSync(target)) {
|
|
334
|
+
const current = readRemovalJournalAt(target);
|
|
335
|
+
if (!current || current.revision !== record.revision || current.handoffId !== record.handoffId) {
|
|
336
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_REVISION_MISMATCH');
|
|
337
|
+
}
|
|
338
|
+
fs.rmSync(target, { force: true });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function clearSandboxRemovalJournal(manifest: SandboxControlManifest): void {
|
|
343
|
+
const target = removalJournalPath(manifest);
|
|
344
|
+
if (fs.existsSync(target)) fs.rmSync(target, { force: true });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
type RemovalJournalCursor = Readonly<{
|
|
348
|
+
record: SandboxRemovalJournal;
|
|
349
|
+
write(phase: SandboxRemovalJournalPhase): SandboxRemovalJournal;
|
|
350
|
+
}>;
|
|
351
|
+
|
|
352
|
+
function defaultRemovalTarget(manifest: Pick<SandboxControlManifest, 'project' | 'branch' | 'container' | 'channelDir'>): SandboxRemovalTargetCommit {
|
|
353
|
+
const targetDigest = createHash('sha256')
|
|
354
|
+
.update(`${manifest.project}\0${manifest.branch}\0${manifest.container}\0${manifest.channelDir}`)
|
|
355
|
+
.digest('hex');
|
|
356
|
+
return {
|
|
357
|
+
branch: manifest.branch,
|
|
358
|
+
project: manifest.project,
|
|
359
|
+
controlRoot: path.dirname(manifest.channelDir),
|
|
360
|
+
targetDigest,
|
|
361
|
+
permitDigest: createHash('sha256').update('none').digest('hex'),
|
|
362
|
+
removeWorktree: false,
|
|
363
|
+
removeBranch: false,
|
|
364
|
+
removeShare: false,
|
|
365
|
+
worktreePaths: [],
|
|
366
|
+
workspaceViewPaths: [],
|
|
367
|
+
toolPaths: [],
|
|
368
|
+
shellPaths: [],
|
|
369
|
+
sharePath: path.join(path.dirname(manifest.channelDir), 'share'),
|
|
370
|
+
permits: []
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function isDefaultSandboxRemovalJournal(journal: SandboxRemovalJournal): boolean {
|
|
375
|
+
if (journal.phase !== 'carrier-removed' && journal.phase !== 'completed') return false;
|
|
376
|
+
const { target } = journal;
|
|
377
|
+
const expected = defaultRemovalTarget({
|
|
378
|
+
project: target.project, branch: target.branch,
|
|
379
|
+
container: path.basename(path.dirname(target.controlRoot)),
|
|
380
|
+
channelDir: path.join(target.controlRoot, 'channel')
|
|
381
|
+
});
|
|
382
|
+
return Object.entries(expected).every(([key, value]) =>
|
|
383
|
+
JSON.stringify(target[key as keyof SandboxRemovalTargetCommit]) === JSON.stringify(value));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function sameRemovalTarget(left: SandboxRemovalTargetCommit, right: SandboxRemovalTargetCommit): boolean {
|
|
387
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function phaseIndex(phase: SandboxRemovalJournalPhase): number {
|
|
391
|
+
return SANDBOX_REMOVAL_JOURNAL_PHASES.indexOf(phase);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function sandboxRemovalPhaseIndex(phase: SandboxRemovalJournalPhase): number {
|
|
395
|
+
return phaseIndex(phase);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function assertNextRemovalPhase(current: SandboxRemovalJournalPhase, next: SandboxRemovalJournalPhase): boolean {
|
|
399
|
+
const currentIndex = phaseIndex(current);
|
|
400
|
+
const nextIndex = phaseIndex(next);
|
|
401
|
+
if (nextIndex === currentIndex) return false;
|
|
402
|
+
if (current === 'target-committed' && next === 'container-absent') return true;
|
|
403
|
+
if (nextIndex !== currentIndex + 1) {
|
|
404
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_PHASE_TRANSITION_INVALID');
|
|
405
|
+
}
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function assertRemovalJournalLock(
|
|
410
|
+
record: SandboxRemovalJournal,
|
|
411
|
+
resourceLock: SandboxResourceLock
|
|
412
|
+
): void {
|
|
413
|
+
const namespace = resolveSandboxLockNamespace(`${record.engine}:${record.containerId}`, {
|
|
414
|
+
lockDomain: record.lockDomain
|
|
415
|
+
});
|
|
416
|
+
if (resourceLock.lockDomain !== record.lockDomain || resourceLock.path !== namespace.lockPath) {
|
|
417
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_LOCK_MISMATCH');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function writeRemovalJournalAt(
|
|
422
|
+
target: string,
|
|
423
|
+
base: Omit<SandboxRemovalJournal, 'phase' | 'expectedOldJournalRevision' | 'revision' | 'recordedAt'>,
|
|
424
|
+
phase: SandboxRemovalJournalPhase,
|
|
425
|
+
expectedOldJournalRevision: number | null
|
|
426
|
+
): SandboxRemovalJournal {
|
|
427
|
+
const existing = readRemovalJournalAt(target);
|
|
428
|
+
if (expectedOldJournalRevision === null
|
|
429
|
+
? existing !== null
|
|
430
|
+
: existing?.revision !== expectedOldJournalRevision) {
|
|
431
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_REVISION_MISMATCH');
|
|
432
|
+
}
|
|
433
|
+
const record: SandboxRemovalJournal = {
|
|
434
|
+
...base,
|
|
435
|
+
phase,
|
|
436
|
+
expectedOldJournalRevision,
|
|
437
|
+
revision: expectedOldJournalRevision === null ? 1 : expectedOldJournalRevision + 1,
|
|
438
|
+
recordedAt: Date.now()
|
|
439
|
+
};
|
|
440
|
+
const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
441
|
+
try {
|
|
442
|
+
fs.writeFileSync(temporary, `${JSON.stringify(record)}\n`, { mode: 0o600, flag: 'wx' });
|
|
443
|
+
if (expectedOldJournalRevision === null && fs.existsSync(target)) {
|
|
444
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_REVISION_MISMATCH');
|
|
445
|
+
}
|
|
446
|
+
fs.renameSync(temporary, target);
|
|
447
|
+
} finally {
|
|
448
|
+
fs.rmSync(temporary, { force: true });
|
|
449
|
+
}
|
|
450
|
+
return record;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function writeRemovalJournal(
|
|
454
|
+
manifest: SandboxControlManifest,
|
|
455
|
+
base: Omit<SandboxRemovalJournal, 'phase' | 'expectedOldJournalRevision' | 'revision' | 'recordedAt'>,
|
|
456
|
+
phase: SandboxRemovalJournalPhase,
|
|
457
|
+
expectedOldJournalRevision: number | null
|
|
458
|
+
): SandboxRemovalJournal {
|
|
459
|
+
return writeRemovalJournalAt(removalJournalPath(manifest), base, phase, expectedOldJournalRevision);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function startRemovalJournal(
|
|
463
|
+
manifest: SandboxControlManifest,
|
|
464
|
+
options: Readonly<{
|
|
465
|
+
target?: SandboxRemovalTargetCommit;
|
|
466
|
+
identityProbe: ProcessIdentityProbe;
|
|
467
|
+
}>
|
|
468
|
+
): RemovalJournalCursor {
|
|
469
|
+
const target = removalJournalPath(manifest);
|
|
470
|
+
const existing = readRemovalJournalAt(target);
|
|
471
|
+
if (existing && (existing.engine !== manifest.engine || existing.containerId !== manifest.containerIdentity.id
|
|
472
|
+
|| existing.generation !== manifest.generation
|
|
473
|
+
|| existing.authorityFingerprint !== manifest.authorityEvidence.authorityFingerprint
|
|
474
|
+
|| existing.lockDomain !== manifest.authorityEvidence.lockDomain)) {
|
|
475
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_IDENTITY_MISMATCH');
|
|
476
|
+
}
|
|
477
|
+
const targetCommit = options.target ?? existing?.target ?? defaultRemovalTarget(manifest);
|
|
478
|
+
if (existing && options.target && !sameRemovalTarget(existing.target, options.target)) {
|
|
479
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH');
|
|
480
|
+
}
|
|
481
|
+
if (existing) {
|
|
482
|
+
const state = options.identityProbe(existing.owner);
|
|
483
|
+
if (state === 'alive') throw new Error('SANDBOX_CONTROL_REMOVE_RETRY_IN_PROGRESS');
|
|
484
|
+
if (state === 'unknown') throw new Error('SANDBOX_CONTROL_REMOVE_OWNER_UNKNOWN');
|
|
485
|
+
}
|
|
486
|
+
const startTime = getProcessStartTime(process.pid);
|
|
487
|
+
if (startTime === null) throw new Error('SANDBOX_CONTROL_REMOVE_OWNER_UNAVAILABLE');
|
|
488
|
+
const base = {
|
|
489
|
+
version: 2 as const,
|
|
490
|
+
operation: 'sandbox-rm' as const,
|
|
491
|
+
oldOperation: 'sandbox-rm' as const,
|
|
492
|
+
newOperation: 'sandbox-rm' as const,
|
|
493
|
+
handoffId: randomUUID(),
|
|
494
|
+
transitionId: randomUUID(),
|
|
495
|
+
engine: manifest.engine,
|
|
496
|
+
containerId: manifest.containerIdentity.id,
|
|
497
|
+
generation: manifest.generation,
|
|
498
|
+
authorityFingerprint: manifest.authorityEvidence.authorityFingerprint,
|
|
499
|
+
carrierIdentityDigest: removalCarrierDigest(manifest.engine, manifest.containerIdentity.id, manifest.generation),
|
|
500
|
+
lockDomain: manifest.authorityEvidence.lockDomain,
|
|
501
|
+
owner: { pid: process.pid, startTime, leaseNonce: randomUUID() },
|
|
502
|
+
target: targetCommit
|
|
503
|
+
};
|
|
504
|
+
const initialPhase = existing?.phase ?? 'prepared';
|
|
505
|
+
let record = writeRemovalJournal(manifest, base, initialPhase, existing?.revision ?? null);
|
|
506
|
+
return {
|
|
507
|
+
get record() { return record; },
|
|
508
|
+
write(phase) {
|
|
509
|
+
if (!assertNextRemovalPhase(record.phase, phase)) return record;
|
|
510
|
+
record = writeRemovalJournal(manifest, base, phase, record.revision);
|
|
511
|
+
return record;
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function claimSandboxRemovalJournal(
|
|
517
|
+
journal: SandboxRemovalJournal,
|
|
518
|
+
options: Readonly<{ identityProbe?: ProcessIdentityProbe; resourceLock?: SandboxResourceLock }> = {}
|
|
519
|
+
): SandboxRemovalJournal {
|
|
520
|
+
const target = removalJournalPathForRecord(journal);
|
|
521
|
+
const resourceLock = options.resourceLock
|
|
522
|
+
?? acquireSandboxResourceLock(`${journal.engine}:${journal.containerId}`, { lockDomain: journal.lockDomain });
|
|
523
|
+
const ownedLock = options.resourceLock ? null : resourceLock;
|
|
524
|
+
try {
|
|
525
|
+
assertRemovalJournalLock(journal, resourceLock);
|
|
526
|
+
const current = readRemovalJournalAt(target);
|
|
527
|
+
if (!current || current.revision !== journal.revision || current.handoffId !== journal.handoffId) {
|
|
528
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_REVISION_MISMATCH');
|
|
529
|
+
}
|
|
530
|
+
const state = (options.identityProbe ?? getProcessIdentityState)(current.owner);
|
|
531
|
+
if (state === 'alive') throw new Error('SANDBOX_CONTROL_REMOVE_RETRY_IN_PROGRESS');
|
|
532
|
+
if (state === 'unknown') throw new Error('SANDBOX_CONTROL_REMOVE_OWNER_UNKNOWN');
|
|
533
|
+
const startTime = getProcessStartTime(process.pid);
|
|
534
|
+
if (startTime === null) throw new Error('SANDBOX_CONTROL_REMOVE_OWNER_UNAVAILABLE');
|
|
535
|
+
const base = {
|
|
536
|
+
...current,
|
|
537
|
+
handoffId: randomUUID(),
|
|
538
|
+
transitionId: randomUUID(),
|
|
539
|
+
owner: { pid: process.pid, startTime, leaseNonce: randomUUID() }
|
|
540
|
+
};
|
|
541
|
+
return writeRemovalJournalAt(target, base, current.phase, current.revision);
|
|
542
|
+
} finally {
|
|
543
|
+
ownedLock?.release();
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export function advanceSandboxRemovalJournalPhase(
|
|
548
|
+
journal: SandboxRemovalJournal,
|
|
549
|
+
phase: SandboxRemovalJournalPhase,
|
|
550
|
+
options: Readonly<{ resourceLock?: SandboxResourceLock }> = {}
|
|
551
|
+
): SandboxRemovalJournal {
|
|
552
|
+
const target = removalJournalPathForRecord(journal);
|
|
553
|
+
const resourceLock = options.resourceLock
|
|
554
|
+
?? acquireSandboxResourceLock(`${journal.engine}:${journal.containerId}`, { lockDomain: journal.lockDomain });
|
|
555
|
+
const ownedLock = options.resourceLock ? null : resourceLock;
|
|
556
|
+
try {
|
|
557
|
+
assertRemovalJournalLock(journal, resourceLock);
|
|
558
|
+
const current = readRemovalJournalAt(target);
|
|
559
|
+
if (!current || current.revision !== journal.revision || current.handoffId !== journal.handoffId) {
|
|
560
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_REVISION_MISMATCH');
|
|
561
|
+
}
|
|
562
|
+
if (!assertNextRemovalPhase(current.phase, phase)) return current;
|
|
563
|
+
const {
|
|
564
|
+
phase: _phase,
|
|
565
|
+
expectedOldJournalRevision: _expected,
|
|
566
|
+
revision: _revision,
|
|
567
|
+
recordedAt: _recordedAt,
|
|
568
|
+
...base
|
|
569
|
+
} = current;
|
|
570
|
+
return writeRemovalJournalAt(target, base, phase, current.revision);
|
|
571
|
+
} finally {
|
|
572
|
+
ownedLock?.release();
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
export function advanceSandboxRemovalJournalToPhase(
|
|
577
|
+
journal: SandboxRemovalJournal,
|
|
578
|
+
phase: SandboxRemovalJournalPhase,
|
|
579
|
+
resourceLock: SandboxResourceLock
|
|
580
|
+
): SandboxRemovalJournal {
|
|
581
|
+
let current = journal;
|
|
582
|
+
while (phaseIndex(current.phase) < phaseIndex(phase)) {
|
|
583
|
+
const next = current.phase === 'target-committed' && phase === 'container-absent'
|
|
584
|
+
? phase : SANDBOX_REMOVAL_JOURNAL_PHASES[phaseIndex(current.phase) + 1];
|
|
585
|
+
if (!next) throw new Error('SANDBOX_CONTROL_REMOVAL_PHASE_TRANSITION_INVALID');
|
|
586
|
+
current = advanceSandboxRemovalJournalPhase(current, next, { resourceLock });
|
|
587
|
+
}
|
|
588
|
+
return current;
|
|
589
|
+
}
|
|
33
590
|
|
|
34
591
|
async function awaitWithDeadline<T>(
|
|
35
592
|
operation: () => Promise<T>,
|
|
@@ -82,6 +639,30 @@ function markSandboxControlRootQuiescing(root: string): void {
|
|
|
82
639
|
}
|
|
83
640
|
}
|
|
84
641
|
|
|
642
|
+
function recordSandboxRemovalPending(
|
|
643
|
+
root: string,
|
|
644
|
+
manifest: SandboxControlManifest,
|
|
645
|
+
phase: 'container-removal' | 'container-verification',
|
|
646
|
+
journal: RemovalJournalCursor
|
|
647
|
+
): void {
|
|
648
|
+
const pendingPath = path.join(root, 'removal-pending.json');
|
|
649
|
+
const temporary = `${pendingPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
650
|
+
try {
|
|
651
|
+
fs.writeFileSync(temporary, `${JSON.stringify({
|
|
652
|
+
version: 1,
|
|
653
|
+
phase,
|
|
654
|
+
containerId: manifest.containerIdentity.id,
|
|
655
|
+
generation: manifest.generation,
|
|
656
|
+
authorityFingerprint: manifest.authorityEvidence.authorityFingerprint,
|
|
657
|
+
recordedAt: Date.now()
|
|
658
|
+
})}\n`, { mode: 0o600, flag: 'wx' });
|
|
659
|
+
fs.renameSync(temporary, pendingPath);
|
|
660
|
+
} finally {
|
|
661
|
+
fs.rmSync(temporary, { force: true });
|
|
662
|
+
}
|
|
663
|
+
journal.write(phase === 'container-verification' ? 'container-absent' : 'container-removal');
|
|
664
|
+
}
|
|
665
|
+
|
|
85
666
|
function readStartupOwner(filePath: string): { raw: string; owner: OwnerIdentity } | null {
|
|
86
667
|
if (!fs.existsSync(filePath)) return null;
|
|
87
668
|
if (!regularFile(filePath)) throw new Error('SANDBOX_CONTROL_BROKER_START_TRANSITION');
|
|
@@ -498,9 +1079,10 @@ function readSandboxControlManifestValue(manifestPath: string): SandboxControlMa
|
|
|
498
1079
|
const expectedKeys = [
|
|
499
1080
|
'branch', 'channelDir', 'container', 'containerIdentity', 'engine', 'generation',
|
|
500
1081
|
'mode', 'processingDir', 'project', 'publicStatusDir', 'repoRoot', 'runtimeDir',
|
|
501
|
-
'taskId', 'token', 'worktreeRoot'
|
|
1082
|
+
'taskId', 'token', 'worktreeRoot', 'authorityEvidence', 'controlRootId',
|
|
502
1083
|
];
|
|
503
|
-
|
|
1084
|
+
const actualKeys = Object.keys(manifest).sort().join(',');
|
|
1085
|
+
if (actualKeys !== expectedKeys.sort().join(',')) {
|
|
504
1086
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
505
1087
|
}
|
|
506
1088
|
if (typeof candidate.containerIdentity !== 'object' || candidate.containerIdentity === null
|
|
@@ -521,8 +1103,10 @@ function readSandboxControlManifestValue(manifestPath: string): SandboxControlMa
|
|
|
521
1103
|
|| typeof candidate.processingDir !== 'string' || typeof candidate.runtimeDir !== 'string'
|
|
522
1104
|
|| typeof candidate.token !== 'string'
|
|
523
1105
|
|| typeof candidate.generation !== 'string'
|
|
1106
|
+
|| typeof candidate.controlRootId !== 'string' || !/^[a-f0-9]{64,128}$/u.test(candidate.controlRootId)
|
|
524
1107
|
|| (candidate.mode === 'task-bound' && (typeof candidate.taskId !== 'string' || candidate.taskId.length === 0))
|
|
525
|
-
|| (candidate.mode === 'branch-only' && candidate.taskId !== null)
|
|
1108
|
+
|| (candidate.mode === 'branch-only' && candidate.taskId !== null)
|
|
1109
|
+
|| !isSandboxAuthorityEvidence(candidate.authorityEvidence)) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
526
1110
|
const root = path.dirname(path.resolve(manifestPath));
|
|
527
1111
|
if (path.resolve(candidate.channelDir) !== path.join(root, 'channel')
|
|
528
1112
|
|| path.resolve(candidate.publicStatusDir) !== path.join(root, 'public')
|
|
@@ -542,15 +1126,17 @@ function readSandboxControlManifestValue(manifestPath: string): SandboxControlMa
|
|
|
542
1126
|
id: containerIdentity.id as string,
|
|
543
1127
|
labels: { ...(containerIdentity.labels as Record<string, string>) }
|
|
544
1128
|
},
|
|
1129
|
+
authorityEvidence: candidate.authorityEvidence,
|
|
545
1130
|
branch: candidate.branch,
|
|
546
1131
|
mode: candidate.mode,
|
|
547
1132
|
taskId: candidate.taskId,
|
|
548
1133
|
token: candidate.token,
|
|
549
1134
|
generation: candidate.generation,
|
|
1135
|
+
controlRootId: candidate.controlRootId,
|
|
550
1136
|
channelDir: candidate.channelDir,
|
|
551
1137
|
publicStatusDir: candidate.publicStatusDir,
|
|
552
1138
|
processingDir: candidate.processingDir,
|
|
553
|
-
runtimeDir: candidate.runtimeDir
|
|
1139
|
+
runtimeDir: candidate.runtimeDir,
|
|
554
1140
|
};
|
|
555
1141
|
return normalized;
|
|
556
1142
|
}
|
|
@@ -816,18 +1402,42 @@ export type RemoveSandboxControlOptions = Readonly<{
|
|
|
816
1402
|
removeContainer: (timeoutMs: number) => Promise<void>;
|
|
817
1403
|
requireAbsent?: boolean;
|
|
818
1404
|
selfOwner?: BrokerOwner;
|
|
1405
|
+
resourceLock?: SandboxResourceLock;
|
|
1406
|
+
retainRemovalJournal?: boolean;
|
|
1407
|
+
removalTarget?: SandboxRemovalTargetCommit;
|
|
819
1408
|
}>;
|
|
820
1409
|
|
|
821
1410
|
export async function removeSandboxControlRoot(
|
|
822
1411
|
root: string,
|
|
823
1412
|
options: RemoveSandboxControlOptions
|
|
1413
|
+
): Promise<void> {
|
|
1414
|
+
try {
|
|
1415
|
+
await removeSandboxControlRootAttempt(root, options);
|
|
1416
|
+
} catch (error) {
|
|
1417
|
+
if (!(error instanceof Error) || error.message !== 'SANDBOX_CONTROL_OWNER_UNAVAILABLE') throw error;
|
|
1418
|
+
await removeSandboxControlRootAttempt(root, options);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
async function removeSandboxControlRootAttempt(
|
|
1423
|
+
root: string,
|
|
1424
|
+
options: RemoveSandboxControlOptions
|
|
824
1425
|
): Promise<void> {
|
|
825
1426
|
const resolvedRoot = path.resolve(root);
|
|
826
1427
|
if (!fs.existsSync(resolvedRoot)) return;
|
|
827
1428
|
const stat = fs.lstatSync(resolvedRoot);
|
|
828
1429
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
829
1430
|
const manifestPath = path.join(resolvedRoot, 'manifest.json');
|
|
1431
|
+
const manifestRawBeforeLock = fs.readFileSync(manifestPath, 'utf8');
|
|
830
1432
|
const manifest = readSandboxControlManifest(manifestPath);
|
|
1433
|
+
const resourceLock = options.resourceLock ?? acquireSandboxResourceLock(
|
|
1434
|
+
`${manifest.engine}:${manifest.containerIdentity.id}`,
|
|
1435
|
+
{ lockDomain: manifest.authorityEvidence.lockDomain }
|
|
1436
|
+
);
|
|
1437
|
+
try {
|
|
1438
|
+
if (fs.readFileSync(manifestPath, 'utf8') !== manifestRawBeforeLock) {
|
|
1439
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_CHANGED');
|
|
1440
|
+
}
|
|
831
1441
|
const inspectContainer = options.inspectContainer
|
|
832
1442
|
?? ((timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }));
|
|
833
1443
|
const timeoutMs = options.timeoutMs ?? options.timing?.quiesceDeadlineMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
|
|
@@ -835,10 +1445,30 @@ export async function removeSandboxControlRoot(
|
|
|
835
1445
|
const deadlineAt = Date.now() + timeoutMs;
|
|
836
1446
|
const forceAt = forceDeadline(deadlineAt, timeoutMs);
|
|
837
1447
|
const remaining = (): number => Math.max(0, deadlineAt - Date.now());
|
|
1448
|
+
const journal = startRemovalJournal(manifest, {
|
|
1449
|
+
target: options.removalTarget,
|
|
1450
|
+
identityProbe
|
|
1451
|
+
});
|
|
1452
|
+
if (journal.record.phase === 'carrier-finalizing') {
|
|
1453
|
+
if (fs.readFileSync(manifestPath, 'utf8') !== manifestRawBeforeLock
|
|
1454
|
+
|| !isSandboxControlRootQuiescing(resolvedRoot)) {
|
|
1455
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_CHANGED');
|
|
1456
|
+
}
|
|
1457
|
+
fs.rmSync(resolvedRoot, { recursive: true, force: true });
|
|
1458
|
+
if (fs.existsSync(resolvedRoot)) throw new Error('SANDBOX_CONTROL_TARGET_MISMATCH');
|
|
1459
|
+
journal.write('carrier-removed');
|
|
1460
|
+
if (!options.retainRemovalJournal) clearSandboxRemovalJournal(manifest);
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
if (sandboxRemovalPhaseIndex(journal.record.phase) >= sandboxRemovalPhaseIndex('carrier-removed')) {
|
|
1464
|
+
throw new Error('SANDBOX_CONTROL_TARGET_MISMATCH');
|
|
1465
|
+
}
|
|
1466
|
+
if (journal.record.phase === 'prepared') journal.write('target-committed');
|
|
838
1467
|
const observation = await awaitWithDeadline(
|
|
839
1468
|
() => inspectContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED'
|
|
840
1469
|
);
|
|
841
1470
|
if (observation.state === 'unknown') throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${observation.reason}`);
|
|
1471
|
+
journal.write(observation.state === 'absent' ? 'container-absent' : 'container-removal');
|
|
842
1472
|
if (options.requireAbsent && observation.state !== 'absent') {
|
|
843
1473
|
throw new Error('SANDBOX_CONTROL_CONTAINER_REAPPEARED');
|
|
844
1474
|
}
|
|
@@ -880,28 +1510,46 @@ export async function removeSandboxControlRoot(
|
|
|
880
1510
|
}
|
|
881
1511
|
|
|
882
1512
|
if (observation.state === 'found') {
|
|
883
|
-
if (Date.now() >= deadlineAt)
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1513
|
+
if (Date.now() >= deadlineAt) {
|
|
1514
|
+
recordSandboxRemovalPending(resolvedRoot, manifest, 'container-removal', journal);
|
|
1515
|
+
throw new Error('SANDBOX_CONTROL_REMOVE_PENDING');
|
|
1516
|
+
}
|
|
1517
|
+
let afterRemoval: ContainerObservation;
|
|
1518
|
+
try {
|
|
1519
|
+
await awaitWithDeadline(
|
|
1520
|
+
() => options.removeContainer(remaining()),
|
|
1521
|
+
deadlineAt,
|
|
1522
|
+
'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED'
|
|
1523
|
+
);
|
|
1524
|
+
afterRemoval = await awaitWithDeadline(
|
|
1525
|
+
() => inspectContainer(remaining()),
|
|
1526
|
+
deadlineAt,
|
|
1527
|
+
'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED'
|
|
1528
|
+
);
|
|
1529
|
+
} catch (error) {
|
|
1530
|
+
if (error instanceof Error && error.message === 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED') {
|
|
1531
|
+
recordSandboxRemovalPending(resolvedRoot, manifest, 'container-removal', journal);
|
|
1532
|
+
throw new Error('SANDBOX_CONTROL_REMOVE_PENDING');
|
|
1533
|
+
}
|
|
1534
|
+
throw error;
|
|
1535
|
+
}
|
|
894
1536
|
if (afterRemoval.state !== 'absent') {
|
|
895
1537
|
throw new Error(afterRemoval.state === 'unknown'
|
|
896
1538
|
? `SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${afterRemoval.reason}`
|
|
897
1539
|
: 'SANDBOX_CONTROL_CONTAINER_STILL_EXISTS');
|
|
898
1540
|
}
|
|
1541
|
+
journal.write('container-absent');
|
|
899
1542
|
}
|
|
900
1543
|
|
|
1544
|
+
const currentManifestRaw = fs.readFileSync(manifestPath, 'utf8');
|
|
901
1545
|
const currentManifest = readSandboxControlManifest(manifestPath);
|
|
902
|
-
if (
|
|
1546
|
+
if (currentManifestRaw !== manifestRawBeforeLock
|
|
1547
|
+
|| currentManifest.engine !== manifest.engine
|
|
1548
|
+
|| currentManifest.containerIdentity.id !== manifest.containerIdentity.id
|
|
1549
|
+
|| currentManifest.authorityEvidence.lockDomain !== manifest.authorityEvidence.lockDomain
|
|
1550
|
+
|| currentManifest.token !== manifest.token || currentManifest.generation !== manifest.generation
|
|
903
1551
|
|| !isSandboxControlRootQuiescing(resolvedRoot)) {
|
|
904
|
-
throw new Error('
|
|
1552
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_CHANGED');
|
|
905
1553
|
}
|
|
906
1554
|
const currentBroker = readBrokerOwner(brokerPath);
|
|
907
1555
|
if (selfOwned && (!currentBroker || !selfOwner || !sameOwner(selfOwner, currentBroker)
|
|
@@ -970,7 +1618,13 @@ export async function removeSandboxControlRoot(
|
|
|
970
1618
|
if (fs.existsSync(brokerPath) || fs.existsSync(statusPath)) {
|
|
971
1619
|
throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_REMAINS');
|
|
972
1620
|
}
|
|
1621
|
+
journal.write('carrier-finalizing');
|
|
973
1622
|
fs.rmSync(resolvedRoot, { recursive: true, force: true });
|
|
1623
|
+
journal.write('carrier-removed');
|
|
1624
|
+
if (!options.retainRemovalJournal) clearSandboxRemovalJournal(manifest);
|
|
1625
|
+
} finally {
|
|
1626
|
+
if (!options.resourceLock) resourceLock.release();
|
|
1627
|
+
}
|
|
974
1628
|
}
|
|
975
1629
|
|
|
976
1630
|
export type GarbageCollectSandboxControlOptions = Omit<RemoveSandboxControlOptions, 'removeContainer' | 'requireAbsent'>;
|