Orchestrata Ultimate › Gates › external-tools
Gate: external-tools
At a glance
| Purpose | Refuse a non-PyPI executable that is declared but missing or too old, or invoked by a literal argv[0] but never declared in [tool.orchestrata.external-tools]. |
| Where it runs | The scripts/gates.py battery (registered as external-tools). |
| Trigger | A battery run whose src/, scripts/ or pyproject.toml fingerprint changed. |
| Exact command | python scripts/check_external_tools.py |
| Blocks | Exit 1 on a violation in either direction, 2 when the declaration cannot be read or parsed, 0 clean. |
| Ruling / ADR | RUL-014 ("a declaration nothing checks is prose, not a mechanism", per the module docstring). The Prolog-side refusal of bare toolchain names is ADR-0089. |
| 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 non-PyPI executable this project shells out to that is either declared but
missing/too old, or invoked but never declared in
[tool.orchestrata.external-tools].
Why it exists
Module docstring: "External-tools gate — enforces [tool.orchestrata.external-tools]
against reality, in BOTH directions (RUL-014: a declaration nothing checks is
prose, not a mechanism)."
The hole it closes, in its own words:
"
check-deps… gates Python IMPORTS; it is structurally blind to a Go/Rust/C binary someone installed by hand, which is exactly how an undeclared external dependency enters unnoticed."
How it runs
- Command:
python scripts/check_external_tools.py - When it fires: the
scripts/gates.pybattery (registered asexternal-tools). - The two directions (docstring):
- DECLARED-BUT-MISSING — "Every tool in the table must be on PATH and its
reported version must be >=
min_version." Version parsing is deliberately defensive because shapes differ ("measured:sops --versionprintssops 3.13.3 (latest);age --versionprints bare1.3.1") — "the first dotted-number run anywhere in the combined stdout+stderr is taken as the version and compared as a TUPLE OF INTS, never as a string (a string compare gets'1.10.0' > '1.9.0'backwards)." - INVOKED-BUT-UNDECLARED — an AST walk (stdlib
ast, never a regex) overscripts/andsrc/forsubprocess.{run,Popen,check_output,check_call}call sites; a literalargv[0]must appear in the declared table, the POSIX/venv allowlist (_ALLOWED_EXECUTABLES), or the project's own[project.scripts]console entry points ("read live, never hard-coded").tests/is deliberately out of scope ("tests legitimately shell out to throwaway binaries"), as are[tool.ruff] extend-excludevendored paths.
- DECLARED-BUT-MISSING — "Every tool in the table must be on PATH and its
reported version must be >=
- Disclosed blind spot (docstring): "Only a literal
argv[0]… can be resolved this way. A variable, an f-string, or any other expression is skipped, and the COUNT of skips is reported alongside the verdict, so the gate states its own blind spot instead of implying completeness it does not have." - Inputs: fingerprint roots
("src", "scripts", "pyproject.toml")— pyproject is "an input root in its own right" because the gate reads three of its tables (declared tools,[project.scripts],[tool.ruff] extend-exclude) directly. - Caching: the verdict is cached in
.cache/gates.jsonagainst_fingerprint(roots): a sha256 over the relative path and bytes of every file under the directory roots whose suffix is.py,.toml,.pl,.json,.mdor.sh(_interesting()inscripts/gates.py), pluspyproject.tomlas a file root. 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 (checked 2026-10-23). - A PATH change is not an input. A tool uninstalled or upgraded on the host changes nothing the fingerprint hashes, so a cached PASS survives it (derived from the code, not measured).
Typical failures and how to fix them
- Exit codes (docstring): "Exit 0 = every declared tool present & new enough, and no undeclared literal invocation. Exit 1 = at least one violation, either direction. Exit 2 = the declaration itself cannot be read/parsed at all — never a silent pass."
- What a failure looks like (representative): either a declared tool reported
absent/below
min_versionwith its install command, or an undeclared literalargv[0]reported with its call site. - Un-blocking: install/upgrade the tool with the declared install command, or add
the executable to
[tool.orchestrata.external-tools]with amin_version— the allowlist is for POSIX baseline tools, not an escape hatch. - Host tooling changed since the last PASS. Re-run fresh:
python scripts/gates.py --only external-tools --all.
Wiring
- Invokers:
scripts/gates.py(battery). - Tests:
tests/scripts/test_check_external_tools.py.
Related
- Gates master index
- Development workflow
- RUL-014 — A ruling that exists only in prose is incomplete work
- ADR-0089
- check-deps — the Python-imports counterpart this gate explicitly complements
- validate-ci — the other gate that reads config as its subject rather than code