fabro/lib/apps/fabro-server/src/worker_runtime.rs
Bryan Helmkamp 809b3891b5
Forward every configured sandbox plugin to the Petri worker
The worker's environment carried only the host, Docker and Daytona plugin
variables from the server's own environment, so a run on a third-party
provider kind never learned where its plugin was, although the server
kept `[server.sandbox.providers.<kind>]` plugin settings for its own
attach. The launch spec now derives `PETRI_SANDBOX_<KIND>_PLUGIN` and
`PETRI_SANDBOX_<KIND>_SHA256` from every enabled kind's plugin settings,
and `PETRI_SANDBOX_PLUGIN_DEV=1` when any of them sets `dev`, set after
the allowlist so the settings win over an ambient variable of the same
name and the allowlist stays the fallback.

The server's default plugin binary name is `sandbox-driver-<kind>`, the
name Petri looks up, since one executable serves both sides.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 14:45:42 -04:00

188 lines
6.3 KiB
Rust

use std::path::PathBuf;
use std::pin::Pin;
use std::process::Stdio;
use anyhow::{Context, Result};
use async_trait::async_trait;
use fabro_static::EnvVars;
use fabro_types::RunId;
use fabro_types::settings::server::LogDestination;
use futures_util::future::BoxFuture;
use tokio::io::AsyncRead;
use tokio::process::Command;
use crate::spawn_env::apply_worker_env;
#[async_trait]
pub(crate) trait WorkerRuntime: Send + Sync {
async fn start(&self, spec: WorkerLaunchSpec) -> Result<StartedWorker>;
async fn request_stop(&self, worker_ref: &WorkerRef);
async fn force_stop(&self, worker_ref: &WorkerRef);
async fn is_alive(&self, worker_ref: &WorkerRef) -> bool;
}
#[derive(Clone, Debug, PartialEq, Eq, strum::IntoStaticStr)]
#[strum(serialize_all = "snake_case")]
pub(crate) enum WorkerRef {
/// A worker running as a local subprocess. `pre_exec_setpgid` ensures the
/// child is the leader of its own process group with `pgid == pid`, so a
/// single PID identifies both the process and its group.
Local { pid: u32 },
}
impl WorkerRef {
pub(crate) fn kind(&self) -> &'static str {
self.into()
}
}
pub(crate) struct WorkerLaunchSpec {
pub(crate) executable: PathBuf,
pub(crate) server_target: String,
pub(crate) storage_dir: PathBuf,
pub(crate) run_dir: PathBuf,
pub(crate) run_id: RunId,
pub(crate) mode: &'static str,
pub(crate) worker_token: String,
pub(crate) log_destination: LogDestination,
pub(crate) fabro_log: Option<String>,
pub(crate) active_config_path: PathBuf,
pub(crate) github_app_private_key: Option<String>,
/// The vault's Daytona API key, for a run on a Daytona environment:
/// Petri's Daytona plugin reads it from the worker's process.
pub(crate) daytona_api_key: Option<String>,
/// The Fabro home the server resolved, so a Petri run's skills step
/// reads the same home whatever the worker's environment says.
pub(crate) fabro_home: PathBuf,
/// The sandbox-driver plugin variables the server's provider settings
/// derive (`spawn_env::sandbox_plugin_env`), so Petri in the worker
/// launches the plugin the settings name for every configured kind.
pub(crate) sandbox_plugin_env: Vec<(String, String)>,
}
pub(crate) struct StartedWorker {
pub(crate) worker_ref: WorkerRef,
pub(crate) stderr: Pin<Box<dyn AsyncRead + Send + 'static>>,
pub(crate) wait: BoxFuture<'static, Result<WorkerExit>>,
}
#[derive(Debug)]
pub(crate) struct WorkerExit {
pub(crate) success: bool,
pub(crate) detail: String,
}
#[derive(Default)]
pub(crate) struct LocalWorkerRuntime;
impl LocalWorkerRuntime {
pub(crate) fn new() -> Self {
Self
}
pub(crate) fn command_for_spec(spec: &WorkerLaunchSpec) -> Command {
let worker_stdout = match spec.log_destination {
LogDestination::Stdout => Stdio::inherit(),
LogDestination::File => Stdio::null(),
};
let log_destination_env: &'static str = spec.log_destination.into();
let mut cmd = Command::new(&spec.executable);
cmd.arg("__run-worker")
.arg("--server")
.arg(&spec.server_target)
.arg("--storage-dir")
.arg(&spec.storage_dir)
.arg("--run-dir")
.arg(&spec.run_dir)
.arg("--run-id")
.arg(spec.run_id.to_string())
.arg("--mode")
.arg(spec.mode)
.arg("--fabro-home")
.arg(&spec.fabro_home)
.stdin(Stdio::null())
.stdout(worker_stdout)
.stderr(Stdio::piped());
apply_worker_env(&mut cmd, &spec.sandbox_plugin_env);
if let Some(level) = spec.fabro_log.as_deref() {
cmd.env(EnvVars::FABRO_LOG, level);
}
cmd.env(EnvVars::FABRO_LOG_DESTINATION, log_destination_env);
cmd.env(EnvVars::FABRO_CONFIG, &spec.active_config_path);
cmd.env_remove(EnvVars::FABRO_WORKER_TOKEN);
cmd.env(EnvVars::FABRO_WORKER_TOKEN, &spec.worker_token);
if let Some(pem) = spec.github_app_private_key.as_deref() {
cmd.env(EnvVars::GITHUB_APP_PRIVATE_KEY, pem);
}
if let Some(key) = spec.daytona_api_key.as_deref() {
cmd.env(EnvVars::DAYTONA_API_KEY, key);
}
#[cfg(unix)]
fabro_proc::pre_exec_setpgid(cmd.as_std_mut());
cmd
}
}
#[async_trait]
impl WorkerRuntime for LocalWorkerRuntime {
async fn start(&self, spec: WorkerLaunchSpec) -> Result<StartedWorker> {
let mut child = Self::command_for_spec(&spec)
.spawn()
.context("spawning run worker process")?;
let Some(pid) = child.id() else {
let _ = child.start_kill();
anyhow::bail!("worker process did not report a PID");
};
let Some(stderr) = child.stderr.take() else {
let _ = child.start_kill();
anyhow::bail!("worker child stderr should be piped");
};
let stderr: Pin<Box<dyn AsyncRead + Send + 'static>> = Box::pin(stderr);
let wait: BoxFuture<'static, Result<WorkerExit>> = Box::pin(async move {
let status = child.wait().await.context("worker wait failed")?;
Ok(WorkerExit {
success: status.success(),
detail: status.to_string(),
})
});
Ok(StartedWorker {
worker_ref: WorkerRef::Local { pid },
stderr,
wait,
})
}
async fn request_stop(&self, worker_ref: &WorkerRef) {
let WorkerRef::Local { pid } = worker_ref;
#[cfg(unix)]
fabro_proc::sigterm_process_group(*pid);
#[cfg(not(unix))]
let _ = pid;
}
async fn force_stop(&self, worker_ref: &WorkerRef) {
let WorkerRef::Local { pid } = worker_ref;
#[cfg(unix)]
fabro_proc::sigkill_process_group(*pid);
#[cfg(not(unix))]
let _ = pid;
}
async fn is_alive(&self, worker_ref: &WorkerRef) -> bool {
let WorkerRef::Local { pid } = worker_ref;
#[cfg(unix)]
{
fabro_proc::process_group_alive(*pid)
}
#[cfg(not(unix))]
{
fabro_proc::process_running(*pid)
}
}
}