liminal-server 0.25.4

Standalone server for the liminal messaging bus
Documentation
/// Participant lifecycle configuration (`[participant]`).
///
/// Present iff the deployment activates the participant protocol. Every field
/// is required — serde carries no defaults here, so a missing field fails
/// config loading with a typed error naming the field, and
/// [`ParticipantConfig::collect_errors`] rejects semantically impossible
/// values during the same accumulated validation pass as the rest of the
/// config. All values are deployment-owner decisions (no assumed defaults).
///
/// Every field here is consumed by the live production handler. Frontier and
/// retention limits are required inputs; there are no deployment defaults.
#[derive(Debug, Clone, Copy, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ParticipantConfig {
    /// Complete participant wire-frame limit (`WF`) negotiated with every
    /// participant-capable connection. Must be at least the protocol's
    /// minimum complete frame; enforced by the shared codec at service
    /// construction and pre-checked during config validation.
    pub wire_frame_limit: u64,
    /// Secret-bearing attach/enrollment receipt lifetime in milliseconds.
    pub attach_receipt_ttl_ms: u64,
    /// Non-secret receipt-provenance lifetime in milliseconds. Must be at
    /// least `attach_receipt_ttl_ms` (provenance explains the receipt and
    /// cannot expire first).
    pub receipt_provenance_ttl_ms: u64,
    /// Reporting threshold for server-wide live secret-bearing receipts
    /// (stage-8 shared scope `LiveReceiptServer`).
    ///
    /// Lane p0-39: this scope is NOT an admission gate. It is where an honest
    /// third party would meet a number someone else's churn consumed, and no
    /// configured refusal is tolerable there — retention is bounded by
    /// `attach_receipt_ttl_ms` alone. This number is a TRIPWIRE: when
    /// in-window occupancy reaches it, the server counts the observation and
    /// warns on the rising edge. Nothing is ever refused because of it.
    /// Setting it higher than the deployment's plausible steady state is the
    /// point; a churn storm is what it is meant to disclose.
    pub live_receipt_server_report_threshold: u64,
    /// Per-participant WINDOW SIZE for live secret-bearing receipts (stage-8
    /// scope `LiveReceiptParticipant`).
    ///
    /// Lane p0-39: this is a bound on retention, not a refusal threshold. At
    /// a full window the participant's own oldest live receipt is displaced
    /// and the new one lands, so a rotation can never be refused by the very
    /// receipt it is about to end. Occupancy never exceeds this value.
    pub max_live_attach_receipts_per_participant: u64,
    /// Reporting threshold for server-wide retained non-secret provenance
    /// fingerprints (stage-8 shared scope `ProvenanceServer`).
    ///
    /// Lane p0-39: a tripwire, never a gate — see
    /// [`Self::live_receipt_server_report_threshold`]. Retention here is
    /// bounded by `receipt_provenance_ttl_ms` alone.
    ///
    /// Board #37 (ruling 2026-08-12): a fingerprint occupies from the moment
    /// the client proves it possesses the secret its receipt minted through
    /// that receipt's own provenance deadline — not from the minting commit.
    /// A receipt whose delivery was never observed classifies through its
    /// window but is never counted here, so an enrol-and-crash cycle leaves
    /// no provenance residue behind it.
    pub receipt_provenance_server_report_threshold: u64,
    /// Reporting threshold for per-conversation provenance fingerprints
    /// (stage-8 shared scope `ProvenanceConversation`). A tripwire, never a
    /// gate: one participant's churn must not refuse the next participant of
    /// the same conversation.
    pub receipt_provenance_per_conversation_report_threshold: u64,
    /// Per-participant WINDOW SIZE for provenance fingerprints (stage-8 scope
    /// `ProvenanceParticipant`).
    ///
    /// Lane p0-39: the participant's retained fingerprints — one per
    /// committed rotation, plus its proven enrollment fingerprint — are held
    /// to this many, oldest displaced first. Per-participant pressure is
    /// self-inflicted, so the number bounds memory without refusing: the
    /// (N+1)th honest fingerprint always lands, including on cold replay from
    /// durable state. Occupancy never exceeds this value.
    pub max_receipt_provenance_per_participant: u64,
    /// Server-wide identity-slot limit (the contract's
    /// `max_retired_identity_slots` server scope): the total number of
    /// participant identities — live or retired — mintable across ALL
    /// conversations. Enrollment refuses with server-scope
    /// `IdentityCapacityExceeded` (tested BEFORE the conversation scope)
    /// when every slot is reserved.
    pub max_retired_identity_slots_server: u64,
    /// Per-CONVERSATION identity limit `I` (the contract's half-open
    /// `0..=I` bound on permanent participant ordinals — the conversation
    /// scope of `max_retired_identity_slots`, NOT a per-participant
    /// reservation). Enrollment assigns monotone participant indices in
    /// `0..I` within one conversation and refuses with conversation-scope
    /// `IdentityCapacityExceeded` when occupancy reaches this value; slots
    /// and ids are never reused. The server-wide companion is
    /// [`Self::max_retired_identity_slots_server`].
    pub identity_slots: u64,
    /// Maximum entries one observer-recovery handshake batch may name.
    pub observer_recovery_max_entries: u64,
    /// Semantic conversations one connection may track — the protocol's
    /// signed connection-conversation limit. Consumed on BOTH of its contract
    /// paths: the stage-6 capacity gate every conversation-scoped semantic
    /// operation runs (register row 5641) and the observer-recovery batch
    /// preflight (register row 5642), over one shared per-connection
    /// dispatch map.
    pub max_semantic_conversations_per_connection: u64,
    /// Maximum canonical entries in one ordinary retained-record row.
    pub max_ordinary_record_entries: u64,
    /// Maximum canonical bytes in one ordinary retained-record row.
    pub max_ordinary_record_bytes: u64,
    /// Maximum canonical entries in one generated marker row.
    pub max_generated_marker_entries: u64,
    /// Maximum canonical bytes in one generated marker row.
    pub max_generated_marker_bytes: u64,
    /// Entry component of the mandatory transaction envelope `Q`.
    pub mandatory_transaction_bound_entries: u64,
    /// Byte component of the mandatory transaction envelope `Q`.
    pub mandatory_transaction_bound_bytes: u64,
    /// Entry component of the full recovery claim `K`.
    pub full_recovery_claim_entries: u64,
    /// Byte component of the full recovery claim `K`.
    pub full_recovery_claim_bytes: u64,
    /// Total retained durable entry capacity per conversation.
    pub retained_capacity_entries: u64,
    /// Total retained canonical-byte capacity per conversation.
    pub retained_capacity_bytes: u64,
    /// Maximum retained causal-record rows restored for one conversation.
    pub max_retained_record_rows: u64,
    /// `[participant.pruning]` (DUR-018, DUR-024 R4): absent, acknowledged
    /// rows behind the newest checkpoint are cut keeping the pinned default
    /// (`DEFAULT_KEEP_ACKNOWLEDGED_BEHIND_CHECKPOINT`, 512 rows) and the
    /// server says so once at start. Present, the configured value governs.
    /// Pruning is never off.
    #[serde(default)]
    pub pruning: Option<PruningConfig>,
    /// Maximum closure churn cycles in one episode.
    pub closure_episode_churn_limit: u64,
}

impl ParticipantConfig {
    /// Accumulates semantic validation errors for the participant section.
    ///
    /// Zero is rejected wherever it would be unlimited-by-silence, gate
    /// nothing, or violate a protocol precondition; the TTL ordering mirrors
    /// the protocol's own frozen configuration precedence.
    pub(crate) fn collect_errors(&self, errors: &mut Vec<String>) {
        // The receipt/identity block follows the contract's frozen nine-field
        // validation order: both TTLs, the five receipt/provenance caps, the
        // server identity limit, then the conversation identity limit.
        let nonzero: [(&str, u64); 23] = [
            ("wire_frame_limit", self.wire_frame_limit),
            ("attach_receipt_ttl_ms", self.attach_receipt_ttl_ms),
            ("receipt_provenance_ttl_ms", self.receipt_provenance_ttl_ms),
            (
                "live_receipt_server_report_threshold",
                self.live_receipt_server_report_threshold,
            ),
            (
                "max_live_attach_receipts_per_participant",
                self.max_live_attach_receipts_per_participant,
            ),
            (
                "receipt_provenance_server_report_threshold",
                self.receipt_provenance_server_report_threshold,
            ),
            (
                "receipt_provenance_per_conversation_report_threshold",
                self.receipt_provenance_per_conversation_report_threshold,
            ),
            (
                "max_receipt_provenance_per_participant",
                self.max_receipt_provenance_per_participant,
            ),
            (
                "max_retired_identity_slots_server",
                self.max_retired_identity_slots_server,
            ),
            ("identity_slots", self.identity_slots),
            (
                "observer_recovery_max_entries",
                self.observer_recovery_max_entries,
            ),
            (
                "max_semantic_conversations_per_connection",
                self.max_semantic_conversations_per_connection,
            ),
            (
                "max_ordinary_record_entries",
                self.max_ordinary_record_entries,
            ),
            ("max_ordinary_record_bytes", self.max_ordinary_record_bytes),
            (
                "max_generated_marker_entries",
                self.max_generated_marker_entries,
            ),
            (
                "max_generated_marker_bytes",
                self.max_generated_marker_bytes,
            ),
            (
                "mandatory_transaction_bound_entries",
                self.mandatory_transaction_bound_entries,
            ),
            (
                "mandatory_transaction_bound_bytes",
                self.mandatory_transaction_bound_bytes,
            ),
            (
                "full_recovery_claim_entries",
                self.full_recovery_claim_entries,
            ),
            ("full_recovery_claim_bytes", self.full_recovery_claim_bytes),
            ("retained_capacity_entries", self.retained_capacity_entries),
            ("retained_capacity_bytes", self.retained_capacity_bytes),
            ("max_retained_record_rows", self.max_retained_record_rows),
        ];
        for (field, value) in nonzero {
            if value == 0 {
                errors.push(format!("participant.{field}: must be greater than zero"));
            }
        }
        if self.receipt_provenance_ttl_ms < self.attach_receipt_ttl_ms {
            errors.push(
                "participant.receipt_provenance_ttl_ms: must be at least \
                 attach_receipt_ttl_ms (provenance cannot expire before the receipt it explains)"
                    .to_owned(),
            );
        }
        if self.full_recovery_claim_entries != self.mandatory_transaction_bound_entries {
            errors.push(
                "participant.full_recovery_claim_entries: must equal \
                 mandatory_transaction_bound_entries"
                    .to_owned(),
            );
        }
        if self.full_recovery_claim_bytes != self.mandatory_transaction_bound_bytes {
            errors.push(
                "participant.full_recovery_claim_bytes: must equal \
                 mandatory_transaction_bound_bytes"
                    .to_owned(),
            );
        }
        if !(2..=u64::from(u32::MAX)).contains(&self.closure_episode_churn_limit) {
            errors.push(
                "participant.closure_episode_churn_limit: must be in 2..=u32::MAX".to_owned(),
            );
        }
        self.collect_unit2_derived_errors(errors);
    }

    fn collect_unit2_derived_errors(&self, errors: &mut Vec<String>) {
        if self
            .max_retained_record_rows
            .checked_mul(self.identity_slots)
            .is_none()
        {
            errors.push("participant.UNIT2_MAX_LIVE_RECIPIENT_OBLIGATIONS: max_retained_record_rows * identity_slots overflows u64".to_owned());
        }
    }
}
/// `[participant.pruning]`: the one bound pruning has (DUR-018 R1). It is a
/// bound on acknowledged rows behind a checkpoint, never on a conversation
/// log's row count or age.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PruningConfig {
    /// Acknowledged rows kept behind the newest checkpoint; everything
    /// acknowledged below them is cut, once a receipt is written.
    pub keep_acknowledged_behind_checkpoint: u64,
}