brokerd: the [runner] section
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user