Replace the runner stub with the Runtime seam and RunSpec
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
@@ -1,12 +1,138 @@
|
||||
//! Tool runner stub. Real tool execution arrives in M3.
|
||||
//! The runner seam: an allowed call becomes a `RunSpec` and is handed to a `Runtime`.
|
||||
//!
|
||||
//! A `RunSpec` can only be built here, from a `Decision`, so a runtime never sees a call policy
|
||||
//! did not allow.
|
||||
//!
|
||||
//! ```compile_fail
|
||||
//! let _ = brokerd::runner::RunSpec {
|
||||
//! tool: brokerd::args::ToolName::Shell,
|
||||
//! arguments: todo!(),
|
||||
//! mounts: Vec::new(),
|
||||
//! egress: None,
|
||||
//! };
|
||||
//! ```
|
||||
//!
|
||||
//! ```
|
||||
//! fn tool_of(spec: &brokerd::runner::RunSpec) -> brokerd::args::ToolName {
|
||||
//! spec.tool()
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use crate::args::{ToolArgs, ToolName};
|
||||
use crate::policy::Decision;
|
||||
use proto::ToolResponse;
|
||||
|
||||
/// Takes the Decision by value, so one decision cannot run a tool twice.
|
||||
pub fn run(decision: Decision) -> ToolResponse {
|
||||
let _ = decision;
|
||||
ToolResponse::Failed {
|
||||
message: "no tool runner until M3".to_string(),
|
||||
/// A directory mounted for one call: a path and whether the runtime may write to it.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Mount {
|
||||
pub path: String,
|
||||
pub writable: bool,
|
||||
}
|
||||
|
||||
/// What one call was turned into before it reached a `Runtime`: the tool, its arguments, the
|
||||
/// directories mounted and the hosts it may reach. Built only here, from a `Decision`.
|
||||
#[derive(Debug)]
|
||||
pub struct RunSpec {
|
||||
tool: ToolName,
|
||||
arguments: ToolArgs,
|
||||
mounts: Vec<Mount>,
|
||||
egress: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl RunSpec {
|
||||
pub fn tool(&self) -> ToolName {
|
||||
self.tool
|
||||
}
|
||||
pub fn arguments(&self) -> &ToolArgs {
|
||||
&self.arguments
|
||||
}
|
||||
pub fn mounts(&self) -> &[Mount] {
|
||||
&self.mounts
|
||||
}
|
||||
pub fn egress(&self) -> Option<&[String]> {
|
||||
self.egress.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
/// A `Runtime`'s answer to one call. Its text reaches the model as a `failed` result labelled
|
||||
/// public and trusted, so a runtime may only put a fixed sentence in it.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum RunError {
|
||||
Failed(String),
|
||||
Unavailable(String),
|
||||
}
|
||||
|
||||
/// What a `Runtime` produced for one call.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RunOutput {
|
||||
pub content: String,
|
||||
pub truncated: bool,
|
||||
}
|
||||
|
||||
/// Runs one call. `Send + Sync` so one `Runtime` can serve several brokers at once.
|
||||
pub trait Runtime: Send + Sync {
|
||||
fn run(&self, spec: &RunSpec) -> Result<RunOutput, RunError>;
|
||||
}
|
||||
|
||||
/// The production runtime for M3a. It runs nothing; every call is `Unavailable`.
|
||||
pub struct Refusing;
|
||||
|
||||
impl Runtime for Refusing {
|
||||
fn run(&self, _spec: &RunSpec) -> Result<RunOutput, RunError> {
|
||||
Err(RunError::Unavailable(REFUSING.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// The sentence `Refusing` puts in every `Unavailable`: fixed, because it reaches the model.
|
||||
pub const REFUSING: &str = "the runner arrives in M3b";
|
||||
|
||||
/// Turn an allowed call into a `RunSpec`, run it, and answer. A `RunError`'s text reaches the
|
||||
/// model unchanged, so a runtime may only put a fixed sentence in it.
|
||||
pub fn run(decision: Decision, runtime: &dyn Runtime) -> proto::ToolResponse {
|
||||
let label = decision.label();
|
||||
let args = decision.args();
|
||||
let (mounts, egress) = match args {
|
||||
ToolArgs::ReadFile { .. } => (matched_mount(decision.matched_path(), false), None),
|
||||
ToolArgs::WriteFile { .. } => (matched_mount(decision.matched_path(), true), None),
|
||||
ToolArgs::Shell { .. } => (
|
||||
decision
|
||||
.paths()
|
||||
.iter()
|
||||
.map(|path| Mount {
|
||||
path: path.clone(),
|
||||
writable: true,
|
||||
})
|
||||
.collect(),
|
||||
None,
|
||||
),
|
||||
ToolArgs::HttpFetch { .. } => (Vec::new(), Some(decision.hosts().to_vec())),
|
||||
};
|
||||
let spec = RunSpec {
|
||||
tool: args.tool(),
|
||||
arguments: args.clone(),
|
||||
mounts,
|
||||
egress,
|
||||
};
|
||||
match runtime.run(&spec) {
|
||||
Ok(output) => ToolResponse::Result {
|
||||
content: output.content,
|
||||
class: label.class,
|
||||
untrusted: label.untrusted,
|
||||
truncated: output.truncated,
|
||||
},
|
||||
Err(RunError::Failed(message) | RunError::Unavailable(message)) => {
|
||||
ToolResponse::Failed { message }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The matched path as one mount, or none when the call matched no path.
|
||||
fn matched_mount(path: Option<&str>, writable: bool) -> Vec<Mount> {
|
||||
match path {
|
||||
Some(at) => vec![Mount {
|
||||
path: at.to_string(),
|
||||
writable,
|
||||
}],
|
||||
None => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user