239 lines
15 KiB
XML
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
|