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
2 changes: 1 addition & 1 deletion crates/busbar-contract/src/caps/egress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ impl VerifiedDestination {
///
/// The lane is named, not spelled: a configured lane's name is a runtime string and
/// [`LaneId`] is a borrowed static one, so the name comes from the composition root's
/// registration interner ([`crate::Registration::lane`]) rather than from a literal
/// registration interner (the kernel's `Registration::lane`) rather than from a literal
/// at the sealing site. That is what lets a deployment's own lanes be sealed at all, and it is
/// why this signature does not take a `String`: a lane id minted per unit would be a leak per
/// request, which is the one thing the interning rule exists to forbid.
Expand Down
169 changes: 0 additions & 169 deletions crates/busbar-contract/src/ids.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,172 +308,3 @@ pub struct Estimate {
/// One line per class the unit may consume.
pub per_class: crate::bounded::BoundedVec<ClassEstimate, { crate::bounded::MAX_USAGE_LINES }>,
}

/// How many distinct keys one image may ever intern.
///
/// The freeze below is what a composition root does. This is what holds where there is no
/// composition root to do it: a test binary, a bench, a fuzz target, or a dynamically loaded plugin
/// linked against its own copy of this crate, which gets its own statics and therefore its own
/// vocabulary. In each of those the freeze never happens, and this ceiling is the whole bound.
///
/// Sized for configuration, not for traffic: it is every open-vocabulary category a plugin may
/// declare against, summed across every kind of plugin key, for a deployment far larger than any
/// that has been configured. A node that reaches it has a defect, not a big configuration.
pub const MAX_VOCABULARY: usize = 4096;

/// The image's one vocabulary, and whether it is still open.
///
/// A `static` rather than state on the registration value because the LEAKED STRINGS are the
/// resource and the resource is process-wide. Ownership of a registration value bounds nothing:
/// the value is constructible by anyone, so a bound that lives inside one instance is a bound per
/// instance, which is no bound at all against a caller that makes a fresh instance per request.
static VOCABULARY: std::sync::LazyLock<std::sync::Mutex<std::collections::HashSet<&'static str>>> =
std::sync::LazyLock::new(|| std::sync::Mutex::new(std::collections::HashSet::new()));
static FROZEN: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);

/// The one place a configured string becomes a declared key.
///
/// Every identifier on this surface is a borrowed static string, because the declarations that
/// carry them are associated constants and a constant cannot own a heap allocation. But lanes,
/// hosts, dialects and other plugin-declared identifiers are CONFIGURED: they are read at boot,
/// and a value read at boot is not static on its own. Something has to bridge those two facts, and
/// until now nothing named what — so every plane coped with a const table the composition root was
/// expected to seal somehow, and one transport leaked a fresh string on every dial, which is a leak
/// per request.
///
/// This is that bridge, and it is the composition root's. The root builds one of these at boot,
/// interns every config-derived open-vocabulary key through it, and hands the resulting static
/// names to the registrations that declare them. The set is finite because configuration is finite;
/// it is leaked ONCE because interning is idempotent; and its size is a fixed term of the node's
/// resident memory rather than a term that grows with traffic. Nothing outside registration may
/// intern: a key minted per unit would be exactly the leak this replaces.
///
/// ## How "nothing outside registration" is held
///
/// Not by who owns this value. This value is constructible by anyone, and it has to be: a plane, a
/// transport and a unit crate all build one in their own tests, and the contract is the plugin-
/// visible ABI, so any seal on the constructor is a seal a plugin can name. What holds instead is
/// that the resource being spent — leaked `&'static str` — is process-wide, so the bound is
/// process-wide too. Every registration in one image reads and writes ONE vocabulary. The
/// composition root fills it at boot and calls [`freeze`](Self::freeze); after that, [`key`] is a
/// LOOKUP: a name the root registered still resolves, from anywhere, and a name it did not is
/// refused. A plane that holds a registration of its own and asks for a name it took off a request
/// gets `None`, having allocated nothing, no matter how many registrations it makes.
///
/// The refusal is returned as a value and never raised as a panic, because the name that reaches it
/// is client-supplied: a panic there is a way for a request to stop the node, which is a worse
/// failure than the leak being closed. And [`MAX_VOCABULARY`] bounds an image whose freeze never
/// happens at all, which is every image that has no composition root in it.
///
/// [`key`]: Self::key
#[derive(Debug, Default)]
pub struct Registration {
interned: std::collections::HashSet<&'static str>,
}

impl Registration {
/// A registration with nothing interned yet.
#[must_use]
pub fn new() -> Self {
Self::default()
}

/// The static name for one configured key: interned if the vocabulary is open, resolved if it
/// is already there, and refused otherwise.
///
/// Idempotent: asking twice yields the same name and leaks once. `None` says the key is not in
/// this image's vocabulary and cannot be added to it — because boot is over, or because the
/// ceiling is reached — which is a refusal to route on that name, not an error to recover from.
pub fn key(&mut self, value: &str) -> Option<&'static str> {
let mut vocabulary = VOCABULARY
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let found = if let Some(existing) = vocabulary.get(value) {
*existing
} else if FROZEN.load(std::sync::atomic::Ordering::Acquire)
|| vocabulary.len() >= MAX_VOCABULARY
{
return None;
} else {
let leaked: &'static str = Box::leak(value.to_owned().into_boxed_str());
vocabulary.insert(leaked);
leaked
};
drop(vocabulary);
self.interned.insert(found);
Some(found)
}

/// The [`LaneId`] for one configured lane name.
///
/// The priced axis is declared as a borrowed static name and a configured lane's name is read
/// at boot, so something has to bridge the two — and the bridge is this one, not a second id
/// type that owns its string. An owned lane id would double the type every rate-card key, every
/// verified destination and every locator comparison is written in, for one configured value;
/// interning keeps the axis one `Copy` name and pays a fixed registration-time allocation for
/// it. Refuses for the same reasons [`key`](Self::key) does: a lane nobody registered is not a
/// lane this node routes to.
pub fn lane(&mut self, name: &str) -> Option<LaneId> {
self.key(name).map(LaneId::new)
}

/// The static name the vocabulary already holds for `value`, or `None`. A LOOKUP ONLY, open
/// vocabulary or closed: it never interns, so it is the one form a per-unit path may call with a
/// name the unit reported. Boot and config registration are what put a name here.
#[must_use]
pub fn resolve(value: &str) -> Option<&'static str> {
VOCABULARY
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(value)
.copied()
}

/// [`Self::resolve`] as the [`MeterClassId`] a usage line carries: a class the vocabulary holds
/// resolves, and a class nobody declared or configured does not.
#[must_use]
pub fn meter_class(value: &str) -> Option<MeterClassId> {
Self::resolve(value).map(MeterClassId::new)
}

/// Close the image's vocabulary. The composition root's last registration-time act.
///
/// Idempotent, and one-way: there is no thaw, because a vocabulary that can be reopened is a
/// vocabulary a request path can reopen. Calling it before the root has finished registering
/// costs the node its own configured names and fails the boot loudly — which is the right shape
/// for a first-party ordering mistake, and is not something a request can cause.
pub fn freeze() {
FROZEN.store(true, std::sync::atomic::Ordering::Release);
}

/// Whether the vocabulary has been closed.
#[must_use]
pub fn is_frozen() -> bool {
FROZEN.load(std::sync::atomic::Ordering::Acquire)
}

/// How many distinct keys this IMAGE has interned.
///
/// The fixed resident-memory term is counted from here, so it is readable rather than inferred,
/// and it is the process's count rather than one registration's because the leak is the
/// process's.
#[must_use]
pub fn interned() -> usize {
VOCABULARY
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len()
}

/// How many distinct keys this registration has resolved.
#[must_use]
pub fn len(&self) -> usize {
self.interned.len()
}

/// Whether this registration has resolved nothing.
#[must_use]
pub fn is_empty(&self) -> bool {
self.interned.is_empty()
}
}
4 changes: 2 additions & 2 deletions crates/busbar-contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,8 @@ pub use grammar::{
pub use ids::{
AdminVerbId, BucketChain, BucketRef, BucketScope, CapDimension, ClaimKey, ClassDirection,
ClassEstimate, CorrelationRef, CorrelationValue, Estimate, LaneId, MeterClassDecl,
MeterClassId, OpClassId, PrincipalId, RecordSchemaId, Registration, SchemeAlt, SchemeKey,
SessionId, StreamId, TransportId, UnitKey, UpstreamIdx, MAX_VOCABULARY,
MeterClassId, OpClassId, PrincipalId, RecordSchemaId, SchemeAlt, SchemeKey, SessionId,
StreamId, TransportId, UnitKey, UpstreamIdx,
};
pub use kinds::{
Ack, Challenge, ChallengeState, ContentFacts, CredentialLocator, EnvelopeFields, Head,
Expand Down
81 changes: 21 additions & 60 deletions crates/busbar-contract/src/records.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,57 +61,25 @@ impl ScopeRef {
}
}

/// THE NEUTRAL SCOPE-KIND REGISTRY — the ENGINE's vocabulary of `ScopeRef` kinds (beyond the
/// built-in `pool`). This crate names NO concrete plane kind: the registry starts with only `pool`
/// (core's own admission kind) and every plane kind (`mcp_server`, `mcp_tool`, an A2A `agent`, …) is
/// registered at boot by the composition root, which iterates each installed `PlaneDecl.scope_kinds`
/// and calls [`register_scope_kind`]. The wire-field NAME for a kind is the frozen convention
/// `allowed_{kind}s` (`pool`→`allowed_pools`, `mcp_server`→`allowed_mcp_servers`, …), so the config
/// grammar keys stay byte-identical while the vocabulary itself lives here rather than being
/// hard-coded.
/// THE SCOPE-KIND WIRE CONVENTION. A [`ScopeRef`] kind beyond the built-in `pool` (core's own
/// admission kind) is a plane's own (`mcp_server`, `mcp_tool`, an A2A `agent`, …) and is DATA off
/// its `PlaneDecl.scope_kinds`; this crate names no concrete plane kind and holds no registry of
/// them. The wire-field NAME for a kind is the frozen convention `allowed_{kind}s`
/// (`pool`→`allowed_pools`, `mcp_server`→`allowed_mcp_servers`, …), so the config grammar keys
/// stay byte-identical.
///
/// ## The registry does NOT gate the wire (1.6.0 SDK-SCOPEKINDS)
/// ## The wire is a carrier, and holds no process state (THE DESIGN A.8)
///
/// The [`VirtualKey`] wire is a pure, lossless CARRIER: every kind, registered or not, serializes to
/// its own `allowed_{kind}s` field and reads back as the same kind (the naming convention is a
/// bijection, see [`scope_kinds::kind_for_wire_field`]). It used to refuse to serialize an
/// unregistered kind, and that made a DROPPED-IN store plugin unable to hand back a key it had just
/// been given: a `cdylib` statically links its own copy of this crate, so its registry is a
/// DIFFERENT, EMPTY static that the engine's boot registration never reaches, while a compiled-in
/// store shares the engine's. The same plugin behaved differently by build (DECISIONS #11 / spec
/// Part 2 #2). Kind VALIDATION is the engine's — it owns the registry and asks
/// [`scope_kinds::is_registered`]; a plugin is an opaque carrier of the kind string.
/// The [`VirtualKey`] wire is a pure, lossless CARRIER: every kind serializes to its own
/// `allowed_{kind}s` field and reads back as the same kind (the naming convention is a bijection,
/// see [`scope_kinds::kind_for_wire_field`]). It used to keep a process-global registry of kinds and
/// refuse to serialize an unregistered one, which made a DROPPED-IN store plugin unable to hand back
/// a key it had just been given: a `cdylib` statically links its own copy of this crate, so its
/// registry was a different, empty static. The refusal went first (1.6.0 SDK-SCOPEKINDS); the
/// registry it consulted went with it, because nothing read it after, and `busbar-contract` holds
/// no `static` (THE DESIGN A.8 "Statelessness", `BUSBAR-1.6.0.md` l.3701; C.2 l.3893). A plugin is
/// an opaque carrier of the kind string.
pub mod scope_kinds {
use std::collections::BTreeSet;
use std::sync::RwLock;

/// The built-in kind every deployment always has — core's admission-pool topology. Seeded so a
/// pool grant serializes with no registration and so the pool-only wire shape is unconditional.
const BUILTIN_POOL_KIND: &str = "pool";

static REGISTERED: RwLock<BTreeSet<String>> = RwLock::new(BTreeSet::new());

/// REGISTER a scope kind into the engine's vocabulary. Idempotent. Called at boot for every
/// `PlaneDecl.scope_kinds` entry — the composition root supplies the string, so no plane token
/// is ever a literal in a neutral crate. Registration is NOT required to persist a grant of the
/// kind: the wire carries every kind (see the module doc).
pub fn register(kind: &str) {
let mut w = REGISTERED.write().unwrap_or_else(|e| e.into_inner());
w.insert(kind.to_string());
}

/// Whether `kind` is in the ENGINE's vocabulary. `pool` is always registered. Only meaningful in
/// the engine's own process: a dropped-in plugin's copy of this registry is never populated.
pub fn is_registered(kind: &str) -> bool {
if kind == BUILTIN_POOL_KIND {
return true;
}
REGISTERED
.read()
.unwrap_or_else(|e| e.into_inner())
.contains(kind)
}

/// The frozen wire-field name for a scope `kind` — `allowed_{kind}s`.
pub fn wire_field_for(kind: &str) -> String {
format!("allowed_{kind}s")
Expand All @@ -127,20 +95,13 @@ pub mod scope_kinds {
}
}

/// REGISTER a `ScopeRef` kind's wire field — see [`scope_kinds`]. The composition root calls this at
/// boot for every installed plane's declared `scope_kinds`, so this neutral crate carries no plane
/// vocabulary of its own.
pub fn register_scope_kind(kind: &str) {
scope_kinds::register(kind);
}

/// The KIND-PARTITIONED wire shape for [`VirtualKey::allowed_scopes`] (1.6.0): each REGISTERED
/// scope kind gets its OWN named wire field, `allowed_{kind}s` — `allowed_pools` (kind `pool`),
/// The KIND-PARTITIONED wire shape for [`VirtualKey::allowed_scopes`] (1.6.0): each scope kind
/// gets its OWN named wire field, `allowed_{kind}s` — `allowed_pools` (kind `pool`),
/// `allowed_mcp_servers` (kind `mcp_server`), `allowed_mcp_tools` (kind `mcp_tool`) — each a plain
/// array of bare value strings, never a `{kind, value}` object. The in-memory
/// `Option<Vec<ScopeRef>>` is partitioned by kind on write and reassembled on read. The kind
/// vocabulary lives in the neutral [`scope_kinds`] registry (populated at boot from each installed
/// `PlaneDecl.scope_kinds`), NOT hard-coded here — so this crate names no concrete plane.
/// `Option<Vec<ScopeRef>>` is partitioned by kind on write and reassembled on read. The kind is
/// the entry's own data, named by the [`scope_kinds`] convention, NOT hard-coded here — so this
/// crate names no concrete plane.
///
/// Wire-compat invariants, all pinned by tests:
/// - the pool-only shape stays BYTE-IDENTICAL to the pre-generalization
Expand Down
22 changes: 6 additions & 16 deletions crates/busbar-contract/src/tests/records_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,10 +267,6 @@ fn attribution_fields_round_trip_and_are_backward_compatible() {
/// partitioned on write and reassembled on read.
#[test]
fn scope_kinds_survive_store_round_trip() {
// The plane scope kinds are registered at boot from each `PlaneDecl.scope_kinds`; a unit test
// registers them itself (idempotent) so the neutral crate's SOURCE names no plane kind.
register_scope_kind("grid_server");
register_scope_kind("grid_tool");
let mut k = sample_key();
k.allowed_scopes = Some(vec![
ScopeRef::pool("fast"),
Expand Down Expand Up @@ -298,22 +294,18 @@ fn scope_kinds_survive_store_round_trip() {
assert!(rt.scope_allowed("pool", "fast"));
}

/// A scope kind this process NEVER registered round-trips VERBATIM under its own
/// `allowed_{kind}s` field (1.6.0 SDK-SCOPEKINDS) - never remapped into `allowed_pools` (the pre-P0
/// escalation) and never dropped (which would widen the grant toward the `None` wildcard).
/// Any scope kind round-trips VERBATIM under its own `allowed_{kind}s` field (1.6.0
/// SDK-SCOPEKINDS) - never remapped into `allowed_pools` (the pre-P0 escalation) and never dropped
/// (which would widen the grant toward the `None` wildcard).
///
/// This replaces the old "unregistered kind is a hard serialize error" rule. That rule made the
/// wire consult a process-global registry, and a dropped-in store plugin's copy of it is never
/// wire consult a process-global registry, and a dropped-in store plugin's copy of it was never
/// populated, so the plugin could not hand back a key carrying a plane grant it had just stored.
/// Kind validation is the engine's; the wire is an opaque carrier. The kind below is one no test
/// in this crate registers, so this runs against an unregistered kind whatever the test order.
/// The registry is gone (the contract holds no static, THE DESIGN A.8); the wire is an opaque
/// carrier, and a kind nobody declared round-trips like any other.
#[test]
fn unregistered_scope_kind_round_trips_verbatim_under_its_own_field() {
let kind = "sdk_scopekinds_never_registered";
assert!(
!scope_kinds::is_registered(kind),
"fixture precondition: the kind must be unregistered"
);
let mut k = sample_key();
k.allowed_scopes = Some(vec![ScopeRef {
kind: kind.to_string(),
Expand Down Expand Up @@ -343,8 +335,6 @@ fn unregistered_scope_kind_round_trips_verbatim_under_its_own_field() {
/// `allowed_pools: []` beside a GRID field stays the EMPTY pool set - never "all".
#[test]
fn grid_scope_wire_fields_are_additive() {
register_scope_kind("grid_server");
register_scope_kind("grid_tool");
// Pool-only and None grants must not grow grid fields on the wire.
let pool_only = sample_key();
let v = serde_json::to_value(&pool_only).unwrap();
Expand Down
Loading