@b00dea/cockpit 0.0.0-bootstrap.0 → 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 +21 -1
- 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 +76 -1
- 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,547 @@
|
|
|
1
|
+
import { initCommand } from '../../fix-catalogue.js';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { lstat, open } from 'node:fs/promises';
|
|
4
|
+
import { basename, dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { SURFACE_PUBLICATION_ANCHORS } from '../../surfaces/harness-registry.js';
|
|
6
|
+
import { classifyGeneratedPaths, encodeOwnershipManifest, projectGeneratedPathFacts, } from '../../surfaces/ownership.js';
|
|
7
|
+
import { observeSiblingGuides } from '../../surfaces/projection.js';
|
|
8
|
+
import { planSurfaceChanges } from '../../surfaces/sync.js';
|
|
9
|
+
import { readAnchoredFile, readAnchoredEntry, readAnchoredBatches, expectedState, admitPublicationPlan, } from '../../generated-environment/transaction.js';
|
|
10
|
+
import { RefusalError, hasErrorCode } from '../contract.js';
|
|
11
|
+
import { cockpitId } from '../identity.js';
|
|
12
|
+
import { revalidateGeneratedCheckoutIdentity } from '../setup-observation.js';
|
|
13
|
+
import { refusalOutcome as refuse } from '../../refusal.js';
|
|
14
|
+
import { COMPOSE_SOURCE_BYTES, MAX_GENERATED_BYTES, MAX_OWNED_FILES, MAX_OWNERSHIP_BYTES, MAX_SURFACE_FILE_BYTES, OWNERSHIP_PATH, SURFACE_ROOT, compareCodeUnits, confined, generatedPathFacts, mergedAdapters, sha256, siblingRootsCurrent, sourceIdentity, } from './shared.js';
|
|
15
|
+
import { expectedOutputExists, prepare, readOwnership, renamedStackRefusal, safePath, syncCommandInput } from './prepare.js';
|
|
16
|
+
import { authenticatedBootstrapPriorSurfaces, unfinishedInitSurfaces } from './bootstrap-skill.js';
|
|
17
|
+
import { prepareSurfaces } from './surfaces.js';
|
|
18
|
+
export async function inputSourcesCurrent(prepared, adapters, deadline) {
|
|
19
|
+
try {
|
|
20
|
+
if (deadline !== undefined && Date.now() >= deadline)
|
|
21
|
+
return false;
|
|
22
|
+
if (prepared.runtimeCheckout && !await revalidateGeneratedCheckoutIdentity(prepared.runtimeCheckout))
|
|
23
|
+
return false;
|
|
24
|
+
if (!await siblingRootsCurrent(prepared.siblingRoots))
|
|
25
|
+
return false;
|
|
26
|
+
if (prepared.guardHandler) {
|
|
27
|
+
const handler = prepared.guardHandler;
|
|
28
|
+
const root = await lstat(prepared.root);
|
|
29
|
+
const config = await lstat(prepared.configPath);
|
|
30
|
+
const module = await readAnchoredFile(dirname(handler.module), basename(handler.module), MAX_GENERATED_BYTES, prepared.node, adapters);
|
|
31
|
+
if (String(root.dev) !== handler.rootDevice || String(root.ino) !== handler.rootInode
|
|
32
|
+
|| config.isSymbolicLink() || String(config.dev) !== handler.configDevice || String(config.ino) !== handler.configInode
|
|
33
|
+
|| module.kind !== 'complete' || module.sha256 !== handler.moduleSha256)
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const target = await safePath(prepared.root, prepared.configSource.path, false);
|
|
37
|
+
if (!target)
|
|
38
|
+
return false;
|
|
39
|
+
for (const expected of prepared.sourceRoots) {
|
|
40
|
+
const status = await lstat(expected.root);
|
|
41
|
+
if (!status.isDirectory() || status.isSymbolicLink() || status.dev !== expected.identity.dev || status.ino !== expected.identity.ino)
|
|
42
|
+
return false;
|
|
43
|
+
if (deadline !== undefined && Date.now() >= deadline)
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (prepared.guideSources.requests.length > 0) {
|
|
47
|
+
const current = await observeSiblingGuides({
|
|
48
|
+
allowedRoots: [...new Set(prepared.guideSources.requests.map(row => row.root))],
|
|
49
|
+
requests: prepared.guideSources.requests,
|
|
50
|
+
});
|
|
51
|
+
const observations = current.kind === 'complete'
|
|
52
|
+
? [...current.observations].sort((left, right) => compareCodeUnits(left.sourcePath, right.sourcePath)) : [];
|
|
53
|
+
const missing = current.kind === 'complete' ? [...current.missing].sort(compareCodeUnits) : [];
|
|
54
|
+
if ((deadline !== undefined && Date.now() >= deadline) || current.kind !== 'complete'
|
|
55
|
+
|| missing.length !== prepared.guideSources.missing.length
|
|
56
|
+
|| missing.some((path, index) => path !== prepared.guideSources.missing[index])
|
|
57
|
+
|| observations.length !== prepared.guideSources.observations.length
|
|
58
|
+
|| observations.some((row, index) => {
|
|
59
|
+
const expected = prepared.guideSources.observations[index];
|
|
60
|
+
return !expected || row.slug !== expected.slug || row.sourcePath !== expected.sourcePath
|
|
61
|
+
|| row.sourceEntryType !== expected.sourceEntryType || row.sourceLinkTarget !== expected.sourceLinkTarget
|
|
62
|
+
|| row.contentPath !== expected.contentPath || row.content !== expected.content
|
|
63
|
+
|| row.contentBytes !== expected.contentBytes || row.contentSha256 !== expected.contentSha256
|
|
64
|
+
|| row.contentIdentity !== expected.contentIdentity;
|
|
65
|
+
}))
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const mcpByRoot = new Map();
|
|
69
|
+
for (const evidence of prepared.sourceEvidence)
|
|
70
|
+
if (evidence.kind === 'mcp') {
|
|
71
|
+
const group = mcpByRoot.get(evidence.root) ?? [];
|
|
72
|
+
group.push(evidence);
|
|
73
|
+
mcpByRoot.set(evidence.root, group);
|
|
74
|
+
}
|
|
75
|
+
for (const [root, evidence] of mcpByRoot) {
|
|
76
|
+
const reads = await readAnchoredBatches(root, evidence.map(row => ({
|
|
77
|
+
path: row.path, maxBytes: MAX_GENERATED_BYTES, action: 'read-file',
|
|
78
|
+
})), prepared.node, adapters, deadline);
|
|
79
|
+
if (!reads || reads.kind !== 'complete' || reads.results.length !== evidence.length || reads.results.some((read, index) => read.kind !== 'complete'
|
|
80
|
+
|| read.sha256 !== evidence[index].sha256 || sourceIdentity(read.identity) !== evidence[index].identity))
|
|
81
|
+
return false;
|
|
82
|
+
if (deadline !== undefined && Date.now() >= deadline)
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
const reads = await readAnchoredBatches(prepared.root, [
|
|
86
|
+
{ path: prepared.configSource.path, maxBytes: MAX_GENERATED_BYTES, action: 'read-file' },
|
|
87
|
+
...prepared.customSources.map(source => ({ path: source.path, maxBytes: source.maxBytes, action: 'read-file' })),
|
|
88
|
+
...(prepared.nativeHookPolicySource ? [{ path: prepared.nativeHookPolicySource.path,
|
|
89
|
+
maxBytes: MAX_SURFACE_FILE_BYTES, action: 'read-file' }] : []),
|
|
90
|
+
...prepared.adoptedSources.map(source => ({ path: source.path, maxBytes: MAX_SURFACE_FILE_BYTES, action: 'read-file' })),
|
|
91
|
+
...prepared.composeSources.map(source => ({ path: source.path, maxBytes: COMPOSE_SOURCE_BYTES, action: 'read-file' })),
|
|
92
|
+
], prepared.node, adapters, deadline);
|
|
93
|
+
const adoptedOffset = prepared.customSources.length + 1 + (prepared.nativeHookPolicySource ? 1 : 0);
|
|
94
|
+
const composeOffset = adoptedOffset + prepared.adoptedSources.length;
|
|
95
|
+
if ((deadline !== undefined && Date.now() >= deadline) || !reads || reads.kind !== 'complete'
|
|
96
|
+
|| reads.results.length !== composeOffset + prepared.composeSources.length)
|
|
97
|
+
return false;
|
|
98
|
+
const source = reads.results[0];
|
|
99
|
+
const expected = prepared.configSourceIdentity;
|
|
100
|
+
const localCurrent = source.kind === 'complete' && source.sha256 === prepared.configSource.sha256
|
|
101
|
+
&& source.identity.root.dev === expected.root.dev && source.identity.root.ino === expected.root.ino
|
|
102
|
+
&& source.identity.file.dev === expected.file.dev && source.identity.file.ino === expected.file.ino
|
|
103
|
+
&& source.identity.chain.length === expected.chain.length
|
|
104
|
+
&& source.identity.chain.every((entry, index) => entry.dev === expected.chain[index]?.dev && entry.ino === expected.chain[index]?.ino)
|
|
105
|
+
&& prepared.customSources.every((custom, index) => {
|
|
106
|
+
const read = reads.results[index + 1];
|
|
107
|
+
return read.kind === 'complete' && read.sha256 === custom.sha256
|
|
108
|
+
&& sourceIdentity(read.identity) === sourceIdentity(custom.identity);
|
|
109
|
+
})
|
|
110
|
+
&& (!prepared.nativeHookPolicySource || (() => {
|
|
111
|
+
const read = reads.results[prepared.customSources.length + 1];
|
|
112
|
+
return read.kind === 'complete' && read.sha256 === prepared.nativeHookPolicySource.sha256
|
|
113
|
+
&& sourceIdentity(read.identity) === sourceIdentity(prepared.nativeHookPolicySource.identity);
|
|
114
|
+
})())
|
|
115
|
+
&& prepared.adoptedSources.every((adopted, index) => {
|
|
116
|
+
const read = reads.results[adoptedOffset + index];
|
|
117
|
+
return read.kind === 'complete' && read.sha256 === adopted.sha256
|
|
118
|
+
&& sourceIdentity(read.identity) === sourceIdentity(adopted.identity);
|
|
119
|
+
})
|
|
120
|
+
&& prepared.composeSources.every((compose, index) => {
|
|
121
|
+
const read = reads.results[composeOffset + index];
|
|
122
|
+
return read.kind === 'complete' && read.sha256 === compose.sha256
|
|
123
|
+
&& sourceIdentity(read.identity) === sourceIdentity(compose.identity);
|
|
124
|
+
});
|
|
125
|
+
if (!localCurrent)
|
|
126
|
+
return false;
|
|
127
|
+
const assetReads = prepared.surfaceAssets.length === 0
|
|
128
|
+
? { kind: 'complete', results: [] }
|
|
129
|
+
: await readAnchoredBatches(SURFACE_ROOT, prepared.surfaceAssets.map(asset => ({
|
|
130
|
+
path: asset.path, maxBytes: MAX_SURFACE_FILE_BYTES, action: 'read-file',
|
|
131
|
+
})), prepared.node, adapters, deadline);
|
|
132
|
+
if (!assetReads || assetReads.kind !== 'complete' || assetReads.results.length !== prepared.surfaceAssets.length || assetReads.results.some((read, index) => read.kind !== 'complete'
|
|
133
|
+
|| read.sha256 !== prepared.surfaceAssets[index].sha256
|
|
134
|
+
|| sourceIdentity(read.identity) !== sourceIdentity(prepared.surfaceAssets[index].identity)))
|
|
135
|
+
return false;
|
|
136
|
+
if (deadline !== undefined && Date.now() >= deadline)
|
|
137
|
+
return false;
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
catch { /* a failed re-observation means the inputs changed; the caller refuses. */
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function publicationInputPath(root, path) {
|
|
145
|
+
const offset = relative(root, isAbsolute(path) ? path : resolve(root, path));
|
|
146
|
+
return offset !== '' && offset !== '..' && !offset.startsWith(`..${sep}`) && !isAbsolute(offset)
|
|
147
|
+
? offset : undefined;
|
|
148
|
+
}
|
|
149
|
+
export async function ownedInputOverlap(prepared, prior) {
|
|
150
|
+
const presentInputPaths = [
|
|
151
|
+
prepared.configPath,
|
|
152
|
+
resolve(prepared.root, prepared.configSource.path),
|
|
153
|
+
...prepared.guideSources.observations.flatMap(observation => [observation.sourcePath, observation.contentPath]),
|
|
154
|
+
...prepared.sourceEvidence.filter((row) => row.kind === 'mcp')
|
|
155
|
+
.map(evidence => resolve(evidence.root, evidence.path)),
|
|
156
|
+
...prepared.customSources.map(source => resolve(prepared.root, source.path)),
|
|
157
|
+
...(prepared.nativeHookPolicySource ? [resolve(prepared.root, prepared.nativeHookPolicySource.path)] : []),
|
|
158
|
+
...prepared.adoptedSources.map(source => resolve(prepared.root, source.path)),
|
|
159
|
+
];
|
|
160
|
+
const requiredPresentPaths = new Set(presentInputPaths);
|
|
161
|
+
const missingGuidePaths = new Set(prepared.guideSources.missing);
|
|
162
|
+
const inputPaths = [...presentInputPaths, ...missingGuidePaths];
|
|
163
|
+
const pathsUnderRoot = [...inputPaths, ...prior.map(row => resolve(prepared.root, row.path))]
|
|
164
|
+
.map(path => publicationInputPath(prepared.root, path))
|
|
165
|
+
.filter((path) => path !== undefined);
|
|
166
|
+
const directories = new Set([prepared.root]);
|
|
167
|
+
for (const path of pathsUnderRoot) {
|
|
168
|
+
const segments = path.split(sep).slice(0, -1);
|
|
169
|
+
let directory = prepared.root;
|
|
170
|
+
for (const segment of segments) {
|
|
171
|
+
directory = resolve(directory, segment);
|
|
172
|
+
directories.add(directory);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (directories.size > MAX_OWNED_FILES * 4 || constants.O_DIRECTORY === undefined || constants.O_NOFOLLOW === undefined) {
|
|
176
|
+
return { kind: 'invalid' };
|
|
177
|
+
}
|
|
178
|
+
const handles = [];
|
|
179
|
+
const watches = [];
|
|
180
|
+
let outcome = { kind: 'invalid' };
|
|
181
|
+
let closeFailed = false;
|
|
182
|
+
const stable = async (watch) => {
|
|
183
|
+
const opened = await watch.handle.stat({ bigint: true });
|
|
184
|
+
const named = await lstat(watch.path, { bigint: true });
|
|
185
|
+
return opened.isDirectory() && named.isDirectory() && !named.isSymbolicLink()
|
|
186
|
+
&& opened.dev === watch.dev && opened.ino === watch.ino
|
|
187
|
+
&& opened.mtimeNs === watch.mtimeNs && opened.ctimeNs === watch.ctimeNs
|
|
188
|
+
&& named.dev === watch.dev && named.ino === watch.ino
|
|
189
|
+
&& named.mtimeNs === watch.mtimeNs && named.ctimeNs === watch.ctimeNs;
|
|
190
|
+
};
|
|
191
|
+
try {
|
|
192
|
+
for (const path of [...directories].sort(compareCodeUnits)) {
|
|
193
|
+
let handle;
|
|
194
|
+
try {
|
|
195
|
+
handle = await open(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
if (hasErrorCode(error, 'ENOENT') && path !== prepared.root)
|
|
199
|
+
continue;
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
handles.push(handle);
|
|
203
|
+
const opened = await handle.stat({ bigint: true });
|
|
204
|
+
const named = await lstat(path, { bigint: true });
|
|
205
|
+
if (!opened.isDirectory() || !named.isDirectory() || named.isSymbolicLink()
|
|
206
|
+
|| opened.dev !== named.dev || opened.ino !== named.ino)
|
|
207
|
+
throw new RefusalError('directory identity changed');
|
|
208
|
+
if (path === prepared.root && (opened.dev !== BigInt(prepared.configSourceIdentity.root.dev)
|
|
209
|
+
|| opened.ino !== BigInt(prepared.configSourceIdentity.root.ino)))
|
|
210
|
+
throw new RefusalError('publication root identity changed');
|
|
211
|
+
watches.push({ path, handle, dev: opened.dev, ino: opened.ino,
|
|
212
|
+
mtimeNs: opened.mtimeNs, ctimeNs: opened.ctimeNs });
|
|
213
|
+
}
|
|
214
|
+
if (!(await Promise.all(watches.map(stable))).every(Boolean))
|
|
215
|
+
throw new RefusalError('directory namespace changed');
|
|
216
|
+
const exact = new Set();
|
|
217
|
+
const physical = [];
|
|
218
|
+
for (const path of [...new Set(inputPaths)].sort(compareCodeUnits)) {
|
|
219
|
+
const input = publicationInputPath(prepared.root, path);
|
|
220
|
+
if (input)
|
|
221
|
+
exact.add(input);
|
|
222
|
+
let status;
|
|
223
|
+
try {
|
|
224
|
+
status = await lstat(path);
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
if (hasErrorCode(error, 'ENOENT')
|
|
228
|
+
&& missingGuidePaths.has(path) && !requiredPresentPaths.has(path))
|
|
229
|
+
continue;
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
physical.push({ dev: status.dev, ino: status.ino });
|
|
233
|
+
}
|
|
234
|
+
for (const row of [...prior].sort((left, right) => compareCodeUnits(left.path, right.path))) {
|
|
235
|
+
if (exact.has(row.path)) {
|
|
236
|
+
outcome = { kind: 'complete', path: row.path };
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
const path = resolve(prepared.root, row.path);
|
|
240
|
+
let status;
|
|
241
|
+
try {
|
|
242
|
+
status = await lstat(path);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
if (hasErrorCode(error, 'ENOENT'))
|
|
246
|
+
continue;
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
if (physical.some(input => input.dev === status.dev && input.ino === status.ino)) {
|
|
250
|
+
outcome = { kind: 'complete', path: row.path };
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (outcome.kind === 'invalid')
|
|
255
|
+
outcome = { kind: 'complete' };
|
|
256
|
+
if (!(await Promise.all(watches.map(stable))).every(Boolean))
|
|
257
|
+
outcome = { kind: 'invalid' };
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
// an unobservable directory is reported as an invalid read.
|
|
261
|
+
outcome = { kind: 'invalid' };
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
for (const handle of handles.reverse())
|
|
265
|
+
try {
|
|
266
|
+
await handle.close();
|
|
267
|
+
}
|
|
268
|
+
catch { /* an unobservable directory is reported as an invalid read. */
|
|
269
|
+
closeFailed = true;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return closeFailed ? { kind: 'invalid' } : outcome;
|
|
273
|
+
}
|
|
274
|
+
async function surfaceObservationMaxBytes(root, path) {
|
|
275
|
+
const target = confined(root, path);
|
|
276
|
+
if (!target)
|
|
277
|
+
return undefined;
|
|
278
|
+
try {
|
|
279
|
+
const status = await lstat(target);
|
|
280
|
+
if (!status.isFile() || status.isSymbolicLink())
|
|
281
|
+
return 1;
|
|
282
|
+
if (!Number.isSafeInteger(status.size) || status.size < 0)
|
|
283
|
+
return undefined;
|
|
284
|
+
return Math.min(MAX_GENERATED_BYTES, Math.max(1, status.size));
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
return hasErrorCode(error, 'ENOENT') ? 1 : undefined;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
export async function observeSurfaceState(prepared, prior, adapters) {
|
|
291
|
+
const paths = new Set([...prepared.surfaces.map(row => row.path), ...prior.map(row => row.path)]);
|
|
292
|
+
if (prepared.surfaces.some(row => row.path === 'opencode.jsonc'))
|
|
293
|
+
paths.add('opencode.json');
|
|
294
|
+
const raw = new Map();
|
|
295
|
+
const observed = [];
|
|
296
|
+
const opaque = new Set();
|
|
297
|
+
const orderedPaths = [...paths].sort(compareCodeUnits);
|
|
298
|
+
const maxBytes = await Promise.all(orderedPaths.map(path => surfaceObservationMaxBytes(prepared.root, path)));
|
|
299
|
+
if (maxBytes.some(limit => limit === undefined))
|
|
300
|
+
return undefined;
|
|
301
|
+
const entries = await readAnchoredBatches(prepared.root, orderedPaths.map((path, index) => ({
|
|
302
|
+
path, maxBytes: maxBytes[index], action: 'read-entry',
|
|
303
|
+
})), prepared.node, adapters);
|
|
304
|
+
if (!entries || entries.kind !== 'complete' || entries.results.length !== orderedPaths.length)
|
|
305
|
+
return undefined;
|
|
306
|
+
for (const [index, path] of orderedPaths.entries()) {
|
|
307
|
+
const entry = entries.results[index];
|
|
308
|
+
if (entry.kind === 'invalid')
|
|
309
|
+
return undefined;
|
|
310
|
+
if (entry.kind === 'complete')
|
|
311
|
+
return undefined;
|
|
312
|
+
raw.set(path, entry);
|
|
313
|
+
if (entry.kind === 'missing')
|
|
314
|
+
observed.push({ path, kind: 'missing' });
|
|
315
|
+
else if (entry.kind === 'file') {
|
|
316
|
+
let bytes;
|
|
317
|
+
try {
|
|
318
|
+
bytes = new TextDecoder('utf-8', { fatal: true }).decode(entry.bytes);
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
// undecodable bytes make the path opaque, never owned.
|
|
322
|
+
opaque.add(path);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
observed.push({ path, kind: 'file', bytes, sha256: entry.sha256, identity: sourceIdentity(entry.identity) });
|
|
326
|
+
}
|
|
327
|
+
else if (entry.kind === 'symlink')
|
|
328
|
+
observed.push({ path, kind: 'symlink', target: entry.target, identity: JSON.stringify(entry.identity) });
|
|
329
|
+
else
|
|
330
|
+
observed.push({ path, kind: 'other', identity: 'other' });
|
|
331
|
+
}
|
|
332
|
+
return { observed, raw, opaque };
|
|
333
|
+
}
|
|
334
|
+
export async function classifyOwnedState(prepared, manifest, raw) {
|
|
335
|
+
const current = generatedPathFacts(prepared, 'declared-before-sync');
|
|
336
|
+
if (current.kind !== 'complete')
|
|
337
|
+
return current;
|
|
338
|
+
const historical = manifest ? projectGeneratedPathFacts(manifest, { root: prepared.root, cockpitId: prepared.cockpitId, configPath: prepared.configPath }, []) : { kind: 'complete', facts: [] };
|
|
339
|
+
if (historical.kind !== 'complete')
|
|
340
|
+
return historical;
|
|
341
|
+
const previous = new Map(historical.facts.map(fact => [fact.path, fact]));
|
|
342
|
+
const desired = new Map(current.facts.map(fact => [fact.path, fact]));
|
|
343
|
+
const facts = [];
|
|
344
|
+
const observations = [];
|
|
345
|
+
for (const [path, observed] of raw) {
|
|
346
|
+
const fact = observed.kind === 'missing'
|
|
347
|
+
? desired.get(path) ?? previous.get(path) : previous.get(path) ?? desired.get(path);
|
|
348
|
+
if (!fact)
|
|
349
|
+
continue;
|
|
350
|
+
facts.push(fact);
|
|
351
|
+
if (observed.kind === 'file')
|
|
352
|
+
observations.push({ path, kind: 'file', bytes: observed.bytes, confinement: 'confined', identity: sourceIdentity(observed.identity) });
|
|
353
|
+
else
|
|
354
|
+
observations.push({ path, kind: observed.kind, confinement: observed.kind === 'missing' ? 'confined' : 'unknown' });
|
|
355
|
+
}
|
|
356
|
+
return classifyGeneratedPaths(facts, observations);
|
|
357
|
+
}
|
|
358
|
+
export function planObservedSurfaces(prepared, priorOwned, state) {
|
|
359
|
+
const plan = planSurfaceChanges({ configuredHarnesses: [...new Set([...prepared.configuredHarnesses, ...priorOwned.flatMap(row => row.harnesses)])].sort(),
|
|
360
|
+
desired: prepared.surfaces.filter(row => !state.opaque.has(row.path)),
|
|
361
|
+
priorOwned: priorOwned.filter(row => !state.opaque.has(row.path)), observed: state.observed });
|
|
362
|
+
if (plan.kind !== 'complete')
|
|
363
|
+
return plan;
|
|
364
|
+
const extra = [];
|
|
365
|
+
for (const path of state.opaque) {
|
|
366
|
+
const prior = priorOwned.find(row => row.path === path);
|
|
367
|
+
const desired = prepared.surfaces.find(row => row.path === path);
|
|
368
|
+
const raw = state.raw.get(path);
|
|
369
|
+
if (!prior || prior.kind !== 'file' || raw?.kind !== 'file' || prior.sha256 !== raw.sha256
|
|
370
|
+
|| desired && desired.kind !== 'file')
|
|
371
|
+
return { kind: 'refusal', diagnostic: { code: 'SURFACE_STATE_CONFLICT', message: 'opaque surface bytes do not match authenticated whole-file ownership', target: path } };
|
|
372
|
+
extra.push(desired ? { kind: 'write', path, desired } : { kind: 'remove', path, priorOwned: prior });
|
|
373
|
+
}
|
|
374
|
+
const actions = [...plan.actions, ...extra].sort((a, b) => (a.kind === 'write' ? 0 : 1) - (b.kind === 'write' ? 0 : 1) || compareCodeUnits(a.path, b.path));
|
|
375
|
+
return { kind: 'complete', actions };
|
|
376
|
+
}
|
|
377
|
+
/** A hand edit to a merged adopted file belongs in its adopted copy; say so where the conflict is reported. */
|
|
378
|
+
export function surfacePlanRefusal(prepared, diagnostic, unfinishedInit) {
|
|
379
|
+
const adopted = diagnostic.code === 'SURFACE_STATE_CONFLICT'
|
|
380
|
+
? prepared.registry.config.surfaces.adopted.find(row => row.path === diagnostic.target) : undefined;
|
|
381
|
+
// Plain sync met a setup file an unfinished init published and can no longer vouch for; the
|
|
382
|
+
// person needs to hear about that init, not only that the file is unrecorded.
|
|
383
|
+
if (diagnostic.code === 'SURFACE_STATE_CONFLICT' && unfinishedInit && !unfinishedInit.surfaces
|
|
384
|
+
&& diagnostic.target !== undefined && unfinishedInit.published.includes(diagnostic.target)) {
|
|
385
|
+
return refuse(diagnostic.code, `${diagnostic.message}; an unfinished init for this root (--target ${unfinishedInit.target}) published it, and it no longer holds the bytes that init recorded or the installed cockpit package changed since`, diagnostic.target, `resume that init with ${initCommand({ root: unfinishedInit.root, target: unfinishedInit.target })} and the options you started it with, which shows its current preview; or ask the person who ran init before you remove cockpit.toml and the files it published so init starts again`);
|
|
386
|
+
}
|
|
387
|
+
return adopted
|
|
388
|
+
? refuse(diagnostic.code, `${diagnostic.message}; ${adopted.path} is merged from ${adopted.source}`, diagnostic.target, `make the change in ${adopted.source}, then ask the person who owns the cockpit before you delete ${adopted.path}, and run npx --no cockpit sync --config <your cockpit.toml>`)
|
|
389
|
+
: refuse(diagnostic.code, diagnostic.message, diagnostic.target, diagnostic.fix);
|
|
390
|
+
}
|
|
391
|
+
function surfaceMutations(actions, raw, compactSurfacePaths) {
|
|
392
|
+
const compactAnchorFor = (path) => {
|
|
393
|
+
return SURFACE_PUBLICATION_ANCHORS.find(anchor => path.startsWith(`${anchor}/`));
|
|
394
|
+
};
|
|
395
|
+
const result = [];
|
|
396
|
+
for (const action of actions) {
|
|
397
|
+
if (action.kind === 'already-absent')
|
|
398
|
+
continue;
|
|
399
|
+
const current = raw.get(action.path) ?? { kind: 'missing' };
|
|
400
|
+
const expected = expectedState(current);
|
|
401
|
+
if (!expected)
|
|
402
|
+
return undefined;
|
|
403
|
+
if (action.kind === 'write') {
|
|
404
|
+
const compactAnchor = compactSurfacePaths.has(action.path) ? compactAnchorFor(action.path) : undefined;
|
|
405
|
+
const bytes = action.desired.kind === 'file' ? action.desired.bytes : action.postimage;
|
|
406
|
+
if (bytes === undefined)
|
|
407
|
+
return undefined;
|
|
408
|
+
result.push({ path: action.path, phase: 'desired', operation: 'write-file', bytes, sha256: sha256(bytes), expected,
|
|
409
|
+
...(compactAnchor ? { compactAnchor } : {}) });
|
|
410
|
+
}
|
|
411
|
+
else if (action.postimage !== undefined)
|
|
412
|
+
result.push({ path: action.path, phase: 'stale', operation: 'write-file', bytes: action.postimage,
|
|
413
|
+
sha256: sha256(action.postimage), expected });
|
|
414
|
+
else
|
|
415
|
+
result.push({ path: action.path, phase: 'stale', operation: 'remove-file', expected });
|
|
416
|
+
}
|
|
417
|
+
return result;
|
|
418
|
+
}
|
|
419
|
+
async function environmentMutations(prepared, manifest, adapters) {
|
|
420
|
+
const prior = manifest?.entries ?? [];
|
|
421
|
+
const surfacePaths = new Set(prepared.surfaces.map(row => row.path));
|
|
422
|
+
const priorEnvironment = new Map(prior.filter((row) => !('harnesses' in row) && !surfacePaths.has(row.path)).map(row => [row.path, row]));
|
|
423
|
+
const desired = new Map(prepared.owned.map(row => [row.path, row]));
|
|
424
|
+
const paths = [...new Set([...priorEnvironment.keys(), ...desired.keys()])].sort(compareCodeUnits);
|
|
425
|
+
const mutations = [];
|
|
426
|
+
const entries = await readAnchoredBatches(prepared.root, paths.map(path => ({
|
|
427
|
+
path, maxBytes: MAX_GENERATED_BYTES, action: 'read-entry',
|
|
428
|
+
})), prepared.node, adapters);
|
|
429
|
+
if (!entries)
|
|
430
|
+
return { kind: 'invalid', path: paths[0] ?? 'generated-environment' };
|
|
431
|
+
if (entries.kind === 'invalid')
|
|
432
|
+
return { kind: 'invalid', path: paths[entries.index] ?? paths[0] ?? 'generated-environment' };
|
|
433
|
+
if (entries.results.length !== paths.length)
|
|
434
|
+
return { kind: 'invalid', path: paths[0] ?? 'generated-environment' };
|
|
435
|
+
const classified = await classifyOwnedState(prepared, manifest, new Map(paths.map((path, index) => [path, entries.results[index]])));
|
|
436
|
+
if (classified.kind !== 'complete')
|
|
437
|
+
return { kind: 'invalid', path: classified.diagnostic.target ?? 'generated-environment' };
|
|
438
|
+
const unknown = classified.paths.find(row => row.state === 'unknown');
|
|
439
|
+
if (unknown)
|
|
440
|
+
return { kind: 'invalid', path: unknown.fact.path };
|
|
441
|
+
for (const [index, path] of paths.entries()) {
|
|
442
|
+
const current = entries.results[index];
|
|
443
|
+
if (current.kind === 'complete')
|
|
444
|
+
return { kind: 'invalid', path };
|
|
445
|
+
const expected = expectedState(current);
|
|
446
|
+
if (!expected)
|
|
447
|
+
return { kind: 'invalid', path };
|
|
448
|
+
const owned = priorEnvironment.get(path);
|
|
449
|
+
const next = desired.get(path);
|
|
450
|
+
if (current.kind !== 'missing' && (!owned || current.kind !== 'file'
|
|
451
|
+
|| current.sha256 !== owned.sha256 && next?.sha256 !== owned.sha256)) {
|
|
452
|
+
return { kind: 'invalid', path };
|
|
453
|
+
}
|
|
454
|
+
if (next) {
|
|
455
|
+
if (current.kind === 'file' && current.sha256 === next.sha256)
|
|
456
|
+
continue;
|
|
457
|
+
mutations.push({ path, phase: 'desired', operation: 'write-file', bytes: next.bytes, sha256: next.sha256, expected });
|
|
458
|
+
}
|
|
459
|
+
else if (current.kind === 'file')
|
|
460
|
+
mutations.push({ path, phase: 'stale', operation: 'remove-file', expected });
|
|
461
|
+
}
|
|
462
|
+
return { kind: 'complete', mutations };
|
|
463
|
+
}
|
|
464
|
+
export async function preparePublication(input, adapterOverrides = {}, seed, knownOwnership) {
|
|
465
|
+
const request = syncCommandInput(input);
|
|
466
|
+
if (!request)
|
|
467
|
+
return refuse('GENERATED_INPUT_INVALID', 'generated environment command input is invalid', 'generated-environment');
|
|
468
|
+
const adapters = mergedAdapters(adapterOverrides);
|
|
469
|
+
const base = seed ?? await prepare({ configPath: request.configPath }, adapters);
|
|
470
|
+
if ('status' in base)
|
|
471
|
+
return base;
|
|
472
|
+
const ownership = knownOwnership ?? await readOwnership(base, adapters);
|
|
473
|
+
if (ownership.kind === 'unsupported')
|
|
474
|
+
return refuse('GENERATED_OWNERSHIP_FORMAT_UNSUPPORTED', `generated ownership format ${ownership.format} is not supported`, OWNERSHIP_PATH);
|
|
475
|
+
if (ownership.kind === 'invalid')
|
|
476
|
+
return refuse('GENERATED_OWNERSHIP_INVALID', 'generated ownership metadata is malformed or unsafe', OWNERSHIP_PATH, 'restore the last trusted ownership metadata before syncing');
|
|
477
|
+
if (ownership.kind === 'complete' && ownership.manifest.cockpitId !== base.cockpitId) {
|
|
478
|
+
const recorded = await renamedStackRefusal(base);
|
|
479
|
+
if (recorded)
|
|
480
|
+
return recorded;
|
|
481
|
+
}
|
|
482
|
+
const unfinishedInit = ownership.kind === 'missing' && !request.bootstrapState
|
|
483
|
+
? await unfinishedInitSurfaces(base.root, base.node, adapters) : undefined;
|
|
484
|
+
const adoptedBootstrap = ownership.kind !== 'missing' ? undefined : request.bootstrapState
|
|
485
|
+
? await authenticatedBootstrapPriorSurfaces(base.root, request.bootstrapState, base.node, adapters)
|
|
486
|
+
: unfinishedInit?.surfaces;
|
|
487
|
+
if (ownership.kind === 'missing' && request.bootstrapState && !adoptedBootstrap)
|
|
488
|
+
return refuse('GENERATED_OWNERSHIP_INVALID', 'the setup skill files init published no longer hold the bytes it recorded, or the installed cockpit package changed since init published them, so sync cannot take them over as cockpit\'s own', 'bootstrap', `if the cockpit package was upgraded, run ${initCommand({ args: '--json' })} and accept its fresh preview; otherwise restore the setup skill files init published (its --json preview lists them under result.files), or ask the person who ran init before you remove cockpit.toml and those files so init starts again`);
|
|
489
|
+
if (ownership.kind !== 'missing' && request.bootstrapState)
|
|
490
|
+
return refuse('GENERATED_OWNERSHIP_INVALID', 'bootstrap adoption is only valid before the first ownership manifest', 'bootstrap');
|
|
491
|
+
if (ownership.kind === 'missing' && await expectedOutputExists(base))
|
|
492
|
+
return refuse('GENERATED_OWNERSHIP_MISSING', 'generated outputs exist without durable ownership metadata', OWNERSHIP_PATH, 'restore the last trusted ownership metadata before syncing');
|
|
493
|
+
const current = await readAnchoredEntry(base.root, OWNERSHIP_PATH, MAX_OWNERSHIP_BYTES, base.node, adapters);
|
|
494
|
+
if (ownership.kind === 'missing' ? current.kind !== 'missing'
|
|
495
|
+
: current.kind !== 'file' || current.sha256 !== ownership.sha256
|
|
496
|
+
|| sourceIdentity(current.identity) !== sourceIdentity(ownership.identity))
|
|
497
|
+
return refuse('GENERATED_OWNERSHIP_INVALID', 'generated ownership metadata changed after observation', OWNERSHIP_PATH);
|
|
498
|
+
const priorEntries = ownership.kind === 'complete' ? ownership.manifest.entries : [];
|
|
499
|
+
const priorSurfaces = [...priorEntries.filter((row) => 'harnesses' in row), ...(adoptedBootstrap ?? [])]
|
|
500
|
+
.sort((left, right) => compareCodeUnits(left.path, right.path));
|
|
501
|
+
const prepared = await prepareSurfaces(base, priorSurfaces, adapters, ownership.kind === 'complete' ? ownership.manifest : undefined);
|
|
502
|
+
if ('status' in prepared)
|
|
503
|
+
return prepared;
|
|
504
|
+
const ownedInput = await ownedInputOverlap(prepared, priorEntries);
|
|
505
|
+
if (ownedInput.kind === 'invalid')
|
|
506
|
+
return refuse('GENERATED_OWNERSHIP_INVALID', 'authenticated input identity could not be compared with generated ownership metadata', OWNERSHIP_PATH, 'restore the last trusted ownership metadata before syncing');
|
|
507
|
+
if (ownedInput.path)
|
|
508
|
+
return refuse('GENERATED_OWNERSHIP_INVALID', 'generated ownership metadata overlaps an authenticated input', ownedInput.path, 'restore the last trusted ownership metadata before syncing');
|
|
509
|
+
const surfaceState = await observeSurfaceState(prepared, priorSurfaces, adapters);
|
|
510
|
+
if (!surfaceState)
|
|
511
|
+
return refuse('SURFACE_STATE_CONFLICT', 'surface state could not be observed safely', 'surfaces');
|
|
512
|
+
const classified = await classifyOwnedState(prepared, ownership.kind === 'complete' ? ownership.manifest : undefined, surfaceState.raw);
|
|
513
|
+
if (classified.kind !== 'complete')
|
|
514
|
+
return refuse(classified.diagnostic.code, classified.diagnostic.message, classified.diagnostic.target);
|
|
515
|
+
const unknown = classified.paths.find(row => row.state === 'unknown');
|
|
516
|
+
if (unknown)
|
|
517
|
+
return refuse('SURFACE_STATE_CONFLICT', 'owned surface identity or confinement is unknown', unknown.fact.path);
|
|
518
|
+
const effectivePriorSurfaces = priorSurfaces;
|
|
519
|
+
const environment = await environmentMutations(prepared, ownership.kind === 'complete' ? ownership.manifest : undefined, adapters);
|
|
520
|
+
if (environment.kind === 'invalid')
|
|
521
|
+
return { prepared, ownership, refusal: refuse('GENERATED_STALE_ARTIFACT_DRIFT', 'a generated whole-file path is unmanaged, changed, or unsafe', environment.path) };
|
|
522
|
+
const plan = planObservedSurfaces(prepared, effectivePriorSurfaces, surfaceState);
|
|
523
|
+
if (plan.kind !== 'complete')
|
|
524
|
+
return surfacePlanRefusal(prepared, plan.diagnostic, unfinishedInit);
|
|
525
|
+
const surface = surfaceMutations(plan.actions, surfaceState.raw, prepared.compactSurfacePaths);
|
|
526
|
+
if (!surface)
|
|
527
|
+
return refuse('SURFACE_STATE_CONFLICT', 'surface change plan could not be admitted', 'surfaces');
|
|
528
|
+
const all = [...environment.mutations, ...surface];
|
|
529
|
+
if (new Set(all.map(row => row.path)).size !== all.length)
|
|
530
|
+
return refuse('SURFACE_PATH_CONFLICT', 'generated output estates overlap', 'generated-environment');
|
|
531
|
+
const publication = await admitPublicationPlan({
|
|
532
|
+
authority: { root: prepared.root, cockpitId: prepared.cockpitId, configPath: prepared.configPath },
|
|
533
|
+
generation: prepared.generation,
|
|
534
|
+
sourceSha256: prepared.sourceSha256,
|
|
535
|
+
rootIdentity: prepared.configSourceIdentity.root,
|
|
536
|
+
node: prepared.node,
|
|
537
|
+
mode: 'sync',
|
|
538
|
+
ledgerExpected: ownership.kind === 'missing' ? { kind: 'missing' }
|
|
539
|
+
: { kind: 'file', sha256: ownership.sha256, identity: ownership.identity.file },
|
|
540
|
+
nextLedgerBytes: encodeOwnershipManifest(prepared).bytes,
|
|
541
|
+
mutations: all,
|
|
542
|
+
surfaceActions: plan.actions,
|
|
543
|
+
}, adapters);
|
|
544
|
+
if (publication.kind === 'refusal')
|
|
545
|
+
return { prepared, ownership, refusal: publication };
|
|
546
|
+
return { prepared, ownership, plan: publication.plan };
|
|
547
|
+
}
|