mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-16 04:33:16 +00:00
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>
188 lines
6.3 KiB
Rust
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)
|
|
}
|
|
}
|
|
}
|