alkgit 0.1.0

Git smart protocol for the alk family: pkt-line substrates, protocol V2 serving (upload-pack/receive-pack), producer/consumer halves on alkcall channels, and the backend seam traits — the shipped alkstore+alkblobs composition is the alkgit-substrate crate (ADR-010/025)
Documentation
//! The bounded-resources budget model (ADR-009, as amended by ADR-020
//! §1 and by the 2026-10-23 ingestion decompressed-size cap).
//!
//! Every session carries a [`Limits`] value: constructed by the
//! door/adapter from assembler config and handed to the wire layer as
//! part of the session tuple (ADR-002) — the transport refuses to
//! start a session without it, so no adapter can forget a budget. The
//! defaults are crate constants ([`Limits::default()`]); overrides are
//! assembler config.
//!
//! Breach behavior (ADR-009): budgets are **fail-closed** — the session
//! ends with a substrate-appropriate error. On duplex, a pkt-line error
//! band + close; on http, *client-fault* budgets (request body size)
//! map to 413 and *server/session* budgets (wall clock, negotiation
//! rounds, blocking-concurrency exhaustion) map to 503. This type only
//! carries the budgets; enforcement lives in the later waves' machines.
//!
//! ADR-009's sideband chunk size is deliberately absent: it is fixed
//! per protocol at [`SIDEBAND_CHUNK_SIZE`], not a per-session budget.

use std::time::Duration;

/// Maximum negotiation rounds on fetch (ADR-009): protocol V2
/// negotiation without `done`.
pub const MAX_NEGOTIATION_ROUNDS: usize = 32;

/// Maximum haves per negotiation round (ADR-009). The client's
/// stateless doubling legitimately reaches 16384, so the default sits
/// at or above it.
pub const MAX_HAVES_PER_ROUND: usize = 16384;

/// Maximum receive-pack size, in bytes (ADR-009: config-bound, tens of
/// MB in v1; receive-pack is the untrusted-input path and carries the
/// hard cap — fetch's pack size is a function of the repo, not the
/// request, and is not budgeted in v1).
pub const MAX_PACK_SIZE_BYTES: u64 = 64 * 1024 * 1024;

/// Maximum http POST request-body size, in bytes (ADR-009: the same
/// budget as [`MAX_PACK_SIZE_BYTES`]).
pub const MAX_REQUEST_BODY_BYTES: u64 = MAX_PACK_SIZE_BYTES;

/// Maximum decompressed size of any single object produced by pack
/// ingestion, in bytes (the ADR-009 decompressed-size cap, review 004
/// R-2's pre-publish booking): each inflated pack-entry payload and
/// each delta's materialized result stays under it by *incremental*
/// reading — nothing in the unpack path ever sizes an allocation from
/// the pack's own declaration (the size-bomb class: a hostile pack
/// within the wire budget can declare an entry whose decompressed size
/// is arbitrarily large; a pre-allocation of that declaration is an
/// allocation-failure abort, not an error path). The default is git's
/// `core.bigFileThreshold` datum (512 MB) — large blobs pass, the
/// bomb-shaped declarations stop.
pub const MAX_OBJECT_SIZE_BYTES: u64 = 512 * 1024 * 1024;

/// Session wall-clock budget (ADR-009: all sessions, tens of minutes —
/// enforced by transport's session loop on every door; alkcall channel
/// caps add a second bound where channels exist).
pub const SESSION_WALL_CLOCK: Duration = Duration::from_secs(30 * 60);

/// Maximum concurrent blocking pipeline tasks, server-wide (ADR-009:
/// the blocking-pool budget — pack generation and pack ingestion alike
/// are `spawn_blocking` consumers; enforced at acceptance time, not
/// per-byte).
pub const MAX_CONCURRENT_BLOCKING_TASKS: usize = 8;

/// Maximum advertisement refs in the ls-refs response (ADR-009:
/// config-bound).
pub const MAX_ADVERTISEMENT_REFS: usize = 10_000;

/// The packless-delete bounded-peek budget (ADR-020 §1): the
/// POC-tested 750 ms — a fast push's pack header is immediate, and a
/// packless client is already waiting.
pub const PACKLESS_PEEK: Duration = Duration::from_millis(750);

/// Sideband chunk size, in bytes (ADR-009): **fixed per protocol at
/// 65000** — not a [`Limits`] field. The substrate task consumes it.
pub const SIDEBAND_CHUNK_SIZE: usize = 65000;

/// The per-session budget bundle (ADR-009 as amended by ADR-020 §1).
///
/// Constructed by the door/adapter from assembler config, carried by
/// every session (ADR-002's tuple). Fields are [`Duration`] for
/// wall-clock budgets, `u64` for sizes, `usize` for counts — see the
/// crate constants for the defaults behind [`Default`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Limits {
    /// Maximum negotiation rounds on fetch (protocol V2, no `done`):
    /// tens (ADR-009).
    pub max_negotiation_rounds: usize,
    /// Maximum haves per negotiation round: thousands, at or above the
    /// client's stateless-doubling ceiling of 16384 (ADR-009).
    pub max_haves_per_round: usize,
    /// Maximum receive-pack size, in bytes: config-bound at tens of MB
    /// for v1 (ADR-009).
    pub max_pack_size: u64,
    /// Maximum http POST request-body size, in bytes: the same budget
    /// as `max_pack_size` (ADR-009). Enforced on both substrates — the
    /// stateless POST body
    /// ([`StatelessRequest::read_post`](crate::StatelessRequest::read_post))
    /// and, as its twin, each duplex command request's framed bytes
    /// ([`Session::read_request`](crate::Session::read_request)).
    pub max_request_body: u64,
    /// Maximum decompressed size of any single object produced by pack
    /// ingestion, in bytes: the pack-declaration-independent expansion
    /// cap (the ADR-009 decompressed-size cap, review 004 R-2). Each
    /// inflated pack-entry payload and each delta's materialized
    /// result incrementally reads against it; a breach is pack-level
    /// validation (`unpack ng`, ADR-013 §8), never a session error —
    /// the doubles enforce the shape, the substrate impl inherits
    /// it (the wave-8 `GitPackIngest` decomposition names the cap).
    pub max_object_size: u64,
    /// Session wall-clock budget: tens of minutes (ADR-009).
    pub session_wall_clock: Duration,
    /// Maximum concurrent blocking pipeline tasks, server-wide: a
    /// small count (ADR-009).
    pub max_concurrent_blocking_tasks: usize,
    /// Maximum advertisement refs in the ls-refs response:
    /// config-bound (ADR-009).
    pub max_advertisement_refs: usize,
    /// The bounded-peek window for a receive-pack pack header after
    /// the command flush (ADR-020 §1, amending ADR-013 §3): default
    /// 750 ms, the POC-tested value. A header within the window
    /// proceeds to ingestion; a timeout takes the packless, CAS-only
    /// path. The four ADR-020 §1 ordering rules home here, pinned so
    /// the implementer cannot improvise: (i) the optional
    /// push-options section (ADR-013 §11) is consumed *before* the
    /// peek — the peek watches only for the pack's first bytes; (ii) a
    /// timeout with **zero** pack bytes takes the packless path; (iii)
    /// a timeout with a **partial** pack-header byte string is a
    /// malformed-push session error, never a silent packless downgrade
    /// (a half-sent pack cannot be re-interpreted as absence); (iv)
    /// the packless path drains and closes cleanly afterward — the
    /// client stays open until the report (ADR-020 §3), never
    /// mid-stream RST.
    pub packless_peek: Duration,
}

impl Default for Limits {
    /// The crate constants (ADR-009's default directions, ADR-020 §1's
    /// 750 ms peek datum). See each constant for its citation.
    fn default() -> Self {
        Self {
            max_negotiation_rounds: MAX_NEGOTIATION_ROUNDS,
            max_haves_per_round: MAX_HAVES_PER_ROUND,
            max_pack_size: MAX_PACK_SIZE_BYTES,
            max_request_body: MAX_REQUEST_BODY_BYTES,
            max_object_size: MAX_OBJECT_SIZE_BYTES,
            session_wall_clock: SESSION_WALL_CLOCK,
            max_concurrent_blocking_tasks: MAX_CONCURRENT_BLOCKING_TASKS,
            max_advertisement_refs: MAX_ADVERTISEMENT_REFS,
            packless_peek: PACKLESS_PEEK,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn defaults_match_the_adr009_direction_column() {
        let limits = Limits::default();
        assert!(
            (10..=90).contains(&limits.max_negotiation_rounds),
            "max negotiation rounds is the tens direction: {:?}",
            limits.max_negotiation_rounds
        );
        assert!(
            (5 * 60..=60 * 60).contains(&limits.session_wall_clock.as_secs()),
            "session wall clock is the tens-of-minutes direction: {:?}",
            limits.session_wall_clock
        );
        assert!(
            (1..=32).contains(&limits.max_concurrent_blocking_tasks),
            "max concurrent blocking tasks is the small-count direction: {:?}",
            limits.max_concurrent_blocking_tasks
        );
        assert_eq!(
            limits.max_request_body, limits.max_pack_size,
            "max request body matches max pack size"
        );
        assert!(
            limits.max_pack_size >= 10 * 1024 * 1024 && limits.max_pack_size <= 100 * 1024 * 1024,
            "max pack size is the tens-of-MB direction: {:?}",
            limits.max_pack_size
        );
    }

    #[test]
    fn haves_default_reaches_the_stateless_doubling_ceiling() {
        assert!(
            Limits::default().max_haves_per_round >= 16384,
            "the client's stateless doubling legitimately reaches 16384"
        );
    }

    #[test]
    fn peek_default_is_the_adr020_datum() {
        assert_eq!(Limits::default().packless_peek, Duration::from_millis(750));
        assert_eq!(PACKLESS_PEEK, Duration::from_millis(750));
    }

    #[test]
    fn sideband_chunk_size_is_a_fixed_crate_constant() {
        assert_eq!(SIDEBAND_CHUNK_SIZE, 65000);
    }

    #[test]
    fn object_size_default_is_the_large_blob_datum() {
        assert!(
            (64 * 1024 * 1024..=1024 * 1024 * 1024).contains(&Limits::default().max_object_size),
            "the decompressed-size cap is the large-blob direction: above the \
             pack default, well below the bomb-shaped declarations"
        );
    }

    #[test]
    fn constants_match_their_fields_in_default() {
        let limits = Limits::default();
        assert_eq!(limits.max_negotiation_rounds, MAX_NEGOTIATION_ROUNDS);
        assert_eq!(limits.max_haves_per_round, MAX_HAVES_PER_ROUND);
        assert_eq!(limits.max_pack_size, MAX_PACK_SIZE_BYTES);
        assert_eq!(limits.max_request_body, MAX_REQUEST_BODY_BYTES);
        assert_eq!(limits.max_object_size, MAX_OBJECT_SIZE_BYTES);
        assert_eq!(limits.max_advertisement_refs, MAX_ADVERTISEMENT_REFS);
        assert_eq!(limits.session_wall_clock, SESSION_WALL_CLOCK);
        assert_eq!(
            limits.max_concurrent_blocking_tasks,
            MAX_CONCURRENT_BLOCKING_TASKS
        );
    }
}