forwarding: a read-only route census so blast radius is knowable before a restart
Closes #17, and lands the enumeration #22's route.closing consumes. Built to your three requirements; all of them are in the code as constraints rather than notes.
The enumeration serves both consumers without compromise
ForwardingTable::endpoint_routes(endpoint) — read lock, no mutation, no frames to modules. Per route it returns full GoodbyeTarget data (connection, channel, epoch, sink), the principal, age, and draining state. So route.closing can address exactly the set the drain will, and the census gets its own fields from the same read. One pass, no second shape.
None of this existed: release_module_endpoint_routes releases as it enumerates, so it can't serve a read.
The two retained fields
As approved — RouteBinding now carries the attested Principal and a bind Instant, both written once at bind, never mutated. The principal was previously computed in handle_route_open, used for the admission-facts check, moved into the RouteBind relay frame and dropped: transited, not stored.
Your three requirements
Control-plane-only — in the op's doc comment as a placement rule:
This privileged census is control-plane-only. It is deliberately not an MCP facade or agent-tool operation: callers holding the daemon control connection may inspect live route ownership, while agent-facing modules must not be able to address that surface at all.
Verified rather than asserted: subc-mcp reaches no supervisor.* op at all — it catalogs module manifests only, so nothing auto-exposes a newly added control op.
Two-tier identity, honest — a typed enum, so an unattested connection cannot be dressed in a name:
pub enum SupervisorRouteConsumer {
Reserved { module_id: String },
Direct { connection_id: u64 },
}
A caller that proved a live daemon-issued launch nonce is named reserved. A direct key-holder has no such attestation, so it is reported as direct with its connection counter instead of an invented module name.
Draining reported, never omitted — and the test asserts presence with state rather than absence:
let draining_routes = forwarding.endpoint_routes(endpoint).unwrap();
assert_eq!(draining_routes.len(), 1);
assert!(draining_routes[0].draining);
That direction matters: a test asserting a draining route is absent would pass while the census silently under-reported blast radius mid-reload, which is the failure this requirement exists to prevent.
The no-modules read as a stated constraint
Also in the doc comment, so the next person to "optimise" it argues with the reason first:
The daemon answers from its forwarding table under a read lock and never consults a module. That makes the read safe during a drain, when a module cannot be queried without recreating the hang/restart hazard that route status reads avoid.
Safe-during-a-drain is the whole point — that's when an operator asks "what am I about to break."
Version collision to expect, flagging early
This bumps subc-control 0.1.1 → 0.1.2 and subc-core 0.3.1 → 0.3.2, against current master. #23 (terminal ring) is ahead of this in the queue and bumps the same crates to 0.2.0 / 0.4.0. Whichever merges first, the other rebases and re-resolves one above it — same mechanical fixup as #21 → #23, and I'd rather name it now than have the version gate refuse a surprise.
I'd suggest merging #23 first since its twin is already green; I'll rebase this behind it.
Gates
cargo test --workspace 31 binaries, 0 failures
cargo clippy --all-targets clean
cargo fmt --check clean
cargo check --workspace --locked clean
check-wire-crate-versions.sh 6 examined, 0 violations
git diff origin/master -- crates/ | grep '^+' | grep /bin/sh → nothing
Golden diff is the authorized set only: three op-list insertions plus the new supervisor_routes fixture, generated through the real handler with a live and a draining binding.
Windows unverified as usual — twin whenever convenient.
One friction note from the build
The workspace closure test carries an explicit catalog/coverage invariant over advertised ops, so a new op has to be added there too. That's a good property — it means an op can't be advertised without something asserting it exists — but it's easy to read as an unrelated failure the first time. Worth knowing if anyone adds an op without hitting the same wall.
Need help on this PR? Tag @codesmith with what you need. Autofix is disabled.
Summary by cubic
Add a read-only route census so operators can see the blast radius before a drain or restart. Previously there was no safe enumeration; now supervisor.routes lists live and draining routes with identity tier and age without consulting modules.
- Stores the attested principal and bind time on each RouteBinding to report consumer identity (reserved vs direct) and route age.
- Adds supervisor.routes as a control-plane-only op; it reads the forwarding table under a lock and never calls modules, so it is safe mid-drain and not exposed via
subc-mcp. - Implements census in
subc-core(forwarding: EndpointRoute, route_census; control: SupervisorRoutes handler), exposes the wire shape insubc-control, and addscksupport (ck routes []). - Updates golden tests and closure checks to include supervisor.routes; draining routes remain visible and are marked draining.
- Bumps
subc-controlto 0.1.2 andsubc-coreto 0.3.2; expect a version bump conflict with the terminal ring PR (#23). Merge order: prefer #23 first; rebase this afterward.