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
158 changes: 158 additions & 0 deletions crates/busbar-contract/src/abi/plane/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,164 @@ pub const fn reason_of(code: u32) -> Option<ReasonCode> {
})
}

// ── the one refusal classification ───────────────────────────────────────────────────────────────

/// THE ONE REFUSAL CLASSIFICATION: which family of answer a refusal reason gets, whatever the
/// plane. This is the P-item "refusal-reason collapse" (spec DONE item 2; TODO L-ENG9).
///
/// A refusal reason is the kernel's own word, and a client never sees it. A client sees the plane's
/// rendering of it. Before this table there were eight hand-copied reason matches, and they
/// disagreed: the kernel's default status, three renderers in one plane, one in each of four
/// others, and the admin surface's. One of them still ended in a catch-all that answered a rate
/// limit, a spent budget or a frozen group as "internal", which tells a caller the node broke when
/// it was a policy refusal, so the caller retries the wrong thing. 1.5.5 never did that: on its one
/// plane each limit reason has its own status and kind (v1.5.5 `crates/busbar/src/ingress/mod.rs:
/// 237-305`: rate 429 `rate_limit_error`, budget 429 `insufficient_quota`, frozen group 403
/// `permission_error`), and none becomes a 500.
///
/// So the reason-to-class grouping lives ONCE, here, and a plane holds only a class-to-wire table:
/// its own codes and words. The grouping follows the one plane 1.5.5 shipped (owner correction
/// 2026-09-28): each class has one kernel default status, and that default plus that plane's stated
/// per-reason rows reproduce every status it answered, byte for byte, against the 1.5.5 golden
/// cells.
///
/// The match in [`RefusalCode::class`] has no `_` arm, so a code added without a class does not
/// compile.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RefusalClass {
/// The plane could not read the request.
Unreadable,
/// The request was readable, and this node will not take it as asked: an unbillable name, a
/// replayed or superseded idempotency key.
Rejected,
/// No usable authority: no credential, a revoked one, or an exchange that never produced one.
Unauthenticated,
/// The caller is known and may not do this.
Forbidden,
/// The request, or what it needs held, is larger than the node allows.
TooLarge,
/// The caller is over its arrival rate.
Throttled,
/// The node's in-flight table, or the idempotency key's unit, is busy.
Busy,
/// A money cap in the caller's chain has no headroom.
QuotaExhausted,
/// There is nowhere for the request to go.
NotFound,
/// A destination exists, and the way to it is shut: unreachable, breaker open, or its budget
/// spent.
Unreachable,
/// The node cannot take the unit now: capacity, drain, journal, or the client left.
Unavailable,
/// The unit ran out of time.
Timeout,
/// A plane call panicked.
PlaneFault,
/// The node got something wrong and says so without saying what.
NodeFault,
}

impl RefusalClass {
/// Every class.
pub const ALL: &'static [RefusalClass] = &[
RefusalClass::Unreadable,
RefusalClass::Rejected,
RefusalClass::Unauthenticated,
RefusalClass::Forbidden,
RefusalClass::TooLarge,
RefusalClass::Throttled,
RefusalClass::Busy,
RefusalClass::QuotaExhausted,
RefusalClass::NotFound,
RefusalClass::Unreachable,
RefusalClass::Unavailable,
RefusalClass::Timeout,
RefusalClass::PlaneFault,
RefusalClass::NodeFault,
];

/// Whether the class is a fault of this node, the one family a plane may render as its
/// internal error. Every other class is a refusal the caller is owed by name.
#[must_use]
pub const fn is_node_fault(self) -> bool {
matches!(self, RefusalClass::PlaneFault | RefusalClass::NodeFault)
}
}

impl RefusalCode {
/// THE class of this code: the one reason-to-class match in the tree.
#[must_use]
pub const fn class(self) -> RefusalClass {
use RefusalClass as C;
match self {
RefusalCode::DecodeFailed => C::Unreadable,
RefusalCode::NoRate
| RefusalCode::Unpriced
| RefusalCode::Replayed
| RefusalCode::Superseded => C::Rejected,
RefusalCode::Unauthenticated
| RefusalCode::Revoked
| RefusalCode::SessionUnbound
| RefusalCode::SchemeNotDeclared
| RefusalCode::ChallengeExhausted => C::Unauthenticated,
RefusalCode::ScopeDenied
| RefusalCode::PoolNotPermitted
| RefusalCode::HookVeto
| RefusalCode::GroupFrozen
| RefusalCode::Untrusted => C::Forbidden,
RefusalCode::BodyTooLarge
| RefusalCode::CursorBudget
| RefusalCode::CredentialBudget => C::TooLarge,
RefusalCode::RateLimited => C::Throttled,
RefusalCode::InFlightCap | RefusalCode::InFlight => C::Busy,
RefusalCode::OverBudget => C::QuotaExhausted,
RefusalCode::NoDestination => C::NotFound,
RefusalCode::DestinationUnreachable
| RefusalCode::BreakerOpen
| RefusalCode::DestinationBudgetExhausted => C::Unreachable,
RefusalCode::SessionBudget
| RefusalCode::SpillBudget
| RefusalCode::ScratchExhausted
| RefusalCode::OpenSlotBusy
| RefusalCode::OverdraftCeiling
| RefusalCode::StaleSlice
| RefusalCode::DurabilityUnavailable
| RefusalCode::TierMismatch
| RefusalCode::Drain
| RefusalCode::ClientGone => C::Unavailable,
RefusalCode::Stalled | RefusalCode::DeadlineExceeded => C::Timeout,
RefusalCode::PlanePanic => C::PlaneFault,
RefusalCode::MeterDisputed
| RefusalCode::HandoffMismatch
| RefusalCode::TaskLost
| RefusalCode::SecretPlaceholder => C::NodeFault,
}
}
}

/// The class of a kernel refusal reason ([`RefusalCode::class`] of its wire code).
#[must_use]
pub const fn class_of(reason: ReasonCode) -> RefusalClass {
wire_code(reason).class()
}

/// The class of a refusal as a plane is handed it ([`RefusalCode::class`] of its wire code).
#[must_use]
pub fn class_of_refusal(reason: crate::unit::RefusalReason) -> RefusalClass {
class_of(ReasonCode::from(reason))
}

/// The class of a reason by its spelling on the journal and the wire (`ReasonCode::as_str`), for
/// every reason a plane can be handed ([`reason_of`]); `None` for any other word, the kernel's own
/// two money verdicts included.
#[must_use]
pub fn class_of_word(word: &str) -> Option<RefusalClass> {
RefusalCode::ALL
.iter()
.find(|c| reason_of(c.code()).is_some_and(|r| r.as_str() == word))
.map(|c| c.class())
}

// ── the Statement tail ───────────────────────────────────────────────────────────────────────────

/// A dialect's default auth style (the 1.5.5 dialect defaults), as data.
Expand Down
10 changes: 10 additions & 0 deletions crates/busbar-contract/src/caps/seat_verdict.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ macro_rules! reasons {
}
}
}

// The same join read the other way, so a plane handed a `RefusalReason` can reach the one
// classification (`abi::plane::RefusalCode::class`) without a reason match of its own.
impl From<crate::unit::RefusalReason> for ReasonCode {
fn from(reason: crate::unit::RefusalReason) -> Self {
match reason {
$(crate::unit::RefusalReason::$refusal => ReasonCode::$name,)*
}
}
}
};
}

Expand Down
113 changes: 113 additions & 0 deletions crates/busbar-contract/tests/p_item_refusal_reason_collapse.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2026 Busbar Inc and contributors

//! P-ITEM: REFUSAL-REASON COLLAPSE (spec DONE item 2, `docs/design/BUSBAR-1.6.0.md`, "All P-item
//! behaviours match 1.5.5"; the drive log's P1/P2, commit 470351a480; TODO L-ENG9).
//!
//! THE 1.5.5 BEHAVIOUR. 1.5.5 had one plane (owner correction 2026-09-28). On it
//! every limit reason reached the caller as its own status and kind, and none of them became an
//! internal error: a rate limit 429 `rate_limit_error`, a spent budget 429 `insufficient_quota`
//! (400 on bedrock), a frozen group 403 `permission_error` (v1.5.5
//! `crates/busbar/src/ingress/mod.rs:237-305`). The bug was a plane answering a policy refusal as
//! "this node broke", which a caller retries the wrong way.
//!
//! THE ROOT. The reason-to-family decision had been hand-copied eight times, and the copies had
//! drifted; one of them still ended in a catch-all that answered every reason it did not name as
//! internal. The fix is ONE classification
//! (`busbar_contract::abi::plane::RefusalCode::class`), with each surface holding only a
//! class-to-wire table. This file pins the classification; `xtask/tests/
//! p_item_refusal_reason_collapse.rs` reads the renderers' source and proves none of them holds a
//! reason match of its own again.

use busbar_contract::abi::plane::{
class_of, class_of_refusal, class_of_word, wire_code, RefusalClass, RefusalCode,
};
use busbar_contract::caps::ReasonCode;
use busbar_contract::unit::RefusalReason;

/// Every reason has exactly one class, read the same way from each of its three spellings.
#[test]
fn p_item_refusal_reason_collapse_every_reason_has_one_class_by_every_spelling() {
assert_eq!(ReasonCode::ALL.len(), RefusalCode::ALL.len());
for reason in ReasonCode::ALL {
let class = class_of(*reason);
assert_eq!(wire_code(*reason).class(), class, "{reason:?}");
let refusal = RefusalReason::from(*reason);
assert_eq!(ReasonCode::from(refusal), *reason, "the bridge round-trips");
assert_eq!(
class_of_refusal(refusal),
class,
"{reason:?} as a plane is handed it"
);
// By its spelling: every reason a plane can be handed; the kernel's own two money verdicts
// never reach a plane (`reason_of`) and read as no reason.
let kernel_only = matches!(
reason,
ReasonCode::OverdraftCeiling | ReasonCode::StaleSlice
);
assert_eq!(
class_of_word(reason.as_str()),
(!kernel_only).then_some(class),
"{reason:?} by its spelling"
);
}
assert_eq!(class_of_word("no_such_reason"), None);
}

/// Every class holds at least one reason: a class nothing lands in is a renderer arm nobody reads.
#[test]
fn p_item_refusal_reason_collapse_every_class_is_reached() {
for class in RefusalClass::ALL {
assert!(
ReasonCode::ALL.iter().any(|r| class_of(*r) == *class),
"{class:?} holds no reason"
);
}
}

/// The reasons 1.5.5's one surface answered keep a family of their own, and none of them is a
/// node fault: each is a refusal the caller is owed by name.
#[test]
fn p_item_refusal_reason_collapse_the_1_5_5_reasons_keep_their_own_family() {
for (reason, class) in [
// v1.5.5 ingress/mod.rs:237-258: a rate limit is 429 `rate_limit_error`.
(ReasonCode::RateLimited, RefusalClass::Throttled),
// v1.5.5 ingress/mod.rs:268-285: a spent budget is `insufficient_quota`.
(ReasonCode::OverBudget, RefusalClass::QuotaExhausted),
// v1.5.5 ingress/mod.rs:288-297: a frozen group is 403 `permission_error`.
(ReasonCode::GroupFrozen, RefusalClass::Forbidden),
// The recorded 1.5.5 cells of these reasons (`request|{unauthenticated,malformed,
// upstream_down}`, `http.crosscut|413|*`).
(ReasonCode::Unauthenticated, RefusalClass::Unauthenticated),
(ReasonCode::DecodeFailed, RefusalClass::Unreadable),
(
ReasonCode::DestinationUnreachable,
RefusalClass::Unreachable,
),
(ReasonCode::BodyTooLarge, RefusalClass::TooLarge),
] {
assert_eq!(class_of(reason), class, "{reason:?}");
assert!(!class.is_node_fault(), "{reason:?} is no node fault");
}
}

/// Only a fault of this node is a node fault. Every admission, capacity, rate, budget, breaker,
/// drain or deadline reason is a refusal the caller is told the family of.
#[test]
fn p_item_refusal_reason_collapse_only_the_nodes_own_faults_are_node_faults() {
let faults: Vec<ReasonCode> = ReasonCode::ALL
.iter()
.copied()
.filter(|r| class_of(*r).is_node_fault())
.collect();
assert_eq!(
faults,
[
ReasonCode::MeterDisputed,
ReasonCode::HandoffMismatch,
ReasonCode::PlanePanic,
ReasonCode::TaskLost,
ReasonCode::SecretPlaceholder,
]
);
}
51 changes: 29 additions & 22 deletions crates/busbar-kernel/src/plane_driver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,9 @@ use busbar_contract::abi::mechanism::call::{
};
use busbar_contract::abi::mechanism::ticket::Ticket;
use busbar_contract::abi::plane::{
reason_code, ArriveIn, ArriveOut, OutField, RefusalIn, RefusalOut, RefusalStatus, UnitCount,
REFUSAL_ANY_DIALECT, REFUSAL_ARRIVE, REFUSAL_GATE, REFUSAL_KERNEL, ROUTE_LOCAL, ROUTE_SESSION,
class_of, reason_code, ArriveIn, ArriveOut, OutField, RefusalClass, RefusalIn, RefusalOut,
RefusalStatus, UnitCount, REFUSAL_ANY_DIALECT, REFUSAL_ARRIVE, REFUSAL_GATE, REFUSAL_KERNEL,
ROUTE_LOCAL, ROUTE_SESSION,
};
use busbar_contract::abi::sdk::door::{blank_in, blank_out};
use busbar_contract::caps::{
Expand Down Expand Up @@ -181,31 +182,37 @@ impl DriverConfig {
}
}

/// Whether `reason` refuses a unit at authentication.
/// Whether `reason` refuses a unit at authentication: its class is
/// [`RefusalClass::Unauthenticated`] (the one classification, `busbar_contract::abi::plane`).
fn is_authentication(reason: ReasonCode) -> bool {
matches!(
reason,
ReasonCode::Unauthenticated
| ReasonCode::Revoked
| ReasonCode::SchemeNotDeclared
| ReasonCode::SessionUnbound
)
class_of(reason) == RefusalClass::Unauthenticated
}

/// The status the kernel hands `refusal` for a reason, when the deployment states no other.
///
/// A class-to-status table over the one classification ([`class_of`]); the kernel holds no reason
/// match of its own. A plane whose dialect answers a reason differently states a row for it
/// ([`DriverConfig::refusal_statuses`]), which is how a plane keeps every status 1.5.5 answered.
pub fn refusal_status(reason: ReasonCode) -> u32 {
match reason {
ReasonCode::DecodeFailed | ReasonCode::SchemeNotDeclared => 400,
ReasonCode::Unauthenticated | ReasonCode::Revoked | ReasonCode::SessionUnbound => 401,
ReasonCode::ScopeDenied
| ReasonCode::PoolNotPermitted
| ReasonCode::HookVeto
| ReasonCode::Untrusted => 403,
ReasonCode::BodyTooLarge => 413,
ReasonCode::RateLimited | ReasonCode::OverBudget | ReasonCode::GroupFrozen => 429,
ReasonCode::DestinationUnreachable | ReasonCode::PlanePanic => 502,
ReasonCode::DeadlineExceeded | ReasonCode::Stalled => 504,
_ => 503,
class_status(class_of(reason))
}

/// The kernel's default status for one refusal class.
pub const fn class_status(class: RefusalClass) -> u32 {
match class {
RefusalClass::Unreadable => 400,
RefusalClass::Unauthenticated => 401,
RefusalClass::Forbidden => 403,
RefusalClass::TooLarge => 413,
RefusalClass::Throttled | RefusalClass::QuotaExhausted => 429,
RefusalClass::PlaneFault => 502,
RefusalClass::Timeout => 504,
RefusalClass::Rejected
| RefusalClass::Busy
| RefusalClass::NotFound
| RefusalClass::Unreachable
| RefusalClass::Unavailable
| RefusalClass::NodeFault => 503,
}
}

Expand Down
Loading
Loading