Skip to content

P-item refusal-reason collapse: one refusal classification; every surface a class-to-wire table - #535

Queued
MattJackson wants to merge 2 commits into
predevfrom
p-items-refusal-class
Queued

MattJackson wants to merge 2 commits into
predevfrom
p-items-refusal-class

Conversation

@MattJackson

@MattJackson MattJackson commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Spec DONE item 2: "All P-item behaviours match 1.5.5 — refusal-reason collapse … fixed as BUGS, not signed (#43/#71)". TODO L-ENG9 asked for one classifier. ARCHITECT ruling (2026-10-07): one shared, total classifier lives in busbar-contract/src/abi/ next to the RefusalCode table. The llm grouping is the authority, because the llm plane is the only one 1.5.5 shipped. The llm stays byte-identical. Each other surface keeps its own class→wire table.

The 1.5.5 behaviour

On the llm surface, 1.5.5 answered each limit reason with its own status and kind, never a 500:

  • rate: 429 rate_limit_error
  • budget: 429 insufficient_quota, or 400 on bedrock
  • frozen group: 403 permission_error

Source: v1.5.5 crates/busbar/src/ingress/mod.rs:237-305. The P-item (drive log P1/P2, 470351a) is a plane answering a policy refusal as "this node broke".

The root

Eight hand-copied reason→family matches had drifted apart:

  • kernel refusal_status
  • llm kind_of, is_authentication, refusal_shape
  • a2a, mcp, decisions and streaming refusal_render
  • admin answer_for

The streaming one still ended in _ => "internal".

The fix

  • RefusalClass (14 classes) and RefusalCode::class() are now the ONE reason→class match. It has no _ arm. Helpers: class_of, class_of_refusal, class_of_word. A reverse From<RefusalReason> for ReasonCode is generated from the same reasons! table.
  • Every renderer above is now a class→wire table. The kernel's default status is class_status(class).
  • llm: no byte moves on the served door path.
    • Kernel default plus the plane's rows give the same status as today for every reason and every dialect.
    • kind_of and is_authentication give the same kind as today for every reason.
    • Two rows became redundant and are gone: DestinationUnreachable 503 is now the Unreachable default, and GroupFrozen 403 is now the Forbidden default.
    • Four any-dialect rows keep the llm's current answer where the class default differs. No 1.5.5 cell records these reasons: SchemeNotDeclared 400, ChallengeExhausted 503, CursorBudget 503, CredentialBudget 503.
    • llm_refusal_statuses.rs, which checks against the 1.5.5 golden cells, stays green unchanged.

Byte moves, all on surfaces that are new in 1.6.0 (fixed P-item bugs; baseline is predev; nothing signed)

Every reason now answers as its class does. "Before → after" lists every reason whose answer changes; no other reason's answer changes.

streaming (the headline: the _ => "internal" arm is gone; error.code, error.message)

  • OverBudget, GroupFrozen, Replayed, Superseded, NoRate, Unpriced, InFlight, Stalled, DeadlineExceeded, ClientGone, SpillBudget, ScratchExhausted, OverdraftCeiling, StaleSlice, DurabilityUnavailable, TierMismatch, DestinationBudgetExhausted, CursorBudget, ChallengeExhausted: internal →
    • rate_limited: OverBudget ("could not be opened"), InFlight ("too many sessions")
    • forbidden: GroupFrozen
    • invalid_request: NoRate, Unpriced, Replayed, Superseded ("could not be opened"); CursorBudget ("could not be read")
    • unauthorized: ChallengeExhausted
    • unavailable: DestinationBudgetExhausted ("no provider is reachable"); every other reason in this list ("could not be opened")
  • SessionBudget, OpenSlotBusy: rate_limited → unavailable (their class is Unavailable; the llm surface answers them 503 overloaded)
  • SchemeNotDeclared: invalid_request → unauthorized
  • Revoked: forbidden → unauthorized
  • CredentialBudget: unauthorized → invalid_request
  • SecretPlaceholder: invalid_request → internal (a node fault)
  • Drain: message "no provider is reachable…" → "the session could not be opened at this time" (code stays unavailable)

a2a / mcp / decisions (same shape on each wire)

  • CursorBudget: policy refusal → INVALID_REQUEST "the request is too large"
  • CredentialBudget: "did not carry usable authority" → "the request is too large"
  • ChallengeExhausted: policy refusal → INVALID_REQUEST "did not carry usable authority"
  • Revoked: "may not perform this operation" → INVALID_REQUEST "did not carry usable authority"
  • GroupFrozen: "could not be served at this time" → "may not perform this operation"
  • DurabilityUnavailable: INTERNAL → the plane's policy refusal (UnsupportedOperation / REFUSED / unsupported_operation)

admin answer_for

  • InFlight: 403 forbidden → 429 rate_limited
  • SessionBudget, SpillBudget, ScratchExhausted, OpenSlotBusy, OverdraftCeiling, TierMismatch, Drain, ClientGone: 403 forbidden → 503 unavailable

kernel default status (class_status; the llm plane's rows keep every llm status as it was)

  • CursorBudget, CredentialBudget: 503 → 413
  • SchemeNotDeclared: 400 → 401
  • ChallengeExhausted: 503 → 401; it now also counts as an authentication refusal for the hook auth tap, as the llm plane's renderer already counted it
  • GroupFrozen: 429 → 403 (1.5.5's own answer)
  • DestinationUnreachable: 502 → 503 (1.5.5's recorded upstream_down answer)

llm refusal_shape (the Plane-trait encoder, which is not on the served door path)

  • SessionBudget, OpenSlotBusy, OverdraftCeiling: 429 → 503
  • Revoked: 403 → 401
  • GroupFrozen: 429 → 403

Each now matches what the served path already answered.

Pins (p_item_refusal_reason_collapse_*)

  • busbar-contract/tests/p_item_refusal_reason_collapse.rs:
    • every reason has one class under every spelling;
    • every class is reached;
    • the 1.5.5 reasons keep their own family (rate → Throttled, budget → QuotaExhausted, frozen group → Forbidden, …);
    • only the node's own five faults are node faults.
  • a2a, mcp, decisions, streaming: a reason renders as the plane's internal code exactly when its class is a node fault, and every reason in one class renders the same answer. Streaming checks this through its encode_refusal bytes.
  • busbar-llm/tests/llm_refusal_statuses.rs: for every dialect and every reason, no llm refusal is a bare 500, and api_error is used only for node faults and the 504 timeout.
  • xtask/tests/p_item_refusal_reason_collapse.rs, the source scan:
    • no renderer names a reason variant (ReasonCode:: / RefusalReason:: / RefusalCode:: / R::), and kind_of names no reason spelling;
    • the classifier names every reason;
    • the scan catches a planted reason match.
    • It lives in xtask because it names every plane, and the crates may not name each other's planes (instance-noun-neutrality).

Proof (Latchkey)

  • RED (cli-b6c66634):
    • Putting predev's streaming refusal_render back fails the streaming pin and the xtask scan.
    • a2a answering Throttled as internal fails the a2a pin.
    • mcp special-casing GroupFrozen fails the mcp pin and the scan.
  • Tests, unfiltered.
    • Green: busbar-contract, plane-llm, plane-a2a, plane-mcp, plane-decisions, plane-streaming (cli-c9f4b1c0); busbar-kernel and busbar-llm, including llm_refusal_statuses.rs (cli-18a97d6a); xtask pin (cli-8e00771c).
    • busbar: 15 root::serve / root::registry tests need example cdylibs the job did not build, and ledger_identity needs the oracle engine checkout. The same tests fail the same way on base d07eaf7 (cli-067ea515). Everything else is green, including money_never_ends_an_admitted_unit.
  • Oracle, filtered record + strict replay vs 1.5.5, base d07eaf7 against this branch (cli-92a85f4f / cli-b5f15520):
  • Lint and gates.
    • fmt and clippy --all-targets -D warnings (contract, kernel, five planes, busbar-llm, busbar, xtask): green.
    • cargo xtask abi-header: green.
    • cargo xtask gate --all: RED in kind-isolation, kind-isolation-ship and ship-ready only, exactly as on base.

No signed diff and no accepted-differences entry.

Re-proof after merging origin/predev (#499, #505), head a6c96ab

  • Conflicts: kernel plane_driver/mod.rs and the a2a, decisions, llm and mcp plane.rs files.
    • The class-to-wire bodies are kept.
    • predev's import lines are kept: BoundedVec/Span are no longer imported.
  • SEAM: trust is the kernel's Approve step: trust.sight_item/serves/decide, /admin/trust approve|revoke|list over one durable decide path; the Plane trait declares no approve/admit #499's new reason Untrusted (code 42) joins RefusalClass::Forbidden.
    • That is exactly where predev put it on every plane: 403 permission (llm), may-not-perform (a2a, mcp, decisions), and the kernel's 403.
    • It is also where the admin _ => 403 already sent it.
    • On streaming it now renders forbidden, where predev's catch-all said internal.
  • Tests, unfiltered: contract, kernel, plane-llm, plane-a2a, plane-mcp, plane-decisions, plane-streaming, busbar-llm and busbar.
    • Branch: 8858 passed, 1 failed (cli-cc7aafae). Base 2bbe362: 8849 passed, the same 1 failed (cli-f2db9d42).
    • The failure is ledger_identity, which needs the oracle engine checkout.
    • The 12 p_item_refusal_reason_collapse pins pass. The xtask scan passes (3 tests).
  • Lint: fmt, clippy --all-targets -D warnings and xtask abi-header are green.
  • Oracle: bin/oracle replay --baseline-version 1.5.5 --strict --id-filter, branch cli-fa9760a8 vs base cli-4dfce92d.
    • Both runs printed: record rc=0, 403 cells recorded, replay rc=3, "strict: 58 unaccepted divergence(s)".
    • Judged rows in the 1.5.5 section, on each side: 266 PASS, 34 ACCEPTED, 58 FAIL (358).
    • Every cell has the same verdict and the same diff-detail sha on both sides.

Streaming byte move: Untrusted (42), internal → forbidden

  • What moves: the streaming plane's refusal for Untrusted changes from error.code internal / "the session could not be opened at this time" to forbidden / "the caller may not open a session for this operation".
  • Why: SEAM: trust is the kernel's Approve step: trust.sight_item/serves/decide, /admin/trust approve|revoke|list over one durable decide path; the Plane trait declares no approve/admit #499 added Untrusted (the kernel's Approve did not trust the unit's stated counterparty). Streaming had no arm for it, so predev's _ => "internal" catch-all called it a node fault. The one classification puts it in Forbidden.
  • The other four planes, on predev: each puts Untrusted in its permission family, so they don't move:
    • llm refusal_shape: RefusalReason::PoolNotPermitted | RefusalReason::Untrusted => (403, KIND_PERMISSION)
    • a2a: CODE_UNSUPPORTED_OPERATION, "the caller may not perform this operation"
    • mcp: CODE_REFUSED, "the caller may not perform this operation"
    • decisions: unsupported_operation, "the caller may not perform this operation"
  • Kernel and admin: the kernel default is ReasonCode::Untrusted => 403, and admin answers 403 forbidden. Neither moves.
  • Dated baseline, streams against golden/1.6.0-pre (--id-filter '^streams\|', branch cli-a7467163 vs base cli-e745b10f):
    • Both runs: record rc=0, 7 cells, replay rc=0.
    • Section "dated baseline: family streams against golden/1.6.0-pre": 7 judged, 7 PASS identical, with the same detail sha on both sides.
    • No 1.6.0-pre cell moves. None of the 7 cells (metadata, mint ×4, sdp, sideband) reaches an Untrusted refusal, so this move is proven by the plane's own test (p_item_refusal_reason_collapse_only_a_node_fault_is_internal_and_one_class_one_answer, which walks every RefusalCode through encode_refusal), not by an oracle cell.

…face a class-to-wire table

RefusalCode::class (busbar-contract abi/plane) is the one reason-to-family match, total,
no catch-all. The kernel's default status, the llm plane's kind_of / is_authentication /
refusal_shape, the a2a, mcp, decisions and streaming refusal_render, and the admin
surface's answer_for become class-to-wire tables. The streaming plane's catch-all that
answered every unnamed reason as internal is gone. The llm plane's served statuses and
kinds stay byte-identical: two rows become the class default, four rows keep its answers.

Pins: p_item_refusal_reason_collapse_* in busbar-contract (the classification), in each
new plane (only a node fault renders internal; one class, one answer), in busbar-llm
(no llm refusal reads as an internal error) and in xtask (the source scan: no renderer
names a reason).
@MattJackson
MattJackson enabled auto-merge October 7, 2026 09:12
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

promote into predev: BOARD @a6c96abcc: 3 failing test row(s), 12 DENY row(s)

Failing tests (3)

crate test step first panic
`` nextest xtask::cli::selftest_runs_every_registered_gates_red_proof test:workspace
transport_dropped_in_serves a_dropped_in_transport_registers_through_the_one_fold_and_serves test:dropped-in-tcp-transport crates/busbar/tests/transport_dropped_in_serves.rs:168:13: busbar exited (ExitStatus(unix_wait_status(512))) before serving; log:
-p busbar --test transport_dropped_in_serves test target failed test:dropped-in-tcp-transport

DENY rows (12)

gate row detail
construction one-pick-site 3 production call site(s) of 'pick_among(' (ceiling 2): crates/busbar-kernel-egress/src/walk.rs:298; crates/busbar-llm/src/engine/exhaustion/fallback.rs:120; crates/busbar-llm/src/engine/pipeline.rs:8
kind-isolation kind-isolation:deps 3 finding(s), 76 shipped edge instance(s) over 27 class(es), 76 declaration(s); 61 '[[dep]]' row(s), 0 question(s): unlisted-dep-edge legacy -> plane busbar-llm -> busbar-plane-llm is a shipped edge w
kind-isolation kind-isolation:test-deps 5 finding(s), 36 test edge instance(s) over 19 class(es), 36 declaration(s); 22 '[[dep]]' row(s), 0 question(s): unlisted-dep-edge cleanliness -> legacy busbar-core-admin -> busbar-a2a is a test edge
kind-isolation-ship kind-isolation:deps 15 finding(s) over 76 shipped edge(s): ship-edge kernel -> hooks busbar-kernel -> busbar-hooks-ranking is 'not-allowed': the architecture grants no kernel -> hooks edge, and the ship criterion is the
kind-isolation-ship kind-isolation:test-deps 13 finding(s) over 36 test edge(s): ship-edge cleanliness -> legacy busbar-core-admin -> busbar-a2a is 'not-allowed': the architecture grants no cleanliness -> legacy edge, and the ship criterion is t
kind-isolation-ship kind-isolation:faces 4 finding(s) over 31 crate(s): foreign-entry crates/busbar busbar is kind 'root' and implements 'Store' 2 time(s) in shipped source — the entry face of kind 'store'. A trait implementation is a claim
kind-isolation-ship kind-isolation:testkit 2 finding(s) over 12 crate(s): battery-ignored crates/busbar-plane-decisions busbar-plane-decisions carries a tests/conformance.rs whose every entry is '#[ignore]'d (or which has none). 'cargo test'
kind-isolation-ship kind-isolation:legacy-drain 5 finding(s): transitional-live qa/kind-isolation.toml 'busbar-a2a -> busbar-core-admin' (legacy drain: the retiring A2A engine's tests drive the admin surface that drained into the cleanliness tier)
kind-isolation-ship kind-isolation:control-path 73 finding(s) over 3 control surface(s): upstream crates/busbar-core-admin/src/admin_codec/meta.rs:84 busbar-core-admin names 'egress' — a control surface has no upstream to reach, so the vocabulary o
ship-ready ship-ready:ship-twin 'kind-isolation-ship' is not green: kind-isolation:deps (a dependency the architecture does not grant is still in the graph); kind-isolation:test-deps (a dependency the architecture does not grant is
instance-noun-neutrality instance-noun-neutrality:voice tracked known-debt census — 8: crates/busbar-plane-llm/src/codec/gemini/handler.rs×7 [cross-plugin] | crates/busbar-plane-llm/src/codec/gemini/tests/handler_tests.rs×2 [cross-plugin] | crates/busbar
structure-lint structure-lint:plane-dup:unledgered 23 finding(s): PLANE-DUPLICATE (module): 'config.rs' — a2a:crates/busbar-a2a/src/a2a/config.rs mcp:crates/busbar-mcp/src/mcp/config.rs voice:crates/busbar-voice/src/config.rs (the ledger row signs for

Judged against base 2bbe362db: 0 new red, 0 worse, 8 standing (excused).

tests passed: 24749, failed: 1. Run: https://github.com/GetBusbar/busbar/actions/runs/37618679456 . Artifact verdict-a6c96abcc5cecc8dd651d848c7d5259ca1ffc90f (failures.json, junit.xml, raw.log; 90 days).

@MattJackson
MattJackson added this pull request to the merge queue Oct 7, 2026
Any commits made after this event will not be merged.
@MattJackson
MattJackson removed this pull request from the merge queue due to a manual request Oct 7, 2026
…: Untrusted joins the Forbidden class; the planes keep predev's imports
@MattJackson
MattJackson enabled auto-merge October 7, 2026 12:06
@MattJackson
MattJackson added this pull request to the merge queue Oct 7, 2026
Any commits made after this event will not be merged.

This branch has not been deployed

No deployments
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