The only repository in the TAIPANBOX estate allowed to know about more than one repository at once.
Sixteen repositories, each with unusually good gate discipline. Each names its invariants. Each has scripts that fail a push when a claim stops being true. Every one of them green.
A day of auditing found that almost every real defect was between them, where no gate could see:
- verdryx held seven of tokenfuse's nine Breaker block-decision wire strings, so for eleven days it counted avoided estimates as real money;
- agent-stack-go's vendored copy of the passport schema fell three weeks behind the canonical one, so its "full schema validation" silently skipped the two fields that exist for AI Act code inventory;
- agent-passport's registry said idryx emits events; idryx has no event writer at all, and three artifacts carried that claim for weeks after the prose was corrected;
- idryx pinned agent-stack-go v0.3.0 while the module was at v0.5.1, and the entire delta was the tamper-evidence chain verifier idryx most needed;
- the same stack is deployed three ways and they disagree on which governance routines run, on a severity default, and on which components come up.
None of those is a bug inside a repository. Every one is a disagreement between two of them, and a single-repo gate cannot have an opinion about a file it is not allowed to read. This repository is the answer to that class, and it is the only place with that permission.
1. A check that cannot fail is worse than no check, because it reports
green forever and somebody trusts it. Every check here is proven capable of
failing by ./selftest.py, which builds a miniature estate, breaks one thing
at a time, and requires the matching finding to fire. It also reads the gates'
own AST, so a FAIL path added tomorrow without a mutation fails the self-test
rather than joining the suite unexamined.
2. A check whose subject has vanished FAILS LOUDLY. A file that is gone, a
repository that is missing, an anchor that matches nothing: all three are red,
with a message naming what disappeared. Silent success on a missing subject is
how the estate got here. The one exception is a repository estate.json
records as having no public remote, and even then the run reports PARTIAL and
names it, never clean.
C1, pin currency (gates/c1-pin-currency.py). For every repository whose
go.mod requires github.com/TAIPANBOX/agent-stack-go, compares the pin with
the module's newest tag. Any lag fails; the finding says whether it is a minor
(a different contract, since the module is pre-1.0) or a patch (the same
contract, missing fixes). A pin ahead of every tag fails too, as does a
replace directive, which makes the pin unenforceable. A fresh release turns
this red for every consumer on the day it is cut, deliberately: that is the
day the estate is most drifted, and a grace period would report green during
the exact window the check exists to describe.
C2, vendored schema equality (gates/c2-vendored-schemas.py). The eight
vendored copies of agent-passport's three canonical schemas, across
agent-stack-go, genaryx, engram and verdryx, compared BYTE for byte with a
unified diff of the first difference. Byte-exact on purpose: two JSON
documents that differ only in whitespace or key order are the same schema to a
validator and a different one to the person who opens the file to see what the
canonical says.
C3, mirrored constants (gates/c3-mirrored-constants.py). verdryx's three
hand-copied mirrors of tokenfuse constants: the Breaker block-decision wire
strings (compared as sets), the default price book (per model and on the
fallback), and the trace Parquet column names verdryx reads (containment: it
reads five of sixteen). It PREFERS tokenfuse's published
contracts/tokenfuse-constants.json when that artifact is present and parses
the Rust when it is not, and it says which mode it used in every run, because
a comparison whose reader does not know what was compared is one somebody will
quote wrongly later.
C3's fourth mirror is the tool-call argument cap: tokenfuse's
TOOL_CALL_ARGUMENTS_CAP_BYTES against wardryx's MaxToolArgumentsBytes, read
from source in both, because tokenfuse's artifact does not carry it. The two
must be equal. Tokenfuse cutting arguments above a bound wardryx refuses, or
below one, decides calls between the two without a verdict or without their
arguments. A value the check cannot evaluate is refused by its text
(c3.argcap-unreadable), never guessed at.
C4, event registry versus actual producers
(gates/c4-event-registry.py). agent-passport SPEC.md section 6.2 is the
registry: source to event types. Both directions. Forward: every source it
lists has a real event-writing code path and every type string it lists
appears at an emit site. Reverse, and more valuable: any type string emitted
by any repository whose source the registry does not carry, or which is
attributed to the wrong source. Anchored on the emit call sites in Rust, Go
and Python, never on strings that look like event names, and a repository
whose shape defeats the parser is reported as a hole rather than skipped. A
producer that is a command inside a shared module rather than a repository of
its own (agent-conform in agent-stack-go) is declared under the module's
entry and read from its const block, and the extractor requires each declared
name to still be written. The search for an undeclared writer asks only
whether a repository is declared, so a SECOND writer added inside a declared
repository is not found by it.
Every producer reads the non-test source files of its code directories, not the one file that
first held its emits. A Go producer resolves its ev* and type constants per package and reads
either the event literals or the emit helper's first argument from every file (wardryx's
identity-mismatch emit sits in agent_token.go, and vouchryx's delegation emits in agents.go and
xaa.go). Python reads every module of the package, and genaryx every crate file that inserts a
literal console type. A type that cannot be read is refused, and an empty read says it measured
nothing. tokenfuse (one closed enum), costcrew (one declared list), taipan (a replayer of one
table) and genaryx's demo and mock generators (computed types) are the named exceptions, recorded in
invariant 30 of CLAUDE.md as partly gated.
C5, deployment parity (gates/c5-deployment-parity.py). stack-up,
stack-single and stack-k8s compared on four things: which governance routines
each installs, the HERALDYX_MIN_SEVERITY default, the fixed local port map,
and which components each brings up. Differences are not automatically
failures, because several are deliberate, so this one fails on any divergence
that expectations/deployment-parity.json does not record with a reason and a
date. It fails the other way too: an expectation recorded for a divergence
that no longer exists is red, so the file cannot become a graveyard of stale
allowances.
The stack-single no-scheduler check reads executable scheduling forms in its
installer and Compose file. A systemd service drop-in that only orders Docker
startup is not a scheduled governance routine. Scheduling forms outside the
check's named patterns remain a limit of this source reader.
C6, cross-language chain vectors (gates/c6-chain-vectors.py).
agent-stack-go/event/testdata/chain-vectors.json pins the RFC 8785
canonicalization and the SPEC 6.5 chain hashes. Three implementations retype
those values as literals in their own suites: Rust in tokenfuse, Python in
engram and in verdryx. A change to the Go canonicalization would not fail any
of them today. This compares every copy with the file.
C7, the agent_id rule copied into code (gates/c7-rule-in-code.py). C2
holds vendored schema FILES byte-identical and cannot see a rule that was read
out of a schema once and retyped as a constant. The estate has four of those
for one rule, SPEC 3.1's agent://<trust-domain>/<name> grammar and its
255-byte cap: agent-stack-go/passport/passport.go, which six repositories
import by tag, the Python copies in engram and verdryx, and the Rust one in
tokenfuse. A drifted file shows up in a diff; a drifted constant compiles,
passes its own suite, and is visible only to somebody holding both repositories
open. Each extractor anchors on a NAME and captures whatever pattern sits
beside it, so a rename breaks loudly rather than quietly finding nothing. Three
anchor on a constant; tokenfuse compiles its regex inside a function, so that
one anchors on the function and refuses to cross another fn.
C8, the registry reaches the record (gates/c8-registry-reaches-the-record.py).
C4 holds SPEC 6.2 against the producers and says nothing about what becomes of
an event afterwards. trailryx is the record plane, and a type its ingest door
does not know is refused as UnknownType and counted. That refusal is correct
behaviour, and it is also what an omission looks like: "we decided this does
not belong in the record" and "nobody got to it" produce the same refusal, the
same counter and the same silence. trailryx already writes its decisions down,
in a doc-comment passage naming the types it refuses on purpose, so this check
asks only that every registered type appears on one of the two lists, the
mapping arms or the refused names. It never requires a mapping: the record
vocabulary is deliberately narrower than the bus, and forcing a record type per
registered type would be the wrong direction. Writing it found policy_updated,
wardryx's admin type at severity high, on neither list, which meant the record
plane was silently dropping the event that says an operator changed the policy
rules.
C9, a git command aimed elsewhere clears the hook's environment
(gates/c9-foreign-git-in-hooks.py). git runs a hook with GIT_DIR set to the
repository being pushed, and git -C <somewhere else> changes the working
directory without clearing it, so the command reads the other repository's
working tree against this one's index and object database. Found in trailryx on
2026-08-26: a check asking whether the local advisory database had untracked
files answered nothing from a terminal and all 1221 of its entries from the
pre-push hook, and refused the push. Three sessions retried instead of looking
at the environment, because the failure only exists in a context nobody debugs
from. The quieter shape is worse than the one that was found: show <ref>:<path>
under the wrong object database resolves the ref in the wrong repository, and
where both hold a file at that path it succeeds and returns the wrong content,
so a check comparing a vendored copy against its original compares the copy
against itself. The single rule that keeps this honest is that the target must
sit before the subcommand, where git requires it, which is what keeps
git archive HEAD | tar -x -C "$dir" out of the answer: five repositories write
that line, and a check matching git and -C anywhere would have reported all
five and been deleted by whoever read the first finding.
C10, the RFC 8693 mapping produces one chain in every language
(gates/c10-delegation-mapping.py). agent-passport SPEC 5.3 says how an RFC
8693 act claim becomes an on_behalf_of chain, and it is the one place in
this estate where a mistake produces something that VERIFIES PERFECTLY and
asserts the opposite of what happened: a signature is over the claims and says
nothing about how a reader turned them into a list. Two mistakes are available
and both were made on 2026-08-26, in the hour the mapping was first written.
The direction: the RFC nests act current-first, SPEC 5 orders the chain
root-first, and reversing it wrongly records that the root delegated to nobody.
The head: the RFC keeps the subject OUT of act and SPEC 5 puts the root INTO
the chain, so the mapping is [sub] + reverse(act), and missing that writes the
chain with the human missing from it. Two implementations exist and neither can
see the other, each holding its expected chain as a literal in its own suite, so
changing one leaves the other passing against the old answer. The gate reads the
ASSERTION rather than the test body, which is the correction its own self-test
forced: taking the last three principals found the same three whatever the
assertion said, because a body also names the principals that BUILD the token,
and the gate could not see the exact failure it exists for.
C12, every envelope member has a plane at the record
(gates/c12-envelope-members-have-a-plane.py). C8 asks whether each event
TYPE has an answer at the record plane. This asks it about the envelope's
MEMBERS, and the failure is worse, because a member has no refusal path.
trailryx's mapper files every member of a line in exactly one of two planes: a
fixed list (CONSUMED) goes to typed metadata, which is kept, and everything
else goes to the payload plane, which a per-event key ERASES. A member the
mapper has never seen is therefore not refused and not counted. It is silently
filed in the erasable half, and the store reports nothing.
What it found when it was written: delegation_proof (agent-passport SPEC
5.2), emitted by tokenfuse since 2026-08-26, which records that the
on_behalf_of chain was proved by an RFC 8693 token. CONSUMED did not name
it, so the chain sat in the kept plane and its proof in the erasable one. SPEC
5.2 reads a chain with no proof beside it as not proven, so a routine payload
erasure would have turned a proven chain into an unproven one, silently, in the
store whose claim is that it holds what happened in a form nobody can quietly
alter.
The check is about coverage, never about which answer. Payload is a legitimate
answer for most members, and for data the only correct one. A member passes
when it is consumed into typed metadata, or when the mapper's plane-boundary
passage names it in backticks and argues why it belongs in the erasable half.
The subjects come from the schema files, never from a list in the script:
every version of the event envelope schema in agent-stack-go
(cmd/agent-conform/schemas) is read and its properties unioned, so a member
added in a newer version is a subject the day it lands. The finding is
c12.member:<name>. A run that finds no schema with properties (c12.schemas),
or cannot find the mapper's CONSUMED list or its plane-boundary passage
(c12.mapper-unreadable), says it measured nothing and fails; that is not a
pass.
C13, the delegation depth cap counts one thing
(gates/c13-delegation-cap.py). SPEC 5.1 reads "Maximum chain depth is 32
entries" and SPEC 5 calls the members of on_behalf_of entries, so the bound
belongs to the assembled chain. SPEC 5.3's mapping is [sub] + reverse(act),
so a producer building that chain out of an RFC 8693 token bounds two
quantities one apart, and one sentence cannot tell it which one it meant.
Measured 2026-08-27 with agent-conform against a real emitted line: both
producers in the estate had bounded the ACTORS at 32 and then prepended the
subject, while every consumer bounded the CHAIN at 32. A token carrying 32
actors verified at the door and every record it produced was quarantined with
maxItems: got 33, want 32. Every number read 32, every repository was
internally consistent, and every suite was green: the disagreement was in the
UNIT, which is precisely what no repository can see about another.
Three sides, all discovered. The SPEC's sentence, parsed for the number and for
the unit word, so a reworded sentence is a red rather than a silent
re-reading. Every JSON Schema anywhere in the estate that DECLARES
on_behalf_of, found by searching each repository for the member, so a
vendored copy is a subject the day it lands and one that bounds nothing is a
consumer accepting what the SPEC forbids. And every cap constant under a
chain or delegation path, where an entries cap must equal the SPEC's number
and an actors cap must equal it minus one AND be derived from the entries cap
rather than retyped.
Rust caps re-exported from a sibling crate are resolved through the declared
Cargo dependency and the exported constant; an unreadable alias is a finding.
The finding to read first is none of those three. A file that maps an act
claim into a chain must state BOTH numbers, and that check does not depend on
finding a cap at all, so a constant renamed out of the anchor cannot switch it
off. Run against the estate as it stood on the morning of 2026-08-27 it names
agent-stack-go/delegation/chain.go and tokenfuse/crates/delegation/src/lib.rs,
which is the defect, from the only place in the estate that could have seen it.
C14, a vendored table is the table
(gates/c14-vendored-tables-are-the-table.py). Some rules cannot be shared.
agent-stack-go/chain holds what the record accepts of a delegation chain,
agent-stack-go/delegation is a door that deps-layering.sh forbids from
importing it, and tokenfuse/crates/delegation is a third implementation in
another language with no seam to either, so the rules exist three times by
construction. Three of them were found disagreeing on 2026-08-27, in one
afternoon. Prose did not hold them, and a gate reading source text could not: a
regex over two languages says a rule is mentioned, never that it answers. The
answer was a TABLE each implementation runs, and a table only holds while every
copy of it is the same table.
The subjects are found by $source, which every canonical table carries and
which names its own path. A copy is any file carrying the same $source at a
different path, so a new language vendoring the table is checked from the day it
lands rather than the day somebody remembers this file. C6 does the same job for
the hash vectors through a hand-written list of copies; this is that check with
the list taken out. Three findings: c14.copy-drifted:<source> when a copy
differs from the canonical by a byte, c14.canonical-missing:<source> when
copies name a path nothing is at, and c14.copy-unexercised:<repo> when no file
with a test declaration in its own language (#[test], func Test,
def test_ and the like) names the copy, because byte-identical copies of a
table nobody runs are files that agree about nothing. A reference that merely
starts with the file name, such as a renamed .disabled copy, does not count.
That last check is evidence that a suite reaches the file, and deliberately not
a claim that the suite asserts on every vector: nothing a read-only gate can do
reaches that far, and the distance is stated rather than implied away. A run
that finds no table at all (c14.no-tables) measured nothing and fails, and a
repository it could not read is named (c14.unreadable:<repo>), never skipped
in silence.
C15, a declaration a repository proves, read across the estate
(gates/c15-component-manifests.py, features/component-manifests.feature).
The runs field in estate.json says which components exist, and nothing reads
a repository to confirm it. That gap cannot be closed from here: the only thing
that knows which binaries a repository builds is the repository, so a component
that was FORGOTTEN is invisible from outside by construction, and runs: [] is a
valid answer no central file can contradict. vouchryx was installable by nothing
for nineteen hours on 2026-08-26 for exactly that reason. Nor can the checks
that matter most be made from here: what separates a service that exits 2
without three variables from one that starts happily with an empty environment
is invisible to every source-reading check and obvious to one that STARTS the
binary, and this repository builds nothing. So the
division is that the repository declares and proves, and this one reads across.
Each component repository may carry components.json at its root. Everything
under a component's checked key is asserted by that repository's own suite, in
its own CI. Everything under declared is a statement nobody can verify and
must carry its own why. This check owns four things, deliberately modest:
- The manifests agree with
estate.json: every component the registry says a repository contributes must be one its own manifest declares (c15.registry-names-what-the-repo-does-not), and a manifest naming a schema this reader does not know (c15.unknown-schema), or declaring nothing (c15.manifest-declares-nothing), is refused rather than misread. - A
declaredentry with nowhyis refused (c15.declared-without-a-reason), so a repository that drops its own test does not quietly drop the rule. - The comparison a single repository could never make: the health path a
component declares against the path a deployment actually polls
(
c15.probe-disagrees). A launcher that deliberately probes something else says why under the component'sdeclaredbucket, and the check then reads a decision instead of a disagreement. - A
serviceordaemonthat no launcher installs (c15.nothing-installs-it), the question the whole per-repository declaration was for. It was asked by hand on 2026-08-28 and foundtokenfuse-cluster, a raft-replicated budget ledger installable by nothing for fifty-seven days.dev-toolis the class for something nothing should install, andtoolis not judged. Adeclaredentry naming the install decision is the answer.
It does not read source, so it cannot tell a manifest that is complete from one
that is missing a component. Adoption is incremental and a repository without a
manifest is a count, not drift; finding none at all is a failure
(c15.no-manifest-anywhere), as are an unreadable manifest, an anchor that
matches nothing (c15.probe-unreadable, c15.probe-anchor-matched-nothing) and
the absence of any launcher manifest or any service to judge
(c15.no-launcher-manifest, c15.launchers-install-nothing,
c15.no-service-to-judge), because each of those would otherwise report
agreement about nothing.
C16, a launcher's environment reaches a reader
(gates/c16-launcher-env-has-a-reader.py). C5 compares the three launchers
against each other, so it has no opinion about whether the thing all three
agree on is wired to anything. This asks the other question, between a launcher
and a binary: every environment variable a launcher hands to a process must be
one some repository declares reading.
Two live instances the day it was written, both the same shape.
stack-single/compose.yaml passed WARDRYX_DSN; wardryx reads WARDRYX_DB
and has never read the other name, so the launcher generated a correct DSN,
declared depends_on: policy-db with condition: service_healthy, waited for
that database to come up, and handed the value over under a name the process
ignores. The database was provisioned, waited on and never used, which left
policy and approvals in memory: a restart dropped every console-written policy
and unfroze the fleet while the console still showed it stopped. And stack-k8s
sets TOKENFUSE_CLOUD_EVENTS_PATH into a container by configMapKeyRef; that
name appears nowhere in tokenfuse, whose cloud reads
TOKENFUSE_CLOUD_REPLAY_EVENTS, which no launcher sets at all.
A key with no reader is the quiet kind of wrong. Nothing is misspelled, nothing errors, the value is correct, the dependency is healthy and the service starts and answers. It is a wire that was never connected while every signal around it says the opposite. Neither instance is visible from inside a repository: the launcher's gates cannot read the binary and the binary's gates cannot read the launcher.
Both sides come from the repositories. The answer is every name under an env
block in any components.json, and those declarations are not this suite's
word for it: each declaring repository proves its own manifest against its own
source in its own CI, per C15's argument about why that division is structural.
The subjects are what the launchers DELIVER, by four forms read off the
launchers themselves, with comments stripped first so that prose about a
variable, including the comment recording the WARDRYX_DSN fix, is not a
delivery. install.sh holds shell variables like WARDRYX_ADMIN_SECRET that
reach no process, and flagging those would bury the real finding.
The subject is delivery to ONE service: a container's own env: entry, a
compose service's own environment: mapping, a shell command prefix. A shared
ConfigMap's keys are not subjects. The first version counted them and reported
seven findings, three of them wrong for the same reason. The clearest was
TOKENFUSE_CLOUD_EVENTS_PATH, which is a KEY whose value reaches the container
under a different NAME (TOKENFUSE_EVENTS_PATH, which tokenfuse declares and
reads); IDRYX_URL is the same shape, and stack-k8s interpolates
TRAILRYX_TRUST_DOMAIN into an argument in a CronJob it writes itself, which
trailryx's own suite deliberately keeps out of its manifest and says so. Three
repositories were right and the check was wrong. Narrowing cost coverage on
purpose: three false findings buy a check somebody still reads at the tenth run.
That is also the shape the original defect had, since WARDRYX_DSN was in the
wardryx service's own environment: block.
Its remaining limit: a variable delivered by a form not listed is invisible here and nothing would say so. The mitigation is that the forms are read from the launchers rather than imagined.
C17, a tag-only workflow is exercised by a pull request first
(gates/c17-tag-workflow-pr-guard.py). A workflow gated on push: tags: v*
runs for the first time, on every commit that ever reached it, on the day
somebody cuts a release. tokenfuse's binaries job failed on both Linux
runners the first time v0.4.2 exercised its musl leg, on a step that had
been wrong since it was written and had simply never run before that tag. The
fix, TAIPANBOX/tokenfuse#251, is two things done together: a pull_request
trigger scoped to the workflow's own path, so an edit to the file is what
exercises it, and every job that publishes guarded off that event, so the
same pull request does not also push an image or cut a release. Subjects are
found, never listed: every file under .github/workflows/ in every
repository estate.json names whose on.push.tags matches v*. A survey by
hand found ten repositories with the shape and no escape hatch at all; this
gate's own discovery finds twelve, naming engram and terraform-provider-taipan
as well, both of which publish (to PyPI and to the Terraform Registry) on a
bare tag push with no pull_request trigger anywhere. Four equivalent guard
expressions are accepted (github.event_name != 'pull_request',
startsWith(github.ref, 'refs/tags/'), github.event_name == 'push',
github.ref_type == 'tag'), because the estate's own clean files use more
than one of them. What counts as publishing is a fixed, named list:
docker/build-push-action with push or push-by-digest true, docker buildx imagetools create, softprops/action-gh-release,
actions/upload-release-asset, gh release, docker push, cosign,
crane, oras. A marker outside that list, or a guard that is logically
equivalent but spelled differently, is invisible to it, which is stated here
rather than silently missed.
C18, every architecture file is current against its repository
(gates/c18-architecture-is-current.py). The architecture record,
TAIPANBOX/architecture, holds one file per service saying how it is built and
why, with the commit of that service's main it was last checked against in its
frontmatter. This gate reads the record and every service repository and asks:
does the file exist, or is the gap recorded with a date; is verified_at a
commit on main; did code land on main after it; does every gate a
gate:/gates:/partly gated: marker in ## 8. Invariants and gates
names actually exist in that repository, however the marker is decorated
(an asterisk parenthetical, a plain parenthetical, a table cell, or any of
those wrapped onto a second line with the script on an indented
continuation; the real estate uses all four). What it does not read is
named, not silent: over the 22 registered repositories it visits,
agent-passport.md names the script BEFORE the marker and cites a path
outside scripts/, stack-single.md names two gates by path with no
gate: word and one by bare file name, and taipan.md puts a comma
between the word and its qualifier; bringing those rows to the same form
as the rest of the estate is the next change, not this gate. A
pending entry whose file exists is red, so the pending list cannot become
a graveyard, and a dossier with no such section at all is red too, reported
as measured nothing rather than passed on an empty read. The record is
private by decision, so in CI this gate reports NOT MEASURED and is
measured in local runs, the same way taipan was before it went public.
C19, a repository at 1.0 declares its compatibility surface
(gates/c19-compat-manifests.py). SemVer's item 5: version 1.0.0 defines the
public API, so a 1.0 tag is a promise about a surface, and a promise nobody can
point at is a mood. The estate's first two 1.0 tags, agent-passport and
agent-stack-go on 2026-09-12, each came with the surface written down (SPEC.md
section 10; api/surface.txt) and a gate in the repository that fails when it
moves. estate.json carries the declaration per repository: major, and
compat naming the manifest and the gate (defaults compat/1.0.json and
scripts/compat-surface.sh), or an exemption with its reason, printed on every
run. For a declared major the manifest and the gate must exist and some
workflow under .github/workflows must name the gate; the newest tag's major
must not be below the declaration. A newest tag at 1.0.0 or above with no
declaration is red, which is the failure the gate exists for. A repository at
0.x with no declaration has nothing to hold. It checks that the manifest and
the gate exist and are wired, not that the manifest is true: that is the
repository's own gate's job.
C20, every README install line resolves to a release that exists
(gates/c20-install-lines-resolve.py). tokenfuse's front page said docker run ... ghcr.io/taipanbox/tokenfuse for two months while latest in the registry
was a July build: the release workflow wrote latest only on the default
branch, which a tag push never is. Route A of decision 10.26: no moving tag
anywhere, a README pins the tag it means, and this gate holds the pin. It reads
README.md of every repository estate.json names for ghcr.io/taipanbox/
image references and github.com/TAIPANBOX/.../releases/ download links,
skipping lines with placeholders. An image with no tag is red unless the owning
repository declares a moving tag; a pinned tag must be a git tag of the owner
(the repository whose name is the longest prefix of the image name, or the
one whose estate.json entry lists the image under images, the exception for
an image not named after its repository such as agent-stack-go's
agent-conform; a -variant suffix is stripped first); a releases/download/<tag>/ link needs a
Release object for that tag and releases/latest/download/ needs at least one,
both read through the GitHub API, or from a fixture the self-test exports. It
does not ask the registry whether the image tag was pushed (outside-view.py
does) and does not check asset names on a Release.
C21, every action a workflow uses is pinned to a commit
(gates/c21-action-pins-are-commits.py). On 2026-09-12 twenty-one repositories
pinned ossf/scorecard-action to the SHA of the annotated TAG OBJECT of v2.4.4
rather than the commit it points to, which git rev-parse and the GitHub API
both hand back for an annotated tag. The workflow ran, and api.scorecard.dev
refused every upload as an "imposter commit", so no repository had a published
score when SUP-4 of the 1.0 proving run went to read one. The same files
pinned github/codeql-action the same way. This gate reads every uses: line
of every workflow, refuses a tag name (@v4) and a tag-object SHA (naming the
peeled commit to pin instead), passes a commit at a ref tip, and reports a SHA
at no tip as read but not judged. Ref tips come from git ls-remote --tags --heads once per action, or from a fixture the self-test exports.
C22, an optional add-on stays off the path every agent request takes
(gates/c22-addons-stay-off-the-request-path.py). Decided on 2026-09-26: an
optional add-on, typryx first, is integrated beside the core and never inside
it, so a stack without it runs as before and a slow or failed add-on cannot
slow or fail a request. The add-on talks to the rest of the stack through the
shared event bus, and any direct link a consumer adds is optional and fails
open. This gate holds the part only a cross-repository reader can see: no
repository marked "request_path": true in estate.json (tokenfuse,
wardryx) names a repository marked "addon": true, or any component its
runs field lists, in any tracked file and in any case. Launchers are where
an add-on is wired, by configuration, and are not read. It does not catch a
dependency that never spells the name, such as a hard-coded address or port.
C23, every built-in model price table matches the vendors' current prices
(gates/c23-model-prices-match-the-vendors.py, readers in gates/_prices.py).
On 2026-10-13 the gateway's book had no row for Claude Haiku 5.5, a current
model at 0.10 / 0.50 per million tokens, so the gateway billed it at the 15 /
75 fallback; its Sonnet 5.5 cache read was 0.20 where the page said 0.10; it
had none of OpenAI's latest models; it priced Gemini 3.x Flash at the 2027
rate while the introductory rate was in force; and costcrew carried Sonnet 5
and Opus 5 at their old prices. A price is a claim about somebody else's list,
and nothing compared the copy with the list. This gate does, in the existing
daily run. Subjects come from estate.json: each repository declaring a
price_table (path, format, and whether it must price every current model).
It reads, live and with no key, Anthropic's pricing page (the model table by
its column headers, and the sentence stating the regional premium, as "10%" or
"10 percent" and in no other words), OpenAI's (every text-token table marked
tier standard, read together: each price from the column its heading names,
proved against the rows the table renders; a model two tables price
identically is one row, and one they price differently is read from neither
and reported), and Vertex AI's (the Standard tabs under "Google models",
Global and Non-global rows, dated rows). A row billed through OpenRouter
(costcrew's openrouter/... engine) must equal, exactly, the price of one of
the providers OpenRouter's /models/{id}/endpoints lists for that model, every
component from the same provider, and is red when no provider is listed; never
OpenRouter's model list, whose single price switches provider. For the other
rows OpenRouter is a secondary signal, the model's own vendor endpoint row;
where it disagrees with a vendor page, the page wins and the disagreement is
printed. A row is compared with the
price in effect today: an id naming the global endpoint with the list, one
naming a regional endpoint with the list plus the stated premium, and one
naming no endpoint (or a model priced in prompt-length tiers) must equal one
of the prices, every component from the same one. Only components both sides
state are compared, within half a percent. Prompt-length tiers are read from
both sides (tokenfuse's price_book.long_context, verdryx's
.with_long_context, the vendors' "over 100,000 / 200K / 272K tokens"
prices): a table that can carry a tier must carry the vendor's threshold and
long price and no tier the vendor does not have; a flat table may carry
either tier. A dated change within 30 days is a WARN line on every run, and
red the day it takes effect. A disagreement a repository chose on purpose
(tokenfuse's dearer dated rate for Gemini 3.x Flash; a long-prompt tier the
vendor's page does not state, which is tier-not-on-page) is recorded in expectations/model-prices.json
with its reason and printed as an EXCEPTION every run; an exception that
excuses nothing is red, so a dated one has to be removed the day its price
takes effect. Red for: a price that differs (with the row as it should read
and the exception key a recorded choice would use), a model the vendor's page
marks current missing from a table declared to price every current model
(with the rows to add), a fallback below a current model's price, a page
whose shape no longer parses, a table that cannot be read, a table none of
whose rows maps to a vendor price, a stale or unreadable exception, and no
declaration at all. A page that could not be fetched is
NOT MEASURED and the run ends incomplete (exit 2), never partial. A tier the
vendor's page does not state is NOT MEASURED the same way unless
expectations/model-prices.json records it under tier-not-on-page; an
undeclared one is counted, and the run exits 3 if its repository has no public
remote or 2 if it has one. "Current" is
what the vendor's page marks: Anthropic's rows above "Additional models",
OpenAI's latest rows; Vertex AI marks none, so a Gemini model no table prices
is not a current-model finding. Every model Vertex AI lists that no table
prices is declared by cause, or NOT MEASURED and counted against the gateway's
book. C23 does not read Bedrock's, DeepSeek's or AWS's own price lists, so the
rows priced from those are declared not judged by cause, each one named by its
id in expectations/model-prices.json. A row no cause declares is NOT MEASURED
and counted, exit 2 in a repository with a public remote.
C24, no owner quotes in any public repository (gates/c24-no-owner-quotes.py).
Decided on 2026-10-13: every public repository is free of quotations of the
owner and of the old provenance marker that carried his first name, and a
decision is recorded as a date and a paraphrase in English. The gate reads the
tracked files of every repository estate.json records a public GitHub remote
for, and refuses four shapes on the line they occur on: the old marker, a
guillemet quotation beside Cyrillic, Ukrainian prose outside a string literal
(string literals in code files may carry Cyrillic, as hostile-input tests do,
and so may a testdata/ file), and the owner's first name capitalised outside
an authorship line. The owner as copyright holder, author or maintainer is
allowed. Output names the file and the line number and never the line's text,
because a CI log of a public repository is public too. A private repository is
skipped, and the output says so. It does not catch an English quotation in
ordinary quotation marks, an attribution that avoids his name, or a translation
passed off as a paraphrase; those are prose a reader judges.
C25, each launcher keeps the security postures it sets today (gates/c25-deployment-posture.py).
The 2026-10-17 review found that the products ship permissive defaults and rely
on their launchers to turn enforcement on, and nothing read what the launchers
set. The gate reads stack-single (compose.yaml and install.sh), stack-k8s
(its manifests and install.sh) and stack-up (up.sh) at the ref the run is
given, and judges twelve postures on each launcher that carries one: client keys
required on the metered door, identity strict, the agent firewall, the gateway's
policy-plane mode and fail mode, the gateway's policy-plane key role, the
declassify key, the MCP broker's DLP and scan blocks, its policy-plane mode and
fail mode, and the admin key. A posture is judged on the text the launcher sets,
and each value is named with the line it came from. An anchor that matches
nothing (a service, a Deployment, the wardryx-wired start of up.sh, the installer's
role line) and a file that is gone are red, naming what vanished, never a skip.
A launcher may lack a posture on purpose only when
expectations/deployment-posture.json records why. That record is printed on
every run, and it goes red when it has no reason or when it excuses nothing any
more. The gate does not catch that a binary enforces what its launcher sets,
which is the product's own suite's question, nor the opt-in components' flags,
the no-wardryx fallback in up.sh, or the opt-in OpenAI door of stack-single. The
header of the gate states each of those limits.
C26, cosign image signatures carry the legacy bundle flags (gates/c26-cosign-legacy-bundle-flags.py).
Decided on 2026-10-19, paraphrased: image signatures use the bundle format
policy-controller reads. The gate reads every workflow file of every repository
estate.json names, finds each cosign sign line (the image subcommand; sign-blob
and attest are not judged), reads its command with its continuation lines, and
requires both --new-bundle-format=false and --use-signing-config=false. A line
missing either is red, named by repository, file and line. Finding no sign line at
all is measured nothing, not a pass. It does not read flags set some other way than
on the command line.
Locally, against the sibling checkouts in the parent directory:
./run-gates.py # every gate, summarised
./gates/c4-event-registry.py # or one on its own
./selftest.py # prove the gates can fail
./scripts/no-long-dashes.sh --proveThree sources, and every run prints which one it used:
./run-gates.py # the working trees, as they are now
./run-gates.py --mode ref --ref origin/main # the estate as published
./run-gates.py --mode clone # fresh shallow clones, what CI does--mode ref is the one to reach for while other people are editing: a working
tree mid-edit will report drift that is somebody's uncommitted fix, and
origin/main answers the question the nightly run answers.
Exit codes: 0 clean (every comparison agrees, and every subject was measured
or is declared not judged by a recorded cause that names it; a declared subject
is named and counted apart from the comparisons), 1 the estate
drifted, 2 a repository that should have been reachable was not, a vendor price
page or OpenRouter answer C23 reads could not be fetched or fully read, or a
subject in a repository with a public remote went unmeasured with no recorded
exception, 3 clean but every unread or unmeasured subject sits in a repository
with no public remote. CI treats 3 as success with a notice and fails on
everything else.
Some checks have to run inside the repository they judge, on its own pull requests, so this repository ships them as tools and reusable workflows that a caller pins to a commit. They are not cross-repository checks and nothing here runs them against another repository.
tools/secretscanand.github/workflows/secretscan.yml: no secret in a tree (invariant 23).tools/diffcoverand.github/workflows/diff-coverage.yml: the coverage floor on the lines a pull request adds, and the whole-repository baseline (invariant 28). Seetools/diffcover/README.mdfor the rules, the measured-nothing cases and what the number does not say.
Written down rather than left implied, because an unstated gap reads as a covered one.
- Repositories with no public remote. bank-in-a-box has only a private
remote and no CI. In CI here it is reported as unmeasured and the run ends
PARTIAL. taipan has been public since 2026-08-28 and is cloned and measured
like the rest, so C4 reads the attribution of what
taipan demowrites in CI as well, not only in local runs. go.sum,replacetargets and vendored Go code. C1 reads the version ingo.mod. It reports the presence of areplaceas a failure but does not follow it.Event types built at runtime.Closed 2026-08-09. This said a type assembled from a variable would be missed with nothing complaining, and that no such producer existed. Both halves were wrong within the day: scopyx names its types as constants and passes the variable at the emit site, so C4 read it as a producer with no types and reported the run clean. The Go parsers now return what they could not resolve and the extractor refuses on it, so that shape is a loud hole. A type built from a format string is still beyond the parser, but it is now a REFUSAL rather than a silence.- The severity column of SPEC 6.2. The registry gives a typical severity per type in parentheses and tokenfuse fixes severity per type in code. Those are a fourth mirror and nothing compares them.
- trailryx and catalog. In
estate.jsonso that a future check has a place to start, read by nothing today. - Whether a claim is TRUE. Every check here compares two statements the estate makes about itself. Two documents that agree can still both be wrong, and no script can tell.
- Anything an operator supplies at install time. C5 reads the defaults in the source. What a given box is running is not a property of a repository.
- Whether a component starts BY DEFAULT. C5's
servicesfamily answers "does this deployment bring this component up", and both heraldyx and scopyx are present in all three while being opt-in in two of them: excluded from stack-k8s'sapply -kset, behind a compose profile in stack-single, and on by default only in the stack-up sandbox. C5 reports them as agreeing, which is true of the question it asks and not of the question a reader may think it asked. Noticed 2026-08-09 while adding scopyx; not modelled, because a family for it would have to distinguish "opt-in for a security reason" from "not wired up yet", and those look identical from the outside. - Prices nobody publishes where C23 reads. Bedrock's and AWS's own price lists, DeepSeek's page, and the data residency multipliers of first-party APIs. Rows priced from those are declared not judged by cause on every run, and a vendor page is the source of truth by definition: C23 cannot tell a wrong page from a right one.
- The estate's OWN gates. Nothing here checks that the per-repo gates still run or still mean what they say. That belongs to each repository.
- A coverage statement.
estate.jsonlists the estate, and there is no machine-checked link between "repositories that exist" and "repositories any check reads". A repository could join the estate and be covered by nothing, and only a person reading this section would notice.
Everything above is what a script decides. GAPS.md is the standing
register of what it cannot: the estate's open security findings, the seams no
gate reaches yet, what agent monitoring can and cannot see, and the invariants
every repository holds by prose alone.
It is a record rather than a source of truth. Nothing in it is authoritative
about anything a command can decide, and its section 1 is a dated reading of a
run-gates.py run rather than a second copy of one. Every item carries how to
re-check it, because in this estate a file:line citation goes stale in days:
on the day the register was opened, all three citations it tried to follow from
a four-day-old audit had moved, and all three findings had been fixed.
Three more files record what the gates are not built to decide:
PROVEN.mdrecords what has actually been run. Every row carries the date, the machine and the artifact, and states its scope, because a run proves what it ran and nothing adjacent. It is appended to by hand, one dated row per run, and never regenerated: it answers "has this ever been executed, and where", which is the question that keeps being answered from memory.OUTSIDE-VIEW.mdis the estate as a stranger finds it: repositories, container images and publish locations. It is regenerated byscripts/outside-view.py --write OUTSIDE-VIEW.mdfromestate.jsonand the image names the stack-k8s manifests pin, stamped with the moment it was taken, and never edited by hand.evidence/holds the raw artifacts individualPROVEN.mdrows cite (today, the captures, probes and A/B scripts of the idryx eBPF sensor runs of 8 and 9 September), so a row can be re-opened rather than taken on trust.
Copies that exist and are not compared, because they could not be anchored exactly and a guess is worse than an honest gap:
- tokenfuse's event-type severities against 6.2's parenthesised ones, as above.
- heraldyx's
internal/rendercatalogue, which maps an event type to what it MEANS for an operator. Four of seventeen entries were found wrong in an audit. Nothing structural can check that an explanation is true. - The stack sentence and the component count that several repositories repeat in prose. Anchoring on English is how a gate learns to cry wolf.
means the estate drifted, not that this repository is broken. That is the whole design: every check compares two other repositories, and this one has no opinion of its own to be wrong about. When it is red, open the finding, and it will name both sides and the file to open.
Several checks are red today. That is correct, and working around any of them by weakening the check would be the one change this repository cannot survive.
C11, a proved chain was proved by something
(gates/c11-proven-means-verified.py). wardryx decides on chain_proven, and
its own comment states the trust boundary plainly: "a caller that lies about
this is believed. That is not a weakness of this field, it is where the boundary
is." That is correct, and it is exactly why the check belongs on this side of
the boundary. The PDP cannot tell a verified true from an asserted one, and
nobody can from inside wardryx: it is visible only to something holding wardryx
and every producer open at once.
The failure it names is the one the 2026-08-25 identity plan called A5, and the
dangerous half is not the one the plan led with. A chain wrongly marked unproven
makes a policy fire that should not have, which somebody notices. A chain
wrongly marked proved makes deny_if_chain_unproven stay silent, and an estate
where that rule never fires looks exactly like an estate where every chain is
proved.
So every non-test file that sets chain_proven true must CALL a verifier. A
call and not an import: use tokenfuse_delegation::verify_delegation; at the
top of a file that never calls it is what a refactor leaves behind, and counting
it would let this pass on the wreckage. It reads text rather than data flow, so
it sees a literal true and not a variable that happens to be one. That is the
shape a rubber stamp actually takes, and the limit is written in the script
rather than left to be discovered.