From 74a104c92e1680708cc3010d96f2e269fe5bf910 Mon Sep 17 00:00:00 2001 From: Jess Sullivan Date: Fri, 3 Jul 2026 17:34:17 -0400 Subject: [PATCH 1/3] feat(tin-2423): substrate-boundary conformance validator (ledger item 30) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port the GF substrate-boundary conformance validator (scripts/validate-substrate-boundary.py, reference: tinyland-inc/GloriousFlywheel PR #1016, feat/tin-2423-substrate-boundary) to this repo. Asserts ADR-008 logical replaceability as a checked invariant: code surfaces may only reach the blahaj substrate through named, provenance-carrying interfaces recorded in config/substrate-boundary-allowlist.json; docs (*.md) stay exempt. Scan result: NAMED-INTERFACE, not a bleed. Two un-allowlisted repo-ref hits before the allowlist: [repo-ref] scripts/lane-dispatch.py:127: default="tinyland-inc/blahaj" (--blahaj-repo arg) [repo-ref] tinyland.repo.json:39: "gitops_receiver": "tinyland-inc/blahaj" Both are the generic ADR-007 receiver dispatch: scripts/lane-dispatch.py constructs + dispatches the `-lane-env` repository_dispatch payload (schema: docs/schemas/blahaj-dispatch.schema.json) via `just lane-dispatch` / `just lane-reap` and .github/workflows/lane-env.yml (tinyland-inc/ci-templates/.github/workflows/spoke-lane-env.yml@v2). tinyland.repo.json's gitops_receiver field is the manifest-level declaration of the same named receiver (docs/CI-SCHEMA.md §4/§9). Neither is an ad-hoc bypass of the generic contract — both are allowlisted with provenance in config/substrate-boundary-allowlist.json. No path-reach or state-key hits found (infra/tofu module source resolves to tinyland-inc/GloriousFlywheel, not blahaj — GF-reach, not blahaj-reach; infra/tofu/backend.hcl state key has no blahaj/ prefix). Wired into scripts/check-conformance.sh as checklist item 13 (own summary/exit-code aggregation, matching this repo's existing conformance idiom), exposed via `just substrate-boundary-check`, and added as an explicit step in the build-and-test job of .github/workflows/ci.yml (this repo's `just conformance` checklist is not itself CI-wired yet, so the narrow check is wired directly rather than piggybacking on an unwired recipe). Committed with -c commit.gpgsign=false --no-verify per TIN-2423 fanout house rules (spokes-A lane). --- .github/workflows/ci.yml | 1 + Justfile | 6 + config/substrate-boundary-allowlist.json | 22 ++++ scripts/check-conformance.sh | 13 +++ scripts/validate-substrate-boundary.py | 138 +++++++++++++++++++++++ 5 files changed, 180 insertions(+) create mode 100644 config/substrate-boundary-allowlist.json create mode 100644 scripts/validate-substrate-boundary.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8718cb3..f1c91b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,7 @@ jobs: command: | nix develop --command just setup nix develop --command just check + nix develop --command just substrate-boundary-check nix develop --command just build push-cache: 'false' diff --git a/Justfile b/Justfile index 1185c7e..7279ed5 100644 --- a/Justfile +++ b/Justfile @@ -403,6 +403,12 @@ lane-reap pr: conformance: cd {{ root }} && bash scripts/check-conformance.sh +# Substrate-boundary conformance: blahaj code-reach must be a named, +# provenance-carrying interface (config/substrate-boundary-allowlist.json), +# not an ad-hoc bleed (TIN-2423 / ledger item 30). +substrate-boundary-check: + cd {{ root }} && python3 scripts/validate-substrate-boundary.py + # ───────────────────────────────────────────── # Flywheel (cache-first; executor opt-in; see docs/CI-SCHEMA.md §5) # Env knob: FLYWHEEL=local|cache|executor|auto (default auto) diff --git a/config/substrate-boundary-allowlist.json b/config/substrate-boundary-allowlist.json new file mode 100644 index 0000000..b826da7 --- /dev/null +++ b/config/substrate-boundary-allowlist.json @@ -0,0 +1,22 @@ +{ + "$comment": "TIN-2423 / ledger item 30: named-interface allowlist for the substrate-boundary validator. Every entry carries provenance (TIN-2398 guardrail #2). A blahaj-reach hit under an allowlisted path_prefix is a NAMED INTERFACE, not a bleed.", + "schema_version": 1, + "allowed": [ + { + "path_prefix": "scripts/lane-dispatch.py", + "interface": "ADR-007 generic receiver dispatch: constructs + POSTs the `-lane-env` repository_dispatch payload (docs/schemas/blahaj-dispatch.schema.json) to tinyland-inc/blahaj. This is the spoke-side half of the generic PR-env provisioning contract, invoked via `just lane-dispatch` / `just lane-reap` and by .github/workflows/lane-env.yml (ci-templates spoke-lane-env.yml@v2).", + "provenance": { + "decision": "ledger item 30 (generic deploy-machinery / dynamic-spoke deployment edge) + blahaj ADR-007 generic receiver contract", + "date": "2026-07-03" + } + }, + { + "path_prefix": "tinyland.repo.json", + "interface": "Declared `gitops_receiver` manifest field naming tinyland-inc/blahaj as the repository_dispatch receiver, per the tinyland-repo-manifest schema and docs/CI-SCHEMA.md §4/§9 (the wire, not blahaj's implementation).", + "provenance": { + "decision": "ledger item 30 (generic deploy-machinery / dynamic-spoke deployment edge) + blahaj ADR-007 generic receiver contract", + "date": "2026-07-03" + } + } + ] +} diff --git a/scripts/check-conformance.sh b/scripts/check-conformance.sh index 5d350ef..29d5d75 100755 --- a/scripts/check-conformance.sh +++ b/scripts/check-conformance.sh @@ -154,6 +154,19 @@ else man "AGENTS.md cites the scaffold tag the repo conforms to (pre-D3 PR7 OK)" fi +# 13. Substrate-boundary conformance (TIN-2423 / ledger item 30): code-surface +# reach into the blahaj substrate must go through a named, provenance-carrying +# interface (config/substrate-boundary-allowlist.json), not an ad-hoc bleed. +if [[ -f scripts/validate-substrate-boundary.py ]]; then + if python3 scripts/validate-substrate-boundary.py >/dev/null 2>&1; then + ok "substrate-boundary: blahaj reach is allowlisted or absent (TIN-2423)" + else + no "substrate-boundary: un-allowlisted blahaj reach — run just substrate-boundary-check for details" + fi +else + no "scripts/validate-substrate-boundary.py is missing" +fi + echo echo "summary: ${pass} pass, ${fail} fail, ${manual} manual" if (( fail > 0 )); then exit 1; fi diff --git a/scripts/validate-substrate-boundary.py b/scripts/validate-substrate-boundary.py new file mode 100644 index 0000000..d3ac830 --- /dev/null +++ b/scripts/validate-substrate-boundary.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +"""Substrate-boundary conformance (TIN-2423 / ledger item 30, consumer-side). + +Asserts ADR-008 "logical replaceability" as a checked invariant instead of +prose: this repo's CODE surfaces may reach the blahaj substrate ONLY through +named interfaces recorded (with provenance) in +config/substrate-boundary-allowlist.json. Docs (*.md) are exempt — the +invariant is about code reach, not prose mentions. + +Flagged reach classes (TIN-2423 item 1): + repo-ref a tinyland-inc/blahaj reference (module source, workflow + dispatch target, checkout) in a code surface + path-reach a sibling/home-relative filesystem reach into a blahaj + checkout (../blahaj, ~/git/blahaj, /git/blahaj) + state-key an OpenTofu backend key under the blahaj/ state prefix + +Exit 0 = conformant (allowlisted hits are reported, not failed). +Exit 1 = un-allowlisted reach (the boundary bleed class of TIN-2398/2406). +""" +from __future__ import annotations + +import json +import re +import subprocess +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent +ALLOWLIST = REPO / "config" / "substrate-boundary-allowlist.json" + +# Code surfaces (git-tracked). Markdown is exempt everywhere. +CODE_GLOBS = [ + "infra/tofu/**", "infra/kustomize/**", ".github/workflows/**", "scripts/**", + "Justfile", "flake.nix", "MODULE.bazel", "**/*.bzl", "**/BUILD.bazel", + "tinyland.repo.json", +] + +PATTERNS = { + "repo-ref": re.compile(r"tinyland-inc/blahaj"), + "path-reach": re.compile(r"(\.\./|~/git/|/git/)blahaj\b"), + "state-key": re.compile(r"key\s*=\s*\"blahaj/"), +} + + +def tracked_code_files() -> list[Path]: + out = subprocess.run( + ["git", "ls-files", "--"] + CODE_GLOBS, + cwd=REPO, capture_output=True, text=True, check=True, + ).stdout.splitlines() + return [Path(p) for p in sorted(set(out)) if not p.endswith(".md")] + + +def load_allowlist() -> list[dict]: + data = json.loads(ALLOWLIST.read_text()) + entries = data["allowed"] + for e in entries: + for field in ("path_prefix", "interface", "provenance"): + if field not in e: + raise SystemExit( + f"allowlist entry missing {field!r} (provenance-carrying " + f"allowlists are the contract, TIN-2398 guardrail #2): {e}" + ) + for field in ("decision", "date"): + if field not in e["provenance"]: + raise SystemExit(f"allowlist provenance missing {field!r}: {e}") + return entries + + +def scan(files: list[Path], allowed: list[dict]): + violations, allowed_hits = [], [] + for rel in files: + path = REPO / rel + try: + text = path.read_text(errors="replace") + except (OSError, IsADirectoryError): + continue + for lineno, line in enumerate(text.splitlines(), 1): + for kind, rx in PATTERNS.items(): + if not rx.search(line): + continue + entry = next( + (e for e in allowed if str(rel).startswith(e["path_prefix"])), + None, + ) + record = (str(rel), lineno, kind, line.strip()[:120]) + (allowed_hits if entry else violations).append(record) + return violations, allowed_hits + + +def self_test() -> None: + cases = { + "repo-ref": 'uses: tinyland-inc/blahaj/.github/workflows/x.yml@main', + "repo-ref-dispatch": 'gh api repos/tinyland-inc/blahaj/dispatches -f event_type=x', + "path-reach": 'source = "../blahaj/tofu/modules/thing"', + "path-reach-home": 'cd ~/git/blahaj && just apply', + "state-key": 'key = "blahaj/mail/terraform.tfstate"', + } + for name, sample in cases.items(): + if not any(rx.search(sample) for rx in PATTERNS.values()): + raise SystemExit(f"self-test FAILED: {name!r} not detected") + clean = 'key = "tinyland-infra/attic/terraform.tfstate" # fine' + if any(rx.search(clean) for rx in PATTERNS.values()): + raise SystemExit("self-test FAILED: false positive on clean line") + # allowlist suppression + allowed = [{"path_prefix": "tofu/modules/spoke-blahaj-app-install/", + "interface": "t", "provenance": {"decision": "t", "date": "t"}}] + v, a = [], [] + entry = next((e for e in allowed + if "tofu/modules/spoke-blahaj-app-install/main.tf".startswith(e["path_prefix"])), None) + (a if entry else v).append("x") + if v or not a: + raise SystemExit("self-test FAILED: allowlist suppression broken") + print("substrate-boundary self-test passed") + + +def main() -> int: + if "--self-test" in sys.argv: + self_test() + return 0 + allowed = load_allowlist() + violations, allowed_hits = scan(tracked_code_files(), allowed) + for rel, lineno, kind, frag in allowed_hits: + print(f"allowed [{kind}] {rel}:{lineno}: {frag}") + if violations: + print(f"\nsubstrate-boundary FAILED: {len(violations)} un-allowlisted " + f"blahaj reach(es) — consume the substrate via a named interface " + f"or add a provenance-carrying allowlist entry (TIN-2423):", + file=sys.stderr) + for rel, lineno, kind, frag in violations: + print(f" [{kind}] {rel}:{lineno}: {frag}", file=sys.stderr) + return 1 + print(f"substrate-boundary validation passed " + f"({len(allowed_hits)} allowlisted hit(s), 0 violations)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From e589ce1a97848f109c96dc23007d3487ac427407 Mon Sep 17 00:00:00 2001 From: Jess Sullivan Date: Fri, 3 Jul 2026 18:17:50 -0400 Subject: [PATCH 2/3] fix(tin-2423): exclude validator's own file from substrate-boundary scan Ports the SELF-exclusion fix from GloriousFlywheel main (6fb1f50): the validator was scanning its own source under scripts/**, which contains the blahaj repo-ref/path-reach/state-key regex literals used for detection and self-testing, producing false-positive violations against itself. SELF is now resolved and excluded in tracked_code_files(). --- scripts/validate-substrate-boundary.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/validate-substrate-boundary.py b/scripts/validate-substrate-boundary.py index d3ac830..105dd81 100644 --- a/scripts/validate-substrate-boundary.py +++ b/scripts/validate-substrate-boundary.py @@ -26,6 +26,7 @@ from pathlib import Path REPO = Path(__file__).resolve().parent.parent +SELF = Path(__file__).resolve().relative_to(REPO) ALLOWLIST = REPO / "config" / "substrate-boundary-allowlist.json" # Code surfaces (git-tracked). Markdown is exempt everywhere. @@ -47,7 +48,11 @@ def tracked_code_files() -> list[Path]: ["git", "ls-files", "--"] + CODE_GLOBS, cwd=REPO, capture_output=True, text=True, check=True, ).stdout.splitlines() - return [Path(p) for p in sorted(set(out)) if not p.endswith(".md")] + return [ + Path(p) + for p in sorted(set(out)) + if not p.endswith(".md") and Path(p) != SELF + ] def load_allowlist() -> list[dict]: From 8c7a38b0978e3e87d060180efda075ac76e0b410 Mon Sep 17 00:00:00 2001 From: Jess Sullivan Date: Fri, 3 Jul 2026 18:55:16 -0400 Subject: [PATCH 3/3] style: prettier-format substrate-boundary allowlist (lint gate) --- config/substrate-boundary-allowlist.json | 40 ++++++++++++------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/config/substrate-boundary-allowlist.json b/config/substrate-boundary-allowlist.json index b826da7..16589e4 100644 --- a/config/substrate-boundary-allowlist.json +++ b/config/substrate-boundary-allowlist.json @@ -1,22 +1,22 @@ { - "$comment": "TIN-2423 / ledger item 30: named-interface allowlist for the substrate-boundary validator. Every entry carries provenance (TIN-2398 guardrail #2). A blahaj-reach hit under an allowlisted path_prefix is a NAMED INTERFACE, not a bleed.", - "schema_version": 1, - "allowed": [ - { - "path_prefix": "scripts/lane-dispatch.py", - "interface": "ADR-007 generic receiver dispatch: constructs + POSTs the `-lane-env` repository_dispatch payload (docs/schemas/blahaj-dispatch.schema.json) to tinyland-inc/blahaj. This is the spoke-side half of the generic PR-env provisioning contract, invoked via `just lane-dispatch` / `just lane-reap` and by .github/workflows/lane-env.yml (ci-templates spoke-lane-env.yml@v2).", - "provenance": { - "decision": "ledger item 30 (generic deploy-machinery / dynamic-spoke deployment edge) + blahaj ADR-007 generic receiver contract", - "date": "2026-07-03" - } - }, - { - "path_prefix": "tinyland.repo.json", - "interface": "Declared `gitops_receiver` manifest field naming tinyland-inc/blahaj as the repository_dispatch receiver, per the tinyland-repo-manifest schema and docs/CI-SCHEMA.md §4/§9 (the wire, not blahaj's implementation).", - "provenance": { - "decision": "ledger item 30 (generic deploy-machinery / dynamic-spoke deployment edge) + blahaj ADR-007 generic receiver contract", - "date": "2026-07-03" - } - } - ] + "$comment": "TIN-2423 / ledger item 30: named-interface allowlist for the substrate-boundary validator. Every entry carries provenance (TIN-2398 guardrail #2). A blahaj-reach hit under an allowlisted path_prefix is a NAMED INTERFACE, not a bleed.", + "schema_version": 1, + "allowed": [ + { + "path_prefix": "scripts/lane-dispatch.py", + "interface": "ADR-007 generic receiver dispatch: constructs + POSTs the `-lane-env` repository_dispatch payload (docs/schemas/blahaj-dispatch.schema.json) to tinyland-inc/blahaj. This is the spoke-side half of the generic PR-env provisioning contract, invoked via `just lane-dispatch` / `just lane-reap` and by .github/workflows/lane-env.yml (ci-templates spoke-lane-env.yml@v2).", + "provenance": { + "decision": "ledger item 30 (generic deploy-machinery / dynamic-spoke deployment edge) + blahaj ADR-007 generic receiver contract", + "date": "2026-07-03" + } + }, + { + "path_prefix": "tinyland.repo.json", + "interface": "Declared `gitops_receiver` manifest field naming tinyland-inc/blahaj as the repository_dispatch receiver, per the tinyland-repo-manifest schema and docs/CI-SCHEMA.md §4/§9 (the wire, not blahaj's implementation).", + "provenance": { + "decision": "ledger item 30 (generic deploy-machinery / dynamic-spoke deployment edge) + blahaj ADR-007 generic receiver contract", + "date": "2026-07-03" + } + } + ] }