Orchestrata Ultimate › Gates › env-contract
Gate: env-contract
At a glance
| Purpose | Refuse a production entrypoint that reaches an infra-config read without reaching a registered env loader, and a second mutating dotenv loader under src/. |
| Where it runs | The scripts/gates.py battery (registered as env-contract); scripts/health_check.py. |
| Trigger | A battery run whose src/ fingerprint changed; a health check. |
| Exact command | python scripts/check_env_contract.py [--repo-root PATH] [--max-depth N] [--json] |
| Blocks | Exit 1 on any violation, 2 when the tree cannot be analysed, 0 clean. |
| Ruling / ADR | RUL-019 (gate scripts/check_env_contract.py RUL-019); the endpoint keys it triggers on are RUL-003's. |
| Last recorded verdict | ok: true in the primary checkout's .cache/gates.json (file last written 2026-09-22 10:01 local). wt-MAP-PRECOMMIT has no .cache/gates.json. |
What it refuses
A production entrypoint whose import closure reaches an infra-config read (a
RUL-003 endpoint key, or a raising resolver seam) without ever reaching a registered
env loader — and, in the other direction, a second mutating dotenv loader appearing
under src/.
Why it exists
The module docstring quotes the ruling verbatim:
"RUL-019 — Config modules use Pydantic BaseSettings, and env files must be auto-loaded by every entrypoint via _load_dotenv() (secrets-only .env, then the active ORCHESTRATA_ENV_PROFILE overlay env/{dev,test,prod}.env with override=True) — never left to direnv or manual sourcing."
And records the measured state that made it mechanizable:
"There is NO canonical two-phase loader in src/ … src/ carries three DIVERGENT copies, none of which does the profile overlay … Two production entrypoints reach a REQUIRED raising infra seam and load nothing: agents/runtime_main.py … and knowledge_graph/server.py … Both are launched by systemd ExecStart under deploy/ and survive only on EnvironmentFile= injection. Verified empirically, not theoretically:
env -i .venv/bin/python -m orchestrata_ultimate.agents.runtime_main→ rc=1, CXPConfigError '…define one in .env.' The process names .env in its own failure message and never reads it."
The ruling's other clause ("config modules use Pydantic BaseSettings") is explicitly
NOT mechanized: "It has no falsifiable subject … That clause stays a second
none (judgment) bullet on the ruling page."
How it runs
- Command:
python scripts/check_env_contract.py [--repo-root PATH] [--max-depth N] [--json] - When it fires: the
scripts/gates.pybattery (registered asenv-contract);scripts/health_check.py. - The entrypoint universe ("enumerable, deterministic; never 'the file is called
cli.py'"):
- E1 —
[project.scripts]targets inpyproject.tomlresolving undersrc/. - E2 —
ExecStart=lines underdeploy/invokingpython -m <mod>or asrc/path (backslash continuations joined first — "a line-wise regex would silently miss the kernel unit entirely, and a missed entrypoint is a FALSE NEGATIVE, the worse direction here"). - E3 —
src/**/*.pycarrying a top-levelif __name__ == "__main__":.
- E1 —
- The two rules:
- Rule B — COVERAGE (the ruling): every in-scope entrypoint's closure must
reach a call to a REGISTERED env loader ("an explicit short list, never a name
heuristic"). Reachability, not unconditional execution — a hermetic-test guard
(
if env is None:) legitimately counts. - Rule A — LOADER CONFORMANCE / NO DUPLICATES: at most ONE module under
src/may define a MUTATING dotenv loader, and that one must referenceORCHESTRATA_ENV_PROFILE, anenv/profile path, and apply the profile with OVERRIDE. "Rule A is what stops Rule B being satisfiable by a stub." Non-mutating.envREADERS are deliberately not flagged.
- Rule B — COVERAGE (the ruling): every in-scope entrypoint's closure must
reach a call to a REGISTERED env loader ("an explicit short list, never a name
heuristic"). Reachability, not unconditional execution — a hermetic-test guard
(
- Depth bound, measured not assumed: the trigger search is bounded
(
--max-depth, default 1) because "unbounded, the gate reports 16 violations; 14 of them trace to ONE trigger site … Importing the adapter reads nothing … A gate that fires there gets switched off, and a switched-off gate enforces nothing." The bound "errs toward FALSE NEGATIVES … and it is VISIBLE, never silently dropped — every run reportsdeep_scope_only" (raise with--max-depth -1to audit). - Scope limit, stated: "this gate proves an entrypoint REACHES a conforming loader; it does not prove the loader RAN before the first infra read (ordering). … Do not claim ordering coverage."
- Inputs: fingerprint root
("src",)only (theGATESentry inscripts/gates.py, read through the AST toolkit on 2026-10-23). An earlier version of this page listedpyproject.tomlanddeployas roots "ADDED in the 2026-08-28 audit"; the registry onorigin/master(a4cbc2bdf) does not carry them. Both feed the entrypoint universe (E1, E2), so an entrypoint added or removed only inpyproject.tomlor underdeploy/leaves the cached PASS standing (derived from the code, not measured). - Caching: the verdict is cached in
.cache/gates.jsonagainst_fingerprint(("src",)): a sha256 over the relative path and bytes of every file undersrc/whose suffix is.py,.toml,.pl,.json,.mdor.sh(_interesting()). There is no per-gate suffix mask: this page previously quoted one, andgit log --all -S GATE_MASKS -- scripts/gates.pyfinds no commit that ever contained aGATE_MASKStable.
Typical failures and how to fix them
- Exit codes: "Exit 0 clean, 1 on any violation, 2 when the tree cannot be
analysed at all (no
src/, unparseableinfra_env_rule.py)." - What a failure looks like (representative): an in-scope entrypoint named with the trigger it reaches and the missing loader (Rule B), or a second mutating loader module named (Rule A).
- Un-blocking: wire the entrypoint to the canonical loader (import it and call it at startup); never add a parallel loader — Rule A exists to refuse exactly that.
- A
deploy/orpyproject.tomlentrypoint change that the battery skips. The battery reuses its cached PASS because neither path is a fingerprint root. Force a fresh verdict withpython scripts/gates.py --only env-contract --all.
Wiring
- Invokers:
scripts/gates.py(battery),scripts/health_check.py. - Declared as: the
gate scripts/check_env_contract.py RUL-019mechanism on the ruling pagerulings/RUL-019-pydantic-dotenv-config-pattern.md— whichrulings_lintverifies by literal substring. - Tests:
tests/scripts/test_check_env_contract.py.
Related
- Gates master index
- Development workflow
- RUL-019 — Config modules: Pydantic BaseSettings + auto-loaded .env
- RUL-003 — Env config from the active env profile
- rulings-lint — verifies the
RUL-019
gatedeclaration by literal substring - env-subnet-scheme — what the profile files must contain; this gate forces processes to read them
- validate-ci — Gates 12–14 are the CI-side env-ownership cousins