Add Grant, Mode and Constraints to proto

Implemented-By: Laguna S 2.1 (OpenCode)
This commit is contained in:
2026-09-17 08:23:48 -07:00
parent d6308c976b
commit 78e13195ea
14 changed files with 232 additions and 0 deletions
Generated
+77
View File
@@ -16,6 +16,12 @@ dependencies = [
"proto", "proto",
] ]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]] [[package]]
name = "gatewayd" name = "gatewayd"
version = "0.1.0" version = "0.1.0"
@@ -23,12 +29,28 @@ dependencies = [
"proto", "proto",
] ]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]] [[package]]
name = "humantime" name = "humantime"
version = "2.4.0" version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15"
[[package]]
name = "indexmap"
version = "2.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]] [[package]]
name = "inferproxy" name = "inferproxy"
version = "0.1.0" version = "0.1.0"
@@ -71,6 +93,7 @@ dependencies = [
"humantime", "humantime",
"serde", "serde",
"serde_json", "serde_json",
"toml",
] ]
[[package]] [[package]]
@@ -125,6 +148,15 @@ dependencies = [
"zmij", "zmij",
] ]
[[package]]
name = "serde_spanned"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
dependencies = [
"serde_core",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "3.0.6" version = "3.0.6"
@@ -136,6 +168,45 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "toml"
version = "1.1.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a"
dependencies = [
"indexmap",
"serde_core",
"serde_spanned",
"toml_datetime",
"toml_parser",
"toml_writer",
"winnow",
]
[[package]]
name = "toml_datetime"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_parser"
version = "1.1.3+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
[[package]] [[package]]
name = "toolkit" name = "toolkit"
version = "0.1.0" version = "0.1.0"
@@ -149,6 +220,12 @@ version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
[[package]]
name = "winnow"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
[[package]] [[package]]
name = "zmij" name = "zmij"
version = "1.0.23" version = "1.0.23"
+1
View File
@@ -23,4 +23,5 @@ proto = { path = "crates/proto" }
serde = { version = "1.0.229", features = ["derive"] } serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.151" serde_json = "1.0.151"
humantime = "2.4.0" humantime = "2.4.0"
toml = "1.1.6"
+3
View File
@@ -13,3 +13,6 @@ serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
humantime.workspace = true humantime.workspace = true
[dev-dependencies]
toml.workspace = true
+50
View File
@@ -0,0 +1,50 @@
//! Grant file schema. A grant is TOML written by the owner to allow one kind of tool call.
use serde::{Deserialize, Serialize};
use crate::{DataClass, Timestamp};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Mode {
Auto,
Ask,
Deny,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase", deny_unknown_fields)]
pub struct Constraints {
#[serde(default)]
pub paths: Vec<String>,
#[serde(default)]
pub hosts: Vec<String>,
#[serde(default)]
pub patterns: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Grant {
pub tool: String,
pub mode: Mode,
pub max_taint: DataClass,
#[serde(default = "default_result_class")]
pub result_class: DataClass,
#[serde(default = "default_untrusted")]
pub untrusted: bool,
#[serde(default)]
pub expires: Option<Timestamp>,
#[serde(default)]
pub secret: Option<String>,
#[serde(default)]
pub constraints: Constraints,
}
fn default_result_class() -> DataClass {
DataClass::Private
}
fn default_untrusted() -> bool {
true
}
+2
View File
@@ -2,11 +2,13 @@
pub mod class; pub mod class;
pub mod frame; pub mod frame;
pub mod grant;
pub mod ids; pub mod ids;
pub mod wire; pub mod wire;
pub use class::DataClass; pub use class::DataClass;
pub use frame::{FrameError, MAX_FRAME, read_frame, write_frame}; pub use frame::{FrameError, MAX_FRAME, read_frame, write_frame};
pub use grant::{Constraints, Grant, Mode};
pub use ids::{CallId, Epoch, Hash32, SessionId, Timestamp, ValueError}; pub use ids::{CallId, Epoch, Hash32, SessionId, Timestamp, ValueError};
pub use wire::{ pub use wire::{
DenyReason, Envelope, ErrorCode, Message, PROTOCOL_VERSION, ToolRequest, ToolResponse, DenyReason, Envelope, ErrorCode, Message, PROTOCOL_VERSION, ToolRequest, ToolResponse,
+3
View File
@@ -0,0 +1,3 @@
tool = "read_file"
mode = "always"
max_taint = "private"
+12
View File
@@ -0,0 +1,12 @@
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 "]
+3
View File
@@ -0,0 +1,3 @@
tool = "read_file"
mode = "auto"
max_taint = "private"
@@ -0,0 +1,2 @@
tool = "read_file"
mode = "auto"
@@ -0,0 +1,6 @@
tool = "read_file"
mode = "auto"
max_taint = "private"
[constraints]
path = ["/etc/**"]
+4
View File
@@ -0,0 +1,4 @@
tool = "read_file"
mode = "auto"
max_taint = "private"
max_tiant = "secret"
+67
View File
@@ -0,0 +1,67 @@
//! Tests for grant files. Do not edit these or the fixtures.
use proto::{Constraints, DataClass, Grant, Mode, Timestamp};
fn parse(name: &str) -> Result<Grant, toml::de::Error> {
let path = format!("{}/tests/fixtures/grant/{name}", env!("CARGO_MANIFEST_DIR"));
let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{path}: {e}"));
toml::from_str(&text)
}
#[test]
fn minimal_grant_gets_safe_defaults() {
let want = Grant {
tool: "read_file".to_string(),
mode: Mode::Auto,
max_taint: DataClass::Private,
result_class: DataClass::Private,
untrusted: true,
expires: None,
secret: None,
constraints: Constraints::default(),
};
assert_eq!(parse("minimal.toml").unwrap(), want);
assert_eq!(Constraints::default().paths, Vec::<String>::new());
}
#[test]
fn full_grant() {
let want = Grant {
tool: "http_fetch".to_string(),
mode: Mode::Ask,
max_taint: DataClass::Secret,
result_class: DataClass::Public,
untrusted: false,
expires: Some(Timestamp::parse("2026-12-31T00:00:00.000Z").unwrap()),
secret: Some("example-api-token".to_string()),
constraints: Constraints {
paths: vec!["/home/kyle/notes/**".to_string()],
hosts: vec!["example.com".to_string(), "api.example.com".to_string()],
patterns: vec!["^GET ".to_string()],
},
};
assert_eq!(parse("full.toml").unwrap(), want);
}
#[test]
fn mistakes_in_grant_files_are_errors() {
for name in [
"unknown_field.toml",
"unknown_constraint.toml",
"bad_mode.toml",
"missing_max_taint.toml",
] {
assert!(parse(name).is_err(), "{name} was accepted");
}
}
#[test]
fn modes_are_lowercase() {
for (mode, text) in [
(Mode::Auto, "auto"),
(Mode::Ask, "ask"),
(Mode::Deny, "deny"),
] {
assert_eq!(serde_json::to_string(&mode).unwrap(), format!("\"{text}\""));
}
}
+1
View File
@@ -7,3 +7,4 @@ Every external crate has a row here. `scripts/check-dep-docs.sh` enforces it.
| `serde` | 1.0.229 | `proto` | Derives serialization for every shared type. MIT OR Apache-2.0. | | `serde` | 1.0.229 | `proto` | Derives serialization for every shared type. MIT OR Apache-2.0. |
| `serde_json` | 1.0.151 | `proto` | JSON for frames and log files. MIT OR Apache-2.0. | | `serde_json` | 1.0.151 | `proto` | JSON for frames and log files. MIT OR Apache-2.0. |
| `humantime` | 2.4.0 | `proto` | RFC 3339 timestamps. No dependencies of its own. MIT OR Apache-2.0. | | `humantime` | 2.4.0 | `proto` | RFC 3339 timestamps. No dependencies of its own. MIT OR Apache-2.0. |
| `toml` | 1.1.6 | `proto` tests; `brokerd` and `bxctl` from M3 | Parses grant files. MIT OR Apache-2.0. |
+1
View File
@@ -9,5 +9,6 @@ reviewer adds findings under "Reviews" once per milestone.
| M1/02-proto-values | 2026-09-17 | done | 1 | pass | none | Implemented ValueError, SessionId, Epoch, CallId, Hash32 and Timestamp in crates/proto/src/ids.rs and DataClass in class.rs, using serde try_from/into for string-backed JSON validation, a hand-written hex encoder and humantime for RFC 3339 parsing with canonical re-serialization. | | M1/02-proto-values | 2026-09-17 | done | 1 | pass | none | Implemented ValueError, SessionId, Epoch, CallId, Hash32 and Timestamp in crates/proto/src/ids.rs and DataClass in class.rs, using serde try_from/into for string-backed JSON validation, a hand-written hex encoder and humantime for RFC 3339 parsing with canonical re-serialization. |
| M1/03-proto-wire | 2026-09-17 | done | 2 | pass | none | Added Envelope, Message, WireError, ErrorCode, ToolRequest, ToolResponse and DenyReason in crates/proto/src/wire.rs, re-exported from lib.rs; all 9 fixture tests pass and `make gate` prints `gate: ok`. | | M1/03-proto-wire | 2026-09-17 | done | 2 | pass | none | Added Envelope, Message, WireError, ErrorCode, ToolRequest, ToolResponse and DenyReason in crates/proto/src/wire.rs, re-exported from lib.rs; all 9 fixture tests pass and `make gate` prints `gate: ok`. |
| M1/04-proto-frame | 2026-09-17 | done | 2 | fail | none | Added crates/proto/src/frame.rs (MAX_FRAME, FrameError, write_frame, read_frame) re-exported from lib.rs; 13 fixture tests pass. Two compile fixes: mapped read_bytes io::Error to FrameError::Io and annotated serde_json::from_slice::<Envelope>; cargo-fmt reordered the lib.rs re-export lines; `make gate` prints `gate: ok`. | | M1/04-proto-frame | 2026-09-17 | done | 2 | fail | none | Added crates/proto/src/frame.rs (MAX_FRAME, FrameError, write_frame, read_frame) re-exported from lib.rs; 13 fixture tests pass. Two compile fixes: mapped read_bytes io::Error to FrameError::Io and annotated serde_json::from_slice::<Envelope>; cargo-fmt reordered the lib.rs re-export lines; `make gate` prints `gate: ok`. |
| M1/05-proto-grant | 2026-09-17 | done | 2 | fail | none | Added crates/proto/src/grant.rs (Mode, Constraints with Default, Grant with serde defaults + deny_unknown_fields) re-exported from lib.rs and toml 1.1.6 as a proto dev-dependency (workspace dep + dependencies.md row); 4 fixture tests pass. cargo-fmt reordered the lib.rs re-exports before the gate. |
## Reviews ## Reviews