Add Grant, Mode and Constraints to proto
Implemented-By: Laguna S 2.1 (OpenCode)
This commit is contained in:
@@ -13,3 +13,6 @@ serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
humantime.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
toml.workspace = true
|
||||
|
||||
|
||||
@@ -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,11 +2,13 @@
|
||||
|
||||
pub mod class;
|
||||
pub mod frame;
|
||||
pub mod grant;
|
||||
pub mod ids;
|
||||
pub mod wire;
|
||||
|
||||
pub use class::DataClass;
|
||||
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 wire::{
|
||||
DenyReason, Envelope, ErrorCode, Message, PROTOCOL_VERSION, ToolRequest, ToolResponse,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
tool = "read_file"
|
||||
mode = "always"
|
||||
max_taint = "private"
|
||||
+12
@@ -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 "]
|
||||
@@ -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/**"]
|
||||
@@ -0,0 +1,4 @@
|
||||
tool = "read_file"
|
||||
mode = "auto"
|
||||
max_taint = "private"
|
||||
max_tiant = "secret"
|
||||
@@ -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}\""));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user