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>
153 lines
5.1 KiB
Markdown
153 lines
5.1 KiB
Markdown
# 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:
|
|
|
|
```toml
|
|
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:
|
|
|
|
```rust
|
|
// 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.**
|
|
|
|
```sh
|
|
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
|
|
toml = "1.1.6"
|
|
```
|
|
|
|
Add a new section at the end of `crates/proto/Cargo.toml`:
|
|
|
|
```toml
|
|
[dev-dependencies]
|
|
toml.workspace = true
|
|
```
|
|
|
|
Add this row to `docs/dependencies.md`:
|
|
|
|
```markdown
|
|
| `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):
|
|
|
|
```rust
|
|
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.**
|
|
|
|
```sh
|
|
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.
|