@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,61 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { consumeWorkEnvironmentConfigProjection, loadCockpitConfig, projectWorkEnvironmentConfig } from '../../config/load.js';
|
|
3
|
+
import { CORE_EVALUATOR_ASSET as EVALUATOR_ASSET } from '../../surfaces/core-declarations.js';
|
|
4
|
+
import { nodeAuthority, readAnchoredFile } from '../../generated-environment/transaction.js';
|
|
5
|
+
import { TESTING_GUIDE_REGISTRY, createGateRegistry } from '../../testing/gates.js';
|
|
6
|
+
import { refusalOutcome as refuse } from '../../refusal.js';
|
|
7
|
+
import { MAX_SURFACE_FILE_BYTES, SURFACE_ROOT, defaultAdapters, issuedApplicationProjections, issuedWorkApplicationAuthorities, projectedWorkApplicationAuthorities, readSurfaceAsset, sha256, } from './shared.js';
|
|
8
|
+
import { commandInput } from './prepare.js';
|
|
9
|
+
export function generatedApplicationProjection(value) {
|
|
10
|
+
return value !== null && typeof value === 'object' && issuedApplicationProjections.has(value)
|
|
11
|
+
? value : undefined;
|
|
12
|
+
}
|
|
13
|
+
export function projectWorkApplicationAuthorities(application) {
|
|
14
|
+
if (application === null || typeof application !== 'object' || projectedWorkApplicationAuthorities.has(application))
|
|
15
|
+
return undefined;
|
|
16
|
+
const record = issuedApplicationProjections.get(application);
|
|
17
|
+
if (!record?.workApplication)
|
|
18
|
+
return undefined;
|
|
19
|
+
const projection = { kind: 'work-application-authorities' };
|
|
20
|
+
projectedWorkApplicationAuthorities.add(application);
|
|
21
|
+
issuedWorkApplicationAuthorities.set(projection, record.workApplication);
|
|
22
|
+
return projection;
|
|
23
|
+
}
|
|
24
|
+
export function consumeWorkApplicationAuthorities(projection) {
|
|
25
|
+
if (projection === null || typeof projection !== 'object')
|
|
26
|
+
return undefined;
|
|
27
|
+
const record = issuedWorkApplicationAuthorities.get(projection);
|
|
28
|
+
if (!record)
|
|
29
|
+
return undefined;
|
|
30
|
+
issuedWorkApplicationAuthorities.delete(projection);
|
|
31
|
+
return record;
|
|
32
|
+
}
|
|
33
|
+
export async function prepareWorkApplicationAuthorities(input) {
|
|
34
|
+
const request = commandInput(input);
|
|
35
|
+
if (!request)
|
|
36
|
+
return refuse('GENERATED_INPUT_INVALID', 'work application input is invalid', 'work');
|
|
37
|
+
const loaded = await loadCockpitConfig(request.configPath);
|
|
38
|
+
if (loaded.kind !== 'complete')
|
|
39
|
+
return {
|
|
40
|
+
kind: 'refusal', artifact: null, destination: null, result: null, status: 2,
|
|
41
|
+
diagnostics: [...loaded.diagnostics],
|
|
42
|
+
};
|
|
43
|
+
const configProjection = projectWorkEnvironmentConfig(loaded.result);
|
|
44
|
+
const workEnvironment = configProjection && consumeWorkEnvironmentConfigProjection(configProjection);
|
|
45
|
+
const node = await nodeAuthority();
|
|
46
|
+
if (!workEnvironment || !node)
|
|
47
|
+
return refuse('GENERATED_RENDER_UNAUTHENTICATED', 'work configuration or Node authority is unavailable', request.configPath);
|
|
48
|
+
const read = await readAnchoredFile(SURFACE_ROOT, EVALUATOR_ASSET, MAX_SURFACE_FILE_BYTES, node, defaultAdapters);
|
|
49
|
+
const evaluator = readSurfaceAsset(EVALUATOR_ASSET, read);
|
|
50
|
+
if (!evaluator)
|
|
51
|
+
return refuse('SURFACE_INPUT_INVALID', 'the shipped evaluator could not be authenticated', EVALUATOR_ASSET);
|
|
52
|
+
const projection = { kind: 'work-application-authorities' };
|
|
53
|
+
issuedWorkApplicationAuthorities.set(projection, Object.freeze({
|
|
54
|
+
workEnvironment,
|
|
55
|
+
evaluator: Object.freeze({ path: resolve(SURFACE_ROOT, evaluator.path), bytes: evaluator.bytes,
|
|
56
|
+
sha256: evaluator.sha256, identity: evaluator.identity, restriction: 'instructed' }),
|
|
57
|
+
installedSkills: Object.freeze([]),
|
|
58
|
+
testing: Object.freeze({ registry: workEnvironment.testing ? createGateRegistry(workEnvironment.testing.declarations) : TESTING_GUIDE_REGISTRY }),
|
|
59
|
+
}));
|
|
60
|
+
return projection;
|
|
61
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type SurfaceTarget } from '../../surfaces/contract.js';
|
|
2
|
+
import type { SurfacePriorOwnedEntry } from '../../surfaces/ownership.js';
|
|
3
|
+
import type { ResolvedExecutable } from '../executable.js';
|
|
4
|
+
import { type GeneratedEnvironmentCommandAdapters, type GeneratedEnvironmentSyncInput, type SelectedBootstrapPublicationAuthority, type SelectedBootstrapResumeInput, type SelectedBootstrapResumeOutcome, type SelectedBootstrapSurfaceInput, type SelectedBootstrapSurfaceOutcome } from './shared.js';
|
|
5
|
+
export declare function prepareSelectedBootstrapSurface(input: SelectedBootstrapSurfaceInput, adapterOverrides?: Partial<GeneratedEnvironmentCommandAdapters>): Promise<SelectedBootstrapSurfaceOutcome>;
|
|
6
|
+
export declare function applySelectedBootstrapSurface(authority: SelectedBootstrapPublicationAuthority): Promise<SelectedBootstrapSurfaceOutcome>;
|
|
7
|
+
export declare function resumeSelectedBootstrapSurface(input: SelectedBootstrapResumeInput, adapterOverrides?: Partial<GeneratedEnvironmentCommandAdapters>): Promise<SelectedBootstrapResumeOutcome>;
|
|
8
|
+
export declare function authenticatedBootstrapPriorSurfaces(root: string, state: NonNullable<GeneratedEnvironmentSyncInput['bootstrapState']>, node: ResolvedExecutable, adapters: GeneratedEnvironmentCommandAdapters): Promise<readonly SurfacePriorOwnedEntry[] | undefined>;
|
|
9
|
+
export declare function unfinishedInitSurfaces(root: string, node: ResolvedExecutable, adapters: GeneratedEnvironmentCommandAdapters): Promise<UnfinishedInit | undefined>;
|
|
10
|
+
export interface UnfinishedInit {
|
|
11
|
+
readonly root: string;
|
|
12
|
+
readonly target: SurfaceTarget;
|
|
13
|
+
readonly published: readonly string[];
|
|
14
|
+
/** Absent when the files no longer hold what init recorded, so sync cannot take them over. */
|
|
15
|
+
readonly surfaces: readonly SurfacePriorOwnedEntry[] | undefined;
|
|
16
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { lstat, realpath } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
import { SURFACE_TARGETS } from '../../surfaces/contract.js';
|
|
4
|
+
import { CORE_PROCEDURE_ASSETS as PROCEDURE_ASSETS, CORE_SKILL_ASSETS as SKILL_ASSETS, shippedSkillSources, } from '../../surfaces/core-declarations.js';
|
|
5
|
+
import { SHIPPED_SETUP_SKILL, inspectSkillSet, projectInspectedSkillOutputs } from '../../surfaces/skill-contract.js';
|
|
6
|
+
import { nodeAuthority, readAnchoredEntry, readAnchoredBatches, expectedState, admitPublicationPlan, runPublicationPlan, } from '../../generated-environment/transaction.js';
|
|
7
|
+
import { errorCause, RefusalError } from '../contract.js';
|
|
8
|
+
import { authenticateStatePublication, readState, selectBootstrapStateRoot } from '../state.js';
|
|
9
|
+
import { exactKeys } from '../../exact-keys.js';
|
|
10
|
+
import { refusalOutcome as refuse } from '../../refusal.js';
|
|
11
|
+
import { MAX_OWNERSHIP_BYTES, MAX_SURFACE_FILE_BYTES, OWNERSHIP_PATH, SURFACE_ROOT, consumedBootstrapStatePublications, issuedSelectedBootstrapAuthorities, mergedAdapters, ownRecord, readSurfaceAsset, sha256, sourceIdentity, } from './shared.js';
|
|
12
|
+
import { prepare, readOwnership } from './prepare.js';
|
|
13
|
+
function selectedBootstrapInput(input) {
|
|
14
|
+
const record = ownRecord(input);
|
|
15
|
+
if (!record || !exactKeys(record, ['root', 'target', 'tools']) || typeof record.root !== 'string'
|
|
16
|
+
|| !isAbsolute(record.root) || resolve(record.root) !== record.root
|
|
17
|
+
|| !SURFACE_TARGETS.includes(record.target)
|
|
18
|
+
|| !Array.isArray(record.tools) || record.tools[0] !== record.target
|
|
19
|
+
|| new Set(record.tools).size !== record.tools.length
|
|
20
|
+
|| record.tools.some(tool => !SURFACE_TARGETS.includes(tool)))
|
|
21
|
+
return undefined;
|
|
22
|
+
return { root: record.root, target: record.target, tools: [...record.tools] };
|
|
23
|
+
}
|
|
24
|
+
/** The shipped skill sources with the packaged collection and procedures that authenticate them. */
|
|
25
|
+
function bootstrapSkillInput(assets, tools) {
|
|
26
|
+
const skills = shippedSkillSources(assets, tools);
|
|
27
|
+
const collection = assets.get('skills/collection.json');
|
|
28
|
+
const procedures = PROCEDURE_ASSETS.map(path => assets.get(path));
|
|
29
|
+
if (!skills || !collection || procedures.some(row => row === undefined))
|
|
30
|
+
return undefined;
|
|
31
|
+
const pick = ({ path, bytes, sha256: digest }) => ({ path, bytes, sha256: digest });
|
|
32
|
+
return { sources: skills.flatMap(skill => skill.sources), collection: pick(collection),
|
|
33
|
+
procedures: procedures.map(pick), tools };
|
|
34
|
+
}
|
|
35
|
+
async function selectedBootstrapAssets(node, adapters) {
|
|
36
|
+
const reads = await readAnchoredBatches(SURFACE_ROOT, SKILL_ASSETS.map(path => ({
|
|
37
|
+
path, maxBytes: MAX_SURFACE_FILE_BYTES, action: 'read-file',
|
|
38
|
+
})), node, adapters);
|
|
39
|
+
if (!reads || reads.kind !== 'complete' || reads.results.length !== SKILL_ASSETS.length)
|
|
40
|
+
return undefined;
|
|
41
|
+
const assets = SKILL_ASSETS.map((path, index) => readSurfaceAsset(path, reads.results[index]));
|
|
42
|
+
return assets.every((asset) => asset !== undefined) ? assets : undefined;
|
|
43
|
+
}
|
|
44
|
+
async function selectedBootstrapRecordCurrent(record) {
|
|
45
|
+
try {
|
|
46
|
+
const root = await lstat(record.root);
|
|
47
|
+
if (!root.isDirectory() || root.isSymbolicLink() || root.dev !== record.rootIdentity.dev || root.ino !== record.rootIdentity.ino)
|
|
48
|
+
return false;
|
|
49
|
+
const reads = await readAnchoredBatches(SURFACE_ROOT, record.assets.map(asset => ({
|
|
50
|
+
path: asset.path, maxBytes: MAX_SURFACE_FILE_BYTES, action: 'read-file',
|
|
51
|
+
})), record.node, record.adapters);
|
|
52
|
+
const state = record.bootstrapState && await readState(record.bootstrapState.address);
|
|
53
|
+
return (!record.bootstrapState || state?.kind === 'complete'
|
|
54
|
+
&& JSON.stringify(state.value) === JSON.stringify(record.bootstrapState.record))
|
|
55
|
+
&& !!reads && reads.kind === 'complete' && reads.results.length === record.assets.length
|
|
56
|
+
&& reads.results.every((read, index) => read.kind === 'complete' && read.sha256 === record.assets[index].sha256
|
|
57
|
+
&& sourceIdentity(read.identity) === sourceIdentity(record.assets[index].identity));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// a failed re-observation means the inputs are no longer authenticated; the caller refuses.
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export async function prepareSelectedBootstrapSurface(input, adapterOverrides = {}) {
|
|
65
|
+
const captured = selectedBootstrapInput(input);
|
|
66
|
+
if (!captured)
|
|
67
|
+
return refuse('GENERATED_INPUT_INVALID', 'selected bootstrap input is invalid', 'bootstrap');
|
|
68
|
+
const adapters = mergedAdapters(adapterOverrides);
|
|
69
|
+
let root;
|
|
70
|
+
let rootIdentity;
|
|
71
|
+
try {
|
|
72
|
+
root = await realpath(captured.root);
|
|
73
|
+
const status = await lstat(root);
|
|
74
|
+
if (root !== captured.root || !status.isDirectory() || status.isSymbolicLink())
|
|
75
|
+
throw new RefusalError('root');
|
|
76
|
+
rootIdentity = { dev: status.dev, ino: status.ino };
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
return refuse('GENERATED_INPUT_INVALID', `selected bootstrap root is not a canonical directory: ${errorCause(error)}`, captured.root);
|
|
80
|
+
}
|
|
81
|
+
const node = await nodeAuthority();
|
|
82
|
+
const assets = node && await selectedBootstrapAssets(node, adapters);
|
|
83
|
+
if (!node || !assets)
|
|
84
|
+
return refuse('SURFACE_INPUT_INVALID', 'packaged bootstrap assets could not be authenticated', 'bootstrap');
|
|
85
|
+
const skillInput = bootstrapSkillInput(new Map(assets.map(asset => [asset.path, asset])), captured.tools);
|
|
86
|
+
if (!skillInput)
|
|
87
|
+
return refuse('SURFACE_INPUT_INVALID', 'packaged bootstrap assets are incomplete', 'bootstrap');
|
|
88
|
+
const { sources } = skillInput;
|
|
89
|
+
const inspection = await inspectSkillSet(skillInput);
|
|
90
|
+
if (inspection.kind !== 'complete')
|
|
91
|
+
return refuse('SURFACE_INPUT_INVALID', 'packaged bootstrap skill set was refused', 'bootstrap');
|
|
92
|
+
const inspected = projectInspectedSkillOutputs(inspection, captured.tools, SHIPPED_SETUP_SKILL);
|
|
93
|
+
if (inspected.kind !== 'complete')
|
|
94
|
+
return refuse('SURFACE_INPUT_INVALID', 'selected bootstrap output set was refused', captured.target);
|
|
95
|
+
const reads = await readAnchoredBatches(root, inspected.files.map(file => ({
|
|
96
|
+
path: file.path, maxBytes: MAX_SURFACE_FILE_BYTES, action: 'read-entry',
|
|
97
|
+
})), node, adapters);
|
|
98
|
+
if (!reads || reads.kind !== 'complete' || reads.results.length !== inspected.files.length) {
|
|
99
|
+
return refuse('SURFACE_STATE_CONFLICT', 'selected bootstrap destinations could not be authenticated', captured.target);
|
|
100
|
+
}
|
|
101
|
+
const mutations = [];
|
|
102
|
+
for (const [index, file] of inspected.files.entries()) {
|
|
103
|
+
const current = reads.results[index];
|
|
104
|
+
if (current.kind !== 'missing' && current.kind !== 'file')
|
|
105
|
+
return refuse('SURFACE_STATE_CONFLICT', 'selected bootstrap destination is not a regular file or missing', file.path);
|
|
106
|
+
if (current.kind === 'file' && current.sha256 !== file.sha256)
|
|
107
|
+
return refuse('SURFACE_STATE_CONFLICT', 'selected bootstrap destination contains unmanaged bytes', file.path);
|
|
108
|
+
if (current.kind === 'missing')
|
|
109
|
+
mutations.push({ path: file.path, phase: 'desired', operation: 'write-file',
|
|
110
|
+
bytes: file.bytes, sha256: file.sha256, expected: { kind: 'missing' } });
|
|
111
|
+
}
|
|
112
|
+
const admitted = await admitPublicationPlan({ mode: 'bootstrap', root, rootIdentity, node, inspected,
|
|
113
|
+
mutations }, adapters);
|
|
114
|
+
if (admitted.kind !== 'complete')
|
|
115
|
+
return admitted;
|
|
116
|
+
const sourceRows = sources.map(source => ({ path: source.path, sha256: sha256(source.bytes) }));
|
|
117
|
+
const files = inspected.files.map(file => ({ path: file.path, sha256: file.sha256 }));
|
|
118
|
+
const previewSha256 = sha256(JSON.stringify({ target: captured.target, sources: sourceRows, files }));
|
|
119
|
+
const result = { root, target: captured.target, sources: sourceRows, files,
|
|
120
|
+
previewSha256, changes: admitted.plan.changes };
|
|
121
|
+
const authority = { kind: 'selected-bootstrap-publication-authority' };
|
|
122
|
+
issuedSelectedBootstrapAuthorities.set(authority, Object.freeze({ root, rootIdentity, target: captured.target, node,
|
|
123
|
+
assets: Object.freeze(assets.map(({ path, sha256: digest, identity }) => ({ path, sha256: digest, identity }))),
|
|
124
|
+
inspected, result, plan: admitted.plan, adapters }));
|
|
125
|
+
return { kind: 'complete', artifact: null, destination: null, diagnostics: [], result, authority, status: 0 };
|
|
126
|
+
}
|
|
127
|
+
export async function applySelectedBootstrapSurface(authority) {
|
|
128
|
+
const record = authority !== null && typeof authority === 'object' ? issuedSelectedBootstrapAuthorities.get(authority) : undefined;
|
|
129
|
+
if (!record)
|
|
130
|
+
return refuse('GENERATED_PUBLICATION_FAILED', 'selected bootstrap publication authority is unknown or already consumed', 'bootstrap');
|
|
131
|
+
issuedSelectedBootstrapAuthorities.delete(authority);
|
|
132
|
+
const outcome = await runPublicationPlan(record.plan, { execute: record.adapters.execute,
|
|
133
|
+
revalidate: async () => selectedBootstrapRecordCurrent(record) });
|
|
134
|
+
if (outcome.kind !== 'complete')
|
|
135
|
+
return outcome;
|
|
136
|
+
return { kind: 'complete', artifact: null, destination: null, diagnostics: [],
|
|
137
|
+
result: record.result, authority, status: 0 };
|
|
138
|
+
}
|
|
139
|
+
export async function resumeSelectedBootstrapSurface(input, adapterOverrides = {}) {
|
|
140
|
+
const captured = selectedBootstrapInput({ root: input.root, target: input.target, tools: input.tools });
|
|
141
|
+
const durable = await readState(input.address);
|
|
142
|
+
if (!captured || durable.kind !== 'complete' || durable.value.kind !== 'bootstrap'
|
|
143
|
+
|| durable.value.value.checkoutRoot !== captured.root || durable.value.target !== captured.target
|
|
144
|
+
|| input.address.checkoutRoot !== captured.root || input.address.target !== captured.target
|
|
145
|
+
|| durable.value.value.stage === 'surface-previewed')
|
|
146
|
+
return refuse('GENERATED_OWNERSHIP_INVALID', 'bootstrap resume requires the current durable state, root and target', 'bootstrap');
|
|
147
|
+
const adapters = mergedAdapters(adapterOverrides);
|
|
148
|
+
let rootIdentity;
|
|
149
|
+
try {
|
|
150
|
+
const status = await lstat(captured.root);
|
|
151
|
+
if (!status.isDirectory() || status.isSymbolicLink() || await realpath(captured.root) !== captured.root)
|
|
152
|
+
throw new RefusalError('root');
|
|
153
|
+
rootIdentity = { dev: status.dev, ino: status.ino };
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
return refuse('GENERATED_INPUT_INVALID', `selected bootstrap root is not a canonical directory: ${errorCause(error)}`, captured.root);
|
|
157
|
+
}
|
|
158
|
+
const node = await nodeAuthority();
|
|
159
|
+
if (!node)
|
|
160
|
+
return refuse('SURFACE_INPUT_INVALID', 'bootstrap resume could not authenticate the node executable', 'bootstrap');
|
|
161
|
+
const ownership = await readAnchoredEntry(captured.root, OWNERSHIP_PATH, MAX_OWNERSHIP_BYTES, node, adapters);
|
|
162
|
+
if (ownership.kind !== 'missing' && ownership.kind !== 'file')
|
|
163
|
+
return refuse('GENERATED_OWNERSHIP_INVALID', 'bootstrap resume ownership could not be read safely', OWNERSHIP_PATH);
|
|
164
|
+
const assets = await selectedBootstrapAssets(node, adapters);
|
|
165
|
+
if (!assets)
|
|
166
|
+
return refuse('SURFACE_INPUT_INVALID', 'bootstrap resume packaged assets could not be authenticated', 'bootstrap');
|
|
167
|
+
const skillInput = bootstrapSkillInput(new Map(assets.map(asset => [asset.path, asset])), captured.tools);
|
|
168
|
+
if (!skillInput)
|
|
169
|
+
return refuse('SURFACE_INPUT_INVALID', 'bootstrap resume packaged assets are incomplete', 'bootstrap');
|
|
170
|
+
const { sources } = skillInput;
|
|
171
|
+
const inspection = await inspectSkillSet(skillInput);
|
|
172
|
+
if (inspection.kind !== 'complete')
|
|
173
|
+
return refuse('SURFACE_INPUT_INVALID', 'bootstrap resume skill set was refused', 'bootstrap');
|
|
174
|
+
const inspected = projectInspectedSkillOutputs(inspection, captured.tools, SHIPPED_SETUP_SKILL);
|
|
175
|
+
if (inspected.kind !== 'complete')
|
|
176
|
+
return refuse('SURFACE_INPUT_INVALID', 'bootstrap resume output projection was refused', captured.target);
|
|
177
|
+
const sourceRows = sources.map(source => ({ path: source.path, sha256: sha256(source.bytes) }));
|
|
178
|
+
const files = inspected.files.map(file => ({ path: file.path, sha256: file.sha256 }));
|
|
179
|
+
const previewSha256 = sha256(JSON.stringify({ target: captured.target, sources: sourceRows, files }));
|
|
180
|
+
const state = durable.value.value;
|
|
181
|
+
const sourcesCurrent = JSON.stringify(state.sources) === JSON.stringify(sourceRows)
|
|
182
|
+
&& JSON.stringify(state.published) === JSON.stringify(files) && state.previewSha256 === previewSha256;
|
|
183
|
+
if (ownership.kind === 'missing') {
|
|
184
|
+
if (sourcesCurrent)
|
|
185
|
+
return { kind: 'pending', status: 0, diagnostics: [],
|
|
186
|
+
result: null, canonicalOwnership: false };
|
|
187
|
+
const priorPreviewSha256 = sha256(JSON.stringify({ target: captured.target, sources: state.sources, files: state.published }));
|
|
188
|
+
if (input.expectedRevision !== durable.value.revision || input.expectedCommandSha256 !== state.commandSha256
|
|
189
|
+
|| priorPreviewSha256 !== state.previewSha256 || state.stage !== 'surface-published'
|
|
190
|
+
|| JSON.stringify(state.published.map(row => row.path)) !== JSON.stringify(files.map(row => row.path)))
|
|
191
|
+
return refuse('GENERATED_SOURCE_CHANGED', 'bootstrap resume package sources differ from the accepted durable preview', 'bootstrap');
|
|
192
|
+
const reads = await readAnchoredBatches(captured.root, inspected.files.map(file => ({
|
|
193
|
+
path: file.path, maxBytes: MAX_SURFACE_FILE_BYTES, action: 'read-entry',
|
|
194
|
+
})), node, adapters);
|
|
195
|
+
if (!reads || reads.kind !== 'complete' || reads.results.length !== inspected.files.length)
|
|
196
|
+
return refuse('SURFACE_STATE_CONFLICT', 'bootstrap refresh destinations could not be authenticated', captured.target);
|
|
197
|
+
const mutations = [];
|
|
198
|
+
for (const [index, file] of inspected.files.entries()) {
|
|
199
|
+
const current = reads.results[index];
|
|
200
|
+
const prior = state.published[index];
|
|
201
|
+
if (current.kind !== 'file' || current.sha256 !== prior.sha256 && current.sha256 !== file.sha256)
|
|
202
|
+
return refuse('SURFACE_STATE_CONFLICT', 'bootstrap refresh destination differs from its authenticated publication', file.path);
|
|
203
|
+
if (current.sha256 === file.sha256)
|
|
204
|
+
continue;
|
|
205
|
+
const expected = expectedState(current);
|
|
206
|
+
if (!expected)
|
|
207
|
+
return refuse('SURFACE_STATE_CONFLICT', 'bootstrap refresh destination could not be authenticated', file.path);
|
|
208
|
+
mutations.push({ path: file.path, phase: 'desired', operation: 'write-file',
|
|
209
|
+
bytes: file.bytes, sha256: file.sha256, expected });
|
|
210
|
+
}
|
|
211
|
+
const admitted = await admitPublicationPlan({ mode: 'bootstrap', root: captured.root, rootIdentity, node, inspected,
|
|
212
|
+
mutations, refresh: { address: input.address,
|
|
213
|
+
revision: durable.value.revision, commandSha256: state.commandSha256 } }, adapters);
|
|
214
|
+
if (admitted.kind !== 'complete')
|
|
215
|
+
return admitted;
|
|
216
|
+
const result = { root: captured.root, target: captured.target,
|
|
217
|
+
sources: sourceRows, files, previewSha256, changes: admitted.plan.changes };
|
|
218
|
+
const authority = { kind: 'selected-bootstrap-publication-authority' };
|
|
219
|
+
issuedSelectedBootstrapAuthorities.set(authority, Object.freeze({ root: captured.root, rootIdentity,
|
|
220
|
+
target: captured.target, node,
|
|
221
|
+
assets: Object.freeze(assets.map(({ path, sha256: digest, identity }) => ({ path, sha256: digest, identity }))),
|
|
222
|
+
inspected, result, plan: admitted.plan, adapters,
|
|
223
|
+
bootstrapState: Object.freeze({ address: input.address, record: durable.value }),
|
|
224
|
+
}));
|
|
225
|
+
return { kind: 'refresh', status: 0, diagnostics: [], result, authority,
|
|
226
|
+
canonicalOwnership: false };
|
|
227
|
+
}
|
|
228
|
+
if (ownership.kind !== 'file')
|
|
229
|
+
return refuse('GENERATED_OWNERSHIP_INVALID', 'bootstrap resume ownership changed after classification', OWNERSHIP_PATH);
|
|
230
|
+
if (!sourcesCurrent)
|
|
231
|
+
return refuse('GENERATED_SOURCE_CHANGED', 'bootstrap resume package sources differ from the accepted durable preview', 'bootstrap');
|
|
232
|
+
const base = await prepare({ configPath: join(captured.root, 'cockpit.toml') }, adapters);
|
|
233
|
+
if ('status' in base)
|
|
234
|
+
return base;
|
|
235
|
+
const canonical = await readOwnership(base, adapters);
|
|
236
|
+
if (base.root !== captured.root || canonical.kind !== 'complete'
|
|
237
|
+
|| canonical.manifest.format !== 'cockpit-generated-ownership/v3' || canonical.sha256 !== ownership.sha256)
|
|
238
|
+
return refuse('GENERATED_OWNERSHIP_INVALID', 'bootstrap resume requires canonical ownership bound to the current checkout and configuration', OWNERSHIP_PATH);
|
|
239
|
+
const current = await readAnchoredEntry(captured.root, OWNERSHIP_PATH, MAX_OWNERSHIP_BYTES, node, adapters);
|
|
240
|
+
const refreshed = await readState(input.address);
|
|
241
|
+
if (current.kind !== 'file' || current.sha256 !== ownership.sha256
|
|
242
|
+
|| sourceIdentity(current.identity) !== sourceIdentity(ownership.identity)
|
|
243
|
+
|| refreshed.kind !== 'complete' || JSON.stringify(refreshed.value) !== JSON.stringify(durable.value))
|
|
244
|
+
return refuse('GENERATED_SOURCE_CHANGED', 'bootstrap resume ownership or state changed during checking', 'bootstrap');
|
|
245
|
+
return { kind: 'complete', status: 0, diagnostics: [], canonicalOwnership: true,
|
|
246
|
+
result: { root: captured.root, target: captured.target, sources: sourceRows, files, previewSha256,
|
|
247
|
+
changes: [] } };
|
|
248
|
+
}
|
|
249
|
+
export async function authenticatedBootstrapPriorSurfaces(root, state, node, adapters) {
|
|
250
|
+
const publication = state.publication;
|
|
251
|
+
if (publication === null || typeof publication !== 'object' || consumedBootstrapStatePublications.has(publication))
|
|
252
|
+
return undefined;
|
|
253
|
+
const authenticated = authenticateStatePublication(state);
|
|
254
|
+
if (!authenticated || authenticated.status !== 'complete' || authenticated.record.kind !== 'bootstrap')
|
|
255
|
+
return undefined;
|
|
256
|
+
const record = authenticated.record;
|
|
257
|
+
if (record.target !== state.address.target || state.address.checkoutRoot !== root)
|
|
258
|
+
return undefined;
|
|
259
|
+
const surfaces = await bootstrapPublishedSurfaces(root, record, node, adapters);
|
|
260
|
+
if (surfaces)
|
|
261
|
+
consumedBootstrapStatePublications.add(publication);
|
|
262
|
+
return surfaces;
|
|
263
|
+
}
|
|
264
|
+
// A plain sync between an interrupted init and its rerun met init's own setup skill files with no
|
|
265
|
+
// ownership record and refused them, and its remedy had an agent delete them. The bootstrap record
|
|
266
|
+
// sync reads here comes from this user's private state root, scoped to this cockpit root, and the
|
|
267
|
+
// files must still hold exactly the bytes init published from the installed package.
|
|
268
|
+
export async function unfinishedInitSurfaces(root, node, adapters) {
|
|
269
|
+
if (process.platform !== 'darwin' && process.platform !== 'linux')
|
|
270
|
+
return undefined;
|
|
271
|
+
for (const target of SURFACE_TARGETS) {
|
|
272
|
+
const namespace = await selectBootstrapStateRoot({ platform: process.platform, home: process.env.HOME,
|
|
273
|
+
xdgStateHome: process.env.XDG_STATE_HOME, checkoutRoot: root, target });
|
|
274
|
+
if (namespace.kind !== 'complete')
|
|
275
|
+
return undefined;
|
|
276
|
+
const read = await readState(namespace.value.paths.bootstrap(target));
|
|
277
|
+
if (read.kind === 'complete')
|
|
278
|
+
return { root, target, published: read.value.value.published.map(row => row.path),
|
|
279
|
+
surfaces: await bootstrapPublishedSurfaces(root, read.value, node, adapters) };
|
|
280
|
+
}
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
async function bootstrapPublishedSurfaces(root, record, node, adapters) {
|
|
284
|
+
if (record.kind !== 'bootstrap' || record.value.stage === 'surface-previewed' || record.value.checkoutRoot !== root
|
|
285
|
+
|| record.value.target !== record.target)
|
|
286
|
+
return undefined;
|
|
287
|
+
const assets = await selectedBootstrapAssets(node, adapters);
|
|
288
|
+
if (!assets)
|
|
289
|
+
return undefined;
|
|
290
|
+
// The record names no tools, so they are the targets whose setup skill it published, in published order.
|
|
291
|
+
const allTools = bootstrapSkillInput(new Map(assets.map(asset => [asset.path, asset])), SURFACE_TARGETS);
|
|
292
|
+
const allInspection = allTools && await inspectSkillSet(allTools);
|
|
293
|
+
const allProjection = allInspection?.kind === 'complete'
|
|
294
|
+
? projectInspectedSkillOutputs(allInspection, SURFACE_TARGETS, SHIPPED_SETUP_SKILL) : undefined;
|
|
295
|
+
if (allProjection?.kind !== 'complete')
|
|
296
|
+
return undefined;
|
|
297
|
+
const targetByPath = new Map(allProjection.files.map(file => [file.path, file.target]));
|
|
298
|
+
const tools = [...new Set(record.value.published.map(row => targetByPath.get(row.path)))];
|
|
299
|
+
if (tools.some(tool => tool === undefined))
|
|
300
|
+
return undefined;
|
|
301
|
+
const skillInput = bootstrapSkillInput(new Map(assets.map(asset => [asset.path, asset])), tools);
|
|
302
|
+
if (!skillInput)
|
|
303
|
+
return undefined;
|
|
304
|
+
const sourceRows = skillInput.sources.map(source => ({ path: source.path, sha256: sha256(source.bytes) }));
|
|
305
|
+
if (JSON.stringify(sourceRows) !== JSON.stringify(record.value.sources))
|
|
306
|
+
return undefined;
|
|
307
|
+
const inspection = await inspectSkillSet(skillInput);
|
|
308
|
+
if (inspection.kind !== 'complete')
|
|
309
|
+
return undefined;
|
|
310
|
+
const inspected = projectInspectedSkillOutputs(inspection, tools, SHIPPED_SETUP_SKILL);
|
|
311
|
+
if (inspected.kind !== 'complete')
|
|
312
|
+
return undefined;
|
|
313
|
+
const files = inspected.files;
|
|
314
|
+
const published = files.map(file => ({ path: file.path, sha256: file.sha256 }));
|
|
315
|
+
if (JSON.stringify(published) !== JSON.stringify(record.value.published))
|
|
316
|
+
return undefined;
|
|
317
|
+
const reads = await readAnchoredBatches(root, files.map(file => ({
|
|
318
|
+
path: file.path, maxBytes: MAX_SURFACE_FILE_BYTES, action: 'read-entry',
|
|
319
|
+
})), node, adapters);
|
|
320
|
+
if (!reads || reads.kind !== 'complete' || reads.results.length !== files.length
|
|
321
|
+
|| reads.results.some((read, index) => read.kind !== 'file' || read.sha256 !== files[index].sha256))
|
|
322
|
+
return undefined;
|
|
323
|
+
return files.map(file => ({
|
|
324
|
+
kind: 'file', path: file.path, harnesses: [file.target], scope: 'always',
|
|
325
|
+
capability: 'instructed', writer: 'cockpit', schemaVersion: 1,
|
|
326
|
+
ownership: 'whole-file', sha256: file.sha256,
|
|
327
|
+
}));
|
|
328
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type PublicationPlan } from '../../generated-environment/transaction.js';
|
|
2
|
+
import { type GeneratedEnvironmentCommandAdapters, type GeneratedEnvironmentCommandInput, type GeneratedEnvironmentCommandOutcome, type GeneratedEnvironmentCurrentness, type GeneratedEnvironmentRepairAuthority, type PreparedEnvironment } from './shared.js';
|
|
3
|
+
interface RepairRecord {
|
|
4
|
+
readonly prepared: PreparedEnvironment;
|
|
5
|
+
readonly plan: PublicationPlan;
|
|
6
|
+
readonly adapters: GeneratedEnvironmentCommandAdapters;
|
|
7
|
+
}
|
|
8
|
+
export declare const issuedRepairs: WeakMap<GeneratedEnvironmentRepairAuthority, RepairRecord>;
|
|
9
|
+
export declare function issueRepairAuthority(record: RepairRecord): GeneratedEnvironmentRepairAuthority;
|
|
10
|
+
export declare function prepareGeneratedEnvironmentCurrentness(input: GeneratedEnvironmentCommandInput, adapterOverrides?: Partial<GeneratedEnvironmentCommandAdapters>): Promise<GeneratedEnvironmentCurrentness>;
|
|
11
|
+
export declare function checkCanonicalGeneratedEnvironment(input: GeneratedEnvironmentCommandInput, seed?: PreparedEnvironment, adapters?: GeneratedEnvironmentCommandAdapters): Promise<GeneratedEnvironmentCommandOutcome>;
|
|
12
|
+
export declare function checkGeneratedEnvironment(input: GeneratedEnvironmentCommandInput, adapterOverrides?: Partial<GeneratedEnvironmentCommandAdapters>): Promise<GeneratedEnvironmentCommandOutcome>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { installedPackageMetadata } from '../../package-metadata.js';
|
|
2
|
+
import { proposeGeneratedEnvironmentRepair } from '../../repair.js';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { loadCockpitConfig, observeConfigSource } from '../../config/load.js';
|
|
5
|
+
import { classifyGeneratedPaths, encodeOwnershipManifest } from '../../surfaces/ownership.js';
|
|
6
|
+
import { SIBLING_GUIDE_MARKER } from '../../surfaces/projection.js';
|
|
7
|
+
import { nodeAuthority, readAnchoredFile, readAnchoredBatches } from '../../generated-environment/transaction.js';
|
|
8
|
+
import { errorCause, RefusalError } from '../contract.js';
|
|
9
|
+
import { resolveDockerExecutable } from '../executable.js';
|
|
10
|
+
import { assessComposeTopology, composeProjectDirectory, observeComposeTopology } from '../compose-topology.js';
|
|
11
|
+
import { declaredRuntime } from '../declaration.js';
|
|
12
|
+
import { refusalOutcome as refuse } from '../../refusal.js';
|
|
13
|
+
import { MAX_GENERATED_BYTES, OWNERSHIP_PATH, compareCodeUnits, complete, defaultAdapters, generatedPathFacts, mergedAdapters, overrideChangedRefusal, sha256, sourceIdentity, } from './shared.js';
|
|
14
|
+
import { prepare, readOwnership } from './prepare.js';
|
|
15
|
+
import { classifyOwnedState, inputSourcesCurrent, observeSurfaceState, ownedInputOverlap, planObservedSurfaces, preparePublication, surfacePlanRefusal, } from './publication.js';
|
|
16
|
+
import { prepareSurfaces } from './surfaces.js';
|
|
17
|
+
export const issuedRepairs = new WeakMap();
|
|
18
|
+
export function issueRepairAuthority(record) {
|
|
19
|
+
const authority = { kind: 'generated-environment-repair' };
|
|
20
|
+
issuedRepairs.set(authority, record);
|
|
21
|
+
return authority;
|
|
22
|
+
}
|
|
23
|
+
export async function prepareGeneratedEnvironmentCurrentness(input, adapterOverrides = {}) {
|
|
24
|
+
const adapters = mergedAdapters(adapterOverrides);
|
|
25
|
+
const base = await prepare(input, adapters);
|
|
26
|
+
const ownership = 'status' in base ? undefined : await readOwnership(base, adapters);
|
|
27
|
+
const publication = 'status' in base ? base : await preparePublication(input, adapters, base, ownership);
|
|
28
|
+
const prepared = 'prepared' in publication ? publication.prepared : undefined;
|
|
29
|
+
const outcome = 'status' in publication ? publication : await checkCanonicalGeneratedEnvironment(input, prepared, adapters);
|
|
30
|
+
const authority = prepared && 'plan' in publication
|
|
31
|
+
? issueRepairAuthority({ prepared, plan: publication.plan, adapters }) : undefined;
|
|
32
|
+
return {
|
|
33
|
+
outcome,
|
|
34
|
+
changes: 'plan' in publication ? publication.plan.changes : [],
|
|
35
|
+
storedProducer: ownership?.kind === 'complete' ? ownership.manifest.producer : null,
|
|
36
|
+
currentProducer: { package: installedPackageMetadata.name, version: installedPackageMetadata.version,
|
|
37
|
+
sourceRevision: installedPackageMetadata.cockpitSourceRevision },
|
|
38
|
+
fixContext: { resolvedConfigPath: prepared?.configPath ?? null,
|
|
39
|
+
generatedTargets: prepared ? [...prepared.owned, ...prepared.surfaces].map(row => row.path) : [] },
|
|
40
|
+
...(authority ? { repairAuthority: authority } : {}),
|
|
41
|
+
...('status' in publication ? { repairRefusal: publication }
|
|
42
|
+
: 'refusal' in publication ? { repairRefusal: publication.refusal } : {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// Each copied guide carries a marker naming its sibling, path and digest, so the guides whose copy in
|
|
46
|
+
// the AGENTS.md on disk differs from the one sync would write can be named rather than the file alone.
|
|
47
|
+
function staleSiblingGuides(prepared, observed) {
|
|
48
|
+
const desired = prepared.surfaces.find(entry => entry.path === 'AGENTS.md');
|
|
49
|
+
const current = observed.find(entry => entry.path === 'AGENTS.md');
|
|
50
|
+
const markers = (text) => new Map([...text.matchAll(SIBLING_GUIDE_MARKER)].map(match => [`${match[1]}/${match[2]}`, match[3]]));
|
|
51
|
+
const want = markers(desired?.kind === 'shared-region' && desired.region.format === 'markdown-delimited' ? desired.region.bytes : '');
|
|
52
|
+
const have = markers(current?.kind === 'file' ? current.bytes : '');
|
|
53
|
+
return [...new Set([...want.keys(), ...have.keys()])].filter(key => want.get(key) !== have.get(key)).sort(compareCodeUnits);
|
|
54
|
+
}
|
|
55
|
+
export async function checkCanonicalGeneratedEnvironment(input, seed, adapters = defaultAdapters) {
|
|
56
|
+
const base = seed ?? await prepare(input, adapters);
|
|
57
|
+
if ('status' in base)
|
|
58
|
+
return base;
|
|
59
|
+
const ownership = await readOwnership(base, adapters);
|
|
60
|
+
if (ownership.kind === 'unsupported')
|
|
61
|
+
return refuse('GENERATED_OWNERSHIP_FORMAT_UNSUPPORTED', `generated ownership format ${ownership.format} is not supported`, OWNERSHIP_PATH);
|
|
62
|
+
if (ownership.kind !== 'complete')
|
|
63
|
+
return refuse(ownership.kind === 'missing' ? 'GENERATED_OWNERSHIP_MISSING' : 'GENERATED_OWNERSHIP_INVALID', 'durable generated ownership metadata is missing or invalid', OWNERSHIP_PATH, 'restore trusted ownership metadata or run npx --no cockpit sync --config <your cockpit.toml>');
|
|
64
|
+
const priorSurfaces = ownership.manifest.entries.filter((row) => 'harnesses' in row);
|
|
65
|
+
const prepared = seed?.surfaceSelftest ? seed : await prepareSurfaces(base, priorSurfaces, adapters, ownership.manifest);
|
|
66
|
+
if ('status' in prepared)
|
|
67
|
+
return prepared;
|
|
68
|
+
if (prepared.changedOverrideOriginals[0])
|
|
69
|
+
return overrideChangedRefusal(prepared.changedOverrideOriginals[0]);
|
|
70
|
+
const ownedInput = await ownedInputOverlap(prepared, ownership.manifest.entries);
|
|
71
|
+
if (ownedInput.kind === 'invalid')
|
|
72
|
+
return refuse('GENERATED_OWNERSHIP_INVALID', 'authenticated input identity could not be compared with generated ownership metadata', OWNERSHIP_PATH, 'restore trusted ownership metadata or run npx --no cockpit sync --config <your cockpit.toml>');
|
|
73
|
+
if (ownedInput.path)
|
|
74
|
+
return refuse('GENERATED_OWNERSHIP_INVALID', 'generated ownership metadata overlaps an authenticated input', ownedInput.path, 'restore trusted ownership metadata or run npx --no cockpit sync --config <your cockpit.toml>');
|
|
75
|
+
const surfaceState = await observeSurfaceState(prepared, priorSurfaces, adapters);
|
|
76
|
+
if (!surfaceState)
|
|
77
|
+
return refuse('SURFACE_STATE_CONFLICT', 'surface state could not be observed safely', 'surfaces');
|
|
78
|
+
const classified = await classifyOwnedState(prepared, ownership.manifest, surfaceState.raw);
|
|
79
|
+
if (classified.kind !== 'complete')
|
|
80
|
+
return refuse(classified.diagnostic.code, classified.diagnostic.message, classified.diagnostic.target);
|
|
81
|
+
const unknown = classified.paths.find(row => row.state === 'unknown');
|
|
82
|
+
if (unknown)
|
|
83
|
+
return refuse('SURFACE_STATE_CONFLICT', 'owned surface identity or confinement is unknown', unknown.fact.path);
|
|
84
|
+
const effectivePriorSurfaces = priorSurfaces;
|
|
85
|
+
const surfacePaths = new Set(prepared.surfaces.map(row => row.path));
|
|
86
|
+
const staleEnvironment = ownership.manifest.entries.filter(row => !('harnesses' in row) && !surfacePaths.has(row.path)
|
|
87
|
+
&& !prepared.owned.some(file => file.path === row.path));
|
|
88
|
+
if (staleEnvironment.length > 0)
|
|
89
|
+
return refuse('GENERATED_STALE_ARTIFACT', 'a formerly owned generated artifact remains in durable ownership metadata', staleEnvironment[0].path, 'a person runs npx --no cockpit sync --config <your cockpit.toml> --interactive in a terminal and approves the stale-removal digest it shows');
|
|
90
|
+
const required = generatedPathFacts(prepared, 'required-after-sync');
|
|
91
|
+
if (required.kind !== 'complete')
|
|
92
|
+
return refuse(required.diagnostic.code, required.diagnostic.message, required.diagnostic.target);
|
|
93
|
+
const canonicalReads = await readAnchoredBatches(prepared.root, prepared.generated.map(file => ({
|
|
94
|
+
path: file.path, maxBytes: MAX_GENERATED_BYTES, action: 'read-file',
|
|
95
|
+
})), prepared.node, adapters);
|
|
96
|
+
// An indexed refusal omits the prefix receipts. Reobserve only that prefix to retain
|
|
97
|
+
// first-failure ordering; the failed row remains fatal even if a later read could succeed.
|
|
98
|
+
for (const [index, file] of prepared.generated.entries()) {
|
|
99
|
+
try {
|
|
100
|
+
const read = canonicalReads?.kind === 'complete' ? canonicalReads.results[index]
|
|
101
|
+
: canonicalReads?.kind === 'invalid' && index < canonicalReads.index
|
|
102
|
+
? await readAnchoredFile(prepared.root, file.path, MAX_GENERATED_BYTES, prepared.node, adapters)
|
|
103
|
+
: undefined;
|
|
104
|
+
if (!read || read.kind !== 'complete')
|
|
105
|
+
throw new RefusalError('generated artifact unavailable');
|
|
106
|
+
const fact = required.facts.find(fact => fact.path === file.path);
|
|
107
|
+
const classification = classifyGeneratedPaths([fact], [{ path: file.path, kind: 'file', bytes: read.bytes,
|
|
108
|
+
confinement: 'confined', identity: sourceIdentity(read.identity) }]);
|
|
109
|
+
if (classification.kind !== 'complete' || classification.paths[0]?.state === 'unknown')
|
|
110
|
+
return refuse('GENERATED_ARTIFACT_MISSING', 'owned generated artifact could not be classified safely', file.path, 'run npx --no cockpit sync --config <your cockpit.toml>');
|
|
111
|
+
if (classification.paths[0]?.state !== 'unchanged')
|
|
112
|
+
return refuse('GENERATED_ARTIFACT_DRIFT', 'owned generated artifact differs from canonical bytes', file.path, 'run npx --no cockpit sync --config <your cockpit.toml>');
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
return refuse('GENERATED_ARTIFACT_MISSING', `owned generated artifact became unreadable: ${errorCause(error)}`, file.path, 'run npx --no cockpit sync --config <your cockpit.toml>');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const comparison = planObservedSurfaces(prepared, effectivePriorSurfaces, surfaceState);
|
|
119
|
+
if (comparison.kind !== 'complete')
|
|
120
|
+
return surfacePlanRefusal(prepared, comparison.diagnostic);
|
|
121
|
+
const drift = comparison.actions.map(action => action.path);
|
|
122
|
+
// Removing a formerly owned path is plain sync's to do only where [autonomy] stale_delete is agent;
|
|
123
|
+
// otherwise it needs a person's approval of its exact digest, so send the reader to the one command that can.
|
|
124
|
+
const stale = comparison.actions.find(action => action.kind === 'remove');
|
|
125
|
+
if (stale)
|
|
126
|
+
return prepared.workEnvironment.policy.autonomy.stale_delete === 'agent'
|
|
127
|
+
? refuse('GENERATED_ARTIFACT_DRIFT', 'a formerly generated path is still on disk and in the ownership record', stale.path, 'run npx --no cockpit sync --config <your cockpit.toml>')
|
|
128
|
+
: refuse('GENERATED_ARTIFACT_DRIFT', 'a formerly generated path is still on disk and in the ownership record', stale.path, 'a person runs npx --no cockpit sync --config <your cockpit.toml> --interactive in a terminal and approves the stale-removal digest it shows');
|
|
129
|
+
const staleGuides = drift.includes('AGENTS.md') ? staleSiblingGuides(prepared, surfaceState.observed) : [];
|
|
130
|
+
if (staleGuides.length > 0)
|
|
131
|
+
return refuse('GENERATED_ARTIFACT_DRIFT', `AGENTS.md holds an out-of-date copy of ${staleGuides.join(', ')}: the guide changed in its sibling, or the declared guides changed, since the last sync`, 'AGENTS.md', 'run npx --no cockpit sync --config <your cockpit.toml>, then commit AGENTS.md together with the guide change');
|
|
132
|
+
if (drift.length > 0)
|
|
133
|
+
return refuse('GENERATED_ARTIFACT_DRIFT', 'owned surface state differs from the canonical model', drift[0], 'run npx --no cockpit sync --config <your cockpit.toml>');
|
|
134
|
+
if (ownership.bytes !== encodeOwnershipManifest(prepared).bytes)
|
|
135
|
+
return refuse('GENERATED_OWNERSHIP_DRIFT', 'generated ownership metadata differs from the current canonical manifest', OWNERSHIP_PATH, 'run npx --no cockpit sync --config <your cockpit.toml>');
|
|
136
|
+
const source = await observeConfigSource(prepared.configPath);
|
|
137
|
+
if (source.state !== 'available' || source.sha256 !== prepared.sourceSha256
|
|
138
|
+
|| !await inputSourcesCurrent(prepared, adapters)) {
|
|
139
|
+
return refuse('GENERATED_SOURCE_CHANGED', 'a generated input changed during generated environment checking', 'generated-environment');
|
|
140
|
+
}
|
|
141
|
+
return complete('check', prepared);
|
|
142
|
+
}
|
|
143
|
+
// A cockpit running its own Compose files owns that topology, so check reads it as Compose resolves
|
|
144
|
+
// it and reports what would break parallel stacks.
|
|
145
|
+
async function composeTopologyDiagnostics(configPath, sourceSha256, adapters) {
|
|
146
|
+
const loaded = await loadCockpitConfig(configPath);
|
|
147
|
+
if (loaded.kind !== 'complete' || loaded.result.source.sha256 !== sourceSha256)
|
|
148
|
+
return [{ severity: 'error',
|
|
149
|
+
code: 'GENERATED_SOURCE_CHANGED', message: 'the configuration changed while check was reading the Compose topology', target: configPath }];
|
|
150
|
+
const config = loaded.result.config;
|
|
151
|
+
if (declaredRuntime(config.environment) !== 'compose')
|
|
152
|
+
return [];
|
|
153
|
+
const env = { PATH: process.env.PATH, DOCKER_HOST: process.env.DOCKER_HOST, DOCKER_CONTEXT: process.env.DOCKER_CONTEXT,
|
|
154
|
+
DOCKER_TLS_VERIFY: process.env.DOCKER_TLS_VERIFY, DOCKER_CERT_PATH: process.env.DOCKER_CERT_PATH };
|
|
155
|
+
const [docker, node] = await Promise.all([resolveDockerExecutable(process.env), nodeAuthority()]);
|
|
156
|
+
const observed = docker.state !== 'available' ? { kind: 'unavailable', reason: docker.reason }
|
|
157
|
+
: !node ? { kind: 'unavailable', reason: 'the current Node executable could not be authenticated' }
|
|
158
|
+
: await observeComposeTopology({ root: dirname(loaded.result.source.path), references: config.environment.compose_files,
|
|
159
|
+
projectName: config.cockpit.name, docker: docker.value, node, env }, adapters.execute);
|
|
160
|
+
if (observed.kind !== 'complete')
|
|
161
|
+
return [{ severity: 'error', code: 'COMPOSE_TOPOLOGY_UNAVAILABLE',
|
|
162
|
+
message: `check could not read the declared Compose topology, so its ports, healthchecks and images are unverified: ${observed.reason}`,
|
|
163
|
+
target: config.environment.compose_files.join(', '),
|
|
164
|
+
fix: 'install Docker with the Compose plugin on PATH and make `docker compose config` succeed for environment.compose_files' }];
|
|
165
|
+
const root = dirname(loaded.result.source.path);
|
|
166
|
+
const siblingsRoot = resolve(root, config.cockpit.siblings_root);
|
|
167
|
+
return assessComposeTopology(observed.services, {
|
|
168
|
+
projectDirectory: composeProjectDirectory(root, config.environment.compose_files),
|
|
169
|
+
portVariables: Object.keys(config.ports.bases),
|
|
170
|
+
siblings: config.sibling.map(sibling => ({
|
|
171
|
+
slug: sibling.slug, var: sibling.var, mount: sibling.mount, path: resolve(siblingsRoot, sibling.dir)
|
|
172
|
+
})),
|
|
173
|
+
hostPaths: config.environment.generated_host_paths.map(declared => ({ declared, path: resolve(root, declared) })),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
export async function checkGeneratedEnvironment(input, adapterOverrides = {}) {
|
|
177
|
+
const adapters = mergedAdapters(adapterOverrides);
|
|
178
|
+
const currentness = await prepareGeneratedEnvironmentCurrentness(input, adapters);
|
|
179
|
+
const findings = currentness.outcome.kind === 'complete'
|
|
180
|
+
? await composeTopologyDiagnostics(input.configPath, currentness.outcome.result.sourceSha256, adapters) : [];
|
|
181
|
+
const diagnostics = [...currentness.outcome.diagnostics, ...findings];
|
|
182
|
+
const outcome = findings.some(row => row.severity === 'error')
|
|
183
|
+
? { kind: 'refusal', artifact: null, destination: null, result: null, status: 2, diagnostics }
|
|
184
|
+
: { ...currentness.outcome, diagnostics };
|
|
185
|
+
return { ...outcome, fixContext: currentness.fixContext, report: {
|
|
186
|
+
storedProducer: currentness.storedProducer, currentProducer: currentness.currentProducer,
|
|
187
|
+
repair: proposeGeneratedEnvironmentRepair(currentness),
|
|
188
|
+
} };
|
|
189
|
+
}
|