Move the tools' arguments and the host rules to proto

Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
2026-09-22 22:59:40 -07:00
parent 655683c9e0
commit f095cca1ee
7 changed files with 213 additions and 87 deletions
+3 -87
View File
@@ -4,7 +4,7 @@
//! path, a host or a URL is well formed. The module is pure: no I/O, no clock. Everything it reads
//! was written by the model, so it is treated as hostile.
use serde::{Deserialize, Serialize};
use proto::tools::{HttpFetchArgs, ReadFileArgs, ShellArgs, WriteFileArgs};
use std::fmt;
/// Maximum length, in bytes, of a path.
@@ -142,34 +142,6 @@ impl std::error::Error for ArgsError {
}
}
/// The argument shape for one tool, decoded then re-serialised.
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct ReadFileArgs {
path: String,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct WriteFileArgs {
path: String,
content: String,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct ShellArgs {
command: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
cwd: Option<String>,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct HttpFetchArgs {
url: String,
}
/// Decode `arguments` for `tool` into a typed value.
pub fn parse(tool: ToolName, arguments: &str) -> Result<ToolArgs, ArgsError> {
match tool {
@@ -257,64 +229,8 @@ pub fn inside(grant_path: &str, path: &str) -> bool {
}
}
/// A host label is 1 to 63 bytes of `a-z`, `0-9` or `-`, and neither starts nor ends with `-`.
fn valid_label(label: &str) -> bool {
if !(1..=63).contains(&label.len()) {
return false;
}
let bytes = label.as_bytes();
if bytes.iter().next() == Some(&b'-') || bytes.iter().last() == Some(&b'-') {
return false;
}
bytes
.iter()
.all(|&byte| matches!(byte, b'a'..=b'z' | b'0'..=b'9' | b'-'))
}
/// A host is 1 to 253 bytes of dot-separated labels, each a whole non-dot name, and the last label
/// starts with a letter. That last rule keeps out every spelling of an IPv4 address.
pub fn valid_host(host: &str) -> bool {
if !(1..=253).contains(&host.len()) {
return false;
}
let labels: Vec<&str> = host.split('.').collect();
if labels.len() < 2 {
return false;
}
for label in &labels {
if !valid_label(label) {
return false;
}
}
match labels.iter().last() {
Some(last) => matches!(last.bytes().next(), Some(byte) if byte.is_ascii_lowercase()),
None => false,
}
}
/// A host, or `*.` followed by a host. Nothing else.
pub fn valid_host_pattern(pattern: &str) -> bool {
match pattern.strip_prefix("*.") {
Some(base) => valid_host(base),
None => valid_host(pattern),
}
}
/// Does `pattern` match `host`? Without `*.` the strings must be equal; with `*.base` the host must
/// end in `.base` with something before the dot.
pub fn host_matches(pattern: &str, host: &str) -> bool {
let base = match pattern.strip_prefix("*.") {
Some(base) => base,
None => return pattern == host,
};
match host.strip_suffix(base) {
Some(before) => match before.strip_suffix('.') {
Some(prefix) => !prefix.is_empty(),
None => false,
},
None => false,
}
}
/// Moved to `proto::hosts` in M3b, so `toolkit` checks hosts with the same rules.
pub use proto::hosts::{host_matches, valid_host, valid_host_pattern};
/// A character that may appear in a host name.
fn host_char(c: char) -> bool {