Files
boxmaker/docs/plans/M1/05-proto-grant.md
T
kyleandClaude Fable 5.1 3e26c2e3c0 Add M1 plan, given tests and fixtures, AGENTS.md and implementer log
Seven task files for the implementing model under docs/plans/M1/, with
the test files, byte-exact fixtures, Makefile, deny.toml and gate-script
self-test they copy into place. All of it was verified against a private
reference implementation: the gate passes after every task in order.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 01:22:43 -07:00

5.1 KiB

M1 task 05: proto grant files

Branch: m1 (run git switch m1; git status --short must be empty, otherwise stop) Commit subject: Add Grant, Mode and Constraints to proto

Goal

Add the data type for a grant file. A grant is a TOML file the owner writes to allow one kind of tool call. This task only parses grants. Deciding whether a grant matches a request is brokerd's job in a later milestone; write no matching logic.

Context

From the design brief: "A grant is a file the owner writes: tool, argument constraints (paths, hosts, patterns), allowed data classes, expiry, mode (auto, ask, deny)."

From the spec: max_taint is "the highest session taint under which the grant applies". result_class is the data class of the tool's results and defaults to private. untrusted says whether results may have been written by someone other than the owner and defaults to true. A typing mistake in a grant file must be an error, never silently ignored: a misspelt max_tiant that fell back to a default would widen what the grant allows.

A full grant file:

tool = "http_fetch"
mode = "ask"
max_taint = "secret"
result_class = "public"
untrusted = false
expires = "2026-12-31T00:00:00.000Z"
secret = "example-api-token"

[constraints]
paths = ["/home/kyle/notes/**"]
hosts = ["example.com", "api.example.com"]
patterns = ["^GET "]

Files

  • Copy: crates/proto/tests/grant.rs, crates/proto/tests/fixtures/grant/ (6 files)
  • Create: crates/proto/src/grant.rs
  • Modify: Cargo.toml, crates/proto/Cargo.toml, crates/proto/src/lib.rs, docs/dependencies.md, docs/implementer-log.md, Cargo.lock (generated)

Interfaces

Consumes from task 02: proto::DataClass, proto::Timestamp.

Produces, in crates/proto/src/grant.rs, re-exported from the crate root:

// TOML and JSON: "auto", "ask", "deny"
pub enum Mode { Auto, Ask, Deny }

pub struct Grant {
    pub tool: String,                 // required
    pub mode: Mode,                   // required
    pub max_taint: DataClass,         // required
    pub result_class: DataClass,      // default: DataClass::Private
    pub untrusted: bool,              // default: true
    pub expires: Option<Timestamp>,   // default: None, meaning no expiry
    pub secret: Option<String>,       // default: None
    pub constraints: Constraints,     // default: all three lists empty
}

pub struct Constraints {              // also derives Default
    pub paths: Vec<String>,           // default: empty
    pub hosts: Vec<String>,           // default: empty
    pub patterns: Vec<String>,        // default: empty
}

Derives: all three get Debug, Clone, PartialEq, Eq, Serialize, Deserialize. Mode also gets Copy. Constraints also gets Default.

Rules the tests check: tool, mode and max_taint are required. Unknown keys are errors in the grant and in [constraints]. An unknown mode is an error. expires is written as a quoted string in the Timestamp spelling from task 02, not as a bare TOML date.

API notes (verified on docs.rs, 2026-09-17)

  • toml::from_str::<T>(&str) -> Result<T, toml::de::Error>. Only the test uses the toml crate, so it is a dev-dependency. grant.rs itself uses only serde.
  • #[serde(default)] on a field uses Default::default() when the key is missing. #[serde(default = "function_name")] calls a function in the same module instead; use it for result_class and untrusted, whose defaults are not the types' Default.
  • #[serde(deny_unknown_fields)] and #[serde(rename_all = "lowercase")] as in earlier tasks.

Steps

  • 1. Copy the test and fixtures, add the dev-dependency.
git switch m1
cp docs/plans/M1/files/crates/proto/tests/grant.rs crates/proto/tests/
cp -r docs/plans/M1/files/crates/proto/tests/fixtures/grant crates/proto/tests/fixtures/

Add to [workspace.dependencies] in the root Cargo.toml:

toml = "1.1.6"

Add a new section at the end of crates/proto/Cargo.toml:

[dev-dependencies]
toml.workspace = true

Add this row to docs/dependencies.md:

| `toml` | 1.1.6 | `proto` tests; `brokerd` and `bxctl` from M3 | Parses grant files. MIT OR Apache-2.0. |
  • 2. See the test fail. cargo test -p proto --test grant. Expected: it does not compile.

  • 3. Write grant.rs, and add to lib.rs (alphabetical order):

pub mod grant;

pub use grant::{Constraints, Grant, Mode};
  • 4. See the test pass. cargo test -p proto --test grant. Expected: 4 passed; 0 failed.

  • 5. Run the gate. make gate. Expected last line: gate: ok. If cargo deny reports a licence that is not allowed or a duplicate crate version, stop and report; do not edit deny.toml.

  • 6. Log and commit.

git add Cargo.toml Cargo.lock crates/proto docs/dependencies.md docs/implementer-log.md
git commit

Done when

  • cargo test -p proto --test grant reports 4 passed.
  • make gate prints gate: ok.
  • diff -r crates/proto/tests/fixtures/grant docs/plans/M1/files/crates/proto/tests/fixtures/grant prints nothing.

Stop and report if

  • cargo deny rejects toml or one of its dependencies.