@b00dea/cockpit 0.0.0-stage → 0.1.0
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 +375 -0
- package/LICENSE +21 -0
- package/README.md +22 -2
- package/bin/cockpit.mjs +33 -0
- package/cockpit.schema.json +2574 -0
- package/dist/autonomy.d.ts +44 -0
- package/dist/autonomy.js +156 -0
- package/dist/bounded-read.d.ts +14 -0
- package/dist/bounded-read.js +42 -0
- package/dist/browser/commands.d.ts +62 -0
- package/dist/browser/commands.js +349 -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 +117 -0
- package/dist/browser/runtime.js +183 -0
- package/dist/browser/surfaces.d.ts +141 -0
- package/dist/browser/surfaces.js +556 -0
- package/dist/browser/ui-gate.d.ts +15 -0
- package/dist/browser/ui-gate.js +18 -0
- package/dist/cli-guard.d.ts +5 -0
- package/dist/cli-guard.js +71 -0
- package/dist/cli-main.d.ts +139 -0
- package/dist/cli-main.js +1405 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +16 -0
- package/dist/command-table.d.ts +113 -0
- package/dist/command-table.js +512 -0
- package/dist/config/load.d.ts +171 -0
- package/dist/config/load.js +345 -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/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 +335 -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/destructive.d.ts +6 -0
- package/dist/environment/application/destructive.js +425 -0
- package/dist/environment/application/doctor.d.ts +89 -0
- package/dist/environment/application/doctor.js +235 -0
- package/dist/environment/application/guard.d.ts +43 -0
- package/dist/environment/application/guard.js +622 -0
- package/dist/environment/application/health.d.ts +134 -0
- package/dist/environment/application/health.js +379 -0
- package/dist/environment/application/lifecycle-context.d.ts +228 -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 +201 -0
- package/dist/environment/application/lifecycle.d.ts +17 -0
- package/dist/environment/application/lifecycle.js +729 -0
- package/dist/environment/application/logs.d.ts +45 -0
- package/dist/environment/application/logs.js +127 -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/claims.d.ts +389 -0
- package/dist/environment/claims.js +1709 -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 +117 -0
- package/dist/environment/compose-topology.js +413 -0
- package/dist/environment/contract.d.ts +489 -0
- package/dist/environment/contract.js +761 -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 +26 -0
- package/dist/environment/executable.js +167 -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 +189 -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 +276 -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 +484 -0
- package/dist/environment/generated-application/shared.js +349 -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 +389 -0
- package/dist/environment/generated-application/surfaces.d.ts +3 -0
- package/dist/environment/generated-application/surfaces.js +699 -0
- package/dist/environment/generated-application/sync.d.ts +6 -0
- package/dist/environment/generated-application/sync.js +109 -0
- package/dist/environment/git-observation.d.ts +183 -0
- package/dist/environment/git-observation.js +544 -0
- package/dist/environment/health-composition.d.ts +11 -0
- package/dist/environment/health-composition.js +628 -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 +308 -0
- package/dist/environment/init-application.js +697 -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 +29 -0
- package/dist/environment/lifecycle-composition.js +675 -0
- package/dist/environment/lifecycle-stage.d.ts +115 -0
- package/dist/environment/lifecycle-stage.js +398 -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/observations.d.ts +46 -0
- package/dist/environment/observations.js +344 -0
- package/dist/environment/operator-application.d.ts +50 -0
- package/dist/environment/operator-application.js +155 -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/read-composition.d.ts +27 -0
- package/dist/environment/read-composition.js +352 -0
- package/dist/environment/readiness-composition.d.ts +38 -0
- package/dist/environment/readiness-composition.js +130 -0
- package/dist/environment/reclaim.d.ts +333 -0
- package/dist/environment/reclaim.js +1835 -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 +505 -0
- package/dist/environment/runtime.js +2335 -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/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-instructions.d.ts +33 -0
- package/dist/environment/sibling-instructions.js +224 -0
- package/dist/environment/sibling-registration.d.ts +45 -0
- package/dist/environment/sibling-registration.js +538 -0
- package/dist/environment/state.d.ts +371 -0
- package/dist/environment/state.js +2013 -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 +96 -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 +102 -0
- package/dist/environment-exec/execute.js +1175 -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 +275 -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/evaluator.d.ts +113 -0
- package/dist/guards/evaluator.js +1226 -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 +40 -0
- package/dist/guards/local-identity.js +123 -0
- package/dist/guards/policy-pin.d.ts +13 -0
- package/dist/guards/policy-pin.js +175 -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 +40 -0
- package/dist/init/command.js +310 -0
- package/dist/init/derive.d.ts +51 -0
- package/dist/init/derive.js +715 -0
- package/dist/init/draft.d.ts +138 -0
- package/dist/init/draft.js +1205 -0
- package/dist/init/render.d.ts +35 -0
- package/dist/init/render.js +82 -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/schema-json.d.ts +1 -0
- package/dist/schema-json.js +177 -0
- package/dist/schema.d.ts +1322 -0
- package/dist/schema.js +934 -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 +180 -0
- package/dist/surfaces/harness-registry.js +117 -0
- package/dist/surfaces/mcp.d.ts +95 -0
- package/dist/surfaces/mcp.js +186 -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 +681 -0
- package/dist/surfaces/render.d.ts +118 -0
- package/dist/surfaces/render.js +633 -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 +512 -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/verdict.d.ts +29 -0
- package/dist/testing/verdict.js +62 -0
- package/dist/work-orders/application.d.ts +99 -0
- package/dist/work-orders/application.js +383 -0
- package/dist/work-orders/composition.d.ts +124 -0
- package/dist/work-orders/composition.js +1010 -0
- package/dist/work-orders/contract.d.ts +61 -0
- package/dist/work-orders/contract.js +122 -0
- package/dist/work-orders/store.d.ts +158 -0
- package/dist/work-orders/store.js +197 -0
- package/docs/README.md +65 -0
- package/docs/architecture.md +136 -0
- package/docs/capability-matrix.md +50 -0
- package/docs/cli-reference.md +599 -0
- package/docs/command-list.md +51 -0
- package/docs/compose-patterns.md +99 -0
- package/docs/concepts.md +171 -0
- package/docs/configuration-essentials.md +172 -0
- package/docs/configuration-reference.md +621 -0
- package/docs/environment.md +760 -0
- package/docs/extending.md +43 -0
- package/docs/faq.md +111 -0
- package/docs/getting-started.md +608 -0
- package/docs/harness-surfaces.md +433 -0
- package/docs/joining-a-team.md +76 -0
- package/docs/release-policy.md +49 -0
- package/docs/safety-and-autonomy.md +419 -0
- package/docs/troubleshooting.md +361 -0
- package/docs/tutorial.md +170 -0
- package/docs/work-and-browser.md +254 -0
- package/libexec/browser-operation.cjs +186 -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 +54 -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 +61 -0
- package/surfaces/skills/procedures/fix.md +81 -0
- package/surfaces/skills/procedures/setup.md +379 -0
- package/surfaces/skills/procedures/work.md +105 -0
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { constants as osConstants } from 'node:os';
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
4
|
+
import { basename, isAbsolute, join, resolve, sep } from 'node:path';
|
|
5
|
+
import { cleanupFencedExecution, repairFencedExecution } from '../../environment-exec/execute.js';
|
|
6
|
+
import { ENVIRONMENT_CODES, forwardedRefusal, } from '../contract.js';
|
|
7
|
+
import { cockpitCommand } from '../../fix-catalogue.js';
|
|
8
|
+
import { decodeLifecycleRecord, earlierNameRefusalMessage, findStackUnderEarlierName, recordLifecycleSeeded, recordLifecycleStarted, } from '../lifecycle-stage.js';
|
|
9
|
+
import { compensateAcquiredPortHolder, deriveEntrypoints, inspectPortLeaseProjection, revalidatePortLeaseProjection, observeRetainedPortFamily, inspectRecordedPortDeclaration, projectRetainedPortFamilyResult, releasePortFamily, } from '../ports.js';
|
|
10
|
+
import { consumeLifecycleIssuance, planSleep, admitUpProgress, planAdmittedUp } from '../lifecycle.js';
|
|
11
|
+
import { admitRuntimeServiceHealth, revalidateRuntimeService, startRuntimeService, observeRuntimeServiceColdStart, resolveRuntimeServiceOrder, stopRuntime, } from '../runtime.js';
|
|
12
|
+
import { admitFailureLogs, failureLogExcerpt, planFailureLogs } from '../logs.js';
|
|
13
|
+
import { inspectSleepDrainRecovery, settleInterruptedDrainRepair, recoverSleepDrainReservation, reserveDrain, reserveDrainAttempt, observeProcessStart, processHost, releaseDeadSessionHolders, } from '../claims.js';
|
|
14
|
+
import { observationSatisfiesExpectation } from '../observations.js';
|
|
15
|
+
import { issuedLifecycleRuns, lifecycleContextRefusal, withLifecycleConfiguration, withLifecycleRunNotices, } from './lifecycle-context.js';
|
|
16
|
+
import { exactCommandConfigPath, invalidateCompletedLifecycle, lifecycleSuggestion, publishDrainOutcome, publishLifecycleRecord, readLifecycleRecord, repairRequired, unheldStack, } from './lifecycle-state.js';
|
|
17
|
+
import { lifecycleRefusal, ownerDiagnostic, uniqueDiagnostics } from './shared.js';
|
|
18
|
+
function seconds(millis) {
|
|
19
|
+
return String(millis / 1_000);
|
|
20
|
+
}
|
|
21
|
+
// A floor refusal is acted on by changing the machine or the declared field, so it states both
|
|
22
|
+
// numbers in the unit that field is written in.
|
|
23
|
+
function machineFloorRefusal(failed) {
|
|
24
|
+
const observed = failed.observed.state === 'available' ? Number(failed.observed.value) : Number.NaN;
|
|
25
|
+
const floor = failed.expectation.kind === 'minimum' ? failed.expectation.value : Number.NaN;
|
|
26
|
+
const mb = (bytes) => `${Math.floor(bytes / 1_048_576)} MB`;
|
|
27
|
+
const cpu = failed.code === ENVIRONMENT_CODES.MACHINE_CPU_BELOW_FLOOR;
|
|
28
|
+
const disk = failed.code === ENVIRONMENT_CODES.MACHINE_DISK_BELOW_FLOOR;
|
|
29
|
+
const [measured, field, declared] = cpu ? [`${observed / 1_000} CPUs`, 'runtime_cpus', `${floor / 1_000} CPUs`]
|
|
30
|
+
: disk ? [`${mb(observed)} of free disk`, 'free_disk_mb', mb(floor)]
|
|
31
|
+
: [`${mb(observed)} of memory`, 'runtime_memory_mb', mb(floor)];
|
|
32
|
+
return { kind: 'refusal', status: 2, result: null, diagnostics: [{
|
|
33
|
+
severity: 'error',
|
|
34
|
+
// Literals, so the diagnostic inventory captures the fix beside each code.
|
|
35
|
+
code: cpu ? 'MACHINE_CPU_BELOW_FLOOR'
|
|
36
|
+
: disk ? 'MACHINE_DISK_BELOW_FLOOR'
|
|
37
|
+
: 'MACHINE_MEMORY_BELOW_FLOOR',
|
|
38
|
+
message: `the container runtime has ${measured}, below the ${declared} that environment.machine.${field} declares`,
|
|
39
|
+
target: failed.source,
|
|
40
|
+
fix: 'give the container runtime more resources (its VM or Docker Desktop settings) or lower the declared environment.machine floor in cockpit.toml deliberately and run npx --no cockpit sync --config <your cockpit.toml>, then retry',
|
|
41
|
+
}] };
|
|
42
|
+
}
|
|
43
|
+
// A compose-first stack starts from its Compose files as well as cockpit.toml; naming only
|
|
44
|
+
// cockpit.toml sent a person who had edited compose.yml looking for a change that was not there.
|
|
45
|
+
function runtimeChangedRefusal(context, changed) {
|
|
46
|
+
return lifecycleRefusal(ENVIRONMENT_CODES.LIFECYCLE_PROGRESS_INVALID, `the recorded stack runs an earlier version of the services, ports, mounts or images: ${changed}, and up does not replace a running stack — fix: run \`${lifecycleSuggestion(context, 'down')}\`, then \`${lifecycleSuggestion(context, 'up')}\``, context.stackId);
|
|
47
|
+
}
|
|
48
|
+
function changedRuntimeInputs(recorded, current, fallback) {
|
|
49
|
+
const names = (values, conjunction) => values.length <= 1 ? values.join('')
|
|
50
|
+
: `${values.slice(0, -1).join(', ')} ${conjunction} ${values.at(-1)}`;
|
|
51
|
+
if (recorded && current) {
|
|
52
|
+
const changed = [...new Set([...Object.keys(recorded), ...Object.keys(current)])]
|
|
53
|
+
.filter((name) => recorded[name] !== current[name]).sort();
|
|
54
|
+
if (changed.length > 0)
|
|
55
|
+
return `${names(changed, 'and')} changed since it started`;
|
|
56
|
+
}
|
|
57
|
+
// Without a recorded digest per file, name every file the stack is started from.
|
|
58
|
+
const candidates = current ? Object.keys(current).sort() : [fallback];
|
|
59
|
+
return `${names(candidates, 'or')} changed since it started`;
|
|
60
|
+
}
|
|
61
|
+
export { decodeLifecycleRecord } from '../lifecycle-stage.js';
|
|
62
|
+
function serviceAdmission(outcome) {
|
|
63
|
+
if (outcome.kind !== 'observed')
|
|
64
|
+
return outcome;
|
|
65
|
+
const evidence = admitRuntimeServiceHealth(outcome.observation);
|
|
66
|
+
return evidence ? { kind: 'complete', evidence }
|
|
67
|
+
: { kind: 'failure', logs: outcome.observation, ...outcome.stop ? { stop: outcome.stop } : {} };
|
|
68
|
+
}
|
|
69
|
+
export async function revalidateService(topology, completed, adapters) {
|
|
70
|
+
return serviceAdmission(await revalidateRuntimeService({
|
|
71
|
+
topology, service: completed.service, expectedContainerId: completed.containerId,
|
|
72
|
+
execute: (step) => adapters.execute(step),
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
async function startService(topology, service, adapters) {
|
|
76
|
+
return serviceAdmission(await startRuntimeService({ topology, service, execute: (step) => adapters.execute(step) }));
|
|
77
|
+
}
|
|
78
|
+
// Failure evidence for a service that did not become healthy: its bounded logs as one diagnostic,
|
|
79
|
+
// read before anything else touches the container. When this `up` started the service, `stop` then
|
|
80
|
+
// stops it: under a restart policy a failed service otherwise restarted until `down`. The
|
|
81
|
+
// container is kept, with its logs, and the next `up` starts it again.
|
|
82
|
+
async function reportFailedService(evidence, service, context, adapters, stop) {
|
|
83
|
+
const logs = await collectFailureLogs(evidence, service, adapters);
|
|
84
|
+
const stopped = stop ? (await adapters.execute(stop)).kind === 'complete' : undefined;
|
|
85
|
+
const next = `fix the error in the log excerpt, then run \`${lifecycleSuggestion(context, 'up')}\` to resume, or \`${lifecycleSuggestion(context, 'down')}\` to remove the stack`;
|
|
86
|
+
const fix = stopped ? `cockpit stopped the failed container and kept it with its logs; ${next}` : next;
|
|
87
|
+
return [
|
|
88
|
+
...logs.map((diagnostic) => diagnostic.code === ENVIRONMENT_CODES.EXECUTION_STEP_FAILED ? { ...diagnostic, fix } : diagnostic),
|
|
89
|
+
...(stopped === false ? [ownerDiagnostic(ENVIRONMENT_CODES.EXECUTION_COMPENSATION_STEP_FAILED, `the failed service ${service} could not be stopped, so it may keep restarting; run \`${lifecycleSuggestion(context, 'down')}\` to remove it`, service)] : []),
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
async function collectFailureLogs(evidence, service, adapters) {
|
|
93
|
+
if (!evidence)
|
|
94
|
+
return [ownerDiagnostic(ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, 'bounded failure logs are unavailable', service)];
|
|
95
|
+
const plan = planFailureLogs({ evidence, tail: 200, timeoutMs: 10_000, outputLimitBytes: 1_048_576 });
|
|
96
|
+
if (plan.kind !== 'planned')
|
|
97
|
+
return [ownerDiagnostic(ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, plan.reason, service)];
|
|
98
|
+
const outcome = await adapters.execute(plan.step);
|
|
99
|
+
const admitted = admitFailureLogs({ plan, outcome });
|
|
100
|
+
if (admitted.kind === 'complete') {
|
|
101
|
+
// One diagnostic per log line buried the cause under dozens of near-identical errors.
|
|
102
|
+
const { records } = admitted;
|
|
103
|
+
if (records.length === 0)
|
|
104
|
+
return [ownerDiagnostic(ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, 'bounded failure logs contained no records', service)];
|
|
105
|
+
return [ownerDiagnostic(ENVIRONMENT_CODES.EXECUTION_STEP_FAILED, failureLogExcerpt(service, records), service)];
|
|
106
|
+
}
|
|
107
|
+
return [ownerDiagnostic('code' in admitted ? admitted.code : ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, 'code' in admitted ? 'failure log collection failed' : 'failure log output could not be admitted', 'service' in admitted ? admitted.service : service)];
|
|
108
|
+
}
|
|
109
|
+
function progressValueForPlan(value) {
|
|
110
|
+
return {
|
|
111
|
+
workBasisSha256: value.workBasisSha256,
|
|
112
|
+
operationGeneration: value.operationGeneration,
|
|
113
|
+
completedServices: value.completedServices,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function matchesPortAcquisition(context, leaseResult, projection) {
|
|
117
|
+
try {
|
|
118
|
+
return leaseResult.kind === 'complete'
|
|
119
|
+
&& leaseResult.lease.stackId === context.stackId
|
|
120
|
+
&& projection.lease.stackId === context.stackId
|
|
121
|
+
&& leaseResult.holderId === context.holderId
|
|
122
|
+
&& projection.ownership.holderId === context.holderId
|
|
123
|
+
&& leaseResult.change === projection.ownership.change
|
|
124
|
+
&& leaseResult.recordRevision === projection.ownership.recordRevision
|
|
125
|
+
&& JSON.stringify(leaseResult.lease) === JSON.stringify(projection.lease);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// A throwing accessor in the lease result is a mismatch, which the caller refuses.
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function orderedRuntimeServices(topology) {
|
|
133
|
+
const order = resolveRuntimeServiceOrder(topology);
|
|
134
|
+
const services = topology.services;
|
|
135
|
+
if (order.length === 0 || order.length !== services.length || new Set(order).size !== order.length)
|
|
136
|
+
return undefined;
|
|
137
|
+
const byId = new Map(services.map((service) => [service.id, service]));
|
|
138
|
+
if (byId.size !== services.length || order.some((serviceId) => !byId.has(serviceId)))
|
|
139
|
+
return undefined;
|
|
140
|
+
return order.map((serviceId) => byId.get(serviceId));
|
|
141
|
+
}
|
|
142
|
+
async function executeUpRound(context, adapters, forceReset, rememberAcquisition, rememberRuntimeStart, initialAcquisition) {
|
|
143
|
+
const operate = async (runtime, stage, ports) => {
|
|
144
|
+
// The floor is measured with the runtime readiness, so a machine below it is refused
|
|
145
|
+
// before any service starts rather than discovered as a slow or failed start.
|
|
146
|
+
const belowFloor = runtime.hostObservations.find((observation) => !observationSatisfiesExpectation(observation));
|
|
147
|
+
if (belowFloor)
|
|
148
|
+
return { kind: 'complete', result: machineFloorRefusal(belowFloor) };
|
|
149
|
+
const current = await readLifecycleRecord(context);
|
|
150
|
+
if (current !== null && 'kind' in current && current.kind !== 'complete') {
|
|
151
|
+
return { kind: 'complete', result: current };
|
|
152
|
+
}
|
|
153
|
+
const stored = current;
|
|
154
|
+
const teardownComplete = stored?.value.teardownComplete === true;
|
|
155
|
+
// The recorded inputs name what the stack was started from, and sleep keeps them, so a
|
|
156
|
+
// stopped stack whose Compose files or runtime declarations changed is refused as a running
|
|
157
|
+
// one is: `up` never recreates a recorded stack in place, and `down` removes it by its
|
|
158
|
+
// ownership labels whatever the configuration says now.
|
|
159
|
+
if (stored && !teardownComplete && stored.value.runtimeInputs !== undefined
|
|
160
|
+
&& !isDeepStrictEqual(stored.value.runtimeInputs, runtime.inputs)) {
|
|
161
|
+
return { kind: 'complete', result: runtimeChangedRefusal(context, changedRuntimeInputs(stored.value.runtimeInputs, runtime.inputs, basename(context.source.path))) };
|
|
162
|
+
}
|
|
163
|
+
if (teardownComplete) {
|
|
164
|
+
const acquisition = initialAcquisition ?? (ports.leaseResult.kind === 'complete' && ports.leaseResult.change === 'acquired'
|
|
165
|
+
? inspectPortLeaseProjection(ports.projection, context.namespace) : undefined);
|
|
166
|
+
const freshSoleHolder = acquisition !== undefined
|
|
167
|
+
&& inspectPortLeaseProjection(acquisition, context.namespace) === acquisition
|
|
168
|
+
&& acquisition.ownership.change === 'acquired'
|
|
169
|
+
&& acquisition.ownership.holderId === context.holderId
|
|
170
|
+
&& acquisition.ownership.holderCount === 1
|
|
171
|
+
&& acquisition.ownership.holderIds[0] === context.holderId
|
|
172
|
+
&& acquisition.ownership.recordRevision === ports.projection.ownership.recordRevision
|
|
173
|
+
&& ports.projection.ownership.holderCount === 1
|
|
174
|
+
&& ports.projection.ownership.holderIds[0] === context.holderId
|
|
175
|
+
&& await revalidatePortLeaseProjection({ projection: acquisition, namespace: context.namespace });
|
|
176
|
+
const sourceCurrent = await withLifecycleConfiguration(context, async () => true);
|
|
177
|
+
const currentProgress = await readLifecycleRecord(context);
|
|
178
|
+
// This checkout already held the torn-down stack before this up claimed anything: a claim an
|
|
179
|
+
// interrupted command left. Rerunning up meets the same claim, so only down releases it.
|
|
180
|
+
const leftClaim = acquisition === undefined && ports.leaseResult.kind === 'complete' && ports.leaseResult.change !== 'acquired'
|
|
181
|
+
&& ports.projection.ownership.holderCount === 1 && ports.projection.ownership.holderIds[0] === context.holderId;
|
|
182
|
+
if (leftClaim) {
|
|
183
|
+
return { kind: 'complete', result: lifecycleRefusal(ENVIRONMENT_CODES.EXECUTION_PRECONDITION_CHANGED, `this checkout still holds a claim on the stack down removed, left by an earlier command, so up cannot start it afresh; run \`${lifecycleSuggestion(context, 'down')}\` to release the claim, then \`${lifecycleSuggestion(context, 'up')}\``, context.stackId, 'run npx --no cockpit down --config <your cockpit.toml> to release the claim, then npx --no cockpit up --config <your cockpit.toml>') };
|
|
184
|
+
}
|
|
185
|
+
if (!freshSoleHolder || !sourceCurrent || !currentProgress || 'status' in currentProgress
|
|
186
|
+
|| !isDeepStrictEqual(currentProgress.record, stored.record)
|
|
187
|
+
|| !await revalidatePortLeaseProjection({ projection: ports.projection, namespace: context.namespace })) {
|
|
188
|
+
return { kind: 'complete', result: lifecycleRefusal(ENVIRONMENT_CODES.EXECUTION_PRECONDITION_CHANGED, `completed teardown requires unchanged configuration, lifecycle progress and a fresh sole-holder claim; rerun \`${lifecycleSuggestion(context, 'up')}\``, context.stackId, 'rerun npx --no cockpit up --config <your cockpit.toml> once nothing else is changing cockpit.toml or bringing the stack up') };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const resetProgress = forceReset || teardownComplete;
|
|
192
|
+
const completedServiceIds = stored ? new Set(stored.value.completedServices.map(({ service }) => service)) : undefined;
|
|
193
|
+
const progressComplete = stored !== null && completedServiceIds?.size === runtime.topology.services.length
|
|
194
|
+
&& runtime.topology.services.every(({ id }) => completedServiceIds.has(id));
|
|
195
|
+
const previousHolderActive = stored !== null
|
|
196
|
+
&& ports.projection.ownership.holderIds.includes(stored.value.holderId);
|
|
197
|
+
const currentHolderActive = ports.projection.ownership.holderIds.includes(context.holderId);
|
|
198
|
+
const completedCoTenant = stored !== null && stored.value.holderId !== context.holderId && progressComplete
|
|
199
|
+
&& previousHolderActive && currentHolderActive;
|
|
200
|
+
if (stored && stored.value.holderId !== context.holderId
|
|
201
|
+
&& (previousHolderActive || !currentHolderActive) && !completedCoTenant) {
|
|
202
|
+
return { kind: 'complete', result: lifecycleRefusal(ENVIRONMENT_CODES.CLAIM_CO_TENANT, `lifecycle progress belongs to another active session; run \`${lifecycleSuggestion(context, 'up')}\` to resume it or \`${lifecycleSuggestion(context, 'down')}\` to release it`, context.stackId) };
|
|
203
|
+
}
|
|
204
|
+
const operationGeneration = stored && !resetProgress
|
|
205
|
+
? stored.value.operationGeneration
|
|
206
|
+
: randomBytes(32).toString('hex');
|
|
207
|
+
const revalidateCompletedServices = async () => {
|
|
208
|
+
const evidence = [];
|
|
209
|
+
if (!stored || resetProgress)
|
|
210
|
+
return { kind: 'complete', evidence };
|
|
211
|
+
for (const completed of stored.value.completedServices) {
|
|
212
|
+
const admitted = await revalidateService(runtime.topology, completed, adapters);
|
|
213
|
+
if (admitted.kind === 'stale')
|
|
214
|
+
return { kind: 'retry' };
|
|
215
|
+
if (admitted.kind !== 'complete') {
|
|
216
|
+
const logDiagnostics = await reportFailedService(admitted.logs, completed.service, context, adapters);
|
|
217
|
+
const primary = lifecycleRefusal(ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, `completed runtime service could not be revalidated: its recorded container is no longer running and healthy, for example after it was killed — fix: run \`${cockpitCommand('sleep', { config: context.source.path })}\`, then \`${lifecycleSuggestion(context, 'up')}\` to start the stack again`, completed.service);
|
|
218
|
+
return { kind: 'refusal', result: {
|
|
219
|
+
...primary,
|
|
220
|
+
diagnostics: uniqueDiagnostics([...primary.diagnostics, ...logDiagnostics]),
|
|
221
|
+
} };
|
|
222
|
+
}
|
|
223
|
+
evidence.push(admitted.evidence);
|
|
224
|
+
}
|
|
225
|
+
return { kind: 'complete', evidence };
|
|
226
|
+
};
|
|
227
|
+
if (completedCoTenant) {
|
|
228
|
+
const revalidated = await revalidateCompletedServices();
|
|
229
|
+
if (revalidated.kind === 'retry')
|
|
230
|
+
return { kind: 'complete', result: lifecycleRefusal(ENVIRONMENT_CODES.CLAIM_CO_TENANT, 'completed lifecycle progress belongs to an active holder but its runtime evidence is stale', context.stackId) };
|
|
231
|
+
if (revalidated.kind === 'refusal')
|
|
232
|
+
return { kind: 'complete', result: revalidated.result };
|
|
233
|
+
return { kind: 'complete', result: {
|
|
234
|
+
kind: 'complete', status: 0, diagnostics: [],
|
|
235
|
+
result: { stackId: context.stackId, change: 'unchanged', ports: ports.projection.lease.ports, entrypoints: ports.projection.entrypoints },
|
|
236
|
+
} };
|
|
237
|
+
}
|
|
238
|
+
const admission = admitUpProgress({
|
|
239
|
+
lifecycleStage: stage,
|
|
240
|
+
runtime: runtime.lifecycle,
|
|
241
|
+
ports: ports.projection,
|
|
242
|
+
progress: stored && !resetProgress ? progressValueForPlan(stored.value) : null,
|
|
243
|
+
operationGeneration,
|
|
244
|
+
});
|
|
245
|
+
if (admission.kind === 'refusal') {
|
|
246
|
+
// A declaration edit shows only as changed work. Restarting it from scratch recreated
|
|
247
|
+
// healthy containers under the new declaration, and stopped them when their labels no
|
|
248
|
+
// longer matched; a fully started stack is refused instead. `up` never recreates a
|
|
249
|
+
// running stack in place; `down` removes it by its ownership labels whatever the
|
|
250
|
+
// configuration says now.
|
|
251
|
+
if (stored && !resetProgress && progressComplete) {
|
|
252
|
+
return { kind: 'complete', result: runtimeChangedRefusal(context, changedRuntimeInputs(stored.value.runtimeInputs, runtime.inputs, basename(context.source.path))) };
|
|
253
|
+
}
|
|
254
|
+
return stored && !resetProgress
|
|
255
|
+
? { kind: 'retry', forceReset: true }
|
|
256
|
+
: { kind: 'complete', result: forwardedRefusal(admission.diagnostics) };
|
|
257
|
+
}
|
|
258
|
+
const revalidated = await revalidateCompletedServices();
|
|
259
|
+
if (revalidated.kind === 'retry')
|
|
260
|
+
return { kind: 'retry', forceReset: true };
|
|
261
|
+
if (revalidated.kind === 'refusal')
|
|
262
|
+
return { kind: 'complete', result: revalidated.result };
|
|
263
|
+
let completedEvidence = [...revalidated.evidence];
|
|
264
|
+
const planned = planAdmittedUp({ authority: admission.authority, completedServices: completedEvidence });
|
|
265
|
+
if (planned.kind === 'refusal') {
|
|
266
|
+
return stored && !resetProgress
|
|
267
|
+
? { kind: 'retry', forceReset: true }
|
|
268
|
+
: { kind: 'complete', result: lifecycleRefusal(ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, 'current lifecycle work could not be planned', context.stackId) };
|
|
269
|
+
}
|
|
270
|
+
const issuance = consumeLifecycleIssuance(planned.plan);
|
|
271
|
+
if (!issuance || issuance.drainRequest !== null || issuance.reclaim !== null) {
|
|
272
|
+
return { kind: 'complete', result: lifecycleRefusal(ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, 'lifecycle plan authority is unavailable', context.stackId) };
|
|
273
|
+
}
|
|
274
|
+
const orderedServices = orderedRuntimeServices(issuance.topology);
|
|
275
|
+
if (!orderedServices || issuance.firstIncompleteServiceIndex < 0
|
|
276
|
+
|| issuance.firstIncompleteServiceIndex > orderedServices.length) {
|
|
277
|
+
return { kind: 'complete', result: lifecycleRefusal(ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, 'runtime service dependency order is unavailable', context.stackId) };
|
|
278
|
+
}
|
|
279
|
+
let published = stored;
|
|
280
|
+
const startedEvidence = [];
|
|
281
|
+
const plannedValue = {
|
|
282
|
+
holderId: context.holderId,
|
|
283
|
+
...planned.plan.progress,
|
|
284
|
+
runtimeInputs: runtime.inputs,
|
|
285
|
+
};
|
|
286
|
+
if (!stored || resetProgress || stored.value.holderId !== context.holderId && !completedCoTenant) {
|
|
287
|
+
// The first lifecycle data starts its durable history, as `setup` does (an existing
|
|
288
|
+
// `started` is unchanged), so lifecycle data without history keeps meaning that history
|
|
289
|
+
// was lost. Publishing data alone let a first `up` pass and every resume refuse.
|
|
290
|
+
if (!stored) {
|
|
291
|
+
const started = await recordLifecycleStarted({ namespace: context.namespace, stackId: context.stackId,
|
|
292
|
+
holderId: context.holderId, observedAt: new Date().toISOString() });
|
|
293
|
+
if (started.kind === 'refusal')
|
|
294
|
+
return { kind: 'complete', result: forwardedRefusal(started.diagnostics) };
|
|
295
|
+
}
|
|
296
|
+
const publication = await publishLifecycleRecord(context, stored?.record ?? null, plannedValue, stored && stored.value.holderId !== context.holderId
|
|
297
|
+
? { previousHolderId: stored.value.holderId, projection: ports.projection }
|
|
298
|
+
: undefined, teardownComplete ? ports.projection : undefined);
|
|
299
|
+
if ('status' in publication)
|
|
300
|
+
return { kind: 'complete', result: publication };
|
|
301
|
+
if (publication.kind === 'indeterminate')
|
|
302
|
+
return { kind: 'complete', result: lifecycleRefusal(ENVIRONMENT_CODES.STATE_DURABILITY_INDETERMINATE, publication.message, publication.target) };
|
|
303
|
+
if (publication.kind !== 'complete')
|
|
304
|
+
return { kind: 'complete', result: lifecycleRefusal(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'lifecycle progress publication returned no record', context.stackId) };
|
|
305
|
+
published = decodeLifecycleRecord(publication.value, context) ?? null;
|
|
306
|
+
if (!published)
|
|
307
|
+
return { kind: 'complete', result: lifecycleRefusal(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'published lifecycle progress is malformed', context.stackId) };
|
|
308
|
+
}
|
|
309
|
+
if (planned.kind === 'noop') {
|
|
310
|
+
return { kind: 'complete', result: {
|
|
311
|
+
kind: 'complete', status: 0, diagnostics: planned.plan.diagnostics,
|
|
312
|
+
result: { stackId: context.stackId, change: 'unchanged', ports: ports.projection.lease.ports, entrypoints: ports.projection.entrypoints },
|
|
313
|
+
} };
|
|
314
|
+
}
|
|
315
|
+
for (let index = issuance.firstIncompleteServiceIndex; index < orderedServices.length; index += 1) {
|
|
316
|
+
const service = orderedServices[index];
|
|
317
|
+
ports.retainOnFailure();
|
|
318
|
+
rememberRuntimeStart();
|
|
319
|
+
const admitted = await startService(issuance.topology, service.id, adapters);
|
|
320
|
+
if (admitted.kind !== 'complete') {
|
|
321
|
+
const logDiagnostics = await reportFailedService(admitted.kind === 'failure' ? admitted.logs : undefined, service.id, context, adapters, admitted.kind === 'failure' ? admitted.stop : undefined);
|
|
322
|
+
const primary = lifecycleRefusal(ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, 'runtime service did not become healthy', service.id);
|
|
323
|
+
return { kind: 'complete', result: {
|
|
324
|
+
...primary,
|
|
325
|
+
diagnostics: uniqueDiagnostics([...primary.diagnostics, ...logDiagnostics]),
|
|
326
|
+
} };
|
|
327
|
+
}
|
|
328
|
+
completedEvidence.push(admitted.evidence);
|
|
329
|
+
startedEvidence.push(admitted.evidence);
|
|
330
|
+
const nextValue = {
|
|
331
|
+
...plannedValue,
|
|
332
|
+
completedServices: [
|
|
333
|
+
...published.value.completedServices,
|
|
334
|
+
{ service: admitted.evidence.service, containerId: admitted.evidence.containerId },
|
|
335
|
+
],
|
|
336
|
+
};
|
|
337
|
+
const publication = await publishLifecycleRecord(context, published.record, nextValue);
|
|
338
|
+
if ('status' in publication)
|
|
339
|
+
return { kind: 'complete', result: publication };
|
|
340
|
+
if (publication.kind === 'indeterminate')
|
|
341
|
+
return { kind: 'complete', result: lifecycleRefusal(ENVIRONMENT_CODES.STATE_DURABILITY_INDETERMINATE, publication.message, publication.target) };
|
|
342
|
+
if (publication.kind !== 'complete')
|
|
343
|
+
return { kind: 'complete', result: lifecycleRefusal(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'lifecycle progress publication returned no record', context.stackId) };
|
|
344
|
+
published = decodeLifecycleRecord(publication.value, context) ?? null;
|
|
345
|
+
if (!published)
|
|
346
|
+
return { kind: 'complete', result: lifecycleRefusal(ENVIRONMENT_CODES.STATE_RECORD_MALFORMED, 'published lifecycle progress is malformed', context.stackId) };
|
|
347
|
+
}
|
|
348
|
+
const coldStartObservation = observeRuntimeServiceColdStart({ services: startedEvidence });
|
|
349
|
+
if (coldStartObservation.observed.state !== 'available'
|
|
350
|
+
|| typeof coldStartObservation.observed.value !== 'number'
|
|
351
|
+
|| coldStartObservation.expectation.kind !== 'maximum') {
|
|
352
|
+
return { kind: 'complete', result: lifecycleRefusal(ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, 'runtime cold-start evidence is unavailable', context.stackId) };
|
|
353
|
+
}
|
|
354
|
+
if (!observationSatisfiesExpectation(coldStartObservation)) {
|
|
355
|
+
return { kind: 'complete', result: { kind: 'refusal', status: 2, result: null, diagnostics: [{
|
|
356
|
+
severity: 'error',
|
|
357
|
+
code: 'MACHINE_COLD_START_EXCEEDED',
|
|
358
|
+
message: `the runtime took ${seconds(coldStartObservation.observed.value)} s to start its services, over the `
|
|
359
|
+
+ `${seconds(coldStartObservation.expectation.value)} s that environment.machine.cold_start_seconds allows`,
|
|
360
|
+
target: coldStartObservation.source,
|
|
361
|
+
fix: 'make the services start faster, or raise environment.machine.cold_start_seconds in cockpit.toml above the measured time and run npx --no cockpit sync --config <your cockpit.toml>; then run npx --no cockpit down --config <your cockpit.toml> and npx --no cockpit up --config <your cockpit.toml>',
|
|
362
|
+
}] } };
|
|
363
|
+
}
|
|
364
|
+
const coldStart = {
|
|
365
|
+
measuredMillis: coldStartObservation.observed.value,
|
|
366
|
+
limitMillis: coldStartObservation.expectation.value,
|
|
367
|
+
headroomMillis: coldStartObservation.expectation.value - coldStartObservation.observed.value,
|
|
368
|
+
};
|
|
369
|
+
return { kind: 'complete', result: {
|
|
370
|
+
kind: 'complete', status: 0, diagnostics: planned.plan.diagnostics,
|
|
371
|
+
result: {
|
|
372
|
+
stackId: context.stackId,
|
|
373
|
+
change: planned.mode === 'start' ? 'started' : 'resumed',
|
|
374
|
+
ports: ports.projection.lease.ports,
|
|
375
|
+
entrypoints: ports.projection.entrypoints,
|
|
376
|
+
coldStart,
|
|
377
|
+
},
|
|
378
|
+
} };
|
|
379
|
+
};
|
|
380
|
+
const unavailable = () => ({ kind: 'complete', result: lifecycleRefusal(ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, 'lifecycle, port or runtime readiness authority is unavailable', context.stackId) });
|
|
381
|
+
const refused = (diagnostics) => ({
|
|
382
|
+
kind: 'complete',
|
|
383
|
+
result: forwardedRefusal(diagnostics.map((diagnostic) => diagnostic.code === ENVIRONMENT_CODES.CLAIM_CO_TENANT
|
|
384
|
+
? ownerDiagnostic(diagnostic.code, `lifecycle progress belongs to another active session; run \`${lifecycleSuggestion(context, 'up')}\` to resume it or \`${lifecycleSuggestion(context, 'down')}\` to release it`, diagnostic.target ?? context.stackId)
|
|
385
|
+
: diagnostic)),
|
|
386
|
+
});
|
|
387
|
+
return await adapters.withLifecycleStage(context, async (stage) => await adapters.withAcquiredPorts(context, async (ports) => {
|
|
388
|
+
if (!matchesPortAcquisition(context, ports.leaseResult, ports.projection)) {
|
|
389
|
+
return { kind: 'complete', result: lifecycleRefusal(ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, 'port acquisition does not match the current lifecycle authority', context.stackId) };
|
|
390
|
+
}
|
|
391
|
+
const outcome = await adapters.withRuntimeReadiness(context, (runtime) => operate(runtime, stage, ports), ports.projection, refused) ?? unavailable();
|
|
392
|
+
rememberAcquisition(ports.leaseResult, ports.projection);
|
|
393
|
+
return outcome;
|
|
394
|
+
}, refused) ?? unavailable(), refused) ?? unavailable();
|
|
395
|
+
}
|
|
396
|
+
export async function runUp(input, adapters) {
|
|
397
|
+
const configPath = exactCommandConfigPath(input);
|
|
398
|
+
if (!configPath)
|
|
399
|
+
return lifecycleContextRefusal();
|
|
400
|
+
const loaded = await adapters.load({ configPath });
|
|
401
|
+
if (loaded.kind === 'refusal')
|
|
402
|
+
return loaded;
|
|
403
|
+
if (loaded.kind === 'preview')
|
|
404
|
+
return lifecycleRefusal(ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, 'lifecycle run context was not fully resolved', input.configPath);
|
|
405
|
+
if (loaded.result.configPath !== configPath || !issuedLifecycleRuns.delete(loaded.result)) {
|
|
406
|
+
return lifecycleRefusal(ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, 'lifecycle run authority is unavailable', configPath);
|
|
407
|
+
}
|
|
408
|
+
const context = loaded.result;
|
|
409
|
+
return withLifecycleRunNotices(context, await upWithContext(context, adapters));
|
|
410
|
+
}
|
|
411
|
+
async function upWithContext(context, adapters) {
|
|
412
|
+
let acquired;
|
|
413
|
+
let initialAcquisition;
|
|
414
|
+
const rememberAcquisition = (lease, projection) => {
|
|
415
|
+
if (!acquired && lease.kind === 'complete' && lease.change === 'acquired') {
|
|
416
|
+
acquired = lease;
|
|
417
|
+
initialAcquisition = inspectPortLeaseProjection(projection, context.namespace);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
// The holder claim and its lease are what own the stack's runtime: `down` and `reset` act only
|
|
421
|
+
// for a holder, and the next `up` resumes from the holder's durable progress. Once this `up`
|
|
422
|
+
// has asked Compose to start a service, containers may exist on the leased ports, so a refusal
|
|
423
|
+
// keeps the claim rather than compensating it. Releasing it orphaned those containers: the
|
|
424
|
+
// next `up` leased the same family and collided with its own ports, and `down` had no holder.
|
|
425
|
+
let runtimeStarted = false;
|
|
426
|
+
const rememberRuntimeStart = () => { runtimeStarted = true; };
|
|
427
|
+
const releasable = () => acquired !== undefined && !runtimeStarted;
|
|
428
|
+
const declaration = await inspectRecordedPortDeclaration({ namespace: context.namespace, stackId: context.stackId, config: context.ports });
|
|
429
|
+
if (declaration.kind === 'refusal')
|
|
430
|
+
return forwardedRefusal(declaration.diagnostics);
|
|
431
|
+
if (declaration.changed)
|
|
432
|
+
return runtimeChangedRefusal(context, `[ports] in ${basename(context.source.path)} changed since it started`);
|
|
433
|
+
// Starting under the new name beside a stack recorded under the old one would run two stacks for
|
|
434
|
+
// one checkout, the old one beyond the reach of down.
|
|
435
|
+
const earlier = await findStackUnderEarlierName({ namespace: context.namespace, cockpitId: context.cockpitId, checkoutId: context.checkoutId });
|
|
436
|
+
if (earlier.kind === 'refusal')
|
|
437
|
+
return lifecycleRefusal(earlier.code, earlier.message, earlier.target);
|
|
438
|
+
if (earlier.name !== undefined)
|
|
439
|
+
return lifecycleRefusal(ENVIRONMENT_CODES.STACK_RECORDED_UNDER_EARLIER_NAME, earlierNameRefusalMessage(context.source.path, earlier.name), context.stackId);
|
|
440
|
+
let forceReset = false;
|
|
441
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
442
|
+
const round = await executeUpRound(context, adapters, forceReset, rememberAcquisition, rememberRuntimeStart, initialAcquisition);
|
|
443
|
+
if (round.kind === 'retry') {
|
|
444
|
+
forceReset = round.forceReset;
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
let result = round.result;
|
|
448
|
+
if (result.kind === 'complete') {
|
|
449
|
+
const seeded = await recordLifecycleSeeded({ namespace: context.namespace, stackId: context.stackId,
|
|
450
|
+
holderId: context.holderId, observedAt: new Date().toISOString() });
|
|
451
|
+
if (seeded.kind === 'refusal')
|
|
452
|
+
return forwardedRefusal(seeded.diagnostics);
|
|
453
|
+
// The stack stays up when its seed fails: the containers are healthy, and the next up
|
|
454
|
+
// runs the seed again because no success was recorded.
|
|
455
|
+
const data = await adapters.seed?.(context, result.result);
|
|
456
|
+
if (data?.kind === 'refusal')
|
|
457
|
+
return { kind: 'refusal', status: 2, result: null,
|
|
458
|
+
diagnostics: uniqueDiagnostics([...result.diagnostics, ...data.diagnostics]) };
|
|
459
|
+
if (data?.kind === 'ran' || data?.kind === 'current')
|
|
460
|
+
result = { ...result, result: { ...result.result, seed: data.kind } };
|
|
461
|
+
}
|
|
462
|
+
if (result.kind === 'refusal' && releasable()) {
|
|
463
|
+
const compensation = await compensateAcquiredPortHolder({
|
|
464
|
+
namespace: context.namespace,
|
|
465
|
+
leaseResult: acquired,
|
|
466
|
+
now: new Date().toISOString(),
|
|
467
|
+
});
|
|
468
|
+
if (compensation.kind === 'refusal')
|
|
469
|
+
return {
|
|
470
|
+
...result,
|
|
471
|
+
diagnostics: uniqueDiagnostics([...result.diagnostics, ...compensation.diagnostics]),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
return result;
|
|
475
|
+
}
|
|
476
|
+
if (releasable()) {
|
|
477
|
+
const compensation = await compensateAcquiredPortHolder({
|
|
478
|
+
namespace: context.namespace,
|
|
479
|
+
leaseResult: acquired,
|
|
480
|
+
now: new Date().toISOString(),
|
|
481
|
+
});
|
|
482
|
+
if (compensation.kind === 'refusal') {
|
|
483
|
+
const primary = lifecycleRefusal(ENVIRONMENT_CODES.LIFECYCLE_PROGRESS_INVALID, 'lifecycle progress could not be reset from fresh evidence', context.stackId);
|
|
484
|
+
return {
|
|
485
|
+
...primary,
|
|
486
|
+
diagnostics: uniqueDiagnostics([...primary.diagnostics, ...compensation.diagnostics]),
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return lifecycleRefusal(ENVIRONMENT_CODES.LIFECYCLE_PROGRESS_INVALID, 'lifecycle progress could not be reset from fresh evidence', context.stackId);
|
|
491
|
+
}
|
|
492
|
+
// The wrapped agent reaches the stack the way its Compose files do: each `[ports.bases]` name holds
|
|
493
|
+
// the leased port, and each entrypoint `up --json` reports is under COCKPIT_ENTRYPOINT_<NAME>.
|
|
494
|
+
// Without them a command such as `curl "http://127.0.0.1:$API_HOST_PORT/"` falls back to port 80.
|
|
495
|
+
// The lease is the truth for this stack, so it replaces a value the caller's shell already set.
|
|
496
|
+
function sessionChildEnvironment(env, up) {
|
|
497
|
+
return {
|
|
498
|
+
...env,
|
|
499
|
+
...Object.fromEntries(Object.entries(up.ports).map(([name, port]) => [name, String(port)])),
|
|
500
|
+
...Object.fromEntries(up.entrypoints.map(({ name, value }) => [`COCKPIT_ENTRYPOINT_${name.toUpperCase().replaceAll('-', '_')}`, value])),
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
// A session over a stack an earlier up still holds ends with the containers running, which read as a
|
|
504
|
+
// failed release when nothing said why. Literals, so the diagnostic inventory captures the fix.
|
|
505
|
+
function sessionStackRetained(stackId) {
|
|
506
|
+
return {
|
|
507
|
+
severity: 'info',
|
|
508
|
+
code: 'SESSION_STACK_RETAINED',
|
|
509
|
+
message: 'the session released its hold, and the stack keeps running because another holder, such as an earlier up from this checkout, still holds it',
|
|
510
|
+
target: stackId,
|
|
511
|
+
fix: 'run npx --no cockpit sleep --config <your cockpit.toml> to release this checkout\'s remaining hold',
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
export async function runSession(input, adapters) {
|
|
515
|
+
if (adapters.signal?.aborted)
|
|
516
|
+
return {
|
|
517
|
+
kind: 'refusal', status: 130, result: null,
|
|
518
|
+
diagnostics: [{ severity: 'error', code: ENVIRONMENT_CODES.EXECUTION_ABORTED,
|
|
519
|
+
message: 'the supervised session was cancelled before environment bring-up', target: input.argv[0] }],
|
|
520
|
+
};
|
|
521
|
+
// A relative command path such as `./node_modules/.bin/tool` names a file under the directory the
|
|
522
|
+
// session runs in, so it resolves there; one that climbs out of it is refused rather than guessed.
|
|
523
|
+
const argv0 = input.argv[0];
|
|
524
|
+
const relativePath = !isAbsolute(argv0) && argv0.includes('/');
|
|
525
|
+
const commandPath = relativePath ? resolve(input.cwd, argv0) : argv0;
|
|
526
|
+
if (relativePath && !commandPath.startsWith(`${input.cwd}${sep}`))
|
|
527
|
+
return lifecycleRefusal(ENVIRONMENT_CODES.EXECUTION_PRECONDITION_CHANGED, `the command path resolves to ${commandPath}, outside the session working directory ${input.cwd}; give an absolute path or a PATH entry name`, argv0, 'name the command after -- by an absolute path, a path inside the session working directory, or a name found on PATH');
|
|
528
|
+
const executable = await adapters.resolveExecutable(commandPath, input.env);
|
|
529
|
+
if (executable.state === 'unavailable')
|
|
530
|
+
return lifecycleRefusal(ENVIRONMENT_CODES.EXECUTION_PRECONDITION_CHANGED, executable.reason, input.argv[0], 'name the command after -- by an absolute path, a path inside the session working directory, or a name found on PATH');
|
|
531
|
+
const up = await adapters.up({ configPath: input.configPath });
|
|
532
|
+
if (up.kind === 'refusal')
|
|
533
|
+
return up;
|
|
534
|
+
if (up.kind !== 'complete')
|
|
535
|
+
return lifecycleRefusal(ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, 'session bring-up did not reach a terminal result', input.configPath);
|
|
536
|
+
const child = await adapters.executeForeground({
|
|
537
|
+
executable: executable.value,
|
|
538
|
+
argv: input.argv.slice(1),
|
|
539
|
+
cwd: input.cwd,
|
|
540
|
+
env: sessionChildEnvironment(input.env, up.result),
|
|
541
|
+
}, { ...(adapters.signal ? { signal: adapters.signal } : {}) });
|
|
542
|
+
if (!child.terminationConfirmed)
|
|
543
|
+
return lifecycleRefusal(ENVIRONMENT_CODES.EXECUTION_TERMINATION_UNCONFIRMED, 'the owned session process group could not be confirmed stopped; lifecycle ownership was preserved', input.argv[0]);
|
|
544
|
+
const sleep = await adapters.sleep({ configPath: input.configPath });
|
|
545
|
+
// The sleep change names what happened to the stack, not to the session's hold, which every
|
|
546
|
+
// completed cleanup released; "cleanup retained" read as the hold having been kept.
|
|
547
|
+
const cleanup = sleep.kind !== 'complete' ? ''
|
|
548
|
+
: sleep.result.change === 'stopped' ? '; its hold was released and the stack stopped'
|
|
549
|
+
: sleep.result.change === 'retained' ? '; its hold was released and another holder keeps the stack running'
|
|
550
|
+
: '; its hold was released';
|
|
551
|
+
// A command that exits nonzero is a normal end of the session, not a cockpit failure: a wrapper
|
|
552
|
+
// that exits 2 for it hides the command's own status from the scripts and agents that branch on
|
|
553
|
+
// it. So a completed cleanup passes that status through, with the shell convention of 128 plus
|
|
554
|
+
// the signal number for a signalled command, and only a failed cleanup exits 2.
|
|
555
|
+
const signalNumber = child.kind === 'complete' && child.signal ? osConstants.signals[child.signal] : undefined;
|
|
556
|
+
const childStatus = child.kind !== 'complete' ? 2
|
|
557
|
+
: signalNumber !== undefined ? 128 + signalNumber : child.exitCode;
|
|
558
|
+
const childDiagnostic = child.kind === 'refusal'
|
|
559
|
+
? { severity: 'error', code: child.code,
|
|
560
|
+
message: `the supervised session process failed${cleanup}`, target: input.argv[0] }
|
|
561
|
+
: childStatus === 0 ? undefined : {
|
|
562
|
+
severity: 'error', code: ENVIRONMENT_CODES.EXECUTION_STEP_FAILED,
|
|
563
|
+
message: `the supervised session process ${child.signal ? `was terminated by ${child.signal}` : `exited with status ${child.exitCode}`}${cleanup}`,
|
|
564
|
+
target: input.argv[0],
|
|
565
|
+
};
|
|
566
|
+
if (sleep.kind !== 'complete' || child.kind === 'refusal')
|
|
567
|
+
return {
|
|
568
|
+
kind: 'refusal', status: sleep.kind === 'complete' && child.kind === 'refusal'
|
|
569
|
+
&& child.code === ENVIRONMENT_CODES.EXECUTION_ABORTED ? 130 : 2, result: null,
|
|
570
|
+
diagnostics: [
|
|
571
|
+
...(childDiagnostic ? [childDiagnostic] : []),
|
|
572
|
+
...sleep.diagnostics,
|
|
573
|
+
],
|
|
574
|
+
};
|
|
575
|
+
const retained = sleep.result.change === 'retained' ? [sessionStackRetained(sleep.result.stackId)] : [];
|
|
576
|
+
return {
|
|
577
|
+
kind: 'complete',
|
|
578
|
+
status: childStatus,
|
|
579
|
+
diagnostics: uniqueDiagnostics([...up.diagnostics, ...sleep.diagnostics, ...retained]),
|
|
580
|
+
result: {
|
|
581
|
+
child: { exitCode: child.exitCode, signal: child.signal },
|
|
582
|
+
sleep: sleep.result,
|
|
583
|
+
},
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
export async function runSleep(input, adapters) {
|
|
587
|
+
const configPath = exactCommandConfigPath(input);
|
|
588
|
+
if (!configPath)
|
|
589
|
+
return lifecycleContextRefusal();
|
|
590
|
+
const loaded = await adapters.load({ configPath });
|
|
591
|
+
if (loaded.kind === 'refusal')
|
|
592
|
+
return loaded;
|
|
593
|
+
if (loaded.kind === 'preview')
|
|
594
|
+
return lifecycleRefusal(ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, 'lifecycle run context was not fully resolved', input.configPath);
|
|
595
|
+
if (loaded.result.configPath !== configPath || !issuedLifecycleRuns.delete(loaded.result)) {
|
|
596
|
+
return lifecycleRefusal(ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, 'lifecycle run authority is unavailable', configPath);
|
|
597
|
+
}
|
|
598
|
+
const context = loaded.result;
|
|
599
|
+
return withLifecycleRunNotices(context, await sleepWithContext(context, adapters));
|
|
600
|
+
}
|
|
601
|
+
async function sleepWithContext(context, adapters) {
|
|
602
|
+
let recovery = await inspectSleepDrainRecovery({
|
|
603
|
+
namespace: context.namespace,
|
|
604
|
+
stackId: context.stackId,
|
|
605
|
+
holderId: context.holderId,
|
|
606
|
+
});
|
|
607
|
+
if (recovery.kind === 'repairable') {
|
|
608
|
+
const terminated = await repairFencedExecution(recovery.fence);
|
|
609
|
+
if (terminated.kind !== 'complete')
|
|
610
|
+
return repairRequired(context);
|
|
611
|
+
const repaired = await settleInterruptedDrainRepair({ namespace: context.namespace, record: recovery.record, evidence: terminated.evidence });
|
|
612
|
+
if (repaired.kind !== 'complete')
|
|
613
|
+
return repairRequired(context);
|
|
614
|
+
if (!await cleanupFencedExecution(recovery.fence))
|
|
615
|
+
return lifecycleRefusal(ENVIRONMENT_CODES.STATE_DURABILITY_INDETERMINATE, 'the interrupted sleep executor stopped but its durable execution fence could not be removed', recovery.fence.markerPath);
|
|
616
|
+
recovery = await inspectSleepDrainRecovery({ namespace: context.namespace, stackId: context.stackId, holderId: context.holderId });
|
|
617
|
+
}
|
|
618
|
+
if (recovery.kind === 'refusal')
|
|
619
|
+
return {
|
|
620
|
+
kind: 'refusal', status: 2, result: null,
|
|
621
|
+
diagnostics: recovery.diagnostics.map((diagnostic) => diagnostic.code === ENVIRONMENT_CODES.DRAIN_ATTEMPT_REPAIR_REQUIRED
|
|
622
|
+
? ownerDiagnostic(diagnostic.code, 'cockpit cannot prove the interrupted sleep executor stopped, so it will not overlap it with another destructive action; preserve the coordination state for explicit repair', diagnostic.target ?? context.stackId)
|
|
623
|
+
: diagnostic),
|
|
624
|
+
};
|
|
625
|
+
if (recovery.kind !== 'recoverable') {
|
|
626
|
+
const reaped = await releaseDeadSessionHolders({
|
|
627
|
+
namespace: context.namespace, stackId: context.stackId,
|
|
628
|
+
successor: { holderId: context.holderId, checkoutId: context.checkoutId },
|
|
629
|
+
now: new Date().toISOString(), observe: (pid) => observeProcessStart(pid, processHost(adapters.execute)),
|
|
630
|
+
});
|
|
631
|
+
if (reaped.kind === 'refusal')
|
|
632
|
+
return forwardedRefusal(reaped.diagnostics);
|
|
633
|
+
}
|
|
634
|
+
const unheld = recovery.kind === 'recoverable' ? undefined : await unheldStack(context);
|
|
635
|
+
if (unheld)
|
|
636
|
+
return unheld;
|
|
637
|
+
const retained = await observeRetainedPortFamily({
|
|
638
|
+
namespace: context.namespace,
|
|
639
|
+
stackId: context.stackId,
|
|
640
|
+
holderId: context.holderId,
|
|
641
|
+
checkoutId: context.checkoutId,
|
|
642
|
+
config: 'recorded',
|
|
643
|
+
});
|
|
644
|
+
if (retained.kind !== 'complete')
|
|
645
|
+
return lifecycleRefusal(retained.diagnostics[0]?.code ?? ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, retained.diagnostics[0]?.message ?? 'retained port authority is unavailable', retained.diagnostics[0]?.target ?? context.stackId);
|
|
646
|
+
const entrypoints = deriveEntrypoints({
|
|
647
|
+
lease: retained.lease,
|
|
648
|
+
declarations: context.entrypointDerivatives,
|
|
649
|
+
});
|
|
650
|
+
if (entrypoints.kind !== 'complete')
|
|
651
|
+
return lifecycleRefusal(entrypoints.diagnostics[0]?.code ?? ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, entrypoints.diagnostics[0]?.message ?? 'entrypoint authority is unavailable', entrypoints.diagnostics[0]?.target ?? context.stackId);
|
|
652
|
+
const ports = await projectRetainedPortFamilyResult({ retainedResult: retained, entrypointResult: entrypoints });
|
|
653
|
+
if (!ports)
|
|
654
|
+
return lifecycleRefusal(ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, 'retained port projection is unavailable', context.stackId);
|
|
655
|
+
const result = await adapters.withRuntimeReadiness(context, async (runtime) => {
|
|
656
|
+
const generation = recovery.kind === 'recoverable'
|
|
657
|
+
? recovery.generation : randomBytes(32).toString('hex');
|
|
658
|
+
const planned = planSleep({
|
|
659
|
+
runtime: runtime.lifecycle,
|
|
660
|
+
ports,
|
|
661
|
+
operationGeneration: generation,
|
|
662
|
+
runtimeStopValidForMs: recovery.kind === 'recoverable'
|
|
663
|
+
? recovery.runtimeStopValidForMs : 60_000,
|
|
664
|
+
});
|
|
665
|
+
if (planned.kind === 'refusal')
|
|
666
|
+
return {
|
|
667
|
+
kind: 'refusal', status: 2, result: null,
|
|
668
|
+
diagnostics: planned.diagnostics,
|
|
669
|
+
};
|
|
670
|
+
const issuance = consumeLifecycleIssuance(planned.plan);
|
|
671
|
+
if (!issuance || issuance.reclaim !== null || issuance.portOwnership.holderId !== context.holderId) {
|
|
672
|
+
return lifecycleRefusal(ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, 'sleep lifecycle authority is unavailable', context.stackId);
|
|
673
|
+
}
|
|
674
|
+
if (planned.kind === 'release-only') {
|
|
675
|
+
if (recovery.kind === 'recoverable')
|
|
676
|
+
return lifecycleRefusal(ENVIRONMENT_CODES.DRAIN_ATTEMPT_REPAIR_REQUIRED, 'persisted lifecycle work no longer matches current ownership', context.stackId);
|
|
677
|
+
const released = await releasePortFamily({
|
|
678
|
+
namespace: context.namespace,
|
|
679
|
+
stackId: context.stackId,
|
|
680
|
+
requesterId: context.holderId,
|
|
681
|
+
holderId: context.holderId,
|
|
682
|
+
now: new Date().toISOString(),
|
|
683
|
+
});
|
|
684
|
+
if (released.kind !== 'complete' || released.change !== 'retained')
|
|
685
|
+
return lifecycleRefusal(ENVIRONMENT_CODES.CLAIM_CO_TENANT, 'shared lifecycle ownership changed before release', context.stackId);
|
|
686
|
+
return {
|
|
687
|
+
kind: 'complete', status: 0, diagnostics: planned.plan.diagnostics,
|
|
688
|
+
result: { stackId: context.stackId, change: 'retained' },
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
if (!issuance.drainRequest)
|
|
692
|
+
return lifecycleRefusal(ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, 'sleep drain authority is unavailable', context.stackId);
|
|
693
|
+
let reserved;
|
|
694
|
+
if (recovery.kind === 'recoverable') {
|
|
695
|
+
reserved = await recoverSleepDrainReservation({
|
|
696
|
+
namespace: context.namespace,
|
|
697
|
+
record: recovery.record,
|
|
698
|
+
request: planned.request,
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
reserved = await reserveDrain({ namespace: context.namespace, request: planned.request });
|
|
703
|
+
}
|
|
704
|
+
if (reserved.kind !== 'complete')
|
|
705
|
+
return lifecycleRefusal(reserved.diagnostics[0]?.code ?? ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, reserved.diagnostics[0]?.message ?? 'sleep drain could not be reserved', reserved.diagnostics[0]?.target ?? context.stackId);
|
|
706
|
+
const attempted = await reserveDrainAttempt({ namespace: context.namespace, reservation: reserved.reservation });
|
|
707
|
+
if (attempted.kind !== 'complete')
|
|
708
|
+
return lifecycleRefusal(attempted.diagnostics[0]?.code ?? ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, attempted.diagnostics[0]?.message ?? 'sleep attempt could not be reserved', attempted.diagnostics[0]?.target ?? context.stackId);
|
|
709
|
+
const stopped = await stopRuntime({ topology: issuance.topology, attempt: attempted, execute: (step) => adapters.execute(step) });
|
|
710
|
+
if (stopped.kind === 'unavailable')
|
|
711
|
+
return repairRequired(context);
|
|
712
|
+
const { outcome, fence } = stopped;
|
|
713
|
+
if (stopped.kind === 'not-started') {
|
|
714
|
+
const finalized = await publishDrainOutcome(context, outcome, fence);
|
|
715
|
+
return finalized ?? lifecycleRefusal(stopped.code, 'runtime stop did not start', context.stackId);
|
|
716
|
+
}
|
|
717
|
+
const invalidated = await invalidateCompletedLifecycle(context, planned.request.operationBasis, planned.request.generation, 'sleep');
|
|
718
|
+
if (invalidated)
|
|
719
|
+
return invalidated;
|
|
720
|
+
const finalized = await publishDrainOutcome(context, outcome, fence);
|
|
721
|
+
if (finalized)
|
|
722
|
+
return finalized;
|
|
723
|
+
return {
|
|
724
|
+
kind: 'complete', status: 0, diagnostics: planned.plan.diagnostics,
|
|
725
|
+
result: { stackId: context.stackId, change: 'stopped' },
|
|
726
|
+
};
|
|
727
|
+
}, ports, forwardedRefusal, 'teardown');
|
|
728
|
+
return result ?? lifecycleRefusal(ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID, 'runtime readiness authority is unavailable', context.stackId);
|
|
729
|
+
}
|