feat: import Chinese-localized Buzz source snapshot
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled

Signed-off-by: cls_宁波本机 <908705107@qq.com>
This commit is contained in:
2026-08-13 18:34:25 +08:00
parent 61c3fa1df9
commit 9dfa06ffee
3785 changed files with 1085458 additions and 2 deletions
+337
View File
@@ -0,0 +1,337 @@
//! Replay engine: validate a sequence of [`TraceStep`]s against the spec's
//! transition relation (re-implemented in [`crate::transitions`]).
//!
//! The checker is intentionally minimal — it walks the trace, bootstraps
//! its model on the first step, and runs [`transitions::check_step`] for
//! each subsequent step. The first failure stops the trace (fail-closed).
//!
//! The other half of the checker's job is **coverage breach**: declaring
//! up-front which critical actions a scenario MUST exercise, and failing
//! the trace if any are missing. Without this, a regression that silently
//! removed an emit site would still pass conformance — the trace would
//! just be shorter. The skill is explicit: this mode is mandatory.
use std::collections::HashSet;
use crate::{
transitions::{check_step, ModelState, TransitionError},
TraceStep,
};
/// A scenario the checker is validating: the recorded trace plus the set
/// of critical actions the scenario asserts must appear.
#[derive(Debug, Clone)]
pub struct Scenario {
/// Trace steps in emission order. Stamps and worker ids are NOT
/// modeled — observations are unordered in the spec, so the only
/// invariant the order enforces is "within one request, observations
/// share the same `state_after`".
pub trace: Vec<TraceStep>,
/// Action kinds that this scenario must include at least once. If any
/// are missing the checker returns a coverage breach.
///
/// Use [`crate::TraceAction::kind`] to get the canonical strings:
/// `"write_insert"`, `"write_insert_global"`, `"write_duplicate"`,
/// `"sanitized_error"`, `"auth_check"`, `"read_message_rows"`,
/// `"read_by_id_rows"`, `"read_host_feed_rows"`.
pub required_critical_actions: HashSet<String>,
}
impl Scenario {
/// Build a scenario with no required actions — used for traces where
/// the only thing being asserted is "every observation is consistent
/// with non-interference". Most ingest fixtures need explicit
/// requirements; this helper is for replays of unstructured traffic.
pub fn unstructured(trace: Vec<TraceStep>) -> Self {
Self {
trace,
required_critical_actions: HashSet::new(),
}
}
/// Builder helper: add a required critical action kind. Returns self
/// for chaining.
pub fn require(mut self, kind: &str) -> Self {
self.required_critical_actions.insert(kind.to_string());
self
}
}
/// Check one scenario. Returns `Ok(())` on conformance; returns the first
/// transition error on any failure.
///
/// Stages:
/// 1. **Bootstrap.** Read the first step's `state_after` as the model.
/// A trace with zero steps fails as a coverage breach (the seam was
/// reached and emitted nothing).
/// 2. **Schema-version check.** Each step's `schema_version` must equal
/// [`crate::SCHEMA_VERSION`] — a divergence means the relay and the
/// checker speak different schemas. We treat that as an illegal
/// transition because no transition rule applies.
/// 3. **Per-step transition check.** [`check_step`] runs on each step.
/// 4. **Coverage check.** After all steps pass, every entry in
/// `required_critical_actions` must appear in the trace.
pub fn check_trace(scenario: &Scenario) -> Result<(), TransitionError> {
if scenario.trace.is_empty() {
return Err(TransitionError::CoverageBreach {
detail: "trace is empty — seam reached without emitting any action; \
this is the no-trace coverage breach"
.to_string(),
});
}
let first = &scenario.trace[0];
if first.schema_version != crate::SCHEMA_VERSION {
return Err(TransitionError::IllegalTransition {
step_index: 0,
detail: format!(
"trace schema_version={} but checker schema_version={}",
first.schema_version,
crate::SCHEMA_VERSION
),
});
}
let model = ModelState::bootstrap(&first.state_after);
for (i, step) in scenario.trace.iter().enumerate() {
if step.schema_version != crate::SCHEMA_VERSION {
return Err(TransitionError::IllegalTransition {
step_index: i,
detail: format!(
"trace schema_version={} but checker schema_version={}",
step.schema_version,
crate::SCHEMA_VERSION
),
});
}
check_step(i, &model, step)?;
}
// Coverage breach: required actions missing.
let mut seen: HashSet<String> = HashSet::with_capacity(scenario.trace.len());
for step in &scenario.trace {
seen.insert(step.action.kind().to_string());
}
let missing: Vec<&String> = scenario
.required_critical_actions
.iter()
.filter(|k| !seen.contains(*k))
.collect();
if !missing.is_empty() {
let mut sorted: Vec<&&String> = missing.iter().collect();
sorted.sort();
return Err(TransitionError::CoverageBreach {
detail: format!(
"scenario required actions never emitted: {:?}",
sorted.iter().map(|s| s.as_str()).collect::<Vec<_>>()
),
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::CommunityLabel;
use crate::{
AbstractState, ActorLabel, ChannelLabel, HostLabel, OpaqueId, SanitizedReason, TraceAction,
Verdict,
};
use uuid::Uuid;
fn cid(u: u128) -> CommunityLabel {
CommunityLabel::from_uuid(Uuid::from_u128(u))
}
fn ch(u: u128) -> ChannelLabel {
ChannelLabel(Uuid::from_u128(u))
}
fn state(c: CommunityLabel) -> AbstractState {
AbstractState {
resolved_community: c,
bound_host: HostLabel("h_local".into()),
actor: ActorLabel("a_alice".into()),
}
}
fn step(action: TraceAction, c: CommunityLabel) -> TraceStep {
TraceStep::new(action, state(c))
}
#[test]
fn empty_trace_is_coverage_breach() {
let sc = Scenario::unstructured(vec![]);
let err = check_trace(&sc).unwrap_err();
assert!(matches!(err, TransitionError::CoverageBreach { .. }));
}
#[test]
fn write_insert_then_read_with_only_resolved_rows_passes() {
let c = cid(1);
let trace = vec![
step(
TraceAction::AuthCheck {
channel: ch(10),
claimed_community: Some(c),
verdict: Verdict::Allow,
},
c,
),
step(
TraceAction::WriteInsert {
msg_id: OpaqueId("m1".into()),
channel: ch(10),
claimed_community: Some(c),
},
c,
),
step(
TraceAction::ReadMessageRows {
channel: Some(ch(10)),
row_communities: vec![c, c],
},
c,
),
];
let sc = Scenario {
trace,
required_critical_actions: ["auth_check", "write_insert", "read_message_rows"]
.iter()
.map(|s| s.to_string())
.collect(),
};
check_trace(&sc).expect("trace should conform");
}
#[test]
fn cross_community_row_bites_non_interference() {
let c = cid(1);
let foreign = cid(2);
let trace = vec![step(
TraceAction::ReadMessageRows {
channel: Some(ch(10)),
row_communities: vec![c, foreign],
},
c,
)];
let err = check_trace(&Scenario::unstructured(trace)).unwrap_err();
assert!(
matches!(err, TransitionError::NonInterference { .. }),
"expected NonInterference, got {err:?}"
);
}
#[test]
fn auth_allow_with_foreign_claim_bites_m2() {
let c = cid(1);
let foreign = cid(2);
let trace = vec![step(
TraceAction::AuthCheck {
channel: ch(10),
claimed_community: Some(foreign),
verdict: Verdict::Allow,
},
c,
)];
let err = check_trace(&Scenario::unstructured(trace)).unwrap_err();
assert!(
matches!(err, TransitionError::IllegalTransition { .. }),
"expected IllegalTransition for M2 bite, got {err:?}"
);
}
#[test]
fn auth_deny_with_foreign_claim_is_fine() {
let c = cid(1);
let foreign = cid(2);
let trace = vec![step(
TraceAction::AuthCheck {
channel: ch(10),
claimed_community: Some(foreign),
verdict: Verdict::Deny,
},
c,
)];
check_trace(&Scenario::unstructured(trace)).expect("deny with foreign claim is in-spec");
}
#[test]
fn state_after_changing_mid_request_is_state_mismatch() {
let c1 = cid(1);
let c2 = cid(2);
let trace = vec![
step(
TraceAction::AuthCheck {
channel: ch(10),
claimed_community: Some(c1),
verdict: Verdict::Allow,
},
c1,
),
step(
TraceAction::ReadMessageRows {
channel: Some(ch(10)),
row_communities: vec![c2],
},
c2,
),
];
let err = check_trace(&Scenario::unstructured(trace)).unwrap_err();
assert!(
matches!(err, TransitionError::StateMismatch { .. }),
"expected StateMismatch, got {err:?}"
);
}
#[test]
fn impl_bug_action_bites_coverage_breach() {
let c = cid(1);
let trace = vec![step(
TraceAction::ImplBug {
kind: "ingest_exited_without_trace".into(),
},
c,
)];
let err = check_trace(&Scenario::unstructured(trace)).unwrap_err();
assert!(
matches!(err, TransitionError::CoverageBreach { .. }),
"expected CoverageBreach from ImplBug, got {err:?}"
);
}
#[test]
fn required_critical_action_missing_bites_coverage_breach() {
let c = cid(1);
let trace = vec![step(
TraceAction::SanitizedError {
reason: SanitizedReason::Restricted,
},
c,
)];
let sc = Scenario {
trace,
required_critical_actions: ["auth_check".to_string()].into_iter().collect(),
};
let err = check_trace(&sc).unwrap_err();
assert!(
matches!(err, TransitionError::CoverageBreach { ref detail } if detail.contains("auth_check")),
"expected CoverageBreach naming auth_check, got {err:?}"
);
}
#[test]
fn sanitized_error_alone_is_well_formed() {
let c = cid(1);
for reason in [
SanitizedReason::Restricted,
SanitizedReason::Invalid,
SanitizedReason::ServerError,
] {
let trace = vec![step(TraceAction::SanitizedError { reason }, c)];
check_trace(&Scenario::unstructured(trace)).expect("sanitized_error alone is in-spec");
}
}
}
+353
View File
@@ -0,0 +1,353 @@
//! Runtime trace schema + independent replay checker for
//! `docs/spec/MultiTenantRelay.tla`.
//!
//! North star (from the runtime-formal-compliance skill): don't ask "did the
//! model pass"; ask "did the running code emit a trace the model accepts."
//!
//! ## What this crate is
//!
//! - The **schema** ([`TraceStep`], [`TraceAction`], [`AbstractState`]) that
//! the relay emits at its ingest/read accept-reject boundary.
//! - An **independent** replay checker ([`check_trace`]) that consumes a
//! sequence of `TraceStep`s and validates them against the TLA+ spec's
//! `Next` transition relation. The checker re-implements the relevant
//! spec actions in Rust; it does NOT call any production reducer.
//!
//! ## What this crate is NOT
//!
//! - A proof. Trace conformance only checks executions you ran. Coverage is
//! widened by integration tests, property tests, and adversarial fixtures.
//! - A re-export of production helpers. Sharing normalization helpers between
//! the emitter (which projects implementation state) and the checker (which
//! judges that projection) would let a bug in the helpers hide itself from
//! both — exactly the failure the skill calls out.
//!
//! ## Failure modes (skill §Phase 4)
//!
//! - **Illegal transition** — the traced action is not allowed from the
//! checker's current model state.
//! - **State mismatch** — `state_after.row_labels` includes a community other
//! than the resolved tenant (`Inv_NonInterference`).
//! - **Coverage breach** — an unknown critical action, a critical seam exit
//! without a trace step ([`TraceAction::ImplBug`]), or a scenario-required
//! action that never appeared.
//!
//! Coverage breach is load-bearing. Without it, trace conformance is
//! decorative logging.
#![deny(unsafe_code)]
#![warn(missing_docs)]
pub mod checker;
pub mod transitions;
use serde::{Deserialize, Serialize};
/// Opaque community label — the underlying UUID a server-resolved
/// `TenantContext::community()` wraps, carried as a value type in the
/// trace schema.
///
/// This deliberately does NOT reuse `buzz_core::CommunityId`. Two reasons:
///
/// 1. **Production fence preservation.** `buzz_core::CommunityId` has no
/// `From<Uuid>`, no `Serialize`, no `Deserialize` — by design, so a
/// `CommunityId` cannot be conjured from client input. Adding Serde to
/// it for our convenience would punch a hole in that fence. Carrying
/// our own newtype keeps that fence intact.
/// 2. **Independence.** The checker re-implements the spec transition
/// relation; the schema sharing zero type machinery with production
/// means a buggy production type cannot launder its bug into the
/// checker mechanically.
///
/// The relay's emitter module converts at the seam:
/// `CommunityLabel::from_uuid(*tenant.community().as_uuid())`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct CommunityLabel(pub uuid::Uuid);
impl CommunityLabel {
/// Wrap a UUID into a community label. Unlike `buzz_core::CommunityId`
/// this conversion IS public — but consumers of `CommunityLabel` are
/// the checker and test fixtures, not the relay's request path. The
/// relay only constructs `CommunityLabel` from a `TenantContext` it
/// already resolved.
pub const fn from_uuid(id: uuid::Uuid) -> Self {
Self(id)
}
}
impl std::fmt::Display for CommunityLabel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
/// Trace schema version. Bump on any backwards-incompatible field change.
pub const SCHEMA_VERSION: u32 = 1;
/// An opaque ID derived from an event id or other secret material. Stable,
/// no payload, no key bytes. Implementations pick a hash; the checker
/// compares strings.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct OpaqueId(pub String);
/// An opaque host label — produced by the relay from the bound `Host` header
/// via a configured registry, never the raw `Host` string. Mirrors the spec's
/// `Hosts` set abstractly.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct HostLabel(pub String);
/// An opaque channel label — the channel UUID directly. Channels are not
/// secret; the production code already exposes them in event tags.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ChannelLabel(pub uuid::Uuid);
/// An opaque actor label — the lower 16 bytes of `blake3(pubkey)`. Stable,
/// non-reversible, secret-free.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ActorLabel(pub String);
/// Auth verdict — the closed alphabet from `AuthCheck` (spec line 794).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Verdict {
/// Authorized.
Allow,
/// Denied. The spec models a single Deny verdict; reason is not exposed
/// at the trace boundary because the spec's error alphabet is closed.
Deny,
}
/// The sanitized error alphabet (spec `Inv_SanitizedErrors`, M6 mutation).
///
/// Errors observed by the client must come from this closed set; raw error
/// strings are NOT projected into the trace because the spec requires error
/// observations carry no tenant-derived information.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SanitizedReason {
/// Host/channel/community fence rejected the request (relay-only kind,
/// archived channel, scope-token mismatch, etc.) — spec "restricted".
Restricted,
/// Malformed event — spec "invalid".
Invalid,
/// Server fault — spec "server_error".
ServerError,
}
/// The abstract state mirrored from `TenantContext`: which community the
/// server resolved, which host bound that resolution. This is what
/// `Inv_NonInterference` checks observations against.
///
/// Carries deliberately the things that reveal violations (claimed vs.
/// resolved community, opaque host) and deliberately not raw payloads,
/// pubkey bytes, signatures, or wall-clock timestamps.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AbstractState {
/// The server-resolved community for this request — the label
/// `Inv_NonInterference` validates against. Sourced **only** from
/// `TenantContext::community()`. Never from event tags, never from
/// client input, never from `event.pubkey`.
pub resolved_community: CommunityLabel,
/// The host that bound this request to that community. Sourced from
/// `TenantContext::host()` via a label registry.
pub bound_host: HostLabel,
/// The actor (authenticated pubkey) for this request, opaque-labelled.
pub actor: ActorLabel,
}
/// One trace step emitted at the ingest/read accept-reject boundary.
///
/// Action vocabulary (spec actions in parentheses):
/// - [`TraceAction::WriteInsert`] (spec `WriteInsert`, lines 514550)
/// - [`TraceAction::WriteInsertGlobal`] (spec `WriteInsertGlobal`, lines 559595)
/// - [`TraceAction::WriteDuplicate`] (spec `WriteDuplicate`, lines 606637)
/// - [`TraceAction::SanitizedError`] (spec `SanitizedError`, line 778)
/// - [`TraceAction::AuthCheck`] (spec `AuthCheck`, line 794) — M2/M8 target
/// - [`TraceAction::ReadMessageRows`] (spec `ReadMessageRows`, line 643)
/// - [`TraceAction::ReadByIdRows`] (spec `ReadByIdRows`, line 681)
/// - [`TraceAction::ReadHostFeedRows`] (spec `ReadHostFeedRows`, line ~720)
/// - [`TraceAction::ImplBug`] — emitted by the coverage-breach guard when
/// the seam exits without a known action; the checker treats this as a
/// coverage breach.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum TraceAction {
/// Channel-bearing write (spec `WriteInsert`).
WriteInsert {
/// Opaque hash of the event id.
msg_id: OpaqueId,
/// The channel the event targets — the "real" community is
/// `ChannelCommunity(channel)` per spec.
channel: ChannelLabel,
/// The community the client *claimed* via its `h` tag, if any.
/// `None` means the client did not assert one. This stays distinct
/// from `state_after.resolved_community` so M2/M8 mutations are
/// visible in the trace.
claimed_community: Option<CommunityLabel>,
},
/// Channel-less write resolved purely from the bound host
/// (spec `WriteInsertGlobal`).
WriteInsertGlobal {
/// Opaque hash of the event id.
msg_id: OpaqueId,
/// The community the client *claimed*, if any. Ignored by the
/// resolver but recorded for the audit trail.
claimed_community: Option<CommunityLabel>,
},
/// Channel-bearing duplicate / no-op write (spec `WriteDuplicate`,
/// `ON CONFLICT (community_id, id)` returning a duplicate result).
WriteDuplicate {
/// Opaque hash of the event id.
msg_id: OpaqueId,
/// The channel the duplicate hit.
channel: ChannelLabel,
/// The community the client *claimed*, if any.
claimed_community: Option<CommunityLabel>,
},
/// Sanitized error (spec `SanitizedError`). Closed-alphabet reason
/// only; no raw error string is projected.
SanitizedError {
/// One of the closed-alphabet reasons.
reason: SanitizedReason,
},
/// Per-(channel, actor) authorization decision (spec `AuthCheck`).
/// M2 and M8 explicitly target this action — leaving it out would
/// make the gate blind to those mutations.
AuthCheck {
/// The channel the check is against.
channel: ChannelLabel,
/// The community the client claimed, if any.
claimed_community: Option<CommunityLabel>,
/// The Allow/Deny verdict the implementation produced.
verdict: Verdict,
},
/// Per-channel-or-channelless row read returning concrete rows
/// (spec `ReadMessageRows`).
ReadMessageRows {
/// Channel filter — `None` means channel-less.
channel: Option<ChannelLabel>,
/// The community label of EACH row returned. NOT deduped to a Set,
/// NOT filtered to "matches resolved": the checker must see every
/// leaked label to fail closed on `Inv_ReadConfinement` / M1/M4/M7.
row_communities: Vec<CommunityLabel>,
},
/// Direct read by event id list (spec `ReadByIdRows`). The search lane
/// emits this for each refetched hit.
ReadByIdRows {
/// Channel filter — `None` means channel-less.
channel: Option<ChannelLabel>,
/// Per-row community labels, same rules as `ReadMessageRows`.
row_communities: Vec<CommunityLabel>,
},
/// Kinds-only feed read (spec `ReadHostFeedRows`). The relay derives
/// the community from the bound host and fans out across that
/// community's channel-less rows plus its accessible channels.
ReadHostFeedRows {
/// Per-row community labels.
row_communities: Vec<CommunityLabel>,
},
/// Coverage-breach guard: the seam exited without a known action. The
/// checker treats this as a coverage breach and fails closed.
ImplBug {
/// A short tag identifying the missing emit site (e.g.
/// `"ingest_exited_without_trace"`).
kind: String,
},
}
impl TraceAction {
/// A short stable string identifying the action kind, for fixture
/// declarations and error messages.
pub fn kind(&self) -> &'static str {
match self {
TraceAction::WriteInsert { .. } => "write_insert",
TraceAction::WriteInsertGlobal { .. } => "write_insert_global",
TraceAction::WriteDuplicate { .. } => "write_duplicate",
TraceAction::SanitizedError { .. } => "sanitized_error",
TraceAction::AuthCheck { .. } => "auth_check",
TraceAction::ReadMessageRows { .. } => "read_message_rows",
TraceAction::ReadByIdRows { .. } => "read_by_id_rows",
TraceAction::ReadHostFeedRows { .. } => "read_host_feed_rows",
TraceAction::ImplBug { .. } => "impl_bug",
}
}
/// Every action at this seam is critical: the spec requires every
/// observation to be labelled. The skill's "coverage breach" mode
/// hinges on every emit site being marked critical.
pub const fn is_critical(&self) -> bool {
true
}
}
/// One step in the trace stream.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TraceStep {
/// Schema version — bump on backwards-incompatible field changes.
pub schema_version: u32,
/// The action that occurred at the seam.
pub action: TraceAction,
/// The abstract state the implementation observed at action time.
/// The checker compares this against its independently-computed model
/// state.
pub state_after: AbstractState,
}
impl TraceStep {
/// Build a step at the current schema version.
pub fn new(action: TraceAction, state_after: AbstractState) -> Self {
Self {
schema_version: SCHEMA_VERSION,
action,
state_after,
}
}
}
/// The emit trait the relay calls. The trait is the *only* surface the
/// production code touches; the schema types stay value types.
pub trait Tracer: Send + Sync {
/// Record one trace step. Implementations MAY be no-ops in production
/// builds and write to JSONL in tests.
fn record(&self, step: TraceStep);
/// Whether recorded steps are actually observed.
///
/// Emitters on hot paths MUST consult this before doing work whose
/// *only* consumer is the trace — most importantly extra database
/// reads that project row labels independently of the fetch query
/// (the read-seam's `communities_of_channels` lookup). With a
/// discarding tracer that work is pure overhead.
///
/// This is the `log.isDebugEnabled()` of the trace seam. It exists to
/// let callers skip *building emit inputs*, never to let them skip an
/// emit they would otherwise have made: when this returns `true`
/// every seam must behave exactly as it did before the gate existed,
/// so the coverage-breach guard stays non-vacuous.
///
/// Defaults to `true` — a new tracer is assumed to observe steps until
/// it says otherwise. Wrappers that delegate to an inner tracer MUST
/// forward this method rather than inherit the default.
fn enabled(&self) -> bool {
true
}
}
/// A no-op tracer for production. Zero cost: the build can omit emission
/// entirely behind a feature, or simply discard records here.
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopTracer;
impl Tracer for NoopTracer {
fn record(&self, _step: TraceStep) {}
/// Nothing is observed, so emitters should skip building inputs.
fn enabled(&self) -> bool {
false
}
}
+330
View File
@@ -0,0 +1,330 @@
//! Independent translation of `docs/spec/MultiTenantRelay.tla`'s `Next`
//! transition relation into Rust.
//!
//! This module is the heart of the conformance gate. It is deliberately
//! **independent** of the production reducer: it reads only the trace
//! schema in [`crate`] and the spec text in `docs/spec/MultiTenantRelay.tla`.
//! It does not import `buzz-relay`, `buzz-db`, `buzz-auth`, or any other
//! production crate that could share a normalization bug with the emitter.
//!
//! ## What an "abstract state" means here
//!
//! The TLA+ spec models the relay as a multi-worker system whose state is
//! the set of accepted rows, projection rows, observations, etc. A runtime
//! trace covers ONE worker handling ONE request — so the model state we
//! carry is much smaller:
//!
//! - `resolved_community` — the server-resolved `TenantContext::community()`
//! for this request. `Inv_NonInterference` requires every row label
//! observed in this request be a subset of `{resolved_community}`.
//! - `bound_host` — the host label `TenantContext::host()` was bound from.
//! `AuthCheck` / channel-less reads require `HostCommunity[host]` agree
//! with the resolved community.
//!
//! The checker rebuilds this state independently from the FIRST trace step
//! it sees and then validates every subsequent step against it.
//!
//! ## Per-action obligations
//!
//! Each action has a triple of obligations distilled from the spec:
//!
//! 1. **State match.** `step.state_after.resolved_community` and
//! `bound_host` agree with the checker's running model (no mid-request
//! tenant flip).
//! 2. **Row-label confinement** (`Inv_NonInterference` line ~983,
//! `Inv_ReadConfinement` line ~1003). Every `row_communities` entry,
//! every accept label, must equal `resolved_community`. A single foreign
//! label fails the trace.
//! 3. **Action-specific guards.** AuthCheck `Allow` requires host/channel
//! agreement; channel-less reads require `HostCommunity[host] = c`;
//! `WriteInsert` claim-vs-resolved is recorded but a mismatch is
//! allowed at the abstract level — the spec ignores it ("host wins"),
//! so the gate that bites mismatches is the row-label confinement on
//! the *next* read.
use crate::{
AbstractState, ChannelLabel, CommunityLabel, SanitizedReason, TraceAction, TraceStep, Verdict,
};
/// A judgment about a single trace step. The checker walks the trace and
/// returns the first failure verdict (fail-fast); per the skill's "fail
/// closed on the first violation" guidance.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Verdict_ {
/// Reserved — internal placeholder.
Ok,
}
/// Failure reasons returned by [`check_step`]. The string payload is
/// human-readable; mechanical consumers should match on the variant.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum TransitionError {
/// The traced action is not allowed from the checker's current model
/// state — e.g. an `AuthCheck { verdict: Allow }` with `claimed != real`.
#[error("illegal transition at step {step_index}: {detail}")]
IllegalTransition {
/// 0-based index of the offending step.
step_index: usize,
/// Human-readable detail.
detail: String,
},
/// The trace's `state_after` does not match the model state the checker
/// computed independently. Indicates the relay either reassigned the
/// tenant context mid-request, or emitted a step from a context other
/// than `TenantContext`.
#[error("state mismatch at step {step_index}: {detail}")]
StateMismatch {
/// 0-based index of the offending step.
step_index: usize,
/// Human-readable detail.
detail: String,
},
/// Row labels include a community other than the resolved tenant —
/// the master `Inv_NonInterference` failure.
#[error("non-interference breach at step {step_index}: {detail}")]
NonInterference {
/// 0-based index of the offending step.
step_index: usize,
/// Human-readable detail.
detail: String,
},
/// A coverage breach: ImplBug action, or a fixture-declared
/// `required_critical_actions` entry never appeared.
#[error("coverage breach: {detail}")]
CoverageBreach {
/// Human-readable detail naming the missing or broken coverage rule.
detail: String,
},
}
/// The model state the checker carries between steps.
#[derive(Debug, Clone)]
pub struct ModelState {
/// The community the FIRST step's `state_after` told us was resolved.
/// Subsequent steps must agree.
pub resolved_community: CommunityLabel,
/// The host the FIRST step's `state_after` told us was bound. Channel-
/// bearing AuthCheck and channel-less reads enforce
/// `host_community(host) == resolved_community`. The checker does NOT
/// know `HostCommunity[_]` at large; it only knows the spec guarantees
/// `HostCommunity[bound_host] = resolved_community` whenever the relay
/// took the success branch.
pub bound_host: crate::HostLabel,
/// The actor for this request — opaque, equality-checked only.
pub actor: crate::ActorLabel,
}
impl ModelState {
/// Bootstrap the model from the very first step. Subsequent calls to
/// [`check_step`] return a `StateMismatch` if `state_after` disagrees.
pub fn bootstrap(first: &AbstractState) -> Self {
Self {
resolved_community: first.resolved_community,
bound_host: first.bound_host.clone(),
actor: first.actor.clone(),
}
}
}
/// Validate one step against the model. Updates nothing (the model is
/// immutable for the lifetime of a single trace); a violation returns the
/// matching [`TransitionError`].
///
/// Spec line numbers below refer to `docs/spec/MultiTenantRelay.tla` at the
/// snapshot pinned in this PR's `docs/spec/`.
pub fn check_step(
step_index: usize,
model: &ModelState,
step: &TraceStep,
) -> Result<(), TransitionError> {
// Universal obligation 1: state_after agrees with the bootstrapped model.
if step.state_after.resolved_community != model.resolved_community {
return Err(TransitionError::StateMismatch {
step_index,
detail: format!(
"resolved_community changed mid-request: bootstrap={:?}, step={:?}",
model.resolved_community, step.state_after.resolved_community
),
});
}
if step.state_after.bound_host != model.bound_host {
return Err(TransitionError::StateMismatch {
step_index,
detail: format!(
"bound_host changed mid-request: bootstrap={:?}, step={:?}",
model.bound_host, step.state_after.bound_host
),
});
}
if step.state_after.actor != model.actor {
return Err(TransitionError::StateMismatch {
step_index,
detail: format!(
"actor changed mid-request: bootstrap={:?}, step={:?}",
model.actor, step.state_after.actor
),
});
}
// Action-specific obligations.
match &step.action {
// --- Spec WriteInsert (lines 514-550) ---
// Resolution: real == ChannelCommunity(ch).
// Success branch requires HostCommunity[host] = real, which the
// emitter guarantees by emitting from inside the success path with
// state_after.resolved_community = real and state_after.bound_host
// = the bound host. The trace records claimed_community separately
// so M2/M8 (host/channel disagreement, claim≠resolved) surface here
// as a state mismatch on the *resolved* side.
//
// What we check at this step: nothing beyond the universal state
// match. The spec ignores claimed_community ("host wins"), so a
// mismatch is allowed at this exact action — the gate that bites
// it is the next read's row labels.
TraceAction::WriteInsert { .. } => Ok(()),
// --- Spec WriteInsertGlobal (lines 559-595) ---
// resolved == HostCommunity[host]. Same shape as WriteInsert.
TraceAction::WriteInsertGlobal { .. } => Ok(()),
// --- Spec WriteDuplicate (lines 606-637) ---
// Carries the same host-axis obligation as WriteInsert: an A-host
// presenting a B-channel id must not learn whether the id exists.
// Same observable: state_after.resolved_community must be the
// real ChannelCommunity(ch), enforced by the universal check.
TraceAction::WriteDuplicate { .. } => Ok(()),
// --- Spec SanitizedError (line 778) ---
// Closed-alphabet reason; labels = {}; carries no row data. The
// emitter must collapse every reject path into one of the three
// SanitizedReason variants. The schema-level type system already
// enforces that — we just check the variant is among the spec's
// closed set (trivially true by construction).
TraceAction::SanitizedError { reason } => match reason {
SanitizedReason::Restricted
| SanitizedReason::Invalid
| SanitizedReason::ServerError => Ok(()),
},
// --- Spec AuthCheck (lines 794-810) ---
// real == ChannelCommunity(ch).
// hostAgrees == real ∈ Communities ∧ HostCommunity[host] = real.
// allowed == hostAgrees ∧ ch ∈ ScopedAccessible(real, a).
// verdict == IF allowed THEN Allow ELSE Deny.
//
// The runtime checker cannot recompute ScopedAccessible (that's
// production state). What it CAN check: when verdict = Allow, the
// claimed_community MUST equal resolved_community. This is the
// M2 bite ("auth verdict driven by claimed instead of resolved")
// and the M8 bite ("A-host driving a B-channel verdict") — both
// collapse to "Allow with a foreign label leak".
//
// We deliberately do NOT bite Deny on claim mismatch (Deny with
// any claim is in-spec — the spec models Deny as the catch-all
// for hostAgrees=false or accessibility=false).
TraceAction::AuthCheck {
channel: _,
claimed_community,
verdict,
} => match (verdict, claimed_community) {
(Verdict::Allow, Some(c)) if c != &model.resolved_community => {
Err(TransitionError::IllegalTransition {
step_index,
detail: format!(
"AuthCheck verdict=Allow with claimed_community={:?} != resolved={:?} \
— M2/M8 (claim or host driving verdict) bite",
c, model.resolved_community
),
})
}
_ => Ok(()),
},
// --- Spec ReadMessageRows (line 643) / ReadByIdRows (line 681) ---
// The action emits rows; `RowLabels(rows)` is the observation's
// labels and Inv_NonInterference requires labels ⊆ {community}.
// For channel-less (ch = NoChannel) the spec ADDS:
// HostCommunity[host] = c ∧ IsAdmitted(c, a).
// The host-agreement piece is enforced at the universal check
// (state_after.resolved_community is host-derived); IsAdmitted is
// production state we cannot recompute, so it lives in fixture
// assertions rather than this generic checker.
//
// What this checker bites: every row label must equal the
// resolved community. ONE foreign label fails NI.
TraceAction::ReadMessageRows {
channel: _,
row_communities,
}
| TraceAction::ReadByIdRows {
channel: _,
row_communities,
} => check_row_labels(step_index, model, row_communities),
// --- Spec ReadHostFeedRows (line ~720) ---
// Community is host-derived; same row-label confinement.
TraceAction::ReadHostFeedRows { row_communities } => {
check_row_labels(step_index, model, row_communities)
}
// --- Coverage breach via the Drop guard ---
// The seam exited without emitting any recognized action.
// Per the skill: this is the load-bearing coverage mode — without
// it, trace conformance is decorative logging.
TraceAction::ImplBug { kind } => Err(TransitionError::CoverageBreach {
detail: format!("ImplBug action emitted by Drop guard: kind={kind:?}"),
}),
}
}
/// Row-label confinement check shared by all three read actions.
///
/// `Inv_NonInterference` (spec line ~983):
/// `\A o \in observations : o.labels \subseteq {o.community}`.
///
/// Translated: every `row_communities` entry must equal `model
/// .resolved_community`. The check is on a `Vec`, not a `Set`, deliberately
/// — if a buggy relay returned the same foreign row twice the checker still
/// bites, and if a buggy emitter de-duped foreign labels to one occurrence
/// the checker still bites. Foreign-label count is unimportant; foreign-
/// label presence is the entire bar.
fn check_row_labels(
step_index: usize,
model: &ModelState,
row_communities: &[CommunityLabel],
) -> Result<(), TransitionError> {
if let Some(foreign) = row_communities
.iter()
.find(|c| **c != model.resolved_community)
{
return Err(TransitionError::NonInterference {
step_index,
detail: format!(
"row labeled {:?} returned in observation scoped to {:?} \
— Inv_NonInterference breach (foreign row leaked through tenant fence)",
foreign, model.resolved_community
),
});
}
Ok(())
}
/// Helper: which channel (if any) does the action target? Used by the
/// checker to bind cross-step claims to a stable channel — and by fixtures
/// asserting that a particular channel surfaced at this seam.
pub fn action_channel(action: &TraceAction) -> Option<&ChannelLabel> {
match action {
TraceAction::WriteInsert { channel, .. } => Some(channel),
TraceAction::WriteDuplicate { channel, .. } => Some(channel),
TraceAction::AuthCheck { channel, .. } => Some(channel),
TraceAction::ReadMessageRows { channel, .. } => channel.as_ref(),
TraceAction::ReadByIdRows { channel, .. } => channel.as_ref(),
TraceAction::WriteInsertGlobal { .. }
| TraceAction::ReadHostFeedRows { .. }
| TraceAction::SanitizedError { .. }
| TraceAction::ImplBug { .. } => None,
}
}