use std::collections::BTreeMap;
use std::io::Write;
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Mutex};
use agent_bridle_core::{Denial, Gate, Scope, Tool, ToolContext, ToolResult, TrustedWorkerRequest};
use serde::{Deserialize, Serialize};
use crate::brush_shell::run_in_brush;
use crate::caveat_interceptor::{CaveatInterceptor, DenialSink};
use crate::output_observer::OutputEmitter;
pub(crate) const WORKER_FLAG: &str = "--agent-bridle-worker";
pub(crate) const WORKER_KIND: &str = "brush";
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct WorkerPayload {
cmd: String,
cwd: Option<String>,
path: String,
env: BTreeMap<String, String>,
max_output: usize,
}
impl WorkerPayload {
pub(crate) fn new(
cmd: String,
cwd: Option<String>,
path: String,
env: BTreeMap<String, String>,
max_output: usize,
) -> Self {
Self {
cmd,
cwd,
path,
env,
max_output,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct WorkerResponse {
pub(crate) exit_code: i32,
pub(crate) stdout: String,
pub(crate) stderr: String,
pub(crate) denials: Vec<Denial>,
pub(crate) error: Option<String>,
}
pub(crate) fn main() -> i32 {
match serve_one() {
Ok(response) => write_response(&response, 0),
Err(error) => {
let response = WorkerResponse {
exit_code: 126,
stdout: String::new(),
stderr: String::new(),
denials: Vec::new(),
error: Some(error),
};
write_response(&response, 126)
}
}
}
fn serve_one() -> Result<WorkerResponse, String> {
let request: TrustedWorkerRequest<WorkerPayload> =
crate::private_control::receive_worker_request()
.map_err(|error| format!("trusted worker authentication failed: {error}"))?;
if !request.has_supported_version() {
return Err("unsupported worker protocol version".to_string());
}
let (nonce, caveats, strength_floor, payload) = request.into_parts();
let expected = std::env::var("AGENT_BRIDLE_WORKER_NONCE")
.map_err(|_| "worker nonce is absent".to_string())?;
if nonce != expected {
return Err("worker nonce mismatch".to_string());
}
let generation = match &caveats.valid_for_generation {
Scope::All => 0,
Scope::Only(values) => values
.iter()
.copied()
.next()
.ok_or_else(|| "worker grant has no valid generation".to_string())?,
};
let tool = WorkerTool;
let cx = Gate::new(generation)
.with_strength_floor(strength_floor)
.authorize(&tool, &caveats)
.map_err(|error| format!("worker authorization failed: {error}"))?;
run(payload, cx)
}
fn run(request: WorkerPayload, cx: ToolContext) -> Result<WorkerResponse, String> {
let sink: DenialSink = Arc::new(Mutex::new(Vec::new()));
let cancel = Arc::new(AtomicBool::new(false));
let interceptor =
CaveatInterceptor::new(cx, Arc::clone(&sink)).with_cancel(Arc::clone(&cancel));
let captured = run_in_brush(
request.cmd,
request.cwd,
request.path,
request.env,
interceptor,
request.max_output,
OutputEmitter::default(),
)
.map_err(|error| error.to_string())?;
let denials = sink
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
Ok(WorkerResponse {
exit_code: captured.exit_code,
stdout: captured.stdout,
stderr: captured.stderr,
denials,
error: None,
})
}
fn write_response(response: &WorkerResponse, process_code: i32) -> i32 {
match serde_json::to_writer(std::io::stdout().lock(), response) {
Ok(()) => {
let _ = std::io::stdout().flush();
process_code
}
Err(error) => {
eprintln!("brush worker response failed: {error}");
125
}
}
}
struct WorkerTool;
#[async_trait::async_trait]
impl Tool for WorkerTool {
fn name(&self) -> &str {
"brush-worker"
}
fn schema(&self) -> serde_json::Value {
serde_json::json!({})
}
async fn invoke(
&self,
_args: serde_json::Value,
_cx: &ToolContext,
) -> ToolResult<serde_json::Value> {
unreachable!("worker tool is used only to mint its context")
}
}