@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,675 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { realpath } from 'node:fs/promises';
|
|
3
|
+
import { createConnection } from 'node:net';
|
|
4
|
+
import { basename, dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { loadCockpitConfig, projectMachineFloorConfig } from '../config/load.js';
|
|
6
|
+
import { cockpitCommand } from '../fix-catalogue.js';
|
|
7
|
+
import { redirectedBindSources } from './compose-topology.js';
|
|
8
|
+
import { authenticatedExecutionOutcome } from '../environment-exec/execute.js';
|
|
9
|
+
import { COMPOSE_OWNERSHIP_FILE, renderComposeOwnership } from './compose-ownership.js';
|
|
10
|
+
import { issueLifecycleRunContext, noticeLifecycleRun, } from './application/lifecycle-context.js';
|
|
11
|
+
import { isProcessStart, observeProcessStart, processHost, releaseDeadSessionHolders } from './claims.js';
|
|
12
|
+
import { errorCause, RefusalError, DOCKER_CLIENT_ENVIRONMENT, ENVIRONMENT_CODES, snapshotExecutionPlan, } from './contract.js';
|
|
13
|
+
import { declaresNothingToRun, runtimeNotDeclared } from './declaration.js';
|
|
14
|
+
import { resolveDiskCapacityExecutable, resolveDockerExecutable, resolveExecutable, resolveGitExecutable } from './executable.js';
|
|
15
|
+
import { readSeededAt, recordSeeded, seedDue } from './seed.js';
|
|
16
|
+
import { observeHostCapacity } from './host-observation.js';
|
|
17
|
+
import { composeProjectName, holderId, stackId } from './identity.js';
|
|
18
|
+
import { projectLifecycleStage, resolveLifecycleStage, } from './lifecycle-stage.js';
|
|
19
|
+
import { acquirePortProjection, compensateAcquiredPortHolder, } from './ports.js';
|
|
20
|
+
import { admitRuntimeMeasurement, measureMachineFloor, createRuntimeAdapter, observeRuntimeTopology, issueMachineFloorAuthority, OBSERVATION_WINDOW_HEADROOM_MS, observeDockerRuntime, observeRuntimeMeasurement, projectRuntimeLifecycle, runtimeMeasurementProbeObservation, } from './runtime.js';
|
|
21
|
+
import { readLocalRuntimeMeasurements } from './runtime-measurement-record.js';
|
|
22
|
+
import { consumeSetupCheckoutProjection, observeSetupCheckout, withSetupCheckoutProjection, } from './setup-observation.js';
|
|
23
|
+
import { selectStateRoot } from './state.js';
|
|
24
|
+
import { readBoundedFile } from '../bounded-read.js';
|
|
25
|
+
const OWNER = 'lifecycle-adapter-composition';
|
|
26
|
+
const COMPOSE_BYTE_LIMIT = 1_048_576;
|
|
27
|
+
const LISTENER_CONCURRENCY = 16;
|
|
28
|
+
function ownerDiagnostics(code, message, target, fix) {
|
|
29
|
+
return [{ severity: 'error', code, message, target, ...(fix === undefined ? {} : { fix }) }];
|
|
30
|
+
}
|
|
31
|
+
function runtimeUnavailable(reason, target) {
|
|
32
|
+
return ownerDiagnostics(ENVIRONMENT_CODES.RUNTIME_UNAVAILABLE, reason, target);
|
|
33
|
+
}
|
|
34
|
+
/** Names each mount difference by the row that declared it, so the refusal says what to edit. */
|
|
35
|
+
function mountMismatchDiagnostics(mismatch, config, siblingsRoot) {
|
|
36
|
+
const bySource = new Map(config.sibling.map((sibling) => [resolve(siblingsRoot, sibling.dir), sibling]));
|
|
37
|
+
return [
|
|
38
|
+
...mismatch.missing.map((source) => {
|
|
39
|
+
const sibling = bySource.get(source);
|
|
40
|
+
return {
|
|
41
|
+
severity: 'error', code: ENVIRONMENT_CODES.MOUNT_POSTURE_UNAVAILABLE,
|
|
42
|
+
target: sibling ? `sibling:${sibling.slug}` : source,
|
|
43
|
+
message: sibling
|
|
44
|
+
? `sibling ${sibling.slug} is declared mount = "always", but no Compose service bind-mounts ${source} or a directory inside it, so cockpit will not vouch for this stack`
|
|
45
|
+
: `${source} is declared for the stack to mount, but no Compose service bind-mounts it, so cockpit will not vouch for this stack`,
|
|
46
|
+
fix: sibling
|
|
47
|
+
? `bind-mount \${${sibling.var}}, or a directory inside it, in a Compose service, or set mount = "none" on sibling ${sibling.slug} in cockpit.toml; then run npx --no cockpit sync --config <your cockpit.toml> and npx --no cockpit check --config <your cockpit.toml>`
|
|
48
|
+
: 'bind-mount it in a Compose service or remove it from environment.generated_host_paths; then run npx --no cockpit sync --config <your cockpit.toml> and npx --no cockpit check --config <your cockpit.toml>',
|
|
49
|
+
};
|
|
50
|
+
}),
|
|
51
|
+
...mismatch.unexpected.map((source) => ({
|
|
52
|
+
severity: 'error', code: ENVIRONMENT_CODES.MOUNT_POSTURE_UNAVAILABLE, target: source,
|
|
53
|
+
message: `a Compose service bind-mounts ${source}, which lies inside no mount = "always" sibling and is no environment.generated_host_paths entry, so cockpit will not hand it to a container`,
|
|
54
|
+
fix: 'move the bind inside a mount = "always" sibling, declare it as one or as an environment.generated_host_paths entry, or remove the bind mount; then run npx --no cockpit sync --config <your cockpit.toml> and npx --no cockpit check --config <your cockpit.toml>',
|
|
55
|
+
})),
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
/** Names the sibling a redirected bind sits in, so the refusal says which checkout holds the link. */
|
|
59
|
+
function redirectedMountDiagnostics(redirected, config, siblingsRoot) {
|
|
60
|
+
const bySource = new Map(config.sibling.map((sibling) => [resolve(siblingsRoot, sibling.dir), sibling]));
|
|
61
|
+
return redirected.map(({ bind, holder }) => {
|
|
62
|
+
const sibling = bySource.get(holder);
|
|
63
|
+
return {
|
|
64
|
+
severity: 'error', code: ENVIRONMENT_CODES.MOUNT_POSTURE_UNAVAILABLE, target: bind,
|
|
65
|
+
message: `a Compose service bind-mounts ${bind}, which resolves through a symbolic link to a path outside ${sibling ? `sibling ${sibling.slug}` : holder}, so cockpit will not hand that path to a container`,
|
|
66
|
+
fix: `replace the link with a real directory, or bind the path it points at directly and declare it as a mount = "always" sibling or an environment.generated_host_paths entry; then run ${cockpitCommand('check')}`,
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function refusal(target, message = 'lifecycle composition authority is unavailable', code = ENVIRONMENT_CODES.LIFECYCLE_EVIDENCE_INVALID) {
|
|
71
|
+
return {
|
|
72
|
+
kind: 'refusal',
|
|
73
|
+
status: 2,
|
|
74
|
+
result: null,
|
|
75
|
+
diagnostics: [{
|
|
76
|
+
severity: 'error',
|
|
77
|
+
code,
|
|
78
|
+
message,
|
|
79
|
+
target,
|
|
80
|
+
}],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function captureInput(value) {
|
|
84
|
+
try {
|
|
85
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
86
|
+
return undefined;
|
|
87
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
88
|
+
const keys = ['configPath', 'env', 'sessionId', 'timeoutMs'];
|
|
89
|
+
const actualKeys = Reflect.ownKeys(descriptors);
|
|
90
|
+
const optionalKeys = ['activeProfiles', 'runtimeMeasurementProbeAuthority', 'holderProcess'].filter((key) => descriptors[key] !== undefined);
|
|
91
|
+
if (actualKeys.length !== keys.length + optionalKeys.length
|
|
92
|
+
|| keys.some((key) => !descriptors[key] || !('value' in descriptors[key]))
|
|
93
|
+
|| optionalKeys.some((key) => !('value' in descriptors[key])))
|
|
94
|
+
return undefined;
|
|
95
|
+
const configPath = descriptors.configPath.value;
|
|
96
|
+
const rawEnvironment = descriptors.env.value;
|
|
97
|
+
const sessionId = descriptors.sessionId.value;
|
|
98
|
+
const timeoutMs = descriptors.timeoutMs.value;
|
|
99
|
+
const activeProfiles = descriptors.activeProfiles?.value;
|
|
100
|
+
const runtimeMeasurementProbeAuthority = descriptors.runtimeMeasurementProbeAuthority?.value;
|
|
101
|
+
const holderProcess = descriptors.holderProcess?.value;
|
|
102
|
+
if (typeof configPath !== 'string' || configPath.length === 0 || configPath.includes('\0')
|
|
103
|
+
|| typeof sessionId !== 'string' || sessionId.length === 0 || sessionId.includes('\0')
|
|
104
|
+
|| typeof timeoutMs !== 'number' || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30_000
|
|
105
|
+
|| activeProfiles !== undefined && (!Array.isArray(activeProfiles) || activeProfiles.length > 64
|
|
106
|
+
|| new Set(activeProfiles).size !== activeProfiles.length
|
|
107
|
+
|| activeProfiles.some((profile) => typeof profile !== 'string' || !/^[a-z0-9][a-z0-9_.-]*$/.test(profile)))
|
|
108
|
+
|| holderProcess !== undefined && (!holderProcess || typeof holderProcess !== 'object'
|
|
109
|
+
|| !Number.isSafeInteger(holderProcess.pid) || holderProcess.pid < 1
|
|
110
|
+
|| holderProcess.processStart !== undefined && !isProcessStart(holderProcess.processStart))
|
|
111
|
+
|| !rawEnvironment || typeof rawEnvironment !== 'object' || Array.isArray(rawEnvironment))
|
|
112
|
+
return undefined;
|
|
113
|
+
const env = {};
|
|
114
|
+
for (const [name, entry] of Object.entries(rawEnvironment)) {
|
|
115
|
+
if (name.length === 0 || name.includes('\0') || name.includes('=')
|
|
116
|
+
|| entry !== undefined && (typeof entry !== 'string' || entry.includes('\0')))
|
|
117
|
+
return undefined;
|
|
118
|
+
env[name] = entry;
|
|
119
|
+
}
|
|
120
|
+
return { configPath: resolve(configPath), env, sessionId, timeoutMs,
|
|
121
|
+
...(activeProfiles === undefined ? {} : { activeProfiles: [...activeProfiles].sort() }),
|
|
122
|
+
...(runtimeMeasurementProbeAuthority === undefined ? {} : { runtimeMeasurementProbeAuthority }),
|
|
123
|
+
...(holderProcess === undefined ? {} : { holderProcess: { pid: holderProcess.pid,
|
|
124
|
+
...(holderProcess.processStart === undefined ? {} : { processStart: holderProcess.processStart }) } }) };
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// A throwing accessor or proxy trap makes the input malformed, which the caller refuses.
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Literals, so the diagnostic inventory captures each fix beside its code.
|
|
132
|
+
function seedFailed(context, message) {
|
|
133
|
+
return { kind: 'refusal', diagnostics: [{
|
|
134
|
+
severity: 'error', code: 'DATA_SEED_FAILED',
|
|
135
|
+
message, target: 'environment.seed',
|
|
136
|
+
fix: `correct environment.seed in ${basename(context.source.path)} or the seed it runs, then rerun npx --no cockpit up --config <your cockpit.toml>`,
|
|
137
|
+
}] };
|
|
138
|
+
}
|
|
139
|
+
function seedUnavailable(context, message) {
|
|
140
|
+
return { kind: 'refusal', diagnostics: [{
|
|
141
|
+
severity: 'error', code: 'DATA_SEED_UNAVAILABLE',
|
|
142
|
+
message: `cockpit could not tell whether the seed is due: ${message}`, target: context.stackId,
|
|
143
|
+
fix: 'make sure the Docker daemon this stack runs on answers docker volume ls, then rerun npx --no cockpit up --config <your cockpit.toml>',
|
|
144
|
+
}] };
|
|
145
|
+
}
|
|
146
|
+
function runtimeEnvironment(env) {
|
|
147
|
+
return Object.fromEntries(Object.entries(env)
|
|
148
|
+
.filter((entry) => DOCKER_CLIENT_ENVIRONMENT.has(entry[0]) && typeof entry[1] === 'string'));
|
|
149
|
+
}
|
|
150
|
+
function confined(root, path) {
|
|
151
|
+
const relation = relative(root, path);
|
|
152
|
+
return relation === '' || relation !== '..' && !relation.startsWith(`..${sep}`) && !isAbsolute(relation);
|
|
153
|
+
}
|
|
154
|
+
async function composeArtifact(root, reference, expectedSha256) {
|
|
155
|
+
if (typeof reference !== 'string' || reference.length === 0 || reference.includes('\0'))
|
|
156
|
+
return undefined;
|
|
157
|
+
const expected = resolve(root, reference);
|
|
158
|
+
if (!confined(root, expected))
|
|
159
|
+
return undefined;
|
|
160
|
+
try {
|
|
161
|
+
const canonicalPath = await realpath(expected);
|
|
162
|
+
if (canonicalPath !== expected)
|
|
163
|
+
return undefined;
|
|
164
|
+
const read = readBoundedFile(canonicalPath, COMPOSE_BYTE_LIMIT);
|
|
165
|
+
if (read.kind !== 'complete')
|
|
166
|
+
return undefined;
|
|
167
|
+
const bytes = read.bytes;
|
|
168
|
+
const source = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
169
|
+
const sha256 = createHash('sha256').update(bytes).digest('hex');
|
|
170
|
+
if (expectedSha256 !== undefined && sha256 !== expectedSha256)
|
|
171
|
+
return undefined;
|
|
172
|
+
return { reference, canonicalPath, source, sha256 };
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Callers treat any unreadable or changed artifact as no file authority and refuse or report unavailable.
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function composeArtifacts(root, references) {
|
|
180
|
+
const artifacts = [];
|
|
181
|
+
for (const reference of references) {
|
|
182
|
+
const artifact = await composeArtifact(root, reference);
|
|
183
|
+
if (!artifact)
|
|
184
|
+
return undefined;
|
|
185
|
+
artifacts.push(artifact);
|
|
186
|
+
}
|
|
187
|
+
return artifacts;
|
|
188
|
+
}
|
|
189
|
+
async function occupiedPort(port, timeoutMs) {
|
|
190
|
+
return new Promise((complete) => {
|
|
191
|
+
const socket = createConnection({ host: '127.0.0.1', port });
|
|
192
|
+
let settled = false;
|
|
193
|
+
const finish = (value) => {
|
|
194
|
+
if (settled)
|
|
195
|
+
return;
|
|
196
|
+
settled = true;
|
|
197
|
+
socket.destroy();
|
|
198
|
+
complete(value);
|
|
199
|
+
};
|
|
200
|
+
socket.setTimeout(timeoutMs, () => finish(undefined));
|
|
201
|
+
socket.once('connect', () => finish(true));
|
|
202
|
+
socket.once('error', (error) => finish(error.code === 'ECONNREFUSED' ? false : undefined));
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
async function listenerEvidence(context, requestedAt, timeoutMs) {
|
|
206
|
+
const deadline = Date.parse(requestedAt) + timeoutMs;
|
|
207
|
+
const candidates = Array.from({ length: context.ports.count }, (_, offset) => {
|
|
208
|
+
const ports = Object.values(context.ports.bases).map((base) => base + offset * context.ports.step);
|
|
209
|
+
return { ports: ports };
|
|
210
|
+
});
|
|
211
|
+
const observed = candidates.map(({ ports }) => ports.map(() => undefined));
|
|
212
|
+
const tasks = candidates.flatMap(({ ports }, family) => ports.map((port, index) => ({ family, index, port })));
|
|
213
|
+
let cursor = 0;
|
|
214
|
+
await Promise.all(Array.from({ length: Math.min(LISTENER_CONCURRENCY, tasks.length) }, async () => {
|
|
215
|
+
while (cursor < tasks.length) {
|
|
216
|
+
const task = tasks[cursor++];
|
|
217
|
+
const remaining = deadline - Date.now();
|
|
218
|
+
if (remaining <= 0)
|
|
219
|
+
return;
|
|
220
|
+
observed[task.family][task.index] = Number.isSafeInteger(task.port) && task.port >= 1 && task.port <= 65_535
|
|
221
|
+
? await occupiedPort(task.port, remaining)
|
|
222
|
+
: undefined;
|
|
223
|
+
}
|
|
224
|
+
}));
|
|
225
|
+
const completedAt = Date.now();
|
|
226
|
+
return candidates.map(({ ports }, family) => {
|
|
227
|
+
const familyObservation = observed[family];
|
|
228
|
+
const unavailable = completedAt > deadline || familyObservation.length !== ports.length
|
|
229
|
+
|| familyObservation.some((value) => value === undefined);
|
|
230
|
+
return {
|
|
231
|
+
ports,
|
|
232
|
+
observation: unavailable
|
|
233
|
+
? {
|
|
234
|
+
state: 'unavailable',
|
|
235
|
+
reason: 'bounded loopback listener observation is unavailable',
|
|
236
|
+
producer: OWNER,
|
|
237
|
+
}
|
|
238
|
+
: {
|
|
239
|
+
state: 'available',
|
|
240
|
+
value: ports.filter((_port, index) => familyObservation[index] === true),
|
|
241
|
+
observedAt: new Date(Math.max(completedAt, Date.parse(requestedAt))).toISOString(),
|
|
242
|
+
producer: OWNER,
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
export function createLifecycleCommandAdapters(rawInput, execute) {
|
|
248
|
+
const input = captureInput(rawInput);
|
|
249
|
+
const bindings = new WeakMap();
|
|
250
|
+
const issuedRuntimeReadiness = new WeakSet();
|
|
251
|
+
const load = async (command) => {
|
|
252
|
+
if (!input || typeof execute !== 'function' || command.configPath !== input.configPath) {
|
|
253
|
+
return refusal(typeof command?.configPath === 'string' ? command.configPath : 'lifecycle');
|
|
254
|
+
}
|
|
255
|
+
const loaded = await loadCockpitConfig(input.configPath);
|
|
256
|
+
if (loaded.kind === 'refusal')
|
|
257
|
+
return loaded;
|
|
258
|
+
if (declaresNothingToRun(loaded.result.config.environment))
|
|
259
|
+
return { kind: 'refusal',
|
|
260
|
+
status: 2, result: null, diagnostics: [runtimeNotDeclared(loaded.result.source.path)] };
|
|
261
|
+
const setupEnvironment = Object.fromEntries(Object.entries(input.env)
|
|
262
|
+
.filter((entry) => typeof entry[1] === 'string'));
|
|
263
|
+
const git = await resolveGitExecutable(input.env);
|
|
264
|
+
if (git.state !== 'available')
|
|
265
|
+
return refusal(loaded.result.source.path, git.reason);
|
|
266
|
+
const observedAt = new Date().toISOString();
|
|
267
|
+
const checkoutObservation = await observeSetupCheckout({
|
|
268
|
+
source: loaded.result.source,
|
|
269
|
+
cockpitName: loaded.result.config.cockpit.name,
|
|
270
|
+
observedAt,
|
|
271
|
+
env: setupEnvironment,
|
|
272
|
+
gitExecutable: git.value,
|
|
273
|
+
});
|
|
274
|
+
const machineConfiguration = projectMachineFloorConfig(loaded.result);
|
|
275
|
+
const projected = await withSetupCheckoutProjection(checkoutObservation, {
|
|
276
|
+
source: loaded.result.source,
|
|
277
|
+
cockpitName: loaded.result.config.cockpit.name,
|
|
278
|
+
evaluatedAt: Date.parse(observedAt),
|
|
279
|
+
}, (projection) => machineConfiguration
|
|
280
|
+
? issueMachineFloorAuthority({ configuration: machineConfiguration, checkout: projection })
|
|
281
|
+
: consumeSetupCheckoutProjection(projection));
|
|
282
|
+
const checkout = projected && 'authority' in projected ? projected.checkout : projected;
|
|
283
|
+
if (!checkout || process.platform !== 'darwin' && process.platform !== 'linux') {
|
|
284
|
+
return refusal(loaded.result.source.path, checkoutObservation.state === 'failed'
|
|
285
|
+
? checkoutObservation.reason : 'authenticated checkout evidence is unavailable');
|
|
286
|
+
}
|
|
287
|
+
const namespace = await selectStateRoot({
|
|
288
|
+
platform: process.platform,
|
|
289
|
+
home: input.env.HOME,
|
|
290
|
+
xdgStateHome: input.env.XDG_STATE_HOME,
|
|
291
|
+
checkoutRoots: [checkout.currentCheckoutPath, checkout.mainCheckoutPath],
|
|
292
|
+
cockpitId: checkout.cockpitId,
|
|
293
|
+
});
|
|
294
|
+
// The state root's own refusal names the directory and the failed check; replacing it with a
|
|
295
|
+
// generic one left a sandboxed agent no way to tell why every lifecycle command refused.
|
|
296
|
+
if (namespace.kind === 'refusal')
|
|
297
|
+
return refusal(namespace.target, namespace.message, namespace.code);
|
|
298
|
+
if (namespace.kind !== 'complete')
|
|
299
|
+
return refusal(loaded.result.source.path, 'state namespace is unavailable');
|
|
300
|
+
const currentStackId = stackId({
|
|
301
|
+
cockpitId: checkout.cockpitId,
|
|
302
|
+
checkoutId: checkout.checkoutId,
|
|
303
|
+
variant: { kind: 'shared' },
|
|
304
|
+
});
|
|
305
|
+
const currentHolderId = holderId({
|
|
306
|
+
stackId: currentStackId,
|
|
307
|
+
checkoutId: checkout.checkoutId,
|
|
308
|
+
sessionId: input.sessionId,
|
|
309
|
+
});
|
|
310
|
+
const confirmedAt = new Date().toISOString();
|
|
311
|
+
const confirmed = await observeSetupCheckout({
|
|
312
|
+
source: loaded.result.source,
|
|
313
|
+
cockpitName: loaded.result.config.cockpit.name,
|
|
314
|
+
observedAt: confirmedAt,
|
|
315
|
+
env: setupEnvironment,
|
|
316
|
+
gitExecutable: git.value,
|
|
317
|
+
});
|
|
318
|
+
const issued = await withSetupCheckoutProjection(confirmed, {
|
|
319
|
+
source: loaded.result.source,
|
|
320
|
+
cockpitName: loaded.result.config.cockpit.name,
|
|
321
|
+
evaluatedAt: Date.parse(confirmedAt),
|
|
322
|
+
}, (projection) => issueLifecycleRunContext({
|
|
323
|
+
configPath: input.configPath,
|
|
324
|
+
checkout: projection,
|
|
325
|
+
configuration: loaded.result,
|
|
326
|
+
namespace: namespace.value,
|
|
327
|
+
stackId: currentStackId,
|
|
328
|
+
holderId: currentHolderId,
|
|
329
|
+
sessionId: input.sessionId,
|
|
330
|
+
}));
|
|
331
|
+
if (!issued || issued.kind !== 'complete')
|
|
332
|
+
return issued ?? refusal(loaded.result.source.path);
|
|
333
|
+
bindings.set(issued.result, {
|
|
334
|
+
config: loaded.result.config,
|
|
335
|
+
checkoutRoot: dirname(loaded.result.source.path),
|
|
336
|
+
siblingsRoot: resolve(dirname(loaded.result.source.path), loaded.result.config.cockpit.siblings_root),
|
|
337
|
+
...(projected && 'authority' in projected ? { machineFloorAuthority: projected.authority } : {}),
|
|
338
|
+
});
|
|
339
|
+
return { ...issued, diagnostics: loaded.diagnostics };
|
|
340
|
+
};
|
|
341
|
+
const hostStep = (id, executable, args, cwd, env, timeoutMs, stackId) => snapshotExecutionPlan({ steps: [{
|
|
342
|
+
id, argv: [executable.canonicalPath, ...args],
|
|
343
|
+
argv0: executable.invocationPath, cwd, env, timeoutMs,
|
|
344
|
+
stdoutLimitBytes: 1_048_576, stderrLimitBytes: 65_536, outputOverflow: 'retain-tail',
|
|
345
|
+
paths: [], ports: [], labels: { stackId }, expected: [],
|
|
346
|
+
consumedFiles: [{ path: executable.canonicalPath, sha256: executable.sha256, maxBytes: 536_870_912,
|
|
347
|
+
argvIndexes: [0], executable: true, executionMode: executable.executionMode }],
|
|
348
|
+
filesystemChecks: [], compensation: [],
|
|
349
|
+
}] })?.steps[0];
|
|
350
|
+
// When each resettable volume of this stack was created, by its Compose key; a volume no service
|
|
351
|
+
// has created yet has no entry. Undefined when Docker could not say.
|
|
352
|
+
const resettableVolumesCreatedAt = async (context, binding) => {
|
|
353
|
+
if (!input)
|
|
354
|
+
return undefined;
|
|
355
|
+
const docker = await resolveDockerExecutable(input.env);
|
|
356
|
+
if (docker.state !== 'available')
|
|
357
|
+
return undefined;
|
|
358
|
+
const env = runtimeEnvironment(input.env);
|
|
359
|
+
const project = composeProjectName({ cockpitName: binding.config.cockpit.name, stackId: context.stackId });
|
|
360
|
+
const listing = hostStep('seed:volume-list', docker.value, ['volume', 'ls', '--filter', `label=com.docker.compose.project=${project}`,
|
|
361
|
+
'--format', '{{.Name}}\t{{.Label "com.docker.compose.volume"}}'], binding.checkoutRoot, env, input.timeoutMs, context.stackId);
|
|
362
|
+
const listed = listing && authenticatedExecutionOutcome(await execute(listing), listing);
|
|
363
|
+
if (!listed || listed.kind !== 'complete' || listed.execution.exitCode !== 0)
|
|
364
|
+
return undefined;
|
|
365
|
+
const keys = new Set(binding.config.environment.resettable_volumes);
|
|
366
|
+
const names = Buffer.from(listed.stdout).toString('utf8').split('\n').filter(Boolean)
|
|
367
|
+
.map((line) => line.split('\t')).filter(([, key]) => key !== undefined && keys.has(key)).map(([name]) => name);
|
|
368
|
+
if (names.length === 0)
|
|
369
|
+
return [];
|
|
370
|
+
const inspection = hostStep('seed:volume-created', docker.value, ['volume', 'inspect', '--format', '{{json .CreatedAt}}', ...names], binding.checkoutRoot, env, input.timeoutMs, context.stackId);
|
|
371
|
+
const inspected = inspection && authenticatedExecutionOutcome(await execute(inspection), inspection);
|
|
372
|
+
if (!inspected || inspected.kind !== 'complete' || inspected.execution.exitCode !== 0)
|
|
373
|
+
return undefined;
|
|
374
|
+
// `{{json .CreatedAt}}` prints one quoted instant per volume; anything else is unreadable.
|
|
375
|
+
const created = Buffer.from(inspected.stdout).toString('utf8').trim().split('\n')
|
|
376
|
+
.map((line) => /^"[^"\\]+"$/.test(line) ? Date.parse(line.slice(1, -1)) : Number.NaN);
|
|
377
|
+
return created.length === names.length && created.every(Number.isFinite) ? created : undefined;
|
|
378
|
+
};
|
|
379
|
+
const seed = async (context, up) => {
|
|
380
|
+
const binding = bindings.get(context);
|
|
381
|
+
const declared = binding?.config.environment.seed;
|
|
382
|
+
if (!input || !binding)
|
|
383
|
+
return seedUnavailable(context, 'the lifecycle binding is unavailable');
|
|
384
|
+
if (!declared)
|
|
385
|
+
return { kind: 'none' };
|
|
386
|
+
const [seededAt, created] = await Promise.all([readSeededAt(context.namespace, context.stackId),
|
|
387
|
+
resettableVolumesCreatedAt(context, binding)]);
|
|
388
|
+
if (seededAt === undefined)
|
|
389
|
+
return seedUnavailable(context, 'the record of the last seed is unreadable');
|
|
390
|
+
if (created === undefined)
|
|
391
|
+
return seedUnavailable(context, 'Docker did not report when the resettable volumes were created');
|
|
392
|
+
if (!seedDue(seededAt, created))
|
|
393
|
+
return { kind: 'current' };
|
|
394
|
+
const env = {
|
|
395
|
+
...Object.fromEntries(Object.entries(input.env).filter((entry) => typeof entry[1] === 'string')),
|
|
396
|
+
...Object.fromEntries(Object.entries(up.ports).map(([name, port]) => [name, String(port)])),
|
|
397
|
+
...Object.fromEntries(up.entrypoints.map(({ name, value }) => [`COCKPIT_ENTRYPOINT_${name.toUpperCase().replaceAll('-', '_')}`, value])),
|
|
398
|
+
};
|
|
399
|
+
const executable = await resolveExecutable(declared.argv[0], env);
|
|
400
|
+
if (executable.state !== 'available')
|
|
401
|
+
return seedFailed(context, `environment.seed names ${declared.argv[0]}, which could not be run: ${executable.reason}`);
|
|
402
|
+
const step = hostStep('seed:run', executable.value, declared.argv.slice(1), binding.checkoutRoot, env, declared.timeout_seconds * 1_000, context.stackId);
|
|
403
|
+
if (!step)
|
|
404
|
+
return seedFailed(context, 'the seed command could not be planned within the execution bounds');
|
|
405
|
+
const ran = authenticatedExecutionOutcome(await execute(step), step);
|
|
406
|
+
if (!ran || ran.kind !== 'complete' || ran.execution.exitCode !== 0) {
|
|
407
|
+
const status = !ran ? 'did not run' : ran.execution.signal ? `was stopped by ${ran.execution.signal}`
|
|
408
|
+
: ran.kind === 'refusal' && ran.code !== ENVIRONMENT_CODES.EXECUTION_STEP_FAILED ? `did not complete (${ran.code})`
|
|
409
|
+
: `exited with status ${ran.execution.exitCode}`;
|
|
410
|
+
return seedFailed(context, `the seed command ${status}; its data may be partly applied, and it runs again on the next up`);
|
|
411
|
+
}
|
|
412
|
+
const recorded = await recordSeeded({ namespace: context.namespace, stackId: context.stackId,
|
|
413
|
+
holderId: context.holderId, seededAt: new Date().toISOString() });
|
|
414
|
+
return recorded ? { kind: 'ran' }
|
|
415
|
+
: seedUnavailable(context, 'the seed ran, and its success could not be recorded, so the next up runs it again');
|
|
416
|
+
};
|
|
417
|
+
const withRuntimeReadiness = (context, use, ports, refuse, purpose) => runtimeReadiness(context, use, ports, refuse, purpose);
|
|
418
|
+
// `resolvedMounts` is set only on a teardown retry: stopping or removing a stack must not depend
|
|
419
|
+
// on its mounts matching cockpit.toml, or a stack whose declaration drifted could never be retired.
|
|
420
|
+
const runtimeReadiness = async (context, use, ports, refuse, purpose, resolvedMounts) => {
|
|
421
|
+
const binding = bindings.get(context);
|
|
422
|
+
if (!input || !binding)
|
|
423
|
+
return undefined;
|
|
424
|
+
const currentBinding = binding;
|
|
425
|
+
// The machine floor bounds cold start and host capacity; without it there is nothing to hold
|
|
426
|
+
// the runtime to, so say which table is missing instead of a generic evidence refusal.
|
|
427
|
+
const machine = currentBinding.config.environment.machine;
|
|
428
|
+
if (!machine) {
|
|
429
|
+
return refuse?.(ownerDiagnostics(ENVIRONMENT_CODES.CONFIG_SCHEMA_INVALID, 'cockpit.toml declares no [environment.machine] table; runtime commands need its runtime_memory_mb, runtime_cpus, free_disk_mb and cold_start_seconds', 'environment.machine'));
|
|
430
|
+
}
|
|
431
|
+
const env = runtimeEnvironment(input.env);
|
|
432
|
+
const docker = await resolveDockerExecutable(input.env);
|
|
433
|
+
if (docker.state !== 'available')
|
|
434
|
+
return refuse?.(runtimeUnavailable(docker.reason, context.stackId));
|
|
435
|
+
let probeAuthority;
|
|
436
|
+
if (input.runtimeMeasurementProbeAuthority !== undefined) {
|
|
437
|
+
try {
|
|
438
|
+
probeAuthority = typeof input.runtimeMeasurementProbeAuthority === 'function'
|
|
439
|
+
? input.runtimeMeasurementProbeAuthority() : input.runtimeMeasurementProbeAuthority;
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
throw new RefusalError('runtime measurement probe authority supplier failed', { cause: error });
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const observed = input.runtimeMeasurementProbeAuthority === undefined
|
|
446
|
+
? await observeDockerRuntime({
|
|
447
|
+
executable: docker.value,
|
|
448
|
+
cwd: binding.checkoutRoot,
|
|
449
|
+
env,
|
|
450
|
+
timeoutMs: input.timeoutMs,
|
|
451
|
+
}, execute)
|
|
452
|
+
: (probeAuthority === undefined ? undefined : runtimeMeasurementProbeObservation(probeAuthority, docker.value.sha256))
|
|
453
|
+
?? { kind: 'unavailable', code: ENVIRONMENT_CODES.RUNTIME_UNAVAILABLE, reason: 'runtime measurement probe authority is unavailable or expired' };
|
|
454
|
+
if (observed.kind !== 'complete')
|
|
455
|
+
return refuse?.(ownerDiagnostics(observed.code, observed.reason, context.stackId, 'fix' in observed ? observed.fix : undefined));
|
|
456
|
+
const measured = admitRuntimeMeasurement(observed, await readLocalRuntimeMeasurements(context.namespace.root));
|
|
457
|
+
const certifiedProbe = input.runtimeMeasurementProbeAuthority !== undefined;
|
|
458
|
+
if (measured.kind !== 'admitted' && !certifiedProbe)
|
|
459
|
+
return refuse?.(ownerDiagnostics(measured.code, measured.reason, context.stackId));
|
|
460
|
+
const measurementObservation = measured.kind === 'admitted'
|
|
461
|
+
? observeRuntimeMeasurement(measured, context.stackId) : undefined;
|
|
462
|
+
let artifacts;
|
|
463
|
+
let inputs = { [basename(context.source.path)]: context.runtimeSha256 };
|
|
464
|
+
const activeProfiles = input.activeProfiles ?? [];
|
|
465
|
+
let expectedMountSources;
|
|
466
|
+
const declared = await composeArtifacts(currentBinding.checkoutRoot, currentBinding.config.environment.compose_files);
|
|
467
|
+
if (!declared)
|
|
468
|
+
return undefined;
|
|
469
|
+
// The cockpit's own Compose files carry no ownership labels; the override `sync`
|
|
470
|
+
// publishes adds them, and it is admitted only while it equals a fresh render.
|
|
471
|
+
const ownership = renderComposeOwnership({
|
|
472
|
+
sources: declared.map(({ reference, source }) => ({ reference, source })),
|
|
473
|
+
stackId: context.stackId, cockpitId: context.cockpitId, configSha256: context.runtimeSha256,
|
|
474
|
+
resettableVolumes: currentBinding.config.environment.resettable_volumes,
|
|
475
|
+
});
|
|
476
|
+
if (ownership.kind !== 'complete')
|
|
477
|
+
return refuse?.(ownerDiagnostics(ENVIRONMENT_CODES.GENERATED_ENVIRONMENT_UNAVAILABLE, `the ownership override cannot be rendered from environment.compose_files: ${ownership.reason}`, ownership.target));
|
|
478
|
+
const override = await composeArtifact(currentBinding.checkoutRoot, COMPOSE_OWNERSHIP_FILE, createHash('sha256').update(ownership.bytes).digest('hex'));
|
|
479
|
+
if (!override)
|
|
480
|
+
return refuse?.(ownerDiagnostics(ENVIRONMENT_CODES.GENERATED_ENVIRONMENT_UNAVAILABLE, `${COMPOSE_OWNERSHIP_FILE} is missing or does not match the current Compose files and cockpit.toml; run ${cockpitCommand('sync')}`, COMPOSE_OWNERSHIP_FILE));
|
|
481
|
+
artifacts = [...declared, override];
|
|
482
|
+
inputs = { ...inputs, ...Object.fromEntries(declared.map(({ reference, sha256 }) => [reference, sha256])) };
|
|
483
|
+
expectedMountSources = [...new Set([
|
|
484
|
+
...currentBinding.config.sibling
|
|
485
|
+
.filter((sibling) => sibling.mount === 'always')
|
|
486
|
+
.map((sibling) => resolve(currentBinding.siblingsRoot, sibling.dir)),
|
|
487
|
+
...currentBinding.config.environment.generated_host_paths
|
|
488
|
+
.map((path) => resolve(currentBinding.checkoutRoot, path)),
|
|
489
|
+
])].sort();
|
|
490
|
+
if (!artifacts || artifacts.length === 0)
|
|
491
|
+
return undefined;
|
|
492
|
+
if (resolvedMounts)
|
|
493
|
+
expectedMountSources = resolvedMounts;
|
|
494
|
+
const siblingDirectories = Object.fromEntries(currentBinding.config.sibling
|
|
495
|
+
.map((sibling) => [sibling.var, resolve(currentBinding.siblingsRoot, sibling.dir)]));
|
|
496
|
+
const batchId = randomBytes(32).toString('hex');
|
|
497
|
+
const runtime = createRuntimeAdapter({
|
|
498
|
+
runtime: observed.runtime,
|
|
499
|
+
composeVersion: observed.composeVersion,
|
|
500
|
+
executable: docker.value,
|
|
501
|
+
operation: 'validate',
|
|
502
|
+
cwd: currentBinding.checkoutRoot,
|
|
503
|
+
composeFiles: artifacts,
|
|
504
|
+
projectName: composeProjectName({ cockpitName: currentBinding.config.cockpit.name, stackId: context.stackId }),
|
|
505
|
+
env,
|
|
506
|
+
ports: ports ? Object.values(ports.lease.ports) : [],
|
|
507
|
+
labels: { stackId: context.stackId, cockpitId: context.cockpitId, configSha256: context.runtimeSha256, batchId },
|
|
508
|
+
timeoutMs: input.timeoutMs,
|
|
509
|
+
stackId: context.stackId,
|
|
510
|
+
coldStartLimitMillis: machine.cold_start_seconds * 1_000,
|
|
511
|
+
expectedMountSources,
|
|
512
|
+
activeProfiles,
|
|
513
|
+
// The stack reads its host ports from the [ports.bases] variables and its sibling checkouts
|
|
514
|
+
// from the [[sibling]] var variables, so both reach Compose as those variables instead of the
|
|
515
|
+
// `${VAR:-default}` fallback.
|
|
516
|
+
}, { ownComposeFiles: true, cockpitId: context.cockpitId, stackId: context.stackId,
|
|
517
|
+
...(ports ? { portLeaseProjection: ports } : {}), siblingDirectories });
|
|
518
|
+
if (runtime.kind !== 'preview')
|
|
519
|
+
return runtime.kind === 'refusal' ? refuse?.(runtime.diagnostics) : undefined;
|
|
520
|
+
const startedAt = new Date().toISOString();
|
|
521
|
+
const topology = await observeRuntimeTopology({
|
|
522
|
+
runtimePlan: runtime.plan,
|
|
523
|
+
stackId: context.stackId,
|
|
524
|
+
configSha256: context.runtimeSha256,
|
|
525
|
+
batchId,
|
|
526
|
+
observationWindow: {
|
|
527
|
+
startedAt,
|
|
528
|
+
expiresAt: new Date(Date.parse(startedAt) + machine.cold_start_seconds * 1_000 + OBSERVATION_WINDOW_HEADROOM_MS).toISOString(),
|
|
529
|
+
},
|
|
530
|
+
execute,
|
|
531
|
+
});
|
|
532
|
+
if (topology.kind !== 'complete') {
|
|
533
|
+
const mismatch = topology.mounts;
|
|
534
|
+
if (!mismatch)
|
|
535
|
+
return undefined;
|
|
536
|
+
if (purpose === 'teardown' && !resolvedMounts) {
|
|
537
|
+
const resolved = [...expectedMountSources.filter((source) => !mismatch.missing.includes(source)), ...mismatch.unexpected].sort();
|
|
538
|
+
return runtimeReadiness(context, use, ports, refuse, purpose, resolved);
|
|
539
|
+
}
|
|
540
|
+
return refuse?.(mountMismatchDiagnostics(mismatch, currentBinding.config, currentBinding.siblingsRoot));
|
|
541
|
+
}
|
|
542
|
+
if (purpose !== 'teardown') {
|
|
543
|
+
const redirected = await redirectedBindSources(topology.topology.services.flatMap(({ bindMountSources }) => bindMountSources), expectedMountSources);
|
|
544
|
+
if (redirected.length > 0)
|
|
545
|
+
return refuse?.(redirectedMountDiagnostics(redirected, currentBinding.config, currentBinding.siblingsRoot));
|
|
546
|
+
}
|
|
547
|
+
const disk = await resolveDiskCapacityExecutable();
|
|
548
|
+
if (disk.state !== 'available')
|
|
549
|
+
return undefined;
|
|
550
|
+
const floor = await measureMachineFloor({
|
|
551
|
+
runtimePlan: runtime.plan,
|
|
552
|
+
diskExecutable: disk.value,
|
|
553
|
+
timeoutMs: input.timeoutMs,
|
|
554
|
+
authority: currentBinding.machineFloorAuthority,
|
|
555
|
+
}, execute);
|
|
556
|
+
if (floor.kind !== 'complete')
|
|
557
|
+
return undefined;
|
|
558
|
+
const hostObservations = observeHostCapacity({ evidence: floor.evidence });
|
|
559
|
+
const lifecycle = projectRuntimeLifecycle(topology.topology);
|
|
560
|
+
if (!lifecycle)
|
|
561
|
+
return undefined;
|
|
562
|
+
const readiness = {
|
|
563
|
+
topology: topology.topology,
|
|
564
|
+
lifecycle,
|
|
565
|
+
hostObservations,
|
|
566
|
+
runtimeObservations: measurementObservation
|
|
567
|
+
? [measurementObservation, ...topology.observations] : [...topology.observations],
|
|
568
|
+
inputs,
|
|
569
|
+
};
|
|
570
|
+
issuedRuntimeReadiness.add(readiness);
|
|
571
|
+
return use(readiness);
|
|
572
|
+
};
|
|
573
|
+
const withMachineFloorReadiness = async (context, use, refuse) => {
|
|
574
|
+
const binding = bindings.get(context);
|
|
575
|
+
if (!input || !binding?.machineFloorAuthority || !binding.config.environment.machine)
|
|
576
|
+
return undefined;
|
|
577
|
+
const env = runtimeEnvironment(input.env);
|
|
578
|
+
const [docker, disk] = await Promise.all([
|
|
579
|
+
resolveDockerExecutable(input.env),
|
|
580
|
+
resolveDiskCapacityExecutable(),
|
|
581
|
+
]);
|
|
582
|
+
if (docker.state !== 'available')
|
|
583
|
+
return refuse?.(runtimeUnavailable(docker.reason, context.stackId));
|
|
584
|
+
if (disk.state !== 'available')
|
|
585
|
+
return refuse?.(runtimeUnavailable(disk.reason, context.stackId));
|
|
586
|
+
const machine = await measureMachineFloor({
|
|
587
|
+
runtimeExecutable: docker.value,
|
|
588
|
+
diskExecutable: disk.value,
|
|
589
|
+
cwd: binding.checkoutRoot,
|
|
590
|
+
env,
|
|
591
|
+
timeoutMs: input.timeoutMs,
|
|
592
|
+
authority: binding.machineFloorAuthority,
|
|
593
|
+
}, execute);
|
|
594
|
+
if (machine.kind === 'refusal')
|
|
595
|
+
return refuse?.(machine.diagnostics);
|
|
596
|
+
return machine.kind === 'complete' ? use(observeHostCapacity({ evidence: machine.evidence })) : undefined;
|
|
597
|
+
};
|
|
598
|
+
const withLifecycleStage = async (context, use, refuse) => {
|
|
599
|
+
if (!bindings.has(context))
|
|
600
|
+
return undefined;
|
|
601
|
+
const observedAt = new Date().toISOString();
|
|
602
|
+
const resolved = await resolveLifecycleStage({ namespace: context.namespace, stackId: context.stackId, observedAt });
|
|
603
|
+
if (resolved.kind !== 'complete')
|
|
604
|
+
return refuse?.(resolved.diagnostics);
|
|
605
|
+
const projection = projectLifecycleStage({
|
|
606
|
+
authority: resolved.authority,
|
|
607
|
+
stackId: context.stackId,
|
|
608
|
+
cockpitId: context.cockpitId,
|
|
609
|
+
earliest: Date.parse(resolved.authority.observedAt),
|
|
610
|
+
latest: Date.parse(resolved.authority.validThrough),
|
|
611
|
+
evaluatedAt: Date.now(),
|
|
612
|
+
});
|
|
613
|
+
return projection ? use(projection) : undefined;
|
|
614
|
+
};
|
|
615
|
+
const withAcquiredPorts = async (context, use, refuse) => {
|
|
616
|
+
if (!bindings.has(context))
|
|
617
|
+
return undefined;
|
|
618
|
+
const holderProcess = input?.holderProcess;
|
|
619
|
+
const reaped = await releaseDeadSessionHolders({
|
|
620
|
+
namespace: context.namespace, stackId: context.stackId,
|
|
621
|
+
successor: { holderId: context.holderId, checkoutId: context.checkoutId, ...holderProcess },
|
|
622
|
+
now: new Date().toISOString(), observe: (pid) => observeProcessStart(pid, processHost(execute)),
|
|
623
|
+
});
|
|
624
|
+
if (reaped.kind === 'refusal')
|
|
625
|
+
return refuse?.(reaped.diagnostics);
|
|
626
|
+
const now = new Date().toISOString();
|
|
627
|
+
const listenerTimeoutMs = Math.min(input?.timeoutMs ?? 1, 30_000);
|
|
628
|
+
const acquired = await acquirePortProjection({
|
|
629
|
+
namespace: context.namespace,
|
|
630
|
+
stackId: context.stackId,
|
|
631
|
+
holderId: context.holderId,
|
|
632
|
+
checkoutId: context.checkoutId,
|
|
633
|
+
config: context.ports,
|
|
634
|
+
now,
|
|
635
|
+
listenerTimeoutMs,
|
|
636
|
+
listenerEvidence: await listenerEvidence(context, now, listenerTimeoutMs),
|
|
637
|
+
declarations: context.entrypointDerivatives,
|
|
638
|
+
...(holderProcess === undefined ? {} : { holderProcess }),
|
|
639
|
+
});
|
|
640
|
+
if (acquired.kind === 'refusal')
|
|
641
|
+
return refuse?.(acquired.diagnostics);
|
|
642
|
+
if (acquired.kind !== 'complete')
|
|
643
|
+
return undefined;
|
|
644
|
+
let retainOnFailure = false;
|
|
645
|
+
const compensationFailure = (error, cleanup) => {
|
|
646
|
+
return new RefusalError(`downstream port preparation failed: ${errorCause(error)}; acquired-holder compensation failed: ${errorCause(cleanup)}`, { cause: new AggregateError([error, cleanup]) });
|
|
647
|
+
};
|
|
648
|
+
try {
|
|
649
|
+
return await use({
|
|
650
|
+
leaseResult: acquired.leaseResult,
|
|
651
|
+
projection: acquired.projection,
|
|
652
|
+
retainOnFailure: () => { retainOnFailure = true; },
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
if (!retainOnFailure && acquired.leaseResult.kind === 'complete' && acquired.leaseResult.change === 'acquired') {
|
|
657
|
+
let compensated;
|
|
658
|
+
try {
|
|
659
|
+
compensated = await compensateAcquiredPortHolder({ namespace: context.namespace,
|
|
660
|
+
leaseResult: acquired.leaseResult, now: new Date().toISOString() });
|
|
661
|
+
}
|
|
662
|
+
catch (cleanup) {
|
|
663
|
+
throw compensationFailure(error, cleanup);
|
|
664
|
+
}
|
|
665
|
+
if (compensated.kind !== 'complete') {
|
|
666
|
+
const cleanup = new RefusalError(compensated.diagnostics
|
|
667
|
+
.map(({ code, message, target }) => `${code}: ${message}${target ? ` (${target})` : ''}`).join('; '));
|
|
668
|
+
throw compensationFailure(error, cleanup);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
throw error;
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
return { load, execute, seed, withRuntimeReadiness, withMachineFloorReadiness, withLifecycleStage, withAcquiredPorts };
|
|
675
|
+
}
|