Project homepage Mailing List  Warmcat.com  API Docs  Github Mirror 
    npro  
 Modern all-safe Rust Network Protocol library supporting h1, h2, h3, ws, wt sans-IO and with socket IO + tls
git clone https://npro.rs/repo/npro
 
root / crates / npro-test / transcripts / h2-ws-peer-close-skint.json
Author[]Andy Green <andy@warmcat.com> 2026-10-03 15:16 UTC
Committer[]Andy Green <andy@warmcat.com> 2026-10-05 06:32 UTC
Treea4c21caf8fbcce15db8dd9b4b5f119126fd093f0   Raw Patch
 
meta: admit dependencies only by name
meta: admit dependencies only by name

npro's dependency tree is meant to hold only crates someone decided to
put there.  Until now that rested on the commit-message rule in
AGENTS.md; make it a gate.

deny.toml's [bans] allow now names every crate that may appear in the
build graph, transitive ones and dev-dependencies included, so anything
an admitted crate drags in fails cargo deny until it is admitted too.
cargo deny reads an empty list as no allowlist, so the workspace's own
crates are named to keep it in force while nothing else is.
[bans.build] refuses build scripts, prebuilt executables and shipped
scripts unless a crate is named there as well.

Checked by adding cfg-if and libc as dev-dependencies to a scratch copy:
cargo deny refuses both as not allowed, and libc's build script too.

docs/dependencies.md is the register: why the policy, how it is
enforced, where dependencies may live by crate layer, how one is
admitted, what is decided (miniz_oxide for pmd), decided against (rand,
serde, sha1/base64 crates, proptest, async runtimes in protocol crates)
and open (tls).  AGENTS.md, agent-context, the plan and the README
point at it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019kg5Eemy68ZaqDBcUJQG6J
diff --git a/AGENTS.md b/AGENTS.md index 53811bf..d8a1e65 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -109,7 +109,10 @@ We are very concerned about: adding it: MSRV-compatible, no `build.rs` that touches the network, no large proc-macro trees behind the core. `cargo deny` and `cargo audit` configs are committed and gate CI. Default features are the minimum - that builds something useful; everything else is opt-in. + that builds something useful; everything else is opt-in. `deny.toml` + admits crates only by name, transitive ones included: a new dependency + is the maintainer's decision, asked for first, and follows + docs/dependencies.md, which is its register. - No `Rc<RefCell<_>>` or `Arc<Mutex<_>>` inside the protocol crates. Interior mutability in the core means the ownership of the design is diff --git a/deny.toml b/deny.toml index ac49745..c798b00 100644 --- a/deny.toml +++ b/deny.toml @@ -1,5 +1,14 @@ -# cargo deny: what may enter the dependency tree. The tree is meant to stay -# close to empty; each dependency is justified in the commit that adds it. +# cargo deny: what may enter the dependency tree. +# +# npro admits a dependency only by name. The tree is meant to stay close to +# empty, and a crate gets in only because someone decided it should, not +# because something already admitted brought it along. docs/dependencies.md +# is the register of what is admitted and why; adding a crate means adding it +# here and there, in the commit that brings it in, with its justification. +# +# The graph is every crate any target of the workspace can pull in, with all +# features, for every platform, including dev-dependencies: tests and tools +# run on developers' machines and builders too. [graph] all-features = true @@ -15,6 +24,37 @@ confidence-threshold = 0.9 multiple-versions = "deny" wildcards = "deny" +# The bouncers at the door. Any crate in the graph that is not named here +# fails the gate, however deep in the tree it sits: a crate admitted for one +# job does not get to bring its own guests. +# +# cargo deny treats an empty list as "no allowlist", so the workspace's own +# crates are named here too, and keep the list in force while nothing else +# is on it. A new workspace crate is added here when it is created. +allow = [ + # the workspace + "npro", + "npro-core", + "npro-test", + + # admitted dependencies, each with its entry in docs/dependencies.md: + # (none) +] + +# A build script runs arbitrary code on the build machine at compile time, +# as a proc-macro does (proc-macros are kept out by the allowlist above). +# No crate may have a build script unless it is also named here, and no +# crate may ship prebuilt executables or scripts. +[bans.build] +allow-build-scripts = [ + # (none) +] +executables = "deny" +interpreted = "deny" +include-dependencies = true +include-workspace = true +include-archives = true + [sources] unknown-registry = "deny" unknown-git = "deny" diff --git a/docs/agent-context.md b/docs/agent-context.md index 5a6a5a7..4518448 100644 --- a/docs/agent-context.md +++ b/docs/agent-context.md @@ -61,7 +61,9 @@ These came from real incidents, not taste. - **Ask before installing tools or system packages.** Never install Node.js or anything with an npm-style uncontrolled dependency tree, even in a throwaway environment. New crate dependencies are covered by - AGENTS.md: close to zero, each justified in its commit. + AGENTS.md: close to zero, each justified in its commit. `deny.toml` + admits crates only by name, and docs/dependencies.md says how one is + admitted; never add one without asking. - **Say what is not done.** If a phase ends incomplete, list exactly what is missing. Never leave something silently half-done. - **When a partner makes a design call, record it and don't reopen it.** diff --git a/docs/dependencies.md b/docs/dependencies.md new file mode 100644 index 0000000..16a4bf3 --- /dev/null +++ b/docs/dependencies.md @@ -0,0 +1,132 @@ +# Dependencies + +npro is built so that what runs in your program is what you chose to put +there. The protocol crates depend on nothing but `core`, and `alloc` where +a feature asks for it. Anything else enters by a decision, made once, +written down here, and enforced on every commit. + +This is a foundation of the project, not a preference. Each crate in a +dependency tree is code you ship, and its authors are people you trust +with your users' bytes and your build machines. A tree that grows by +default, a crate at a time, ends up trusting hundreds of people for +things a few lines would have done. npro takes the other path: a very +small set of chosen dependencies, each worth what it costs. + +That does not mean writing everything ourselves. Where a crate does a +hard job well, such as an async runtime or a TLS stack, npro integrates +with it rather than competing with it, behind a feature or in an adapter +crate that only the people who want it build. + +## The door + +[`deny.toml`](../deny.toml) at the top of the tree is the door, and +`cargo deny check` is the bouncer. `scripts/ci.sh` runs it on every +commit, and sai runs it on every push. + +- **`[bans] allow`** names every crate that may appear anywhere in the + build graph. Any other crate fails the gate, however deep in the tree + it sits. A crate admitted for one job does not get to bring its own + guests: each of its dependencies needs its own entry, and its own line + in the register below. +- **`[bans.build] allow-build-scripts`** names the crates that may have a + build script, code that runs on the build machine at compile time. A + crate with one fails the gate unless it is named there as well. +- **`executables` and `interpreted`** fail the gate if a crate ships + prebuilt binaries or scripts. +- **`[licenses] allow`** admits only MIT. A crate offered as + `MIT OR Apache-2.0` passes; one only under another licence needs that + licence admitted too, as part of the same decision. +- **`multiple-versions = "deny"`**: one version of each crate. +- **`[sources]`**: crates.io only, no git or other registries. +- **`cargo audit`** (also in `ci.sh`) fails the gate on any RustSec + advisory against a crate in the tree. + +The graph checked is the whole one: every crate, every feature, every +platform, and dev-dependencies too. Tests and tools run on developers' +machines and builders, so they are let in on the same terms. + +cargo deny treats an empty allowlist as no allowlist, so the list also +names the workspace's own crates. That keeps it in force while nothing +else is on it. A new workspace crate is added to it when it is created. + +## Where dependencies may live + +| crates | may depend on | +|---|---| +| the protocol crates: npro-core, -h1, -ws, -h2, -h3, -quic, -wt, -mqtt, -http | nothing, except crates agreed below, each behind an opt-in feature, `no_std`, and with no build script | +| npro-io, the IO adapter | std, and what its tls decision admits | +| integrations with a runtime or framework, such as tokio | that runtime, in its own opt-in crate or feature, never a default | +| npro-test, fuzz targets, tools | what testing needs, admitted like anything else; fuzz targets live in their own workspace with their own lock file | +| npro, the facade | the npro crates only | + +A protocol crate never depends on an IO crate, an async runtime, or a +crate that does IO. Time and random are inputs, not dependencies. + +## Admitting a crate + +1. Ask first. A new dependency is a design decision for the maintainer, + not a step in implementing something. +2. In its own commit, add it to the `Cargo.toml` that needs it, behind a + feature unless it is needed by every build. +3. Run `cargo deny --all-features check bans`. It names every crate the + addition brings in, and every one with a build script. Each of those + is part of the decision too. +4. Add each crate to `allow` in `deny.toml`, and to + `allow-build-scripts` if it has one, with a comment naming it here. +5. Add each to the register below. +6. Say in the commit message why it is worth having, covering what the + register asks for. + +What the register records for each crate, and what decides it: + +- **What it is for**, and what doing without it would cost. +- **Who maintains it**, and how actively. +- **Its unsafe code**: none, or how much, where, and why that is + acceptable. +- **Its build script and proc-macros**, if any, and what they do. A build + script that touches the network is refused. +- **Its MSRV**, which must be no newer than npro's `rust-version`. +- **`no_std`**, for anything a protocol crate uses. +- **Its licence.** +- **What it brings with it.** + +Removing a dependency is the same in reverse: take it out of +`Cargo.toml`, `deny.toml` and the register, in one commit. + +## The register + +Admitted: **none**. npro builds from its own sources and the Rust +toolchain alone. + +| crate | used by | feature | why | build script | unsafe | admitted in | +|---|---|---|---|---|---|---| +| (none yet) | | | | | | | + +## Decided, not yet admitted + +- **`miniz_oxide`**, with its dependency `adler2`, for permessage-deflate, + behind npro-ws's opt-in `pmd` feature. Pure Rust, `no_std` with + `alloc`. It is admitted, with its register entry, in the commit that + adds permessage-deflate. + +## Decided against + +- **`rand`**: random is an input the caller provides + (`npro_core::random::Random`). +- **`serde`**: the test transcripts are read by a small strict reader in + npro-test. Nothing else in npro needs a serialisation framework. +- **SHA-1 and base64 crates**: the ws handshake's needs are about 150 + lines, written in npro-core and checked against the RFC vectors. +- **`proptest` and similar**: property tests are written over npro-core's + seeded generator, which also reproduces C's random draws. +- **An async runtime in any protocol crate**: integrations live outside + them. + +## Open + +- **tls**, for npro-io. rustls is the obvious candidate, but it needs a + crypto backend, and its usual ones (`ring`, `aws-lc-rs`) carry C and + assembly with build scripts. The alternatives are adapters to the + platform's tls or to OpenSSL. This is decided when npro-io needs it + (phase 1g and after, in [port-plan.md](port-plan.md)), and recorded + here. diff --git a/docs/port-plan.md b/docs/port-plan.md index 8734240..2bc96b0 100644 --- a/docs/port-plan.md +++ b/docs/port-plan.md @@ -68,7 +68,8 @@ it. gets a 403 through the control-character check, and a block with no method is refused later with no status. Both need checking against `main-dev` first. -4. **Dependencies (agreed).** +4. **Dependencies (agreed).** [dependencies.md](dependencies.md) is the + register, and `deny.toml` admits crates only by name. - **SHA-1 and base64** (the ws accept): write them in-tree in the core crate, as C does in `lib/misc`, checked against the RFC 3174 and RFC 4648 vectors. They are about 150 lines together and not worth a diff --git a/docs/sai.md b/docs/sai.md index c438bdd..1240a6d 100644 --- a/docs/sai.md +++ b/docs/sai.md @@ -72,7 +72,7 @@ disk. | x86_64 and aarch64 Linux, riscv64, both macOS | `test` | rustup with stable (`--profile default`) | | fedora44 x86_64 | `gate`, `features`, `nostd`, `miri-*`, `c-oracle` | everything below | | freebsd/aarch64 | `test-freebsd` | `pkg install rust`, 1.85 or later; rustup has no FreeBSD aarch64 host | -| w11/x86_64 | `test-windows` | rustup with stable for the MSVC host, on the sai service's `PATH`: see below | +| w11/x86_64 | `test-windows` | rustup with stable for the MSVC host, and an `env` for the platform in the builder's configuration: see below | **Every builder with rustup**, as the `sai` user: @@ -114,17 +114,14 @@ with its checksum beside it at the same URL plus `.sha256` (`certutil -hashfile rustup-init.exe SHA256` to compare). `winget install Rustlang.Rustup` fetches the same thing. -The job step runs a bare `cargo`, so cargo must be on the `PATH` the sai -builder service starts with. rustup normally installs per user and adds -its `bin` to that user's `PATH` only, which a service running as -LocalSystem never sees. Install it to a fixed place and put that on the -system `PATH` instead. `setx /M` sets them for processes started later, -so the installer also needs them set in the shell it runs from. From an -administrator PowerShell: +sai-builder does not pass its own environment to a job: on Windows, as +on unix, the job starts with only a fixed `PATH`, `LANG` and `TERM`, plus +`HOME` and the `SAI_*` variables. So the system `PATH` does not reach it, +and everything cargo needs is set per platform in the builder's +configuration instead (below). rustup goes in a fixed place outside any +profile. From an administrator PowerShell: ```powershell -setx /M RUSTUP_HOME C:\rust\rustup -setx /M CARGO_HOME C:\rust\cargo $env:RUSTUP_HOME = 'C:\rust\rustup' $env:CARGO_HOME = 'C:\rust\cargo' .\rustup-init.exe -y --default-host x86_64-pc-windows-msvc --profile default --no-modify-path @@ -136,16 +133,49 @@ PowerShell: there `set` makes a PowerShell variable, not an environment variable, and rustup then installs under `%USERPROFILE%` as usual. The installer ends by printing where it installed; it should say `C:\rust`. -then add `C:\rust\cargo\bin` to the system `PATH` (System Properties → -Environment Variables), and reboot: services only see a changed system -environment after one. If the service account is not an administrator, -give it read access to `C:\rust`, and write access to -`C:\rust\cargo\registry`, where cargo would cache downloaded crates. +The account the jobs run as must be able to read `C:\rust\rustup` and +write `C:\rust\cargo`, where cargo keeps its package cache and the lock on +it. For an ordinary account, here `sai`: + + ```powershell + icacls C:\rust\rustup /grant 'sai:(OI)(CI)RX' /T + icacls C:\rust\cargo /grant 'sai:(OI)(CI)M' /T + ``` + +Then give the platform an `env` in the builder's configuration file. It +is JSON, so every backslash is doubled: a single one is read as an escape, +and `\r` in `C:\rust\rustup` becomes a carriage return. + +```json +{ + "name": "w11/x86_64-amd/msvc", + "env": [ + "PATH=c:\\rust\\cargo\\bin;c:\\Windows\\System32;c:\\Windows", + "RUSTUP_HOME=c:\\rust\\rustup", + "CARGO_HOME=c:\\rust\\cargo", + "SystemRoot=c:\\Windows", + "TEMP=c:\\Users\\sai.sai-vm\\AppData\\Local\\Temp", + "TMP=c:\\Users\\sai.sai-vm\\AppData\\Local\\Temp" + ], + ... +} +``` + +- `PATH`: rustup's `cargo` first, then the system's own programs. +- `RUSTUP_HOME`, `CARGO_HOME`: without them `cargo` looks for its + toolchains under the profile, and finds none. +- `SystemRoot`: much of Win32, sockets and crypto among it, fails + without it. +- `TEMP`, `TMP`: where rustc and `link.exe` write temporary files. They + must be in the profile of the account the jobs run as, which need not + be named after it: here the jobs run in `C:\Users\sai.sai-vm`, and + `link.exe` failed with `LNK1104: cannot open file '...\Temp\lnk{...}.tmp'` + while they pointed at `C:\Users\sai`. The linker is MSVC's `link.exe`, from the Visual Studio Build Tools the -builder already has for building C lws. `rustup-init.exe` warns if it -cannot find them. As the service's account (or in a new administrator -`cmd`), `cargo --version` should then print 1.85 or later. +builder already has for building C lws. rustc finds Visual Studio +itself, so none of its variables (`LIB`, `INCLUDE`, `VCINSTALLDIR`) are +needed in `env`. ### Checking a builder
Page fetched 0s ago, creation time: 4ms (vhost etag hits: 0%, cache hits: 0%)