thrall 0.0.19

thrall: the foot — a tool-execution client for a yog server
Documentation
//! **The publication guard** (bl-d25a, prep for bl-006e): what `cargo publish`
//! would upload is read off the real `cargo package --list`, and every path in
//! it must belong to a class that was ruled in.
//!
//! **Why a test and not a checklist line.** A publication is the one act in
//! this repo with no undo — a yanked version stays downloadable — so the check
//! has to fire before the act, on every gate run, at no one's discretion. And
//! an `include` allowlist without a test is a comment: nothing else notices
//! when a later edit widens it, and the notice would otherwise arrive after
//! the version is public. Upstream shipped the operator's home paths and three
//! transcripts exactly that way, because its manifest declared no list at all.
//!
//! **The classes below are a SECOND statement of the manifest's policy**, which
//! is deliberate. A check that derived its allowlist from the `include` key
//! would widen with it and stay green through the exact edit it exists to
//! catch.
//!
//! **It lives in `src` and not in a `tests/` crate**, unlike the upstream guard
//! it mirrors, because it forks a child: `cargo package --list` is a
//! subprocess, the spawn boundary is `pub(crate)` (DESIGN §4), and an
//! integration crate could only reach a bare `Command::new` — which the
//! confinement rules refuse, with no test carve-out. The boundary is also what
//! makes this safe to run under a commit hook: `git` exports `GIT_DIR` into its
//! children and it OUTRANKS a `current_dir`, so an unscrubbed `cargo` here
//! would read whichever repository invoked the suite.
//!
//! **Both directions, because a shape guard dies by matching nothing.**
//! [`the_list_is_not_vacuous`] fails a spawn that answered with a short list,
//! and [`the_allowlist_sees_its_own_violations`] fails an [`is_ruled_in`] that
//! has quietly become true of everything.
//!
//! **The allowlist's one cost is paid next door**, in [`embeds`]: a file the
//! crate reads at COMPILE time must be ruled in here and named by `include`, or
//! it builds on this box and on no other. That half moved out with bl-bb7d,
//! when the crate gained its first embed and the question turned from *is there
//! one* into *where does each one resolve*.

mod embeds;

use std::path::{Path, PathBuf};

/// The real answer to *"what would `cargo publish` upload?"*, one path per line.
///
/// `--offline` keeps the guard hermetic — the lockfile is committed and every
/// dependency is resolved long before a test binary runs. `--allow-dirty` is
/// required because `cargo package` refuses a worktree with uncommitted changes
/// outright, and a claim worktree mid-edit is the normal case for the author
/// this test is addressed to.
fn packaged() -> Vec<String> {
    let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_owned());
    let mut cmd = crate::spawn::command(&cargo);
    cmd.current_dir(root())
        .args(["package", "--list", "--offline", "--allow-dirty"]);
    let out = crate::spawn::output(&mut cmd).expect("cargo runs");
    assert!(
        out.status.success(),
        "cargo package --list did not answer: {}",
        String::from_utf8_lossy(&out.stderr)
    );
    String::from_utf8_lossy(&out.stdout)
        .lines()
        .map(str::to_owned)
        .collect()
}

/// The crate root, off this file's own location rather than the working
/// directory, so the answer does not depend on who started the test binary.
fn root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}

/// The classes ruled into the published crate: the crate's own Rust source, the
/// two files crates.io renders, and the one file under `docs/` the crate itself
/// reads at compile time (bl-bb7d). `Cargo.toml.orig` and `.cargo_vcs_info.json`
/// are minted by cargo into the tarball and are not tree files at all.
///
/// **`docs/tools.example.json` is named and `docs/**` is not.** The rest of
/// that directory is commentary addressed to a contributor — the design
/// document, which is the largest file in the tree — and shipping it to every
/// consumer of the binary is the defect this guard exists for. The example is
/// ruled in for a reason that is not editorial at all: it is a build input, and
/// `src/packaged_tests/embeds.rs` is what holds the two facts together.
fn is_ruled_in(path: &str) -> bool {
    let named = matches!(
        path,
        "Cargo.toml"
            | "Cargo.lock"
            | "Cargo.toml.orig"
            | ".cargo_vcs_info.json"
            | "PROTOCOL"
            | "build.rs"
            | "README.md"
            | "LICENSE"
            | "docs/tools.example.json"
    );
    named
        || path
            .strip_prefix("src/")
            .is_some_and(|rest| Path::new(rest).extension().is_some_and(|ext| ext == "rs"))
}

/// The defect: the design document, the gate apparatus, the agent guide and a
/// corpus of fabricated secrets shipping to a registry with the binary. Stated
/// as an allowlist so the next file class added to the tree is red here instead
/// of public there.
#[test]
fn no_commentary_or_apparatus_ships() {
    let strays: Vec<String> = packaged().into_iter().filter(|p| !is_ruled_in(p)).collect();
    assert!(
        strays.is_empty(),
        "paths `cargo publish` would upload that no class rules in. A yanked \
         version stays downloadable, so widen `include` in Cargo.toml only with \
         a reason, and add the class beside it here:\n{}",
        strays.join("\n")
    );
}

/// The other side of a fail-closed list: the crate must still be a crate.
#[test]
fn the_files_a_registry_needs_ship() {
    let list = packaged();
    for needed in [
        "Cargo.toml",
        "Cargo.lock",
        // The wire version and the script that compiles it in: without either,
        // the published crate does not build at all (bl-c618).
        "PROTOCOL",
        "build.rs",
        "README.md",
        "LICENSE",
        "src/lib.rs",
        "src/main.rs",
        "docs/tools.example.json",
    ] {
        assert!(
            list.iter().any(|p| p == needed),
            "{needed} is not in the packaged list — `include` dropped a file the \
             registry or the build needs"
        );
    }
}

/// A guard that measured nothing must not read as a pass: a failed spawn or an
/// empty stdout would otherwise be an empty stray list, which is a green.
#[test]
fn the_list_is_not_vacuous() {
    let list = packaged();
    let sources = list.iter().filter(|p| p.starts_with("src/")).count();
    assert!(
        sources > 30,
        "the packaged list carries {sources} src paths over {} entries — the \
         spawn is broken, not the tree",
        list.len()
    );
}

/// The negative direction for the restated policy: every excluded class, and
/// the unanchored-pattern trap the manifest's comment records, must be seen as
/// a violation — and the classes that ship must not.
#[test]
fn the_allowlist_sees_its_own_violations() {
    for stray in [
        "docs/DESIGN.md",
        "AGENTS.md",
        "CLAUDE.md",
        "Makefile",
        "Containerfile",
        "deny.toml",
        "clippy.toml",
        "tarpaulin.toml",
        "rules/no-bare-command.yml",
        "rules/fixtures/violations.rs",
        ".githooks/pre-commit",
        ".github/workflows/release-plz.yml",
        "scripts/leak-scan.sh",
        // the unanchored-pattern sighting: a bare `README.md` include pattern
        // ships this, and no `scripts` class rules it in
        "scripts/leak-fixtures/README.md",
        "scripts/leak-fixtures/private-key.txt",
    ] {
        assert!(!is_ruled_in(stray), "{stray} must not be ruled in");
    }
    for shipped in [
        "src/main.rs",
        "src/channel/tls.rs",
        "LICENSE",
        "docs/tools.example.json",
    ] {
        assert!(is_ruled_in(shipped), "{shipped} must be ruled in");
    }
}

/// **The crate's front page names the route onto a box** (bl-b250).
///
/// `README.md` is one of the two files ruled in above precisely because
/// crates.io renders it: a stranger arriving at the registry entry for this
/// crate reads exactly these bytes and nothing else. So the one thing they came
/// for — how to get it — has to be in them, and for four months it was not:
/// the file had a Build section addressed to a contributor and a complete image
/// route, and the string `cargo install` appeared nowhere, on the crate whose
/// own README says at length that it IS published.
///
/// It reads the shipped file rather than a copy, and the exact command rather
/// than the words around it, because `--locked` is the half that makes the
/// route reproducible: without it a stranger builds a dependency set no gate
/// has ever run.
#[test]
fn the_readme_names_the_install_route_for_the_published_crate() {
    let readme = std::fs::read_to_string(root().join("README.md")).expect("the README");
    assert!(
        readme.contains("cargo install thrall --locked"),
        "the README no longer names the crate route. It is the file crates.io \
         renders, so it is the one page a stranger who found this crate reads, \
         and `cargo install` is what they came for"
    );
}