Repository navigation
Conversation
…ault
The previous commit gave the configuration the right vocabulary and left
the old semantics underneath it. A key carried a *list* of routes, the
default route was a `default: bool` on an arbitrary member, and every
request stitched the default's rules together with the assigned ones
before evaluating the result. The objects were new; the engine was still
the flat accumulating table it had always been.
**The default route is the one a key walks when it is not bound to
anything — not one every key has to pass through.** That distinction is
the whole model. The first reading means evaluation always looks at
exactly one rule list. The second means every request begins by joining
two.
So:
default_route: 默认 # top-level name
routes:
- name: 默认
rules: [...]
- name: 长上下文
rules: [...]
clients:
- name: codex
key: tw-…
route: 长上下文 # singular. bound, not stacked
`Engine::rules_for` is five lines now: look up what the key is bound to,
fall back to the default route's name, return that route's rules. There
is no join.
**`default_route` is a top-level name rather than a flag on a member,
and that removes a validation rule rather than adding one.** "At most one
default" existed only to maintain a uniqueness the structure did not
have. A single field cannot have two values.
Two errors replace it, each for a state that was otherwise silent: a
`default_route` naming a route that does not exist (unbound keys would
match no rule at all, while the config looks complete), and a key bound
to a route that does not exist.
## The argument I lost
I defended the flat table by claiming cross-rule accumulation was needed
to express "everyone gets redaction, codex also swaps the model". That
was wrong, and checking it is what settled this: global redaction is
`security.redact`, per-upstream is `provider.redact`, and a rule's
`guard` only ever tightens one path — it belongs to whichever route that
path is in. Nothing needed to accumulate across routes.
## The cost, stated
Two routes wanting the same rule each carry a copy. That is the price of
binding over layering and it is not being papered over with inheritance,
which would put back the complexity just removed and in a place that is
harder to explain. The escape hatch needs no new concept: put the shared
rules in the default route and leave that key unbound.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong with #10
That PR gave the configuration the right vocabulary and left the old semantics underneath it. A key carried a list of routes, the default route was a
default: boolon an arbitrary member, and every request stitched the default's rules together with the assigned ones before evaluating the result.The objects were new. The engine was still the flat accumulating table it had always been.
The distinction that fixes it
The default route is the one a key walks when it is not bound to anything — not one every key has to pass through.
That is the whole model. The first reading means evaluation always looks at exactly one rule list. The second means every request begins by joining two.
Engine::rules_foris five lines now: look up what the key is bound to, fall back to the default route's name, return that route's rules. There is no join.A validation rule removed, not added
default_routeis a top-level name rather than a flag on a member, so "at most one default" is gone. That rule existed only to maintain a uniqueness the structure did not have; a single field cannot hold two values.Two errors replace it, each covering a state that was otherwise silent:
default_routenames a route that does not exist — unbound keys would match no rule at all, while the config looks complete.The argument I lost
I defended the flat table by claiming cross-rule accumulation was needed to express "everyone gets redaction, codex also swaps the model". That was wrong, and checking it is what settled the design: global redaction is
security.redact, per-upstream isprovider.redact, and a rule'sguardonly ever tightens one path — so it belongs to whichever route that path is in. Nothing needed to accumulate across routes.The cost, stated plainly
Two routes wanting the same rule each carry a copy. That is the price of binding over layering, and it is not being papered over with inheritance — that would put back the complexity just removed, in a place harder to explain.
The escape hatch needs no new concept: put the shared rules in the default route and leave that key unbound.
Verification
cargo fmt --all -- --check,cargo clippy --workspace --all-targets -- -D warnings, 1088 tests, andscripts/smoke.sh— 46 checks, 0 failures.🤖 Generated with Claude Code