Skip to content

A key is bound to one route, not layered onto a default - #12

Merged
fylorn merged 1 commit into
mainfrom
dev
Sep 14, 2026
Merged

fylorn merged 1 commit into
mainfrom
dev

Conversation

@fylorn

@fylorn fylorn commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

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: 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 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.

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.

A validation rule removed, not added

default_route is 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_route names a route that does not exist — unbound keys would match no rule at all, while the config looks complete.
  • a key is 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 the design: global redaction is security.redact, per-upstream is provider.redact, and a rule's guard only 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, and scripts/smoke.sh — 46 checks, 0 failures.

🤖 Generated with Claude Code

…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>
@fylorn
fylorn merged commit 5950956 into main Sep 14, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant