Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 7 additions & 14 deletions .cargo/audit.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,13 @@

[advisories]
ignore = [
# RUSTSEC-2026-0235, rkyv 0.7.46: out-of-bounds reads on archives containing
# Rc/Arc. Fixed in rkyv 0.8.17, which nothing here can move to.
# RUSTSEC-2026-0235, rkyv 0.7.46: out-of-bounds reads on archives containing Rc/Arc.
# Fixed in rkyv 0.8.17. The 0.7 series is no longer supported upstream and gets no fix.
#
# The path is tokenfuse-gateway --features cluster -> tokenfuse-cluster ->
# openraft 0.9.24 -> byte-unit 5.2.5 -> rust_decimal 1.42.1 -> rkyv. In
# rust_decimal, rkyv is behind an OPTIONAL feature (`rkyv = ["dep:rkyv"]`)
# that nothing in this graph enables, so cargo records it in Cargo.lock and
# never compiles it. cargo-audit reads the lockfile, not the build, which is
# the right default and the reason this entry exists.
#
# rust_decimal 1.42.1 is the latest published version, so there is no upgrade
# that removes the lockfile entry. What can change is the feature: anything
# that turns rust_decimal's `rkyv` feature on makes this vulnerability real
# and this entry wrong. That is exactly what scripts/audit-ignores.sh checks,
# by asserting rkyv is absent from the built dependency graph.
# Since the rust_decimal 1.43.0 bump in crates/cluster/Cargo.lock (2026-10-19) this entry
# matches nothing: rust_decimal 1.43.0 no longer depends on rkyv, so rkyv is out of that
# lockfile (the root lockfile never had it). The entry stays only until the follow-up that
# removes it, which also re-points the teeth cases that edit it. scripts/audit.sh still
# asserts that rkyv is absent from the build graph.
"RUSTSEC-2026-0235",
]
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -515,9 +515,7 @@ build)`, `cloud apns (feature build)`.
the sentence stops being true without saying so and the entry then protects
nothing while looking like a decision.

Today's single entry is rkyv (RUSTSEC-2026-0235), which reaches the
lockfile through openraft, byte-unit and rust_decimal, sits behind an
optional rust_decimal feature nothing enables, and is therefore never built.
Today's single entry is rkyv (RUSTSEC-2026-0235). It no longer reaches any lockfile: rust_decimal 1.43.0 dropped its rkyv dependency, so `cargo update -p rust_decimal --precise 1.43.0` in crates/cluster removed rkyv from that lockfile (`@measured` 2026-10-19), and the entry is stale until its follow-up removes it.
That is a checkable fact rather than a judgement, so `scripts/audit.sh`
asserts it: the crate must be absent from the build graph of both manifests
under `--all-features --target all`, and an ignore with no recorded crate is
Expand Down Expand Up @@ -1921,6 +1919,8 @@ A PDP holding the token and the proof together can check the proof's freshness,

`@claude` 2026-10-19, under delegated authority, open to reversal: 4 KiB, matching `on_behalf_of`'s header cap; the member is omitted rather than sent as null, so a PDP that predates the field sees the body it always saw. (test: `agenttoken::tests::a_token_for_the_policy_plane_is_only_an_agent_token`, `a_token_for_the_policy_plane_is_bounded_at_4_kib`, `a_proof_for_the_policy_plane_is_the_dpop_header_within_the_cap`; `wardryx::tests::the_wire_body_carries_the_agent_token_only_when_the_call_had_one`; `tests/it/static_key_allow_list.rs`: `the_policy_plane_is_sent_the_agent_token_when_a_token_admitted_the_call`, `a_static_key_call_is_never_sent_an_agent_token`, `an_oversized_agent_token_is_admitted_but_not_forwarded`; `tests/it/mcp_agent_token.rs`: `the_broker_sends_the_agent_token_to_the_pdp`, `the_broker_sends_no_agent_token_for_a_static_key_call`. Scenarios: `features/the-policy-plane-is-given-the-agent-token.feature`.)

141. **A delegation proof names one of the public origins the operator configured, and a `Host` header never stands in for one.** A gateway reached at more than one address (a public name and an internal one) must accept a proof addressed to any of them, and must refuse one addressed to anything else. `TOKENFUSE_DELEGATION_ORIGINS`, a comma-separated list, is read beside `TOKENFUSE_DELEGATION_URL` (one origin, unchanged); `TOKENFUSE_MCP_PROOF_ORIGINS` is read beside `TOKENFUSE_MCP_PROOF_URL` at the broker's proof door. The single origin and the list are one set. Every entry is normalised as wardryx's `normalizeOrigin` does, and an entry that is not `scheme://host[:port]` with nothing after the port refuses to start, naming its variable; so does a list naming no origin at all. A proof is accepted for a route when its `htu`, without its query, is one configured origin plus that route's path. A fragment, user information, another path or an unlisted origin is refused. The check picks the origin from the `htu` read before the signature is checked (`dpop::origin::expected_url`), and the verifier then checks the signature over that same claim, so nothing can change it after the choice. `@claude 2026-10-19`, choices under delegated authority, open to reversal: the list and the single origin are a union; an empty entry refuses the list rather than being skipped; and the verifier refuses a fragment in an `htu` (RFC 9449 section 4.3), which changes every door, not only the new list. This amends the single origin of invariant 30 and the `htu` rule of invariant 31 without changing either for a deployment that names one origin. *(test: `dpop::origin::tests`, `chainproof::origin_list_tests::the_single_origin_and_the_list_are_one_set_and_a_bad_entry_names_its_variable`, `tests/it/mcp_door.rs` (the list and fragment tests), `tests/it/delegation_origins.rs` (eight over the real router, both doors); scenarios: `features/a-proof-names-one-of-the-public-origins.feature`, eleven, each bound. Not a script gate: held by `cargo test`.)*

## Decisions that have no gate yet

This list is debt, and it is here to stay visible rather than to be tidy.
Expand Down
2 changes: 1 addition & 1 deletion PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ comparison in #132.

**Counts re-measured 2026-10-05**, each by the command named, because the set
here once said 100 where the workspace ran 747 and nothing had been watching:
`cargo test --all` runs **2391 passing** (core 469, dpop 23, delegation 75, gateway 1480, cloud 343, umbrella 1, by `cargo test -p <crate>`). @measured `cargo test --all --no-fail-fast` on this branch after merging origin/main (8ed7744, PR #468 merged), 2026-10-19 as root: 2389 passed and 2 failed. The two failures are the tests that need a 0500 directory (`events_export_startup`, `events::tests::an_unwritable_directory_is_named_at_warn_and_the_export_is_off`), which invariant 78 records as root-only; as any other user they pass, so the CI figure is 2391. The breakdown counts the same binaries per crate: core 460 lib and 9 integration, cloud 203 lib and 140 integration, gateway 993 lib and 487 integration (the root-only test counted in the lib figure), dpop 23, delegation 75, umbrella 1. Against main's 2364 the branch adds 27: the static-key allow-list and the agent token to the policy plane (invariants 138 to 140) add 26 to `gateway`, 11 in `gateway::agenttoken` and `wardryx` and 15 in `tests/it` (`static_key_allow_list`, `static_key_allow_list_startup`, `mcp_agent_token`); the identity-source cache key adds 1 in `gateway::wardryx`, and the governed wide-bind agreement of invariant 137 adds 1 in `tests/it/static_key_allow_list_startup`. The governed profile of invariants 136 and 137 is already in main's figure and is not counted here. The governed profile (invariant 136, 2026-10-19) adds 29 to `gateway`: 8 in `defaults`, 3 in `adminkeys` and 18 in `tests/it/governed_profile.rs`. Its broker and no-mode firewall follow-up adds 11 more: 7 in `tests/it/governed_profile_scope.rs`, 3 in `firewall::absent_mode_tests` and 1 in `defaults`. Earlier totals quoted in this section are dated history of how each count grew, not the current figure. The CORS allow-list, the digested device credentials and org keys, and the dashboard's fragment key (invariants 131 to 133) add 28 to `cloud`, all new tests, counted from the diff against main. The CORS allow-list, the digested device credentials and org keys, and the dashboard's
`cargo test --all` runs **2414 passing** (core 469, dpop 29, delegation 75, gateway 1497, cloud 343, umbrella 1, by `cargo test -p <crate>`). The door origin lists (invariant 141, 2026-10-19) add 23 to the 2391 below, counted from the diff against main and not measured again here: 6 in `dpop::origin`, 1 in `gateway::chainproof`, and 16 in `tests/it` (8 in `delegation_origins`, 8 in `mcp_door`). @measured `cargo test --all --no-fail-fast` on this branch after merging origin/main (8ed7744, PR #468 merged), 2026-10-19 as root: 2389 passed and 2 failed. The two failures are the tests that need a 0500 directory (`events_export_startup`, `events::tests::an_unwritable_directory_is_named_at_warn_and_the_export_is_off`), which invariant 78 records as root-only; as any other user they pass, so the CI figure is 2391. The breakdown counts the same binaries per crate: core 460 lib and 9 integration, cloud 203 lib and 140 integration, gateway 993 lib and 487 integration (the root-only test counted in the lib figure), dpop 23, delegation 75, umbrella 1. Against main's 2364 the branch adds 27: the static-key allow-list and the agent token to the policy plane (invariants 138 to 140) add 26 to `gateway`, 11 in `gateway::agenttoken` and `wardryx` and 15 in `tests/it` (`static_key_allow_list`, `static_key_allow_list_startup`, `mcp_agent_token`); the identity-source cache key adds 1 in `gateway::wardryx`, and the governed wide-bind agreement of invariant 137 adds 1 in `tests/it/static_key_allow_list_startup`. The governed profile of invariants 136 and 137 is already in main's figure and is not counted here. The governed profile (invariant 136, 2026-10-19) adds 29 to `gateway`: 8 in `defaults`, 3 in `adminkeys` and 18 in `tests/it/governed_profile.rs`. Its broker and no-mode firewall follow-up adds 11 more: 7 in `tests/it/governed_profile_scope.rs`, 3 in `firewall::absent_mode_tests` and 1 in `defaults`. Earlier totals quoted in this section are dated history of how each count grew, not the current figure. The CORS allow-list, the digested device credentials and org keys, and the dashboard's fragment key (invariants 131 to 133) add 28 to `cloud`, all new tests, counted from the diff against main. The CORS allow-list, the digested device credentials and org keys, and the dashboard's
fragment key (invariants 131 to 133) add 28 to `cloud`, all of them new tests: 9 in its library
and 19 in `tests/it` (8 CORS, 2 startup refusal, 7 device credentials, nonces and the restart second, 2 dashboard),
counted from the diff against main (2276, so 2304). @measured `cargo test --all --no-fail-fast`
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
> The kill-switch isn't a dashboard button you press after the fact - it's an HTTP 402 the gateway returns mid-run, before the provider bills you.

![release](https://img.shields.io/badge/release-v1.19.0-brightgreen)
![tests](https://img.shields.io/badge/tests-2391-brightgreen)
![tests](https://img.shields.io/badge/tests-2414-brightgreen)
![image](https://img.shields.io/badge/ghcr.io-tokenfuse-blue?logo=docker)
![license](https://img.shields.io/badge/license-Apache--2.0-blue)
![core](https://img.shields.io/badge/core-Rust-orange)
Expand Down Expand Up @@ -603,6 +603,8 @@ Since 2026-08-06, a non-loopback bind with nothing configured on the broker's do

There are two ways to put something on that door, and the second is newer and stronger. `TOKENFUSE_MCP_KEYS` is a shared secret in a header: whoever captures it holds it. `TOKENFUSE_MCP_CLIENT_IDS` instead names client metadata documents in the shape [CIMD](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/) defines, each published by a client at its own https `client_id` URL and naming that client's public keys; a caller then proves possession of one of those keys per request with an [RFC 9449](https://www.rfc-editor.org/rfc/rfc9449) DPoP proof, single-use. Nothing an operator holds is worth stealing, and a captured header is worth nothing after a minute. Both doors can be configured at once while clients move across, and `TOKENFUSE_MCP_REQUIRE_PROOF=1` is how that ends: [docs/24](docs/24-mcp-proof-door.md).

The proof door takes one public origin in `TOKENFUSE_MCP_PROOF_URL`, or a comma-separated list in `TOKENFUSE_MCP_PROOF_ORIGINS` when the broker is reached at more than one address, for example a public name and an internal one. The delegation door takes the same pair, `TOKENFUSE_DELEGATION_URL` and `TOKENFUSE_DELEGATION_ORIGINS`. Each entry is an origin written `scheme://host[:port]` with nothing after the port, and a proof is accepted when its `htu` is one of them at the route's path (CLAUDE.md invariant 141). The `Host` header is never read, and an entry that is not an origin stops the process at startup.

A third, experimental since 2026-09-24: an access token that [vouchryx](https://github.com/TAIPANBOX/vouchryx) issued through Cross App Access, where an enterprise identity provider's assertion about a person (an ID-JAG, `draft-ietf-oauth-identity-assertion-authz-grant-04`) is redeemed for a five-minute token scoped to this broker, presented as `Authorization: Bearer`. Set `TOKENFUSE_MCP_ACCEPT_XAA=on` and `TOKENFUSE_MCP_RESOURCE` (this broker's own resource URL, https, which the token's audience must equal exactly), with `TOKENFUSE_DELEGATION_ISSUER` and `TOKENFUSE_DELEGATION_JWKS` naming vouchryx and `TOKENFUSE_DELEGATION_URL` the broker's public origin (the delegation door in the same process requires it). The broker then publishes [RFC 9728](https://www.rfc-editor.org/rfc/rfc9728) metadata at `/.well-known/oauth-protected-resource`, every refusal points at it in `WWW-Authenticate`, a call is attributed to the agent the token names with the person as the root of its chain, and a token for another resource, a token bound to a key, a delegation token presented as bearer, or a token naming a revoked person or agent is refused. With XAA on, a caller presenting no credential is refused even when no other door is configured. The grant is an IETF draft, so both settings are experimental in `compat/1.0.json`; CLAUDE.md invariant 62 holds the rules.

---
Expand Down
6 changes: 6 additions & 0 deletions components.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@
"TOKENFUSE_DELEGATION_JWKS_URL": {
"required": false
},
"TOKENFUSE_DELEGATION_ORIGINS": {
"required": false
},
"TOKENFUSE_DELEGATION_REVOCATIONS": {
"required": false
},
Expand Down Expand Up @@ -220,6 +223,9 @@
"TOKENFUSE_MCP_LOCK": {
"required": false
},
"TOKENFUSE_MCP_PROOF_ORIGINS": {
"required": false
},
"TOKENFUSE_MCP_PROOF_URL": {
"required": false
},
Expand Down
Loading
Loading