From 27ac54b32d181cda54a2873527d71d31f0e74a54 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 22 Aug 2026 12:34:25 +0200 Subject: [PATCH] Enforce private-by-default enablement templates Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02669-87ee-7a31-b111-edc95a16e0fa --- INTENT.md | 29 ++++++------ Makefile | 8 ++++ SCOPE.md | 14 +++--- WORK-RECORDS.md | 4 +- docs/private-by-default-template-contract.md | 16 +++++++ .../test_private_defaults.cpython-312.pyc | Bin 0 -> 2714 bytes tests/test_private_defaults.py | 35 ++++++++++++++ tools/check_private_defaults.py | 44 ++++++++++++++++++ ...EN-WP-0001-private-by-default-templates.md | 16 +++++-- 9 files changed, 138 insertions(+), 28 deletions(-) create mode 100644 docs/private-by-default-template-contract.md create mode 100644 tests/__pycache__/test_private_defaults.cpython-312.pyc create mode 100644 tests/test_private_defaults.py create mode 100755 tools/check_private_defaults.py diff --git a/INTENT.md b/INTENT.md index f7db069..edbe897 100644 --- a/INTENT.md +++ b/INTENT.md @@ -108,26 +108,25 @@ This layer is: * a **promotion handoff** surface between source repos and S5 deployments * a future **developer portal** and self-service entry point * a place to codify delivery knowledge into **repeatable automation** -* as of 2026-08-11, the owner of the **forge layer responsibility**: runner - placement and labels, registry retention, artifact lifecycle, and package - credentials +* the owner of the **consumer-facing forge contract**: which runner labels, + registry interfaces, credentials, and artifact evidence paved paths require; + `railiance-forge` remains the operational provider of those capabilities --- -## Inherited: the forge layer responsibility +## Forge accountability and operating boundary -Decision `d151d817` (2026-08-11) placed `railiance-forge`. The Forgejo -**workload** becomes `rapp-forgejo`; the **layer responsibility** folds into S4 -— here — because this layer already declared a handoff contract with forge for -runner labels, package credentials, registry endpoints, and artifact evidence. -Owning the contract's other side is the natural resolution. +Decision `d151d817` (2026-08-11) placed the consumer-facing forge +responsibility in S4. That means this repo owns the reusable workflow contract +for runner labels, package credentials, registry endpoints, and artifact +evidence. It does not make this repo the operator of those systems: +`railiance-forge` continues to own the live forge, registries, runners, +credentials, retention, and operating evidence until a separately approved +migration changes that boundary. The Forgejo workload itself belongs in +`rapp-forgejo`. -This is S4's first concrete owned responsibility. That is worth stating plainly: -until now this layer has been almost entirely aspiration — a rich Direction of -Evolution with 25 commits and no workplans behind it. The forge responsibility -gives it something real to be accountable for, and the gap between what this -INTENT promises and what the repo contains should be read as an open debt, not -as a description of the present. +The split is deliberate: S4 makes the correct delivery path reusable; +`railiance-forge` makes the underlying capability dependable. --- diff --git a/Makefile b/Makefile index 6e6089c..b75a35c 100644 --- a/Makefile +++ b/Makefile @@ -3,3 +3,11 @@ SHELL := /usr/bin/env bash help: ## Show this help @grep -E '^[a-zA-Z0-9_-]+:.*?## ' $(MAKEFILE_LIST) | sort | sed 's/:.*##/: /' + +check: ## Verify workflow templates remain private and deployment-free + python3 tools/check_private_defaults.py + +test: ## Run enablement regression tests + python3 -m unittest discover -s tests -v + +.PHONY: help check test diff --git a/SCOPE.md b/SCOPE.md index 92da574..af3f59e 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -74,13 +74,11 @@ services. S5 applications consume S4 templates and conventions, while ## Current State -- Status: emerging — **now owns the forge layer responsibility** (decision - `d151d817`, 2026-08-11), which is S4's first concrete owned accountability; - still no S4-owned workplans -- Inherited from `railiance-forge`: runner placement and labels, registry - retention, artifact lifecycle, package credentials. The Forgejo **workload** - becomes `rapp-forgejo`; this layer takes the **layer** responsibility, because - it already declared the handoff contract for exactly those concerns +- Status: emerging, with reusable Forgejo workflow templates and an active + private-by-default template workplan. +- S4 owns the consumer-facing contract for runner labels, registries, + credentials, and artifact evidence. `railiance-forge` continues to operate + those capabilities; they are not duplicated here. - Implementation: ArgoCD is deployed in the `argocd` namespace on CoulombCore as a cluster addon managed from S2 — verified still true 2026-08-12 - **But GitOps does not reach the live cluster.** ArgoCD is not deployed on @@ -151,5 +149,5 @@ keywords: [template, sdk, helm, deployment, developer, buildpack] ## Getting Oriented - Start with: `CLAUDE.md` (session protocol, OAS boundary rules) -- Key files / directories: `workplans/` (empty), `Makefile` +- Key files / directories: `workflows/`, `docs/`, `workplans/`, `Makefile` - Pre-conditions: railiance-platform (S3) must be operational diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 755febd..8fca021 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -8,5 +8,5 @@ | Kind | ID | Status | Lane | Source | | --- | --- | --- | --- | --- | -| workplan | RAIL-EN-WP-0001 | ready | — | workplans/RAIL-EN-WP-0001-private-by-default-templates.md | -| task | RAIL-EN-WP-0001-T01 | todo | — | workplans/RAIL-EN-WP-0001-private-by-default-templates.md | +| workplan | RAIL-EN-WP-0001 | finished | — | workplans/RAIL-EN-WP-0001-private-by-default-templates.md | +| task | RAIL-EN-WP-0001-T01 | done | — | workplans/RAIL-EN-WP-0001-private-by-default-templates.md | diff --git a/docs/private-by-default-template-contract.md b/docs/private-by-default-template-contract.md new file mode 100644 index 0000000..b2d72e0 --- /dev/null +++ b/docs/private-by-default-template-contract.md @@ -0,0 +1,16 @@ +# Private-by-default template contract + +Railiance enablement templates produce build and promotion evidence; they do +not create a public listener. ADR-0008 is enforced by the execution rail: + +- Kubernetes workload scaffolding is owned by `rail-kubernetes`. +- Its default Service is `ClusterIP`, its generated ingress policy is + default-deny, and its Stage 2 values do not enable Ingress. +- A public Ingress requires matching rapp and reef declarations at the rail's + deploy gate. An Ingress object or successful deployment is not a grant. +- Operator access uses the named tunnel documented by the owning rail or rapp. + +`make check` rejects enablement workflow templates that embed Ingress, +LoadBalancer, NodePort, or direct Kubernetes/Helm deployment commands. This +keeps reusable build workflows from becoming an accidental application or +cluster deployment owner. diff --git a/tests/__pycache__/test_private_defaults.cpython-312.pyc b/tests/__pycache__/test_private_defaults.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0aa4083d5ba72b8ee0c8ccaa557fa02634968b50 GIT binary patch literal 2714 zcmcgu&2JM&6rb5$dpC=XNy3ML23!y!MmQ0LwkWEq(xxqyNK7H3jcK*oc*n^)>s@zt zonR}Vpi&V|fT}9yM312K(0`%lUPw)pXbXo5^}sD^kdQd_yU~MTF|S}({29N)7+fYJvP7!dd2eGVdi>nzBzNS zN)#w;l&=09tl~9O1<6`(6_^4fP5w5yD0q>kLZ*c^pbd(!UlgasV)Njv*&v^$hVYa4 zA{k!W+UT@6M%U)ZMB4Dg%D3x2l({ zTBPC?GRFZ{h1q$>FzvB)#8=R!U|P)Y7zWAVI({;;U2K^qA(c3m(VV z(~>W_%qsX2b|Ed5eW?uTbJ0Ep?b!!?<4%e7O*!1nkN0uiv`pR3vkbHKQHzzB?e%#G zctEvsmjQV*oz32l9evQ*Rrl(JM(2U2GMGO_pTCeib0JOS zhs=qX_)9EuVw;?Qo_6Seek48tWsXdf;`&kO!#0Nf%^MQ-gw%MR$QKE$lC&^XJ6v=| z-F`Kd8m*X?fkG<9E4G_)?EX~Vu|3_i8P5cxYFdE1O|G(fPcQJhP~r{h!e3 zhAY;eny8FIimjAhE?d=9rjRmN*>b8_qJpy70-CM>8Y8HbTHyN^ zBnKCjp#YYlw8Xc7r7yZ3_oJM76>itsEYtPQL(kV~f$zs*X~?lzTJaT4v-J|wG+))U zl4DdXlw+EPwIdt}?*blI-i2f@k~e|eCBc$GJ>mEw1!OY_F4|lntAZ5iUX9VnS9CQ> zBO?JFrcXf5($E*(Fo+VAFilHu#V`sL2pYoRSR`R1RhP#Cvx;qch^sH-YhszB;R6Kj z_(DP4GF(i@nq+9eAw#Y&oylg0IesDeQPak%$oHYLJ$NEsds%`9%vXkCk&kq+4t8|_ z9VCp9!>7!;O+QoK3n6=-zXJnUDcp-dRzynaL$dD?QGX{1_&+4cKjclbC%?V4BzG;y zT@88fl6-JMK6r;N$cJX6Wg=cxE-P0PmlHSW&7(KG`q<4&jn3}- list[str]: + with tempfile.TemporaryDirectory() as temp: + path = Path(temp) / "template.yaml" + path.write_text(text, encoding="utf-8") + return violations([path]) + + def test_build_workflow_is_allowed(self) -> None: + self.assertEqual([], self.check("jobs:\n build:\n runs-on: container-build\n")) + + def test_ingress_is_rejected(self) -> None: + self.assertTrue(self.check("apiVersion: networking.k8s.io/v1\nkind: Ingress\n")) + + def test_public_service_is_rejected(self) -> None: + self.assertTrue(self.check("kind: Service\nspec:\n type: LoadBalancer\n")) + + def test_direct_apply_is_rejected(self) -> None: + self.assertTrue(self.check("run: kubectl apply -f deployment.yaml\n")) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/check_private_defaults.py b/tools/check_private_defaults.py new file mode 100755 index 0000000..b6737a3 --- /dev/null +++ b/tools/check_private_defaults.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""Fail when an enablement template creates a public Kubernetes path.""" + +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + + +FORBIDDEN = { + "Ingress resource": re.compile(r"(?m)^\s*kind:\s*(Ingress|IngressRoute)\s*$"), + "public Service type": re.compile(r"(?m)^\s*type:\s*(LoadBalancer|NodePort)\s*$"), + "direct kubectl apply": re.compile(r"\bkubectl\s+(?:[^\n]*\s)?apply\b"), + "direct helm deployment": re.compile(r"\bhelm\s+(upgrade|install)\b"), +} + + +def violations(paths: list[Path]) -> list[str]: + found: list[str] = [] + for path in paths: + text = path.read_text(encoding="utf-8") + for label, pattern in FORBIDDEN.items(): + if pattern.search(text): + found.append(f"{path}: {label}") + return found + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("paths", nargs="*", type=Path) + args = parser.parse_args() + paths = args.paths or sorted(Path("workflows").glob("*.yaml")) + problems = violations(paths) + if problems: + print("\n".join(problems), file=sys.stderr) + return 1 + print(f"private-default template check passed: {len(paths)} file(s)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/workplans/RAIL-EN-WP-0001-private-by-default-templates.md b/workplans/RAIL-EN-WP-0001-private-by-default-templates.md index 769aba2..a15a986 100644 --- a/workplans/RAIL-EN-WP-0001-private-by-default-templates.md +++ b/workplans/RAIL-EN-WP-0001-private-by-default-templates.md @@ -4,11 +4,11 @@ type: workplan title: "Templates emit private Services, never a public Ingress by default" domain: financials repo: railiance-enablement -status: ready +status: finished owner: codex topic_slug: railiance created: "2026-08-15" -updated: "2026-08-15" +updated: "2026-08-22" related: - RMASTER-WP-0023 - ADR-0008 @@ -29,7 +29,7 @@ They never emit a public Ingress by default. If a template mentions ```task id: RAIL-EN-WP-0001-T01 -status: todo +status: done priority: high state_hub_task_id: "ceea9e5a-385e-4f13-b8ea-0b1e72cddaee" ``` @@ -40,3 +40,13 @@ that cites ADR-0008. **Done when:** a new package from the paved path is private unless the author adds an explicit grant. + +**Outcome (2026-08-22):** all reusable workflow templates are checked by +`tools/check_private_defaults.py`. The check rejects Ingress/IngressRoute, +LoadBalancer/NodePort Services, and direct deployment commands. The documented +template contract points public exposure through the rail-owned ADR-0008 gate. + +## Completion evidence + +- `make check`: 4 workflow templates passed. +- `make test`: 4 regression tests passed.