toolkit: the tool program, read_file and write_file

Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
2026-09-23 00:00:03 -07:00
parent 644fda14da
commit 0d444dd5bf
10 changed files with 446 additions and 6 deletions
+68
View File
@@ -0,0 +1,68 @@
//! The first two tools, `read_file` and `write_file`. `shell` and `http_fetch` come later; until
//! then `run` never reaches them, so they stay unknown tools.
use std::fs;
use std::io::Read;
use std::path::Path;
use proto::tools::{ReadFileArgs, WriteFileArgs};
use crate::Outcome;
pub const MAX_READ: usize = 1024 * 1024;
fn tool_err(tool: &str, path: &str, why: &str) -> Outcome {
Outcome::tool_error(format!("{tool}: {path}: {why}"))
}
pub fn read_file(args: &ReadFileArgs) -> Outcome {
let path = args.path.as_str();
let is_dir = match fs::metadata(path) {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return tool_err("read_file", path, "no such file");
}
Err(e) => return tool_err("read_file", path, &e.to_string()),
Ok(meta) => meta.is_dir(),
};
if is_dir {
return tool_err("read_file", path, "is a directory");
}
let file = match fs::File::open(path) {
Err(e) => return tool_err("read_file", path, &e.to_string()),
Ok(f) => f,
};
let mut buf = Vec::new();
let n = match file.take(MAX_READ as u64 + 1).read_to_end(&mut buf) {
Err(e) => return tool_err("read_file", path, &e.to_string()),
Ok(n) => n,
};
if n > MAX_READ {
return tool_err("read_file", path, &format!("larger than {MAX_READ} bytes"));
}
match String::from_utf8(buf) {
Ok(text) => Outcome::done(text),
Err(_) => tool_err("read_file", path, "not UTF-8 text"),
}
}
pub fn write_file(args: &WriteFileArgs) -> Outcome {
let path = args.path.as_str();
let content = args.content.as_str();
match Path::new(path).parent() {
Some(p) if p.is_dir() => {}
_ => return tool_err("write_file", path, "the directory does not exist"),
}
if Path::new(path).is_dir() {
return tool_err("write_file", path, "is a directory");
}
if let Err(e) = fs::write(path, content) {
return tool_err("write_file", path, &e.to_string());
}
Outcome::done(format!("wrote {} bytes to {path}", content.len()))
}
+48
View File
@@ -0,0 +1,48 @@
//! Reading and parsing the arguments `brokerd` puts on `toolkit`'s standard input.
use std::io::Read;
pub const MAX_INPUT: usize = 2 * 1024 * 1024;
#[derive(Debug)]
pub enum InputError {
TooLarge,
NotUtf8,
Io(std::io::Error),
}
impl std::fmt::Display for InputError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InputError::TooLarge => write!(f, "the arguments are larger than {MAX_INPUT} bytes"),
InputError::NotUtf8 => write!(f, "the arguments are not UTF-8"),
InputError::Io(e) => write!(f, "cannot read the arguments: {e}"),
}
}
}
impl std::error::Error for InputError {}
impl From<std::io::Error> for InputError {
fn from(e: std::io::Error) -> Self {
InputError::Io(e)
}
}
/// All of `stdin`, at most MAX_INPUT bytes, as UTF-8. Read at most MAX_INPUT + 1 bytes
/// (`Read::take`); more than MAX_INPUT is TooLarge.
pub fn read_input(stdin: &mut dyn std::io::Read) -> Result<String, InputError> {
let mut buf = Vec::new();
let n = stdin.take(MAX_INPUT as u64 + 1).read_to_end(&mut buf)?;
if n > MAX_INPUT {
return Err(InputError::TooLarge);
}
String::from_utf8(buf).map_err(|_| InputError::NotUtf8)
}
/// Parse `text` as the arguments of tool `name`; the error is the whole line for stderr:
/// "toolkit: {name}: the arguments do not parse: {serde's error}".
pub fn parse<T: serde::de::DeserializeOwned>(name: &str, text: &str) -> Result<T, String> {
serde_json::from_str(text)
.map_err(|e| format!("toolkit: {name}: the arguments do not parse: {e}"))
}
+82 -1
View File
@@ -1 +1,82 @@
//! Entry points that run inside tool containers.
//! The programs that run inside tool containers.
use proto::tools::{ReadFileArgs, WriteFileArgs};
pub mod files;
pub mod input;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Exit {
Done,
ToolError,
Misuse,
}
impl Exit {
/// 0, 1, 2. No `as` cast: a `match`.
pub fn code(self) -> u8 {
match self {
Exit::Done => 0,
Exit::ToolError => 1,
Exit::Misuse => 2,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Outcome {
pub exit: Exit,
pub stdout: String,
pub stderr: String,
}
impl Outcome {
pub fn done(stdout: String) -> Outcome {
Outcome {
exit: Exit::Done,
stdout,
stderr: String::new(),
}
}
pub fn tool_error(stdout: String) -> Outcome {
Outcome {
exit: Exit::ToolError,
stdout,
stderr: String::new(),
}
}
pub fn misuse(stderr: String) -> Outcome {
Outcome {
exit: Exit::Misuse,
stdout: String::new(),
stderr,
}
}
}
/// Run tool `name` with the arguments on `stdin`.
pub fn run(name: &str, stdin: &mut dyn std::io::Read) -> Outcome {
let text = match input::read_input(stdin) {
Ok(t) => t,
Err(e) => return Outcome::misuse(format!("toolkit: {e}")),
};
match name {
"read_file" => {
let args = match input::parse::<ReadFileArgs>(name, &text) {
Ok(a) => a,
Err(e) => return Outcome::misuse(e),
};
files::read_file(&args)
}
"write_file" => {
let args = match input::parse::<WriteFileArgs>(name, &text) {
Ok(a) => a,
Err(e) => return Outcome::misuse(e),
};
files::write_file(&args)
}
_ => Outcome::misuse(format!("toolkit: unknown tool {name:?}")),
}
}
+14 -3
View File
@@ -1,4 +1,15 @@
fn main() {
eprintln!("toolkit: not implemented until M3");
std::process::exit(2);
use std::io::Write;
use std::process::ExitCode;
fn main() -> ExitCode {
let mut rest = std::env::args_os().skip(1);
let name = match (rest.next(), rest.next()) {
(Some(arg), None) => arg.to_str().unwrap_or("").to_string(),
_ => String::new(),
};
let outcome = toolkit::run(&name, &mut std::io::stdin().lock());
let _ = std::io::stdout().write_all(outcome.stdout.as_bytes());
let _ = std::io::stderr().write_all(outcome.stderr.as_bytes());
ExitCode::from(outcome.exit.code())
}