@b00dea/cockpit 0.0.0-stage → 0.1.1
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/CHANGELOG.md +474 -0
- package/LICENSE +21 -0
- package/README.md +25 -2
- package/bin/cockpit.mjs +33 -0
- package/cockpit.schema.json +2644 -0
- package/dist/autonomy.d.ts +45 -0
- package/dist/autonomy.js +173 -0
- package/dist/bounded-read.d.ts +14 -0
- package/dist/bounded-read.js +42 -0
- package/dist/browser/commands.d.ts +117 -0
- package/dist/browser/commands.js +453 -0
- package/dist/browser/package-paths.d.ts +1 -0
- package/dist/browser/package-paths.js +6 -0
- package/dist/browser/proxy.d.ts +22 -0
- package/dist/browser/proxy.js +37 -0
- package/dist/browser/runtime.d.ts +118 -0
- package/dist/browser/runtime.js +186 -0
- package/dist/browser/surfaces.d.ts +141 -0
- package/dist/browser/surfaces.js +561 -0
- package/dist/browser/ui-gate.d.ts +15 -0
- package/dist/browser/ui-gate.js +18 -0
- package/dist/canonical-json.d.ts +2 -0
- package/dist/canonical-json.js +5 -0
- package/dist/cli-guard.d.ts +5 -0
- package/dist/cli-guard.js +71 -0
- package/dist/cli-main.d.ts +176 -0
- package/dist/cli-main.js +1584 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +16 -0
- package/dist/command-table.d.ts +121 -0
- package/dist/command-table.js +664 -0
- package/dist/config/load.d.ts +183 -0
- package/dist/config/load.js +367 -0
- package/dist/config/sibling-toml.d.ts +48 -0
- package/dist/config/sibling-toml.js +160 -0
- package/dist/config/surface-tools-toml.d.ts +9 -0
- package/dist/config/surface-tools-toml.js +37 -0
- package/dist/config/unconfirmed-toml.d.ts +9 -0
- package/dist/config/unconfirmed-toml.js +29 -0
- package/dist/config/verified-write.d.ts +43 -0
- package/dist/config/verified-write.js +89 -0
- package/dist/diagnostics.d.ts +26 -0
- package/dist/diagnostics.js +344 -0
- package/dist/docs/check.d.ts +16 -0
- package/dist/docs/check.js +197 -0
- package/dist/docs/decision-record.d.ts +34 -0
- package/dist/docs/decision-record.js +135 -0
- package/dist/docs/decisions.d.ts +16 -0
- package/dist/docs/decisions.js +103 -0
- package/dist/docs/markdown.d.ts +36 -0
- package/dist/docs/markdown.js +88 -0
- package/dist/docs/references.d.ts +32 -0
- package/dist/docs/references.js +148 -0
- package/dist/emit.d.ts +22 -0
- package/dist/emit.js +43 -0
- package/dist/entrypoint-kinds.d.ts +3 -0
- package/dist/entrypoint-kinds.js +11 -0
- package/dist/environment/application/confirm.d.ts +35 -0
- package/dist/environment/application/confirm.js +122 -0
- package/dist/environment/application/destructive.d.ts +6 -0
- package/dist/environment/application/destructive.js +429 -0
- package/dist/environment/application/doctor.d.ts +94 -0
- package/dist/environment/application/doctor.js +247 -0
- package/dist/environment/application/guard.d.ts +49 -0
- package/dist/environment/application/guard.js +677 -0
- package/dist/environment/application/health.d.ts +143 -0
- package/dist/environment/application/health.js +385 -0
- package/dist/environment/application/lifecycle-context.d.ts +235 -0
- package/dist/environment/application/lifecycle-context.js +154 -0
- package/dist/environment/application/lifecycle-state.d.ts +25 -0
- package/dist/environment/application/lifecycle-state.js +202 -0
- package/dist/environment/application/lifecycle.d.ts +18 -0
- package/dist/environment/application/lifecycle.js +737 -0
- package/dist/environment/application/logs.d.ts +45 -0
- package/dist/environment/application/logs.js +127 -0
- package/dist/environment/application/services.d.ts +60 -0
- package/dist/environment/application/services.js +89 -0
- package/dist/environment/application/setup.d.ts +129 -0
- package/dist/environment/application/setup.js +741 -0
- package/dist/environment/application/shared.d.ts +11 -0
- package/dist/environment/application/shared.js +61 -0
- package/dist/environment/application/siblings.d.ts +43 -0
- package/dist/environment/application/siblings.js +269 -0
- package/dist/environment/claims.d.ts +389 -0
- package/dist/environment/claims.js +1701 -0
- package/dist/environment/codex-hook-trust.d.ts +11 -0
- package/dist/environment/codex-hook-trust.js +185 -0
- package/dist/environment/compose-environment.d.ts +1 -0
- package/dist/environment/compose-environment.js +6 -0
- package/dist/environment/compose-ownership.d.ts +21 -0
- package/dist/environment/compose-ownership.js +117 -0
- package/dist/environment/compose-topology.d.ts +125 -0
- package/dist/environment/compose-topology.js +490 -0
- package/dist/environment/contract.d.ts +522 -0
- package/dist/environment/contract.js +794 -0
- package/dist/environment/declaration.d.ts +14 -0
- package/dist/environment/declaration.js +39 -0
- package/dist/environment/destructive-composition.d.ts +29 -0
- package/dist/environment/destructive-composition.js +333 -0
- package/dist/environment/docker-daemon.d.ts +30 -0
- package/dist/environment/docker-daemon.js +78 -0
- package/dist/environment/docker-endpoint.d.ts +10 -0
- package/dist/environment/docker-endpoint.js +19 -0
- package/dist/environment/env-schema-observation.d.ts +46 -0
- package/dist/environment/env-schema-observation.js +241 -0
- package/dist/environment/evidence.d.ts +136 -0
- package/dist/environment/evidence.js +437 -0
- package/dist/environment/executable.d.ts +38 -0
- package/dist/environment/executable.js +190 -0
- package/dist/environment/generated-application/authorities.d.ts +5 -0
- package/dist/environment/generated-application/authorities.js +61 -0
- package/dist/environment/generated-application/bootstrap-skill.d.ts +16 -0
- package/dist/environment/generated-application/bootstrap-skill.js +328 -0
- package/dist/environment/generated-application/check.d.ts +13 -0
- package/dist/environment/generated-application/check.js +191 -0
- package/dist/environment/generated-application/custom-surface.d.ts +26 -0
- package/dist/environment/generated-application/custom-surface.js +289 -0
- package/dist/environment/generated-application/prepare.d.ts +29 -0
- package/dist/environment/generated-application/prepare.js +278 -0
- package/dist/environment/generated-application/publication.d.ts +33 -0
- package/dist/environment/generated-application/publication.js +547 -0
- package/dist/environment/generated-application/shared.d.ts +492 -0
- package/dist/environment/generated-application/shared.js +354 -0
- package/dist/environment/generated-application/surface-observation.d.ts +4 -0
- package/dist/environment/generated-application/surface-observation.js +69 -0
- package/dist/environment/generated-application/surface-outputs.d.ts +71 -0
- package/dist/environment/generated-application/surface-outputs.js +400 -0
- package/dist/environment/generated-application/surfaces.d.ts +3 -0
- package/dist/environment/generated-application/surfaces.js +704 -0
- package/dist/environment/generated-application/sync.d.ts +6 -0
- package/dist/environment/generated-application/sync.js +124 -0
- package/dist/environment/git-observation.d.ts +202 -0
- package/dist/environment/git-observation.js +553 -0
- package/dist/environment/health-composition.d.ts +12 -0
- package/dist/environment/health-composition.js +645 -0
- package/dist/environment/host-observation.d.ts +56 -0
- package/dist/environment/host-observation.js +52 -0
- package/dist/environment/identity.d.ts +48 -0
- package/dist/environment/identity.js +85 -0
- package/dist/environment/init-application.d.ts +333 -0
- package/dist/environment/init-application.js +726 -0
- package/dist/environment/install-policy-observation.d.ts +71 -0
- package/dist/environment/install-policy-observation.js +284 -0
- package/dist/environment/install-policy.d.ts +48 -0
- package/dist/environment/install-policy.js +96 -0
- package/dist/environment/instance.d.ts +33 -0
- package/dist/environment/instance.js +122 -0
- package/dist/environment/instruction-pointer.d.ts +19 -0
- package/dist/environment/instruction-pointer.js +144 -0
- package/dist/environment/lifecycle-composition.d.ts +31 -0
- package/dist/environment/lifecycle-composition.js +742 -0
- package/dist/environment/lifecycle-stage.d.ts +120 -0
- package/dist/environment/lifecycle-stage.js +408 -0
- package/dist/environment/lifecycle.d.ts +75 -0
- package/dist/environment/lifecycle.js +354 -0
- package/dist/environment/logs.d.ts +132 -0
- package/dist/environment/logs.js +519 -0
- package/dist/environment/map-limit.d.ts +2 -0
- package/dist/environment/map-limit.js +13 -0
- package/dist/environment/mcp-observation.d.ts +9 -0
- package/dist/environment/mcp-observation.js +46 -0
- package/dist/environment/observations.d.ts +46 -0
- package/dist/environment/observations.js +346 -0
- package/dist/environment/operator-application.d.ts +79 -0
- package/dist/environment/operator-application.js +197 -0
- package/dist/environment/ports.d.ts +182 -0
- package/dist/environment/ports.js +1205 -0
- package/dist/environment/posture-observation.d.ts +68 -0
- package/dist/environment/posture-observation.js +253 -0
- package/dist/environment/presence.d.ts +4 -0
- package/dist/environment/presence.js +26 -0
- package/dist/environment/read-composition.d.ts +32 -0
- package/dist/environment/read-composition.js +397 -0
- package/dist/environment/readiness-composition.d.ts +57 -0
- package/dist/environment/readiness-composition.js +151 -0
- package/dist/environment/reclaim.d.ts +370 -0
- package/dist/environment/reclaim.js +1940 -0
- package/dist/environment/repository-identity.d.ts +13 -0
- package/dist/environment/repository-identity.js +108 -0
- package/dist/environment/runtime-measurement-record.d.ts +38 -0
- package/dist/environment/runtime-measurement-record.js +111 -0
- package/dist/environment/runtime-measurement.d.ts +27 -0
- package/dist/environment/runtime-measurement.js +224 -0
- package/dist/environment/runtime-probe-residue.d.ts +43 -0
- package/dist/environment/runtime-probe-residue.js +148 -0
- package/dist/environment/runtime-probe.d.ts +72 -0
- package/dist/environment/runtime-probe.js +725 -0
- package/dist/environment/runtime.d.ts +551 -0
- package/dist/environment/runtime.js +2405 -0
- package/dist/environment/seed.d.ts +16 -0
- package/dist/environment/seed.js +43 -0
- package/dist/environment/selftest-application.d.ts +86 -0
- package/dist/environment/selftest-application.js +713 -0
- package/dist/environment/sensitive.d.ts +3 -0
- package/dist/environment/sensitive.js +48 -0
- package/dist/environment/served-checkout-grants.d.ts +53 -0
- package/dist/environment/served-checkout-grants.js +182 -0
- package/dist/environment/setup-observation.d.ts +62 -0
- package/dist/environment/setup-observation.js +1473 -0
- package/dist/environment/setup.d.ts +43 -0
- package/dist/environment/setup.js +176 -0
- package/dist/environment/sibling-checkouts.d.ts +119 -0
- package/dist/environment/sibling-checkouts.js +254 -0
- package/dist/environment/sibling-instructions.d.ts +33 -0
- package/dist/environment/sibling-instructions.js +224 -0
- package/dist/environment/sibling-observation.d.ts +27 -0
- package/dist/environment/sibling-observation.js +45 -0
- package/dist/environment/sibling-pull.d.ts +23 -0
- package/dist/environment/sibling-pull.js +95 -0
- package/dist/environment/sibling-registration.d.ts +45 -0
- package/dist/environment/sibling-registration.js +538 -0
- package/dist/environment/stack-inputs.d.ts +18 -0
- package/dist/environment/stack-inputs.js +210 -0
- package/dist/environment/stack-inventory-prune.d.ts +31 -0
- package/dist/environment/stack-inventory-prune.js +129 -0
- package/dist/environment/stack-inventory-report.d.ts +44 -0
- package/dist/environment/stack-inventory-report.js +78 -0
- package/dist/environment/stack-inventory.d.ts +94 -0
- package/dist/environment/stack-inventory.js +418 -0
- package/dist/environment/state.d.ts +410 -0
- package/dist/environment/state.js +2153 -0
- package/dist/environment/templates.d.ts +140 -0
- package/dist/environment/templates.js +685 -0
- package/dist/environment/tool-registration.d.ts +9 -0
- package/dist/environment/tool-registration.js +92 -0
- package/dist/environment/upgrade-application.d.ts +19 -0
- package/dist/environment/upgrade-application.js +175 -0
- package/dist/environment-exec/execute.d.ts +105 -0
- package/dist/environment-exec/execute.js +1179 -0
- package/dist/environment-exec/libexec-program.d.ts +6 -0
- package/dist/environment-exec/libexec-program.js +22 -0
- package/dist/environment-exec/verified-digest.d.ts +4 -0
- package/dist/environment-exec/verified-digest.js +16 -0
- package/dist/exact-keys.d.ts +7 -0
- package/dist/exact-keys.js +15 -0
- package/dist/fix-catalogue.d.ts +32 -0
- package/dist/fix-catalogue.js +60 -0
- package/dist/freshness.d.ts +29 -0
- package/dist/freshness.js +75 -0
- package/dist/generated-environment/adopt.d.ts +16 -0
- package/dist/generated-environment/adopt.js +271 -0
- package/dist/generated-environment/command.d.ts +47 -0
- package/dist/generated-environment/command.js +75 -0
- package/dist/generated-environment/transaction.d.ts +186 -0
- package/dist/generated-environment/transaction.js +1770 -0
- package/dist/grading/evaluate.d.ts +71 -0
- package/dist/grading/evaluate.js +403 -0
- package/dist/grading/grade.d.ts +33 -0
- package/dist/grading/grade.js +57 -0
- package/dist/guards/answer-confirmation.d.ts +41 -0
- package/dist/guards/answer-confirmation.js +109 -0
- package/dist/guards/evaluator.d.ts +130 -0
- package/dist/guards/evaluator.js +1258 -0
- package/dist/guards/harness-adapter.d.ts +17 -0
- package/dist/guards/harness-adapter.js +57 -0
- package/dist/guards/host.d.ts +8 -0
- package/dist/guards/host.js +10 -0
- package/dist/guards/invocation-parser.d.ts +66 -0
- package/dist/guards/invocation-parser.js +595 -0
- package/dist/guards/local-identity.d.ts +44 -0
- package/dist/guards/local-identity.js +129 -0
- package/dist/guards/policy-change.d.ts +43 -0
- package/dist/guards/policy-change.js +159 -0
- package/dist/guards/policy-pin.d.ts +13 -0
- package/dist/guards/policy-pin.js +177 -0
- package/dist/guards/protected-locations.d.ts +62 -0
- package/dist/guards/protected-locations.js +461 -0
- package/dist/guards/read-only.d.ts +37 -0
- package/dist/guards/read-only.js +315 -0
- package/dist/init/command.d.ts +41 -0
- package/dist/init/command.js +330 -0
- package/dist/init/derive.d.ts +53 -0
- package/dist/init/derive.js +819 -0
- package/dist/init/draft.d.ts +140 -0
- package/dist/init/draft.js +1228 -0
- package/dist/init/render.d.ts +47 -0
- package/dist/init/render.js +104 -0
- package/dist/init/scaffold.d.ts +53 -0
- package/dist/init/scaffold.js +121 -0
- package/dist/invocation.d.ts +13 -0
- package/dist/invocation.js +20 -0
- package/dist/package-metadata.d.ts +9 -0
- package/dist/package-metadata.js +19 -0
- package/dist/readiness.d.ts +53 -0
- package/dist/readiness.js +88 -0
- package/dist/references/named.d.ts +24 -0
- package/dist/references/named.js +27 -0
- package/dist/refusal.d.ts +15 -0
- package/dist/refusal.js +7 -0
- package/dist/registry-derivation.d.ts +31 -0
- package/dist/registry-derivation.js +32 -0
- package/dist/registry-resolve/command.d.ts +47 -0
- package/dist/registry-resolve/command.js +226 -0
- package/dist/registry-resolve/registry.d.ts +68 -0
- package/dist/registry-resolve/registry.js +292 -0
- package/dist/registry-resolve/render-shell.d.ts +18 -0
- package/dist/registry-resolve/render-shell.js +36 -0
- package/dist/repair.d.ts +29 -0
- package/dist/repair.js +8 -0
- package/dist/repository-path.d.ts +9 -0
- package/dist/repository-path.js +16 -0
- package/dist/schema-json.d.ts +1 -0
- package/dist/schema-json.js +177 -0
- package/dist/schema.d.ts +1289 -0
- package/dist/schema.js +985 -0
- package/dist/setup/command.d.ts +4 -0
- package/dist/setup/command.js +4 -0
- package/dist/sha256.d.ts +1 -0
- package/dist/sha256.js +4 -0
- package/dist/surfaces/adoption.d.ts +41 -0
- package/dist/surfaces/adoption.js +217 -0
- package/dist/surfaces/consumer-instructions.d.ts +20 -0
- package/dist/surfaces/consumer-instructions.js +43 -0
- package/dist/surfaces/contract.d.ts +42 -0
- package/dist/surfaces/contract.js +169 -0
- package/dist/surfaces/core-declarations.d.ts +24 -0
- package/dist/surfaces/core-declarations.js +79 -0
- package/dist/surfaces/development-markers.d.ts +1 -0
- package/dist/surfaces/development-markers.js +18 -0
- package/dist/surfaces/generator-adapter.d.ts +71 -0
- package/dist/surfaces/generator-adapter.js +809 -0
- package/dist/surfaces/harness-registry.d.ts +260 -0
- package/dist/surfaces/harness-registry.js +146 -0
- package/dist/surfaces/mcp.d.ts +145 -0
- package/dist/surfaces/mcp.js +349 -0
- package/dist/surfaces/merge.d.ts +30 -0
- package/dist/surfaces/merge.js +105 -0
- package/dist/surfaces/ownership.d.ts +190 -0
- package/dist/surfaces/ownership.js +795 -0
- package/dist/surfaces/projection.d.ts +136 -0
- package/dist/surfaces/projection.js +716 -0
- package/dist/surfaces/render.d.ts +79 -0
- package/dist/surfaces/render.js +636 -0
- package/dist/surfaces/restrictions.d.ts +130 -0
- package/dist/surfaces/restrictions.js +447 -0
- package/dist/surfaces/selftest.d.ts +162 -0
- package/dist/surfaces/selftest.js +393 -0
- package/dist/surfaces/skill-contract.d.ts +85 -0
- package/dist/surfaces/skill-contract.js +449 -0
- package/dist/surfaces/sync.d.ts +57 -0
- package/dist/surfaces/sync.js +164 -0
- package/dist/team-metadata.d.ts +41 -0
- package/dist/team-metadata.js +513 -0
- package/dist/testing/gates.d.ts +91 -0
- package/dist/testing/gates.js +182 -0
- package/dist/testing/hooks.d.ts +59 -0
- package/dist/testing/hooks.js +139 -0
- package/dist/testing/test-protection.d.ts +34 -0
- package/dist/testing/test-protection.js +137 -0
- package/dist/testing/verdict.d.ts +29 -0
- package/dist/testing/verdict.js +62 -0
- package/dist/testing/weakening.d.ts +15 -0
- package/dist/testing/weakening.js +30 -0
- package/dist/work-orders/application-closure.d.ts +7 -0
- package/dist/work-orders/application-closure.js +76 -0
- package/dist/work-orders/application-context.d.ts +162 -0
- package/dist/work-orders/application-context.js +137 -0
- package/dist/work-orders/application-decisions.d.ts +27 -0
- package/dist/work-orders/application-decisions.js +121 -0
- package/dist/work-orders/application-grading.d.ts +11 -0
- package/dist/work-orders/application-grading.js +212 -0
- package/dist/work-orders/application.d.ts +6 -0
- package/dist/work-orders/application.js +223 -0
- package/dist/work-orders/composition.d.ts +101 -0
- package/dist/work-orders/composition.js +1202 -0
- package/dist/work-orders/contract.d.ts +77 -0
- package/dist/work-orders/contract.js +140 -0
- package/dist/work-orders/decisions.d.ts +23 -0
- package/dist/work-orders/decisions.js +55 -0
- package/dist/work-orders/store.d.ts +207 -0
- package/dist/work-orders/store.js +291 -0
- package/docs/README.md +68 -0
- package/docs/architecture.md +136 -0
- package/docs/capability-matrix.md +58 -0
- package/docs/cli-reference.md +807 -0
- package/docs/command-list.md +62 -0
- package/docs/compose-patterns.md +99 -0
- package/docs/concepts.md +173 -0
- package/docs/configuration-essentials.md +177 -0
- package/docs/configuration-reference.md +636 -0
- package/docs/docs-check.md +76 -0
- package/docs/environment.md +948 -0
- package/docs/extending.md +43 -0
- package/docs/faq.md +113 -0
- package/docs/getting-started.md +692 -0
- package/docs/harness-surfaces.md +500 -0
- package/docs/joining-a-team.md +76 -0
- package/docs/release-policy.md +49 -0
- package/docs/safety-and-autonomy.md +463 -0
- package/docs/troubleshooting.md +382 -0
- package/docs/tutorial.md +171 -0
- package/docs/work-and-browser.md +420 -0
- package/libexec/browser-operation.cjs +270 -0
- package/libexec/fence-marker.cjs +101 -0
- package/libexec/fence-marker.d.cts +24 -0
- package/libexec/fenced-group-runner.cjs +77 -0
- package/libexec/fenced-supervisor.cjs +137 -0
- package/libexec/generated-filesystem.cjs +731 -0
- package/libexec/install-filesystem.cjs +225 -0
- package/libexec/rulesync-generator.NOTICE.md +1112 -0
- package/libexec/rulesync-generator.cjs +743 -0
- package/libexec/surface-generator-worker.cjs +266 -0
- package/libexec/surface-output-reader.cjs +99 -0
- package/libexec/surface-scratch-cleanup.cjs +14 -0
- package/libexec/validate-harness-evidence.d.mts +183 -0
- package/libexec/validate-harness-evidence.mjs +1312 -0
- package/package.json +74 -4
- package/surfaces/README.md +33 -0
- package/surfaces/agents/evaluator.agent.md +76 -0
- package/surfaces/guards/bash-guard.sh +48 -0
- package/surfaces/guards/claude.adapter.json +25 -0
- package/surfaces/guards/codex.adapter.json +24 -0
- package/surfaces/guards/opencode.adapter.json +25 -0
- package/surfaces/references/browser.md +63 -0
- package/surfaces/references/test-data-and-personas.md +84 -0
- package/surfaces/references/testing-tiers.md +11 -0
- package/surfaces/skills/cockpit-browser.skill.md +59 -0
- package/surfaces/skills/cockpit-explain.skill.md +32 -0
- package/surfaces/skills/cockpit-fix.skill.md +37 -0
- package/surfaces/skills/cockpit-setup.skill.md +39 -0
- package/surfaces/skills/cockpit-work.skill.md +34 -0
- package/surfaces/skills/collection.json +5 -0
- package/surfaces/skills/procedures/explain.md +66 -0
- package/surfaces/skills/procedures/fix.md +95 -0
- package/surfaces/skills/procedures/setup.md +452 -0
- package/surfaces/skills/procedures/work.md +134 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type UnconfirmedAnswerName } from '../../guards/answer-confirmation.js';
|
|
2
|
+
import { type Diagnostic } from '../contract.js';
|
|
3
|
+
import type { EvaluatedObservation } from '../observations.js';
|
|
4
|
+
export interface ConfirmCommandInput {
|
|
5
|
+
readonly configPath: string;
|
|
6
|
+
readonly answer: UnconfirmedAnswerName;
|
|
7
|
+
}
|
|
8
|
+
type Observed = Readonly<{
|
|
9
|
+
status: number;
|
|
10
|
+
diagnostics: readonly Diagnostic[];
|
|
11
|
+
}>;
|
|
12
|
+
export interface ConfirmCommandAdapters {
|
|
13
|
+
/** The preflight command, run against the configuration on disk. */
|
|
14
|
+
readonly preflight: (input: Readonly<{
|
|
15
|
+
configPath: string;
|
|
16
|
+
}>) => Promise<Observed & Readonly<{
|
|
17
|
+
result: Readonly<{
|
|
18
|
+
checks: readonly EvaluatedObservation[];
|
|
19
|
+
}>;
|
|
20
|
+
}>>;
|
|
21
|
+
/** The check command, run against the configuration on disk. */
|
|
22
|
+
readonly check: (input: Readonly<{
|
|
23
|
+
configPath: string;
|
|
24
|
+
}>) => Promise<Observed>;
|
|
25
|
+
readonly now: () => Date;
|
|
26
|
+
}
|
|
27
|
+
export interface ConfirmCommandOutcome {
|
|
28
|
+
readonly status: 0 | 2;
|
|
29
|
+
readonly artifact: string | null;
|
|
30
|
+
readonly destination: null;
|
|
31
|
+
readonly result: unknown;
|
|
32
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
33
|
+
}
|
|
34
|
+
export declare function runConfirm(input: ConfirmCommandInput, adapters: ConfirmCommandAdapters): Promise<ConfirmCommandOutcome>;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Sequences `cockpit confirm <answer>`: refuse an answer only a person may confirm, run the existing
|
|
2
|
+
// observation that verifies the rest against this machine and the running product, record what ran,
|
|
3
|
+
// then drop the answer from `unconfirmed` in cockpit.toml. The record goes first, so a failed edit
|
|
4
|
+
// leaves a record nothing uses, never a removed entry the guard cannot account for.
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { canonicalJson } from '../../canonical-json.js';
|
|
7
|
+
import { loadCockpitConfig, readBoundedInput } from '../../config/load.js';
|
|
8
|
+
import { removeUnconfirmedAnswer } from '../../config/unconfirmed-toml.js';
|
|
9
|
+
import { writeVerifiedConfig } from '../../config/verified-write.js';
|
|
10
|
+
import { cockpitCommand } from '../../fix-catalogue.js';
|
|
11
|
+
import { ANSWER_CONFIRMATION_PATH, ANSWER_CONFIRMATIONS, confirmationConfigurationSha256, encodeAnswerConfirmations, parseAnswerConfirmations, personConfirmationFix, } from '../../guards/answer-confirmation.js';
|
|
12
|
+
import { writeCockpitLocalFiles } from '../../guards/local-identity.js';
|
|
13
|
+
import { refusalOutcome } from '../../refusal.js';
|
|
14
|
+
import { sha256Hex } from '../../sha256.js';
|
|
15
|
+
import { ENVIRONMENT_CODES, hasErrorCode } from '../contract.js';
|
|
16
|
+
import { declaredRuntime } from '../declaration.js';
|
|
17
|
+
// The rows of preflight that say the runtime measured on this machine meets the declared floor.
|
|
18
|
+
const RUNTIME_FLOOR_CODES = [ENVIRONMENT_CODES.MACHINE_MEMORY_BELOW_FLOOR, ENVIRONMENT_CODES.MACHINE_CPU_BELOW_FLOOR,
|
|
19
|
+
ENVIRONMENT_CODES.MACHINE_DISK_BELOW_FLOOR, ENVIRONMENT_CODES.RUNTIME_UNMEASURED];
|
|
20
|
+
// Preflight passes only with the stack up; this row says the port family it holds is leased on the
|
|
21
|
+
// declared bases with current listener evidence.
|
|
22
|
+
const PORT_FAMILY_CODES = [ENVIRONMENT_CODES.PORT_FAMILY_COLLISION];
|
|
23
|
+
const failing = (diagnostics) => diagnostics.filter(row => row.severity === 'error');
|
|
24
|
+
async function observe(observation, configPath, config, adapters) {
|
|
25
|
+
// A machine floor alone is still measured; the stack and its Compose files need a declared runtime.
|
|
26
|
+
if (observation !== 'runtime-floor' && declaredRuntime(config.environment) !== 'compose') {
|
|
27
|
+
return { passed: false, reason: 'cockpit.toml declares no Compose files, so there is no stack or topology to observe', diagnostics: [] };
|
|
28
|
+
}
|
|
29
|
+
if (observation === 'check') {
|
|
30
|
+
const checked = await adapters.check({ configPath });
|
|
31
|
+
return checked.status === 0 ? { passed: true, evidence: { status: checked.status, diagnostics: checked.diagnostics } }
|
|
32
|
+
: { passed: false, reason: 'check did not pass', diagnostics: failing(checked.diagnostics) };
|
|
33
|
+
}
|
|
34
|
+
const preflight = await adapters.preflight({ configPath });
|
|
35
|
+
const codes = observation === 'preflight' ? PORT_FAMILY_CODES : RUNTIME_FLOOR_CODES;
|
|
36
|
+
const rows = preflight.result.checks.filter(check => codes.includes(check.code));
|
|
37
|
+
const unmet = codes.filter(code => !rows.some(row => row.code === code && row.verdict === 'pass'));
|
|
38
|
+
const evidence = { checks: rows.map(({ code, verdict, source, measured, observedAt }) => ({ code, verdict, source, measured, observedAt })) };
|
|
39
|
+
if (observation === 'preflight' && preflight.status !== 0) {
|
|
40
|
+
return { passed: false, reason: 'preflight did not pass, so the stack is not up and healthy on the declared ports', diagnostics: failing(preflight.diagnostics) };
|
|
41
|
+
}
|
|
42
|
+
return unmet.length === 0 ? { passed: true, evidence }
|
|
43
|
+
: { passed: false, reason: `preflight did not observe ${unmet.join(', ')} passing`, diagnostics: failing(preflight.diagnostics) };
|
|
44
|
+
}
|
|
45
|
+
async function readConfirmations(root) {
|
|
46
|
+
try {
|
|
47
|
+
const read = await readBoundedInput(join(root, ANSWER_CONFIRMATION_PATH));
|
|
48
|
+
return read.kind === 'complete' ? read.bytes : undefined;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (hasErrorCode(error, 'ENOENT'))
|
|
52
|
+
return undefined;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export async function runConfirm(input, adapters) {
|
|
57
|
+
const { answer } = input;
|
|
58
|
+
const confirmation = ANSWER_CONFIRMATIONS[answer];
|
|
59
|
+
if (confirmation.by === 'person') {
|
|
60
|
+
return refusalOutcome('CONFIRM_PERSON_REQUIRED', `${answer} is confirmed only by a person: ${confirmation.trustDecision ? `it is a trust decision, because ${confirmation.why}` : confirmation.why}`, answer, personConfirmationFix([answer], input.configPath));
|
|
61
|
+
}
|
|
62
|
+
const loaded = await loadCockpitConfig(input.configPath);
|
|
63
|
+
if (loaded.kind !== 'complete')
|
|
64
|
+
return { status: 2, artifact: null, destination: null, result: null, diagnostics: loaded.diagnostics };
|
|
65
|
+
const configPath = loaded.result.source.path;
|
|
66
|
+
const config = loaded.result.config;
|
|
67
|
+
if (!config.unconfirmed?.includes(answer)) {
|
|
68
|
+
return refusalOutcome('CONFIRM_ANSWER_NOT_LISTED', `${answer} is not listed under unconfirmed in ${configPath}, so there is nothing to confirm`, 'unconfirmed', `run ${cockpitCommand('doctor', { config: configPath })}; DECLARATION_UNCONFIRMED names the answers still to confirm`);
|
|
69
|
+
}
|
|
70
|
+
const ranAt = adapters.now().toISOString();
|
|
71
|
+
const verdict = await observe(confirmation.observation, configPath, config, adapters);
|
|
72
|
+
if (!verdict.passed) {
|
|
73
|
+
const refused = refusalOutcome('CONFIRM_OBSERVATION_FAILED', `${answer} stays unconfirmed: ${verdict.reason}; confirming it needs ${confirmation.verifies}`, answer, `clear the diagnostics reported beside this one, then run ${cockpitCommand('confirm', { config: configPath, operand: answer })} again; if the value itself is wrong, correct it in cockpit.toml first`);
|
|
74
|
+
return { ...refused, diagnostics: [...refused.diagnostics, ...verdict.diagnostics] };
|
|
75
|
+
}
|
|
76
|
+
// The observation re-reads cockpit.toml itself, so the record binds to what it observed only if the
|
|
77
|
+
// file is still the one loaded above. An agent edit made meanwhile cannot be reverted before this
|
|
78
|
+
// read: the guard holds every call but sync and check once the file differs from the synced one.
|
|
79
|
+
const after = await loadCockpitConfig(configPath);
|
|
80
|
+
if (after.kind !== 'complete' || after.result.source.sha256 !== loaded.result.source.sha256) {
|
|
81
|
+
return refusalOutcome(ENVIRONMENT_CODES.CONFIG_CHANGED_DURING_EXECUTION, 'cockpit.toml changed while the observation ran, so it cannot show which configuration passed; nothing was recorded or removed', configPath, `run ${cockpitCommand('confirm', { config: configPath, operand: answer })} again`);
|
|
82
|
+
}
|
|
83
|
+
const root = dirname(configPath);
|
|
84
|
+
const record = { observation: confirmation.observation, ranAt, result: 'passed',
|
|
85
|
+
evidenceSha256: sha256Hex(canonicalJson(verdict.evidence)), configurationSha256: confirmationConfigurationSha256(config) };
|
|
86
|
+
const written = await writeCockpitLocalFiles(root, [{ name: 'answer-confirmations',
|
|
87
|
+
bytes: encodeAnswerConfirmations({ ...parseAnswerConfirmations(await readConfirmations(root)), [answer]: record }) }]);
|
|
88
|
+
if (written.kind !== 'complete') {
|
|
89
|
+
return refusalOutcome('CONFIRM_RECORD_UNWRITABLE', `the observation passed, but its record could not be written: ${written.message}; cockpit.toml was not changed`, ANSWER_CONFIRMATION_PATH, `make ${ANSWER_CONFIRMATION_PATH} writable for this user, then run ${cockpitCommand('confirm', { config: configPath, operand: answer })} again`);
|
|
90
|
+
}
|
|
91
|
+
const remaining = config.unconfirmed.filter(row => row !== answer);
|
|
92
|
+
const expected = canonicalJson({ ...config, unconfirmed: remaining.length ? remaining : undefined });
|
|
93
|
+
const exact = (candidate) => canonicalJson(candidate) === expected;
|
|
94
|
+
const rerun = `run ${cockpitCommand('confirm', { config: configPath, operand: answer })} again`;
|
|
95
|
+
const byHand = personConfirmationFix([answer], configPath);
|
|
96
|
+
const edited = await writeVerifiedConfig({
|
|
97
|
+
path: configPath, expectedSha256: loaded.result.source.sha256, temporaryTag: 'confirm',
|
|
98
|
+
edit: text => {
|
|
99
|
+
const edit = removeUnconfirmedAnswer(text, answer);
|
|
100
|
+
return edit.kind === 'edited' ? edit.text : text;
|
|
101
|
+
},
|
|
102
|
+
isExactChange: exact,
|
|
103
|
+
messages: {
|
|
104
|
+
changedBefore: { message: 'cockpit.toml changed while the observation ran; nothing was removed', fix: rerun },
|
|
105
|
+
changedDuring: { message: 'cockpit.toml changed while the edit was being prepared; nothing was removed', fix: rerun },
|
|
106
|
+
invalid: reason => ({ message: `removing ${answer} would leave cockpit.toml invalid (${reason}); nothing was removed`, fix: byHand }),
|
|
107
|
+
notExact: { message: `cockpit.toml does not list ${answer} on one unconfirmed = [...] line this command can edit; nothing was removed`, fix: byHand },
|
|
108
|
+
writtenMismatch: { message: `${answer} was removed, but re-reading cockpit.toml shows a change beyond it`,
|
|
109
|
+
fix: `review cockpit.toml for the change you did not make before running ${cockpitCommand('sync', { config: configPath, args: '--dry-run' })}` },
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
if (edited.kind !== 'complete')
|
|
113
|
+
return refusalOutcome(edited.code, edited.message, configPath, edited.fix);
|
|
114
|
+
const sync = cockpitCommand('sync', { config: configPath });
|
|
115
|
+
return {
|
|
116
|
+
status: 0, destination: null, diagnostics: [],
|
|
117
|
+
artifact: `Confirmed ${answer}: ${confirmation.verifies}. Removed it from unconfirmed in ${configPath} and recorded the observation in ${ANSWER_CONFIRMATION_PATH}.\n`
|
|
118
|
+
+ `Run ${sync} to apply the edit; an agent may where [autonomy] sync = "agent", because the recorded observation makes the removal non-widening.\n`,
|
|
119
|
+
result: { answer, observation: confirmation.observation, ranAt, outcome: 'passed', evidenceSha256: record.evidenceSha256,
|
|
120
|
+
record: ANSWER_CONFIRMATION_PATH, unconfirmed: remaining, next: sync },
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type DownCommandAdapters, type DownCommandInput, type DownCommandResult, type OperatorResetSession, type ResetCommandAdapters, type ResetCommandInput, type ResetCommandResult } from './lifecycle-context.js';
|
|
2
|
+
export declare function runDown(input: DownCommandInput, adapters: DownCommandAdapters): Promise<DownCommandResult>;
|
|
3
|
+
export declare function runReset(input: ResetCommandInput, adapters: ResetCommandAdapters): Promise<ResetCommandResult>;
|
|
4
|
+
export declare function beginOperatorReset(input: ResetCommandInput, adapters: ResetCommandAdapters): Promise<OperatorResetSession | Extract<ResetCommandResult, {
|
|
5
|
+
kind: 'refusal';
|
|
6
|
+
}>>;
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { cleanupFencedExecution, repairFencedExecution } from '../../environment-exec/execute.js';
|
|
5
|
+
import { ENVIRONMENT_CODES, forwardedRefusal } from '../contract.js';
|
|
6
|
+
import { readState } from '../state.js';
|
|
7
|
+
import { compensateAcquiredPortHolder } from '../ports.js';
|
|
8
|
+
import { consumeLifecycleIssuance, planDown, planResetLifecycle } from '../lifecycle.js';
|
|
9
|
+
import { stopRuntime } from '../runtime.js';
|
|
10
|
+
import { inspectDestructiveDrainRecovery, settleInterruptedDrainRepair, recoverDrainReservation, reserveDrain, reserveDrainAttempt, } from '../claims.js';
|
|
11
|
+
import { executeReclaimAction, keptTeardownResources, planReset, planTeardown, reconcileReclaimAction, } from '../reclaim.js';
|
|
12
|
+
import { selectDestructiveInstance } from '../instance.js';
|
|
13
|
+
import { issuedDestructiveRuns, lifecycleContextRefusal, provisionalClaims, withLifecycleRunNotices, } from './lifecycle-context.js';
|
|
14
|
+
import { exactCommandConfigPath, invalidateCompletedLifecycle, lifecycleSuggestion, publishDrainOutcome, publishDrainOutcomeRecord, } from './lifecycle-state.js';
|
|
15
|
+
import { lifecycleRefusal, uniqueDiagnostics } from './shared.js';
|
|
16
|
+
import { forgetStackInputs } from '../stack-inputs.js';
|
|
17
|
+
function destructiveRepairRequired(context) {
|
|
18
|
+
return lifecycleRefusal(ENVIRONMENT_CODES.DRAIN_ATTEMPT_REPAIR_REQUIRED, 'cockpit cannot prove the interrupted destructive executor stopped, so it will not overlap it with another destructive action; preserve the coordination state for explicit repair', context.stackId);
|
|
19
|
+
}
|
|
20
|
+
// A stack no session holds is claimed only so down or reset can read it, and the claim belongs to
|
|
21
|
+
// the drain the run reserves, which releases it. A run that reserves no drain (a preview, a decline,
|
|
22
|
+
// a refusal before the drain) gives it back, or the next up meets a holder it did not take and
|
|
23
|
+
// refuses on every retry.
|
|
24
|
+
async function settleProvisionalClaim(context, outcome) {
|
|
25
|
+
const claim = provisionalClaims.get(context);
|
|
26
|
+
if (!claim)
|
|
27
|
+
return outcome;
|
|
28
|
+
provisionalClaims.delete(context);
|
|
29
|
+
const released = await compensateAcquiredPortHolder({ namespace: context.namespace, leaseResult: claim, now: new Date().toISOString() });
|
|
30
|
+
if (released.kind === 'complete')
|
|
31
|
+
return outcome;
|
|
32
|
+
return forwardedRefusal(uniqueDiagnostics([...outcome.kind === 'refusal' ? outcome.diagnostics : [], ...released.diagnostics]));
|
|
33
|
+
}
|
|
34
|
+
async function loadDestructiveContext(input, adapters) {
|
|
35
|
+
const configPath = exactCommandConfigPath(input);
|
|
36
|
+
if (!configPath)
|
|
37
|
+
return lifecycleContextRefusal();
|
|
38
|
+
const loaded = await adapters.load({ configPath });
|
|
39
|
+
if (loaded.kind !== 'complete')
|
|
40
|
+
return loaded.kind === 'refusal' ? loaded : lifecycleContextRefusal();
|
|
41
|
+
if (loaded.result.configPath !== configPath || !issuedDestructiveRuns.delete(loaded.result)) {
|
|
42
|
+
return lifecycleRefusal(ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, 'destructive run authority is unavailable', configPath);
|
|
43
|
+
}
|
|
44
|
+
return loaded;
|
|
45
|
+
}
|
|
46
|
+
function selectedObservation(context, observation) {
|
|
47
|
+
if (!observation)
|
|
48
|
+
return undefined;
|
|
49
|
+
try {
|
|
50
|
+
const selected = selectDestructiveInstance(observation.instance);
|
|
51
|
+
return selected.kind === 'selected' && selected.stackId === context.stackId
|
|
52
|
+
&& selected.checkoutId === context.checkoutId ? observation : undefined;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// An observation whose instance cannot be read is not this run's, which the caller refuses.
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function observeDestructive(context, adapters) {
|
|
60
|
+
const observed = await adapters.observe(context);
|
|
61
|
+
return observed && 'diagnostics' in observed ? observed : selectedObservation(context, observed);
|
|
62
|
+
}
|
|
63
|
+
// Each removal is proven by a fresh discovery, but re-measuring the runtime and re-listing the
|
|
64
|
+
// daemon twice per removal made `down` take minutes on a busy shared daemon. A reserved drain's
|
|
65
|
+
// steps re-read only reclaim evidence, and one discovery both reconciles a removal and plans the next.
|
|
66
|
+
async function observeDestructiveReclaim(context, adapters) {
|
|
67
|
+
const observed = adapters.observeReclaim ? await adapters.observeReclaim(context) : await adapters.observe(context);
|
|
68
|
+
return observed && 'diagnostics' in observed ? observed : selectedObservation(context, observed);
|
|
69
|
+
}
|
|
70
|
+
// Before this run reserves a drain there is no attempt of its own to repair, so a missing
|
|
71
|
+
// observation is an evidence failure. Reporting DRAIN_ATTEMPT_REPAIR_REQUIRED here sent the
|
|
72
|
+
// operator of a stopped runtime looking for an interrupted attempt that did not exist.
|
|
73
|
+
function destructivePreconditionRefusal(context, observed) {
|
|
74
|
+
return observed ?? lifecycleRefusal(ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, 'destructive preconditions (configuration, retained ports, runtime readiness or reclaim discovery) could not be observed; this run reserved no destructive attempt', context.stackId);
|
|
75
|
+
}
|
|
76
|
+
// After a reservation the attempt is real and must be repaired; the owner's own diagnostics
|
|
77
|
+
// still say why the observation failed.
|
|
78
|
+
function destructiveRepairAfterObservation(context, observed) {
|
|
79
|
+
const repair = destructiveRepairRequired(context);
|
|
80
|
+
return observed ? forwardedRefusal(uniqueDiagnostics([...repair.diagnostics, ...observed.diagnostics])) : repair;
|
|
81
|
+
}
|
|
82
|
+
async function publishDestructiveOutcome(context, outcome, fence) {
|
|
83
|
+
const result = await publishDrainOutcome(context, outcome, fence);
|
|
84
|
+
return result?.kind === 'refusal' ? result : undefined;
|
|
85
|
+
}
|
|
86
|
+
async function executeRuntimeDrain(context, adapters, topology, attempt) {
|
|
87
|
+
if (attempt.kind !== 'complete')
|
|
88
|
+
return forwardedRefusal(attempt.diagnostics);
|
|
89
|
+
const stopped = await stopRuntime({ topology, attempt, execute: (step) => adapters.execute(step) });
|
|
90
|
+
if (stopped.kind === 'unavailable')
|
|
91
|
+
return destructiveRepairRequired(context);
|
|
92
|
+
const { outcome, fence } = stopped;
|
|
93
|
+
if (stopped.kind === 'not-started')
|
|
94
|
+
return { outcome, fence };
|
|
95
|
+
const reservation = attempt.reservation;
|
|
96
|
+
if (reservation.purpose === 'down' && reservation.actions.length === 1) {
|
|
97
|
+
const invalidated = await invalidateCompletedLifecycle(context, reservation.operationBasis, reservation.generation, 'down');
|
|
98
|
+
if (invalidated)
|
|
99
|
+
return invalidated;
|
|
100
|
+
}
|
|
101
|
+
return { outcome, fence };
|
|
102
|
+
}
|
|
103
|
+
// Teardown removes only what it can prove is this stack's and unused, so a finished drain can
|
|
104
|
+
// still leave the stack's own resources in place. Reporting `removed` then told the operator the
|
|
105
|
+
// stack was gone while its containers and network stayed, so down refuses and names each one.
|
|
106
|
+
function downKeptRefusal(context, observed) {
|
|
107
|
+
const kept = keptTeardownResources(observed.down);
|
|
108
|
+
if (kept === undefined)
|
|
109
|
+
return lifecycleRefusal(ENVIRONMENT_CODES.RECLAIM_ACCOUNTING_MISMATCH, 'down finished its removals, but its last resource discovery could not be classified, so it cannot confirm the stack is gone; inspect the stack with docker compose ls and docker ps --all', context.stackId);
|
|
110
|
+
if (kept.length === 0)
|
|
111
|
+
return undefined;
|
|
112
|
+
const listed = kept.slice(0, 8).map(({ kind, id, name, reason }) => `${kind} ${name} (${id}): ${reason}`).join('; ');
|
|
113
|
+
return forwardedRefusal([{
|
|
114
|
+
severity: 'error', code: ENVIRONMENT_CODES.DOWN_RESOURCES_KEPT, target: context.stackId,
|
|
115
|
+
message: `down removed what it could, but kept ${kept.length} of this stack's own resources: ${listed}${kept.length > 8 ? `; and ${kept.length - 8} more` : ''}`,
|
|
116
|
+
fix: `release each kept resource as its reason says (remove the image's other tag, stop the container still attached to the network), then run \`${lifecycleSuggestion(context, 'down')}\``,
|
|
117
|
+
}]);
|
|
118
|
+
}
|
|
119
|
+
async function executeDestructivePlan(context, adapters, purpose, lifecyclePlan, firstPlan, plannedFrom, recoveredReservation, recoveredRequest) {
|
|
120
|
+
if (lifecyclePlan.kind !== 'drain')
|
|
121
|
+
return forwardedRefusal(lifecyclePlan.diagnostics);
|
|
122
|
+
const issuance = consumeLifecycleIssuance(lifecyclePlan.plan);
|
|
123
|
+
if (!issuance?.drainRequest || !issuance.reclaim || issuance.reclaim.purpose !== purpose) {
|
|
124
|
+
return destructiveRepairRequired(context);
|
|
125
|
+
}
|
|
126
|
+
const request = recoveredRequest ?? lifecyclePlan.request;
|
|
127
|
+
// From here the drain owns the stack's claim: it releases it on completion, and keeps it for repair.
|
|
128
|
+
provisionalClaims.delete(context);
|
|
129
|
+
const reserved = recoveredReservation ?? await reserveDrain({ namespace: context.namespace, request });
|
|
130
|
+
if (reserved.kind !== 'complete')
|
|
131
|
+
return forwardedRefusal(reserved.diagnostics);
|
|
132
|
+
let availableReservation = reserved.reservation.completedActionCount === 0 ? undefined : reserved;
|
|
133
|
+
if (reserved.reservation.completedActionCount === 0) {
|
|
134
|
+
const runtimeAttempt = await reserveDrainAttempt({ namespace: context.namespace, reservation: reserved.reservation });
|
|
135
|
+
const runtimeOutcome = await executeRuntimeDrain(context, adapters, issuance.topology, runtimeAttempt);
|
|
136
|
+
if ('kind' in runtimeOutcome)
|
|
137
|
+
return runtimeOutcome;
|
|
138
|
+
const runtimePublished = await publishDestructiveOutcome(context, runtimeOutcome.outcome, runtimeOutcome.fence);
|
|
139
|
+
if (runtimePublished)
|
|
140
|
+
return runtimePublished;
|
|
141
|
+
}
|
|
142
|
+
let plan = firstPlan;
|
|
143
|
+
// The latest discovery: what the stack still holds once the last removal is reconciled.
|
|
144
|
+
let observed = plannedFrom;
|
|
145
|
+
while (plan.action !== null) {
|
|
146
|
+
const recovered = availableReservation ?? await recoverDrainReservation({ namespace: context.namespace, request });
|
|
147
|
+
availableReservation = undefined;
|
|
148
|
+
if (recovered.kind !== 'complete')
|
|
149
|
+
return forwardedRefusal(recovered.diagnostics);
|
|
150
|
+
const attempt = await reserveDrainAttempt({ namespace: context.namespace, reservation: recovered.reservation });
|
|
151
|
+
if (attempt.kind !== 'complete')
|
|
152
|
+
return forwardedRefusal(attempt.diagnostics);
|
|
153
|
+
const ran = await executeReclaimAction({ action: plan.action, attempt, execute: (step) => adapters.execute(step) });
|
|
154
|
+
if (ran.kind === 'unavailable')
|
|
155
|
+
return destructiveRepairRequired(context);
|
|
156
|
+
if (ran.kind === 'not-started') {
|
|
157
|
+
const published = await publishDestructiveOutcome(context, ran.outcome, ran.fence);
|
|
158
|
+
return published ?? forwardedRefusal([{
|
|
159
|
+
severity: 'error', code: ran.code,
|
|
160
|
+
message: 'destructive action did not start', target: context.stackId,
|
|
161
|
+
}]);
|
|
162
|
+
}
|
|
163
|
+
const postAction = await observeDestructiveReclaim(context, adapters);
|
|
164
|
+
if (!postAction || 'diagnostics' in postAction)
|
|
165
|
+
return destructiveRepairAfterObservation(context, postAction);
|
|
166
|
+
observed = postAction;
|
|
167
|
+
const reconciled = await reconcileReclaimAction({
|
|
168
|
+
executed: ran.executed,
|
|
169
|
+
discovery: purpose === 'down' ? postAction.down.discovery : postAction.reset.discovery,
|
|
170
|
+
});
|
|
171
|
+
if (reconciled.kind === 'refusal') {
|
|
172
|
+
// An already-absent target ran nothing, so the owner's reason is the whole story.
|
|
173
|
+
return ran.executed.receipt === null ? forwardedRefusal(reconciled.diagnostics) : destructiveRepairRequired(context);
|
|
174
|
+
}
|
|
175
|
+
if (purpose === 'down' && reconciled.continuation === null) {
|
|
176
|
+
const invalidated = await invalidateCompletedLifecycle(context, request.operationBasis, request.generation, 'down');
|
|
177
|
+
if (invalidated)
|
|
178
|
+
return invalidated;
|
|
179
|
+
}
|
|
180
|
+
const fence = ran.executed.fence;
|
|
181
|
+
const published = fence === null
|
|
182
|
+
? await publishDrainOutcomeRecord(context, reconciled.outcome)
|
|
183
|
+
: await publishDestructiveOutcome(context, reconciled.outcome, fence);
|
|
184
|
+
if (published)
|
|
185
|
+
return published;
|
|
186
|
+
if (reconciled.continuation === null)
|
|
187
|
+
break;
|
|
188
|
+
// The discovery that reconciled this removal plans the next; only the coordination record
|
|
189
|
+
// moved since, by the outcome just published, so it is read again.
|
|
190
|
+
const coordination = await readState(context.namespace.paths.coordination(context.stackId));
|
|
191
|
+
if (coordination.kind !== 'complete')
|
|
192
|
+
return destructiveRepairRequired(context);
|
|
193
|
+
const next = purpose === 'down'
|
|
194
|
+
? await planTeardown({ ...postAction.down, coordinationRecord: coordination.value,
|
|
195
|
+
expectedDiscoveryDigest: request.planDigest, continuation: reconciled.continuation })
|
|
196
|
+
: await planReset({ ...postAction.reset, coordinationRecord: coordination.value,
|
|
197
|
+
expectedCoordinationRevision: coordination.value.revision,
|
|
198
|
+
confirmationDigest: request.planDigest, continuation: reconciled.continuation });
|
|
199
|
+
if (next.kind !== 'complete')
|
|
200
|
+
return next.kind === 'refusal'
|
|
201
|
+
? forwardedRefusal(next.diagnostics) : destructiveRepairRequired(context);
|
|
202
|
+
plan = next;
|
|
203
|
+
}
|
|
204
|
+
return purpose === 'down'
|
|
205
|
+
? downKeptRefusal(context, observed) ?? {
|
|
206
|
+
kind: 'complete', status: 0, diagnostics: lifecyclePlan.plan.diagnostics,
|
|
207
|
+
result: { stackId: context.stackId, change: 'removed' },
|
|
208
|
+
}
|
|
209
|
+
: {
|
|
210
|
+
kind: 'complete', status: 0, diagnostics: lifecyclePlan.plan.diagnostics,
|
|
211
|
+
result: { stackId: context.stackId, change: 'reset' },
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function destructivePlanningBasis(request) {
|
|
215
|
+
const runtimeStop = request?.actions.find(({ stage }) => stage === 'runtime-stop');
|
|
216
|
+
const reclaim = request?.actions.find(({ stage }) => stage === 'reclaim');
|
|
217
|
+
return {
|
|
218
|
+
generation: request?.generation ?? randomBytes(32).toString('hex'),
|
|
219
|
+
runtimeStopValidForMs: runtimeStop?.validForMs ?? 60_000,
|
|
220
|
+
reclaimValidForMs: reclaim?.validForMs ?? 60_000,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function matchesPersistedDrainRequest(planned, persisted) {
|
|
224
|
+
if (persisted === undefined)
|
|
225
|
+
return true;
|
|
226
|
+
const { expectedCoordinationRevision: plannedRevision, ...plannedWork } = planned;
|
|
227
|
+
const { expectedCoordinationRevision: persistedRevision, ...persistedWork } = persisted;
|
|
228
|
+
return plannedRevision >= persistedRevision && isDeepStrictEqual(plannedWork, persistedWork);
|
|
229
|
+
}
|
|
230
|
+
async function prepareDown(context, adapters, recovered) {
|
|
231
|
+
const previewObservation = await observeDestructive(context, adapters);
|
|
232
|
+
if (!previewObservation || 'diagnostics' in previewObservation)
|
|
233
|
+
return destructivePreconditionRefusal(context, previewObservation);
|
|
234
|
+
const preview = await planTeardown({ ...previewObservation.down,
|
|
235
|
+
...(recovered ? { recoveredReservation: recovered.reservation } : {}) });
|
|
236
|
+
if (preview.kind !== 'complete')
|
|
237
|
+
return forwardedRefusal(preview.diagnostics);
|
|
238
|
+
// The plan needs a discovery after the preview's consent; the runtime the preview measured
|
|
239
|
+
// seconds ago still stands for planning the drain.
|
|
240
|
+
const observation = await observeDestructiveReclaim(context, adapters);
|
|
241
|
+
if (!observation || 'diagnostics' in observation)
|
|
242
|
+
return destructivePreconditionRefusal(context, observation);
|
|
243
|
+
return { runtime: previewObservation.runtime, ports: previewObservation.ports, observation, preview };
|
|
244
|
+
}
|
|
245
|
+
async function inspectDestructiveRecoveryWithRepair(context, purpose) {
|
|
246
|
+
let inspection = await inspectDestructiveDrainRecovery({
|
|
247
|
+
namespace: context.namespace, stackId: context.stackId, holderId: context.holderId, purpose,
|
|
248
|
+
});
|
|
249
|
+
if (inspection.kind !== 'repairable')
|
|
250
|
+
return inspection;
|
|
251
|
+
const terminated = await repairFencedExecution(inspection.fence);
|
|
252
|
+
if (terminated.kind !== 'complete')
|
|
253
|
+
return inspection;
|
|
254
|
+
const repaired = await settleInterruptedDrainRepair({ namespace: context.namespace, record: inspection.record, evidence: terminated.evidence });
|
|
255
|
+
if (repaired.kind !== 'complete')
|
|
256
|
+
return inspection;
|
|
257
|
+
if (!await cleanupFencedExecution(inspection.fence))
|
|
258
|
+
return inspection;
|
|
259
|
+
inspection = await inspectDestructiveDrainRecovery({
|
|
260
|
+
namespace: context.namespace, stackId: context.stackId, holderId: context.holderId, purpose,
|
|
261
|
+
});
|
|
262
|
+
return inspection;
|
|
263
|
+
}
|
|
264
|
+
export async function runDown(input, adapters) {
|
|
265
|
+
const loaded = await loadDestructiveContext(input, adapters);
|
|
266
|
+
if (loaded.kind !== 'complete')
|
|
267
|
+
return loaded;
|
|
268
|
+
const outcome = await settleProvisionalClaim(loaded.result, await downWithContext(loaded.result, adapters));
|
|
269
|
+
// A removed stack runs nothing a later doctor could call stale.
|
|
270
|
+
const forgotten = outcome.kind === 'complete' && outcome.result.change === 'removed' ? await forgetStackInputs(dirname(loaded.result.source.path)) : [];
|
|
271
|
+
return withLifecycleRunNotices(loaded.result, forgotten.length === 0 ? outcome : { ...outcome, diagnostics: uniqueDiagnostics([...outcome.diagnostics, ...forgotten]) });
|
|
272
|
+
}
|
|
273
|
+
async function downWithContext(context, adapters) {
|
|
274
|
+
const inspection = await inspectDestructiveRecoveryWithRepair(context, 'down');
|
|
275
|
+
if (inspection.kind === 'refusal')
|
|
276
|
+
return forwardedRefusal(inspection.diagnostics);
|
|
277
|
+
if (inspection.kind === 'repairable')
|
|
278
|
+
return destructiveRepairRequired(context);
|
|
279
|
+
const persistedRequest = inspection.kind === 'recoverable' ? inspection.request : undefined;
|
|
280
|
+
const recovered = persistedRequest === undefined ? undefined
|
|
281
|
+
: await recoverDrainReservation({ namespace: context.namespace, request: persistedRequest });
|
|
282
|
+
if (recovered?.kind === 'refusal')
|
|
283
|
+
return forwardedRefusal(recovered.diagnostics);
|
|
284
|
+
const prepared = await prepareDown(context, adapters, recovered);
|
|
285
|
+
if ('diagnostics' in prepared)
|
|
286
|
+
return prepared;
|
|
287
|
+
const basis = destructivePlanningBasis(persistedRequest);
|
|
288
|
+
const lifecycle = planDown({
|
|
289
|
+
runtime: prepared.runtime,
|
|
290
|
+
ports: prepared.ports,
|
|
291
|
+
reclaim: prepared.preview.envelope,
|
|
292
|
+
operationGeneration: basis.generation,
|
|
293
|
+
runtimeStopValidForMs: basis.runtimeStopValidForMs,
|
|
294
|
+
reclaimValidForMs: basis.reclaimValidForMs,
|
|
295
|
+
});
|
|
296
|
+
if (lifecycle.kind !== 'drain')
|
|
297
|
+
return forwardedRefusal(lifecycle.diagnostics);
|
|
298
|
+
if (!matchesPersistedDrainRequest(lifecycle.request, persistedRequest))
|
|
299
|
+
return destructiveRepairRequired(context);
|
|
300
|
+
const plan = await planTeardown({
|
|
301
|
+
...prepared.observation.down,
|
|
302
|
+
expectedDiscoveryDigest: prepared.preview.discoveryDigest,
|
|
303
|
+
...(recovered?.kind === 'complete'
|
|
304
|
+
? { recoveredReservation: recovered.reservation }
|
|
305
|
+
: prepared.preview.consent === null ? {} : { consent: prepared.preview.consent }),
|
|
306
|
+
});
|
|
307
|
+
if (plan.kind !== 'complete')
|
|
308
|
+
return forwardedRefusal(plan.diagnostics);
|
|
309
|
+
return await executeDestructivePlan(context, adapters, 'down', lifecycle, plan, prepared.observation, recovered?.kind === 'complete' ? recovered : undefined, persistedRequest);
|
|
310
|
+
}
|
|
311
|
+
const privateResetSessions = new WeakMap();
|
|
312
|
+
async function resetPreview(context, adapters) {
|
|
313
|
+
const observation = await observeDestructive(context, adapters);
|
|
314
|
+
if (!observation || 'diagnostics' in observation)
|
|
315
|
+
return destructivePreconditionRefusal(context, observation);
|
|
316
|
+
const preview = await planReset(observation.reset);
|
|
317
|
+
return preview.kind === 'preview' ? preview : preview.kind === 'refusal'
|
|
318
|
+
? forwardedRefusal(preview.diagnostics) : destructivePreconditionRefusal(context, undefined);
|
|
319
|
+
}
|
|
320
|
+
function publicResetPreview(context, preview) {
|
|
321
|
+
return {
|
|
322
|
+
kind: 'preview', status: 0, diagnostics: [],
|
|
323
|
+
result: {
|
|
324
|
+
stackId: context.stackId, planDigest: preview.digest,
|
|
325
|
+
targets: preview.targets.map((target) => ({ ...target })),
|
|
326
|
+
noop: preview.noop,
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
export async function runReset(input, adapters) {
|
|
331
|
+
const loaded = await loadDestructiveContext(input, adapters);
|
|
332
|
+
if (loaded.kind !== 'complete')
|
|
333
|
+
return loaded;
|
|
334
|
+
const preview = await resetPreview(loaded.result, adapters);
|
|
335
|
+
return withLifecycleRunNotices(loaded.result, await settleProvisionalClaim(loaded.result, 'status' in preview ? preview : publicResetPreview(loaded.result, preview)));
|
|
336
|
+
}
|
|
337
|
+
export async function beginOperatorReset(input, adapters) {
|
|
338
|
+
const loaded = await loadDestructiveContext(input, adapters);
|
|
339
|
+
if (loaded.kind !== 'complete')
|
|
340
|
+
return loaded;
|
|
341
|
+
const context = loaded.result;
|
|
342
|
+
const preview = await resetPreview(context, adapters);
|
|
343
|
+
if ('status' in preview)
|
|
344
|
+
return withLifecycleRunNotices(context, await settleProvisionalClaim(context, preview));
|
|
345
|
+
const token = {};
|
|
346
|
+
privateResetSessions.set(token, Object.freeze({ context, adapters, preview }));
|
|
347
|
+
const session = {
|
|
348
|
+
preview: withLifecycleRunNotices(context, publicResetPreview(context, preview)),
|
|
349
|
+
decide: (decision) => continueOperatorReset(token, decision),
|
|
350
|
+
};
|
|
351
|
+
return session;
|
|
352
|
+
}
|
|
353
|
+
async function continueOperatorReset(token, decision) {
|
|
354
|
+
const session = privateResetSessions.get(token);
|
|
355
|
+
privateResetSessions.delete(token);
|
|
356
|
+
const unapproved = forwardedRefusal([{
|
|
357
|
+
severity: 'error', code: ENVIRONMENT_CODES.RESET_CONFIRMATION_MISMATCH,
|
|
358
|
+
message: decision === 'decline' ? 'reset was declined' : 'reset operator session is unavailable', target: 'reset',
|
|
359
|
+
}]);
|
|
360
|
+
if (!session)
|
|
361
|
+
return unapproved;
|
|
362
|
+
return settleProvisionalClaim(session.context, decision === 'approve' ? await approveOperatorReset(session) : unapproved);
|
|
363
|
+
}
|
|
364
|
+
async function approveOperatorReset(session) {
|
|
365
|
+
const inspection = await inspectDestructiveRecoveryWithRepair(session.context, 'reset');
|
|
366
|
+
if (inspection.kind === 'refusal')
|
|
367
|
+
return forwardedRefusal(inspection.diagnostics);
|
|
368
|
+
if (inspection.kind === 'repairable')
|
|
369
|
+
return destructiveRepairRequired(session.context);
|
|
370
|
+
const persistedRequest = inspection.kind === 'recoverable' ? inspection.request : undefined;
|
|
371
|
+
const recovered = persistedRequest === undefined ? undefined
|
|
372
|
+
: await recoverDrainReservation({ namespace: session.context.namespace, request: persistedRequest });
|
|
373
|
+
if (recovered?.kind === 'refusal')
|
|
374
|
+
return forwardedRefusal(recovered.diagnostics);
|
|
375
|
+
const observation = await observeDestructive(session.context, session.adapters);
|
|
376
|
+
if (!observation || 'diagnostics' in observation)
|
|
377
|
+
return destructivePreconditionRefusal(session.context, observation);
|
|
378
|
+
if (session.preview.noop && !persistedRequest) {
|
|
379
|
+
const noop = await planReset({
|
|
380
|
+
...observation.reset,
|
|
381
|
+
confirmationDigest: session.preview.digest,
|
|
382
|
+
consent: session.preview.consent,
|
|
383
|
+
});
|
|
384
|
+
if (noop.kind !== 'complete' || !noop.noop || noop.action !== null) {
|
|
385
|
+
return noop.kind === 'refusal' ? forwardedRefusal(noop.diagnostics) : destructivePreconditionRefusal(session.context, undefined);
|
|
386
|
+
}
|
|
387
|
+
const finalInspection = await inspectDestructiveDrainRecovery({ namespace: session.context.namespace,
|
|
388
|
+
stackId: session.context.stackId, holderId: session.context.holderId, purpose: 'reset' });
|
|
389
|
+
if (finalInspection.kind !== 'none')
|
|
390
|
+
return finalInspection.kind === 'refusal'
|
|
391
|
+
? forwardedRefusal(finalInspection.diagnostics) : destructiveRepairRequired(session.context);
|
|
392
|
+
return {
|
|
393
|
+
kind: 'complete', status: 0, diagnostics: [],
|
|
394
|
+
result: { stackId: session.context.stackId, change: 'unchanged' },
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
const recoveryPlan = recovered ? await planReset({ ...observation.reset,
|
|
398
|
+
confirmationDigest: persistedRequest.planDigest,
|
|
399
|
+
recoveredReservation: recovered.reservation, operatorApproval: session.preview }) : undefined;
|
|
400
|
+
if (recoveryPlan && recoveryPlan.kind !== 'complete')
|
|
401
|
+
return recoveryPlan.kind === 'refusal'
|
|
402
|
+
? forwardedRefusal(recoveryPlan.diagnostics) : destructiveRepairRequired(session.context);
|
|
403
|
+
const basis = destructivePlanningBasis(persistedRequest);
|
|
404
|
+
const lifecycle = planResetLifecycle({
|
|
405
|
+
runtime: observation.runtime,
|
|
406
|
+
ports: observation.ports,
|
|
407
|
+
reclaim: recoveryPlan ? recoveryPlan.envelope : session.preview.envelope,
|
|
408
|
+
operationGeneration: basis.generation,
|
|
409
|
+
runtimeStopValidForMs: basis.runtimeStopValidForMs,
|
|
410
|
+
reclaimValidForMs: basis.reclaimValidForMs,
|
|
411
|
+
});
|
|
412
|
+
if (lifecycle.kind !== 'drain')
|
|
413
|
+
return forwardedRefusal(lifecycle.diagnostics);
|
|
414
|
+
if (!matchesPersistedDrainRequest(lifecycle.request, persistedRequest))
|
|
415
|
+
return destructiveRepairRequired(session.context);
|
|
416
|
+
const actionablePlan = recoveryPlan ?? await planReset({
|
|
417
|
+
...observation.reset,
|
|
418
|
+
confirmationDigest: session.preview.digest,
|
|
419
|
+
...(recovered?.kind === 'complete'
|
|
420
|
+
? { recoveredReservation: recovered.reservation }
|
|
421
|
+
: { consent: session.preview.consent }),
|
|
422
|
+
});
|
|
423
|
+
if (actionablePlan.kind !== 'complete')
|
|
424
|
+
return actionablePlan.kind === 'refusal'
|
|
425
|
+
? forwardedRefusal(actionablePlan.diagnostics) : destructiveRepairRequired(session.context);
|
|
426
|
+
const applied = await executeDestructivePlan(session.context, session.adapters, 'reset', lifecycle, actionablePlan, observation, recovered?.kind === 'complete' ? recovered : undefined, persistedRequest);
|
|
427
|
+
return applied.kind === 'complete' ? { ...applied, result: { ...applied.result,
|
|
428
|
+
targets: publicResetPreview(session.context, session.preview).result.targets } } : applied;
|
|
429
|
+
}
|