provisioning-catalog/.governance/laws.ncl

239 lines
15 KiB
XML

# sow/laws.ncl — standing laws for the provisioning catalog.
#
# T2 of the frozen contract: provisioning/.coder/2026-07-03_leyes-catalogo-provisioning.plan.md
# This file is a DRAFT until J3 closes: the human signs it with the dedicated minisign
# key (see §9 J1/J3 of the contract). Until `signature` below is filled in and verified,
# witness.nu MUST refuse to treat these laws as binding (guard: no valid signature, no
# start — same rule as the SOW ratification gate in ADR-063).
#
# Every classification below (exempt/candidates) is traceable to the contract's §2/§5,
# which record the verified evidence (file contents, git/Time-Machine snapshot diffs,
# script headers) each ruling rests on. Nothing here is invented; where evidence was
# ambiguous, the contract's §9 records the human ruling that resolved it.
let schema = import "laws-schema.ncl" in
{
laws = [
{
id = "L1-no-parsing-on-targets",
status = 'Active,
why = m%"
PAP: controllers resolve, targets execute. Parsing (json/yaml/templates/
versions) belongs in prepare/postdeploy on the controller (nushell/nickel);
targets receive fully-resolved artifacts. Burn: garage+loki deploy sessions
shipped `python3 -c "import json…"` parsing to target nodes (2026-06-23
interview). R4 (2026-07-03) sharpened the boundary after a first-draft
exemption for "live-state reads" was correctly rejected: the violation is not
parsing or reading live state, it is THE TARGET DECIDING. `kubectl apply`
(ideally --server-side) of a manifest prepare rendered fully-resolved is
compliant even against a resource that didn't exist at prepare-time —
Kubernetes' own merge semantics resolve "add if missing" with no target-side
decision. Reading live state, deciding in ANY language, and imperatively
patching is the same violation as the original python3 -c cases; the language
carrying the decision logic is incidental.
"%,
kind = 'static,
scope = ["**/*.sh", "**/*.j2", "**/*.yaml"],
exclude = ["wrks/**", "providers/*/bin/**", "providers/*/templates/**"],
# exclude rationale:
# wrks/** — R1: temporary/archived material, not part of the final
# project. Verified 2026-07-03 unchanged from original scan (no disguised
# fix): install-stalwart.sh, install-metallb.sh, install-oci-reg.sh,
# install-kubernetes.sh still carry their original python3 -c lines.
# providers/*/bin/** — controller/operator-side cloud-provisioning
# tooling (needs cloud API credentials a target never holds). Confirmed as a
# directory-level convention repeated across aws/hetzner/local/upcloud, not
# a one-off. Added after classifying providers/aws/bin/get-image.sh.
# providers/*/templates/** — RESOLVED 2026-07-03, second pass. Running
# the check for real (T3 red-team) surfaced 4 hetzner templates
# (hetzner_networks.j2, hetzner_storage_box.j2, hetzner_servers.j2,
# hetzner_volumes.j2) doing `hcloud ... -o json | nu -c '$env.HJSON | from
# json | ...'` — get-decide-patch against the LIVE Hetzner Cloud API (R4
# violation shape), in nushell instead of jq. Confirmed controller-side:
# referenced from core/nulib/orchestration/servers/create.nu, needs Hetzner
# API credentials no target ever holds — same category as
# providers/*/bin/get-image.sh, different subdirectory. Excluded rather than
# tracked as a candidate (unlike zot-lib.sh/coredns_vpn) because it is
# controller-side orchestration, not target execution — out of L1's scope by
# definition, the same reasoning as backup_manager.
check = m%"! rg -q 'python3? (-c|<<|-m )|import json' . --glob '*.sh' --glob '*.j2' --glob '*.yaml' --glob '!wrks/**' --glob '!providers/*/bin/**' --glob '!providers/*/templates/**' --glob '!components/odoo/cluster/templates/deployment.yaml.j2'"%,
# check bug fix 2026-07-03: dropped the bare `from json` alternative — it matched
# both Python's `from json import ...` AND Nushell's native `from json` filter
# (`| from json |`), a false-positive collision across languages. `import json`
# alone still catches every original burn case (stalwart/metallb/oci-reg/
# kubernetes all use `import json`, none use `from json import`).
baseline = "",
exempt = [
{
path = "components/odoo/cluster/templates/deployment.yaml.j2",
why = m%"
R2: all python occurrences (14) execute inside k8s initContainers/
container commands on the odoo image, which ships python. Deploy
machinery and target hosts assume nothing. This is the one exemption
that matters for the mechanical check above (the only file where the
python-pattern regex would otherwise fire).
"%,
},
{
path = "components/fleet_daemon/cluster/install-fleet_daemon.sh",
why = m%"
jq -e . is syntax validation only (no field extraction, no live read) —
a fail-fast guard on a value the controller already supplied via
ACCESS_POLICY_JSON, catching malformed JSON at the bash boundary instead
of inside the daemon at startup. Grants no new target-side authority or
decision-making. Not matched by the python-only mechanical check; recorded
here for audit completeness of the R4 jq classification pass.
"%,
},
{
path = "components/backup_manager/cluster/install-backup_manager.sh",
why = m%"
The jq-using block runs where `nickel` is available — controller-side
reporting for the human operator ("Run 'provisioning component install
backup_manager --target-hosts' to deploy binaries"), not target execution.
Out of L1's scope by definition, not by exemption from a violation.
"%,
},
{
path = "components/lian_build/cluster/setup-buildkit-client.sh",
why = m%"
component.json, per the script's own header, is "NCL-derived config" — a
controller-resolved artifact optionally dropped alongside the script as a
fallback to env vars. The jq -r ... // empty call is a plain key lookup
with default fallback on an already-resolved artifact, not a target-side
decision. This script is a reference example of the R4-compliant pattern:
its header states "declarative apply of lian-build's mTLS identity" and it
does envsubst < tmpl | kubectl apply -f -, not get-decide-patch.
"%,
},
{
path = "components/coredns_vpn/taskserv/install-coredns_vpn.sh:_patch_cluster_coredns",
why = m%"
RECLASSIFIED 2026-07-03 (was a candidate). Verified directly against
k0s's own source (pkg/component/controller/coredns.go, upstream
k0sproject/k0s): the default Corefile it generates has NO `import
custom/*.override` or `coredns-custom` ConfigMap mechanism that
pattern exists on k3s (a different, unrelated project despite the
similar name), not k0s, which this catalog targets (_resolve_kubeconfig
references k0s admin.conf paths throughout). The field being touched
(ConfigMap data.Corefile) is an opaque string blob with no native
Kubernetes list-merge mechanism (unlike Gateway API's spec.listeners,
which IS mergeable see zot-lib.sh's candidate entry). Rendering the
full Corefile statically in prepare would require prepare to know the
cluster's actual current baseline in advance, which it cannot without
either querying the live cluster (defeating the purpose) or assuming a
static baseline that could silently diverge from reality and clobber
unrelated cluster DNS config. The existing read-current-then-append-
missing-zones pattern (idempotent: skips zones already present) is the
most robust option actually available on this distribution today not
a violation avoided out of habit, a genuine absence of a safe
declarative alternative.
"%,
},
{
path = "components/zot/cluster/zot-lib.sh:_patch_public_gateway_listener,_remove_public_gateway_listener",
why = m%"
RESOLVED 2026-07-03 (was a candidate, then partially remediated, now
fully). Both functions redesigned to declarative Server-Side Apply
under field-manager=zot-registry: add applies
manifests/public-gateway-listener.yaml.j2 (rendered, verified with
nu_plugin_tera the project's real mechanism, not the standalone tera
CLI tried first against 3 cases: cross-namespace cert, same-
namespace, gateway-not-configured, all valid YAML); remove applies an
explicit empty `listeners: []` under the same field manager. Gateway
API's spec.listeners is x-kubernetes-list-type=map (merge key: name),
so SSA tracks per-manager ownership of individual list entries
applying zero entries under zot-registry prunes exactly what that
manager previously contributed, leaving listeners owned by any other
field manager on this SHARED Gateway untouched; zot-registry never
declared gatewayClassName or other top-level fields, so this cannot
affect them either. Accepted on Gateway API's documented behavior and
SSA's standard per-manager ownership semantics not separately
exercised against a live cluster (real-cluster validation was
considered and explicitly deferred in favor of accepting the
documented mechanism, human call). No get/decide/patch remains in
either function; real catalog re-checked ACCEPTED after the change.
"%,
},
],
candidates = [],
},
{
id = "L2-no-undeclared-interpreters",
status = 'Deferred,
why = m%"
Deferred 2026-07-03 (J2), two independent reasons:
(1) R3 — no evidence on the active surface. The only occurrences (polkadot
installers) live in wrks/**, excluded by R1.
(2) No oracle — verified against the real schema. metadata.ncl's
`dependencies` field is inter-component only (e.g. `["kubernetes"]` in
local_path_provisioner/metadata.ncl, hccm/nickel/version.ncl) it never
names OS packages or language runtimes, and no schema.ncl constrains it.
A check for "undeclared interpreter" has nothing to check against today.
Path back to ratifiability (both required): a real burn lands on the active
surface, AND metadata.ncl gains a typed field for declared OS/runtime
dependencies.
"%,
kind = 'static,
scope = ["**/*.sh", "**/*.j2", "**/*.yaml"],
exclude = ["wrks/**", "providers/*/bin/**", "providers/*/templates/**"],
check = "echo 'L2 is Deferred not enforced, see status field and why' >&2; exit 1",
baseline = "",
exempt = [],
candidates = [],
},
{
id = "L3-versions-from-lock",
status = 'Active,
why = m%"
ADR-056: accredited, not generated. A version/tag with no verified
provenance is not usable knowledge — the second original burn alongside
L1's parsing violation ("se despliega con una tag inventada o la primera
al caso... si hay incompatibilidades... ya se verá", 2026-07-03 interview).
Looked for the existing mechanism before building anything (corrected
mid-session after inventing an unnecessary `versions.lock` file first —
see model log): `components/*/nickel/version.ncl` already exists (24 of 48
components use a rich shape: version.current/source/check_latest/
grace_period), and core/nulib/domain/cache/ already reads it for
staleness tracking. The actual gap: nothing ever verified that
`version.current` exists for real at `version.source` — the cache system
trusts what's declared, it never accredits it. L3 closes exactly that gap;
it does not duplicate the existing convention.
"%,
kind = 'static,
# scope is deliberately narrow — matches exactly what check-pinned-versions.nu
# actually verifies today (zot, forgejo), not the broader "components/*/
# nickel/version.ncl" pattern that would overstate coverage. Widening this
# to the other 22 rich-shape components needs integration with the existing
# cache/grace_period subsystem first (deferred — see why, and §11 of the
# frozen contract) to avoid hammering registries on every witness run.
scope = ["components/zot/nickel/version.ncl", "components/forgejo/nickel/version.ncl"],
exclude = ["wrks/**"],
check = "nu scripts/check-pinned-versions.nu",
baseline = "",
exempt = [],
candidates = [
{
path = "components/cert_manager/nickel/contracts.ncl:nushell_image",
pattern = "auxiliary/helper image (ghcr.io/nushell/nushell:latest) with no version.ncl entry — cert_manager's own version.ncl already tracks a DIFFERENT thing (CERT_MANAGER_VERSION=v1.19.5, its own release), so this can't just be enriched the same way as zot/forgejo without conflating the two",
remediation = "decide whether version.ncl should support multiple tracked images (a components/cert_manager/nickel/version.ncl images array) or a separate lock entry for auxiliary/helper containers — needs its own small design decision before fixing",
},
{
path = "components/woodpecker/nickel/{defaults,contracts}.ncl",
pattern = "woodpecker/server:latest, woodpecker/agent:latest — 2 occurrences, real tag data not yet obtained",
remediation = "docker.io anonymous registry lookup was denied (rate-limit/auth) when checked 2026-07-03 — retry via an authenticated pull, a mirror (e.g. ghcr.io if woodpecker publishes there), or `crane` (available locally) instead of skopeo against docker.io directly.",
},
{
path = "components/ontoref_panel/nickel/{defaults,contracts}.ncl",
pattern = "reg.librecloud.online/lamina/ontoref-panel:latest — private registry, real tag data not yet obtained",
remediation = "needs credentials for the internal registry (reg.librecloud.online) to run a real skopeo/crane lookup — not publicly queryable like the other 3.",
},
],
},
],
# `signature` is intentionally OMITTED (schema: optional). It is filled in only at
# J3, by the human, in their own shell — never by the agent, never as a placeholder.
# witness.nu's guard: absent or unverifiable signature → these laws are non-binding.
} | schema.Laws