brokerd: the [runner] section

Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
2026-09-23 01:07:21 -07:00
parent 95872d94b8
commit 81ce5a3345
9 changed files with 312 additions and 0 deletions
+150
View File
@@ -45,6 +45,79 @@ impl Default for Approvals {
}
}
fn default_podman() -> PathBuf {
PathBuf::from("podman")
}
fn default_egress_network() -> String {
"pasta".to_string()
}
fn default_output_cap() -> u64 {
262_144
}
fn default_memory() -> String {
"512m".to_string()
}
fn default_pids() -> u32 {
128
}
fn default_read_file_ms() -> u64 {
30_000
}
fn default_write_file_ms() -> u64 {
30_000
}
fn default_shell_ms() -> u64 {
100_000
}
fn default_http_fetch_ms() -> u64 {
60_000
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Runner {
#[serde(default = "default_podman")]
pub podman: PathBuf,
pub image: String,
#[serde(default = "default_egress_network")]
pub egress_network: String,
#[serde(default = "default_output_cap")]
pub output_cap: u64,
#[serde(default = "default_memory")]
pub memory: String,
#[serde(default = "default_pids")]
pub pids: u32,
#[serde(default = "default_read_file_ms")]
pub read_file_ms: u64,
#[serde(default = "default_write_file_ms")]
pub write_file_ms: u64,
#[serde(default = "default_shell_ms")]
pub shell_ms: u64,
#[serde(default = "default_http_fetch_ms")]
pub http_fetch_ms: u64,
}
impl Runner {
/// The time limit for one call of `tool`: the matching `_ms` field.
pub fn time_limit(&self, tool: crate::args::ToolName) -> std::time::Duration {
let ms = match tool {
crate::args::ToolName::ReadFile => self.read_file_ms,
crate::args::ToolName::WriteFile => self.write_file_ms,
crate::args::ToolName::Shell => self.shell_ms,
crate::args::ToolName::HttpFetch => self.http_fetch_ms,
};
std::time::Duration::from_millis(ms)
}
}
/// `memory` is well formed if it is one or more digits followed by `b`, `k`, `m` or `g`.
fn valid_memory(memory: &str) -> bool {
let bytes = memory.as_bytes();
bytes.len() >= 2
&& matches!(bytes[bytes.len() - 1], b'b' | b'k' | b'm' | b'g')
&& bytes[..bytes.len() - 1].iter().all(|&c| c.is_ascii_digit())
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct Config {
@@ -54,6 +127,8 @@ pub struct Config {
pub sockets: Sockets,
#[serde(default)]
pub approvals: Approvals,
#[serde(default)]
pub runner: Option<Runner>,
}
/// The longest an approval may wait: a day, the longest `loopd` waits after a pending frame.
@@ -99,6 +174,78 @@ impl Config {
),
));
}
if let Some(runner) = &config.runner {
let where_image = format!(
"[runner] image is {}; it must be named by digest: <name>@sha256:<64 hex digits>",
runner.image
);
let (name, hex) = match runner.image.rsplit_once("@sha256:") {
Some(pair) => pair,
None => {
return Err(ConfigError::Invalid(
path.to_path_buf(),
where_image.clone(),
));
}
};
if name.is_empty()
|| hex.len() != 64
|| !hex.chars().all(|c| matches!(c, '0'..='9' | 'a'..='f'))
{
return Err(ConfigError::Invalid(path.to_path_buf(), where_image));
}
if !valid_memory(&runner.memory) {
return Err(ConfigError::Invalid(
path.to_path_buf(),
format!(
"[runner] memory is {}; it must be a number and one of b, k, m, g",
runner.memory
),
));
}
if runner.egress_network.is_empty() || runner.podman.as_os_str().is_empty() {
return Err(ConfigError::Invalid(
path.to_path_buf(),
"[runner] podman and egress_network must not be empty".to_string(),
));
}
if runner.output_cap == 0 {
return Err(ConfigError::Invalid(
path.to_path_buf(),
"[runner] output_cap must be at least 1".to_string(),
));
}
if runner.pids == 0 {
return Err(ConfigError::Invalid(
path.to_path_buf(),
"[runner] pids must be at least 1".to_string(),
));
}
if runner.read_file_ms == 0 {
return Err(ConfigError::Invalid(
path.to_path_buf(),
"[runner] read_file_ms must be at least 1".to_string(),
));
}
if runner.write_file_ms == 0 {
return Err(ConfigError::Invalid(
path.to_path_buf(),
"[runner] write_file_ms must be at least 1".to_string(),
));
}
if runner.shell_ms == 0 {
return Err(ConfigError::Invalid(
path.to_path_buf(),
"[runner] shell_ms must be at least 1".to_string(),
));
}
if runner.http_fetch_ms == 0 {
return Err(ConfigError::Invalid(
path.to_path_buf(),
"[runner] http_fetch_ms must be at least 1".to_string(),
));
}
}
Ok(config)
}
pub fn broker_socket(&self) -> PathBuf {
@@ -121,4 +268,7 @@ impl Config {
pub fn state_dir(&self) -> PathBuf {
self.paths.home.join("broker/sessions")
}
pub fn egress_dir(&self) -> PathBuf {
self.paths.home.join("run/egress")
}
}