refactor: tidy the in-process sandbox provider wiring

Load the Daytona key for fork and prune through one AppState method
instead of two copied vault reads, and pass the sandbox configuration
into runtime_spec rather than building it and overwriting it. The
worker reuses the CLI's process_env_var lookup.

Share one Docker availability check and the backend-requirement
variable through fabro-test, drop the built-in plugin path and pin
constants nothing reads any more, and let enabled_plugins() exclude the
bundled kinds itself. Refresh the comments and the spawn_env test that
still described built-in plugins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Scott Werner 2026-09-25 11:29:29 -04:00
parent 6ac6d5495f
commit 8a09e8fa08
16 changed files with 115 additions and 173 deletions

View file

@ -618,9 +618,9 @@ async fn runtime_spec(
.read()
.await
.get(EnvVars::DAYTONA_API_KEY)
.map(|key| DaytonaCredentials::from_api_key(key.to_owned(), provider_env));
.map(|key| DaytonaCredentials::from_api_key(key.to_owned(), crate::process_env_var));
Ok(RuntimeSpec {
sandbox: SandboxProviderConfig::from_lookup(daytona, provider_env),
sandbox: SandboxProviderConfig::from_lookup(daytona, crate::process_env_var),
settings_toml: None,
mcp_catalog_toml: None,
model_client,
@ -629,12 +629,3 @@ async fn runtime_spec(
run_tools,
})
}
/// Non-secret provider selection inherited from the server.
#[expect(
clippy::disallowed_methods,
reason = "worker boundary snapshots inherited provider selection"
)]
fn provider_env(name: &str) -> Option<String> {
std::env::var(name).ok()
}

View file

@ -182,9 +182,9 @@ impl miette::Diagnostic for CliDiagnostic {
#[expect(
clippy::disallowed_methods,
reason = "CLI main reads documented process-env controls before telemetry and worker dispatch."
reason = "CLI main reads documented process-env controls before telemetry and worker dispatch, and the worker snapshots inherited sandbox provider selection."
)]
fn process_env_var(name: &str) -> Option<String> {
pub(crate) fn process_env_var(name: &str) -> Option<String> {
std::env::var(name).ok()
}

View file

@ -1752,10 +1752,10 @@ async fn built_in_host_runs_and_prunes_without_plugins() {
let server = RunningServer::start_with_env("", &[], &[
(EnvVars::PATH, path),
(
EnvVars::PETRI_SANDBOX_HOST_PLUGIN,
"PETRI_SANDBOX_HOST_PLUGIN",
"/nonexistent/sandbox-driver-host",
),
(EnvVars::PETRI_SANDBOX_HOST_SHA256, "invalid-pin"),
("PETRI_SANDBOX_HOST_SHA256", "invalid-pin"),
(EnvVars::PETRI_SANDBOX_PLUGIN_DEV, "0"),
])
.await;

View file

@ -17,7 +17,6 @@
clippy::disallowed_methods,
reason = "these scenarios inspect backend availability and drive the Docker daemon with its CLI"
)]
#![expect(clippy::print_stderr, reason = "a skipped test says why on its stderr")]
use std::env;
use std::path::{Path, PathBuf};
@ -37,29 +36,10 @@ use super::petri::{
use crate::support::TEST_DEV_TOKEN;
/// The server-side environment the runs select.
const REQUIRE_ENV: &str = "FABRO_REQUIRE_SANDBOX_BACKENDS";
const ENVIRONMENT: &str = "docker";
/// The twin's model, for the Ask Fabro session.
const MODEL: &str = "gpt-5.4";
/// A reachable Docker daemon. CI requires the backend instead of skipping.
fn docker_available() -> bool {
let daemon = Command::new("docker")
.args(["version", "--format", "{{.Server.Version}}"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok_and(|status| status.success());
if !daemon {
assert!(
env::var_os(REQUIRE_ENV).is_none(),
"{REQUIRE_ENV} is set, but no Docker daemon answers"
);
eprintln!("skipping: no Docker daemon answers");
}
daemon
}
/// A server with a Docker environment beside the default local one.
async fn docker_server() -> RunningServer {
docker_server_with("", &[]).await
@ -274,7 +254,7 @@ fn restore_actions(server: &RunningServer, run_id: &str) -> Vec<String> {
/// nothing of the workspace is on the host.
#[tokio::test(flavor = "multi_thread")]
async fn a_docker_run_publishes_every_stages_checkpoint_from_the_container() {
if !docker_available() {
if !fabro_test::docker_available() {
return;
}
let context = test_context!();
@ -305,7 +285,7 @@ async fn a_docker_run_publishes_every_stages_checkpoint_from_the_container() {
/// the second stage sees the first stage's files and nothing else.
#[tokio::test(flavor = "multi_thread")]
async fn a_retained_container_whose_workspace_drifted_is_reset_on_restart() {
if !docker_available() {
if !fabro_test::docker_available() {
return;
}
let context = test_context!();
@ -350,7 +330,7 @@ async fn a_retained_container_whose_workspace_drifted_is_reset_on_restart() {
/// repository, and the second stage sees the first stage's files.
#[tokio::test(flavor = "multi_thread")]
async fn a_lost_container_is_replaced_and_its_workspace_restored_from_the_snapshot() {
if !docker_available() {
if !fabro_test::docker_available() {
return;
}
let context = test_context!();
@ -443,7 +423,7 @@ async fn question_inputs(twin: &fabro_test::TwinOpenAi, namespace: &str) -> Vec<
/// follow-up request carries the file's content back as the tool's answer.
#[tokio::test(flavor = "multi_thread")]
async fn an_ask_fabro_turn_reads_a_file_inside_the_runs_container() {
if !docker_available() {
if !fabro_test::docker_available() {
return;
}
let context = test_context!();

View file

@ -17,14 +17,11 @@
use std::path::Path;
use std::process::{Command, Stdio};
use fabro_test::{TestContext, expect_reqwest_status};
use fabro_test::{REQUIRE_SANDBOX_BACKENDS, TestContext, expect_reqwest_status};
use serde_json::json;
use crate::cmd::support::server_endpoint;
/// Set in CI so a missing daemon or image fails the test instead of
/// skipping it.
const REQUIRE_ENV: &str = "FABRO_REQUIRE_SANDBOX_BACKENDS";
const DOCKER_IMAGE: &str = "buildpack-deps:noble";
/// The environment id the scenario selects with `--environment`.
pub(crate) const ENVIRONMENT: &str = "docker";
@ -33,11 +30,11 @@ pub(crate) const ENVIRONMENT: &str = "docker";
/// [`DOCKER_IMAGE`]. Returns the environment id, or `None` when the
/// prerequisites are missing and the test should skip.
pub(crate) fn configure(context: &mut TestContext) -> Option<&'static str> {
let required = std::env::var_os(REQUIRE_ENV).is_some();
let required = std::env::var_os(REQUIRE_SANDBOX_BACKENDS).is_some();
if !docker_image_available() {
assert!(
!required,
"{REQUIRE_ENV} is set but no Docker daemon with {DOCKER_IMAGE} is available"
"{REQUIRE_SANDBOX_BACKENDS} is set but no Docker daemon with {DOCKER_IMAGE} is available"
);
eprintln!("skipping: no Docker daemon with {DOCKER_IMAGE}");
return None;

View file

@ -233,7 +233,12 @@ pub(crate) async fn check_prepared_manifest(
None,
);
let dry_run = prepared.settings.run.execution.mode == RunMode::DryRun;
let runtime = petri_runs::runtime_spec(state, ready_providers, dry_run);
let runtime = petri_runs::runtime_spec(
state,
ready_providers,
dry_run,
state.sandbox_provider_config(None),
);
let has_ready_provider = !ready_providers.is_empty();
let prepared = prepared.clone();
task::spawn_blocking(move || {
@ -1530,7 +1535,12 @@ mod tests {
None,
None,
);
let runtime = crate::server::petri_runs::runtime_spec(state, ready_providers, false);
let runtime = crate::server::petri_runs::runtime_spec(
state,
ready_providers,
false,
state.sandbox_provider_config(None),
);
validate_prepared_manifest(
prepared,
&HashMap::new(),

View file

@ -144,7 +144,7 @@ pub(crate) enum ConnectError {
/// a run's host sandbox is reached through the run's own registry by
/// [`attach_run_sandbox`].
/// `docker` connects to the daemon the process environment names, the
/// same variables Petri hands its Docker plugin, without requiring the
/// same variables Fabro forwards to its worker, without requiring the
/// daemon to answer: `health` reports an unreachable daemon so preflight
/// and the doctor see the cause. `daytona` needs the vault key. Any other
/// kind launches the plugin executable its settings name and supervises

View file

@ -1558,6 +1558,25 @@ impl AppState {
)
}
/// [`Self::sandbox_provider_config`] with the Daytona key read from the
/// vault, for server-side fork and prune; a secret store failure is a
/// 500.
pub(crate) async fn load_sandbox_provider_config(
&self,
) -> Result<SandboxProviderConfig, ApiError> {
let daytona_api_key = self
.vault_secret(EnvVars::DAYTONA_API_KEY)
.await
.map_err(|err| {
error!(error = ?err, "Loading sandbox credentials failed");
ApiError::new(
StatusCode::INTERNAL_SERVER_ERROR,
"secret store operation failed",
)
})?;
Ok(self.sandbox_provider_config(daytona_api_key))
}
/// Everything a reconnect needs to reach a run's provider: the server's
/// provider settings and the Daytona credentials from the vault (`None`
/// when no key is stored).
@ -2888,18 +2907,9 @@ async fn delete_run_sandbox_resource(
.run_scratch(&id)
.root()
.join("petri");
let daytona_api_key = state
.vault_secret(EnvVars::DAYTONA_API_KEY)
.await
.map_err(|err| {
error!(error = ?err, "Loading sandbox credentials failed");
ApiError::new(
StatusCode::INTERNAL_SERVER_ERROR,
"secret store operation failed",
)
})?;
let sandbox = state.load_sandbox_provider_config().await?;
let report = prune::prune(PruneRequest {
sandbox: state.sandbox_provider_config(daytona_api_key),
sandbox,
run_id: id.to_string(),
run_dir,
store: state.petri_runs.shared_store(),

View file

@ -26,7 +26,6 @@ use fabro_petri::SqliteRunStore;
use fabro_petri::fork::{self as petri_fork, ForkError, ForkRequest};
use fabro_petri::petri::RunStore;
use fabro_petri::platform_records::SqlitePlatformRecords;
use fabro_static::EnvVars;
use fabro_store::{PlatformRecordKind, RunProjection};
use fabro_types::{FailureReason, Principal, RunId};
use fabro_util::error as error_util;
@ -282,16 +281,7 @@ async fn fork_at(
.await
.map_err(|err| fork_error(&err))?;
let daytona_api_key = state
.vault_secret(EnvVars::DAYTONA_API_KEY)
.await
.map_err(|err| {
error!(error = ?err, "Loading sandbox credentials failed");
ApiError::new(
StatusCode::INTERNAL_SERVER_ERROR,
"secret store operation failed",
)
})?;
let sandbox = state.load_sandbox_provider_config().await?;
let new_run_id = RunId::new();
let storage = Storage::new(state.server_storage_dir());
let source_run_dir = storage.run_scratch(&id).root().to_path_buf();
@ -310,7 +300,7 @@ async fn fork_at(
.map_err(workflow_operation_error)?;
let seeded = petri_fork::fork(ForkRequest {
sandbox: state.sandbox_provider_config(daytona_api_key),
sandbox,
source: id,
fork: new_run_id,
source_run_dir: source_run_dir.join("petri"),

View file

@ -46,6 +46,7 @@ use fabro_petri::hooks::HooksSpec;
use fabro_petri::interview::{Approval, FabroInterviewer};
use fabro_petri::petri::{Access, Digest, LogId, Record, RunKey, RunLogs, RunStore, StoreError};
use fabro_petri::platform_records::SqlitePlatformRecords;
use fabro_petri::providers::SandboxProviderConfig;
use fabro_petri::recovery::{self, Recovery, RecoveryRequest};
use fabro_petri::runtime::{self, RuntimeSpec};
use fabro_petri::secrets::VaultSecrets;
@ -73,11 +74,12 @@ use crate::run_compiler::{AdmittedRun, PreparedRun, RunCompilerError};
/// The runtime Petri gets, at create and at execution: the server's run
/// defaults and environment catalog as the settings layer, the MCP
/// catalog, the model client over the server's catalog and credentials for
/// the eligible providers, and the run mode.
/// the eligible providers, the sandbox providers, and the run mode.
pub(crate) fn runtime_spec(
state: &AppState,
eligible: &[ProviderId],
dry_run: bool,
sandbox: SandboxProviderConfig,
) -> RuntimeSpec {
let settings_toml = settings_layer_toml(state);
let mcp_catalog_toml = mcp_catalog_toml(&state.mcp_server_store().catalog_settings());
@ -95,7 +97,7 @@ pub(crate) fn runtime_spec(
}
};
RuntimeSpec {
sandbox: state.sandbox_provider_config(None),
sandbox,
settings_toml,
mcp_catalog_toml,
model_client,
@ -276,7 +278,12 @@ pub(crate) async fn admit(
settings,
prepared.vars(),
launch,
runtime_spec(state, eligible, dry_run),
runtime_spec(
state,
eligible,
dry_run,
state.sandbox_provider_config(None),
),
false,
)
.map_err(RunCompilerError::Workflow)?;
@ -458,9 +465,12 @@ pub(crate) async fn execute(state: Arc<AppState>, run_id: RunId) {
))),
&run_state.spec.settings.run,
);
let mut runtime = runtime_spec(&state, &eligible, dry_run);
runtime.sandbox =
state.sandbox_provider_config(vault.get(EnvVars::DAYTONA_API_KEY).map(str::to_owned));
let runtime = runtime_spec(
&state,
&eligible,
dry_run,
state.sandbox_provider_config(vault.get(EnvVars::DAYTONA_API_KEY).map(str::to_owned)),
);
let request = RunRequest {
run_id: run_id.to_string(),
run_dir: run_dir.join("petri"),

View file

@ -115,9 +115,6 @@ pub(crate) fn sandbox_plugin_env(
let mut env = Vec::new();
let mut dev = false;
for (kind, plugin) in providers.enabled_plugins() {
if kind.bundled().is_some() {
continue;
}
let upper = kind.as_str().to_ascii_uppercase().replace('-', "_");
if let Some(path) = &plugin.path {
env.push((format!("PETRI_SANDBOX_{upper}_PLUGIN"), path.clone()));
@ -403,7 +400,7 @@ mod tests {
/// while built-in paths and pins and disabled third-party plugins never
/// cross.
#[tokio::test]
async fn configured_plugins_reach_the_worker_and_win_over_ambient_variables() {
async fn third_party_plugins_reach_the_worker_and_built_ins_never_do() {
let mut providers = ServerSandboxProvidersSettings::default();
providers.entries.extend([
provider("e2b", true, SandboxPluginSettings {
@ -422,23 +419,13 @@ mod tests {
..SandboxPluginSettings::default()
}),
]);
let env = HashMap::from([
("PATH".to_string(), "/bin".to_string()),
(
"PETRI_SANDBOX_HOST_PLUGIN".to_string(),
"/ambient/sandbox-driver-host".to_string(),
),
(
"PETRI_SANDBOX_DOCKER_PLUGIN".to_string(),
"/ambient/sandbox-driver-docker".to_string(),
),
(
"PETRI_SANDBOX_DAYTONA_PLUGIN".to_string(),
"/ambient/sandbox-driver-daytona".to_string(),
),
]);
let mut env = env;
let mut env = HashMap::from([("PATH".to_string(), "/bin".to_string())]);
for kind in ["HOST", "DOCKER", "DAYTONA"] {
let lower = kind.to_ascii_lowercase();
env.insert(
format!("PETRI_SANDBOX_{kind}_PLUGIN"),
format!("/ambient/sandbox-driver-{lower}"),
);
env.insert(
format!("PETRI_SANDBOX_{kind}_SHA256"),
"invalid-pin".to_string(),

View file

@ -13,10 +13,8 @@
clippy::disallowed_methods,
reason = "the tests inspect backend availability through the process environment"
)]
#![expect(clippy::print_stderr, reason = "a skipped test says why on its stderr")]
use std::collections::BTreeMap;
use std::env;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::Arc;
@ -43,8 +41,6 @@ use crate::helpers::{
test_settings, wait_for_run_status,
};
const REQUIRE_ENV: &str = "FABRO_REQUIRE_SANDBOX_BACKENDS";
const OPENAI_MODEL: &str = "gpt-5.4";
/// A command-only workflow: one script stage between start and exit.
@ -103,24 +99,6 @@ const PARALLEL_DOT: &str = r#"digraph Parallel {
pub(super) const PLAIN_SETTINGS: &str = "_version = 1\n\n[workflow]\ngraph = \"workflow.fabro\"\n";
/// A reachable Docker daemon. CI requires the backend instead of skipping.
fn docker_available() -> bool {
let daemon = Command::new("docker")
.args(["version", "--format", "{{.Server.Version}}"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok_and(|status| status.success());
if !daemon {
assert!(
env::var_os(REQUIRE_ENV).is_none(),
"{REQUIRE_ENV} is set, but no Docker daemon answers"
);
eprintln!("skipping: no Docker daemon answers");
}
daemon
}
/// Register a version whose entrypoint is `workflow.fabro`, with the given
/// files beside it.
pub(super) async fn register_version(app: &axum::Router, files: &[(&str, &str)]) -> String {
@ -892,7 +870,7 @@ async fn a_delete_right_after_the_run_reads_ended_is_accepted() {
/// attaches to it on the daemon.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_runs_projection_carries_its_docker_sandbox_instance() {
if !docker_available() {
if !fabro_test::docker_available() {
return;
}
let settings = settings_from_toml("_version = 1\n\n[run.environment]\nid = \"docker\"\n");
@ -1009,7 +987,7 @@ async fn a_runs_projection_carries_its_docker_sandbox_instance() {
/// run label.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_server_attaches_to_the_container_petri_created() {
if !docker_available() {
if !fabro_test::docker_available() {
return;
}
let twin = twin_openai().await;
@ -1368,7 +1346,7 @@ async fn a_bundle_naming_a_catalog_environment_runs_on_docker_with_its_image() {
graph["params"]["fabro.launch"]
);
if !docker_available() {
if !fabro_test::docker_available() {
return;
}
start_run(&app, &run_id).await;

View file

@ -11,7 +11,6 @@
clippy::disallowed_methods,
reason = "the tests inspect backend availability and read the workspace's history with git"
)]
#![expect(clippy::print_stderr, reason = "a skipped test says why on its stderr")]
use std::collections::BTreeMap;
use std::env;
@ -44,8 +43,6 @@ use tokio_util::sync::CancellationToken;
mod support;
const REQUIRE_ENV: &str = "FABRO_REQUIRE_SANDBOX_BACKENDS";
/// A command-only bundle: the stage lines go between `start` and `exit`,
/// the edge lines after them.
fn workflow(stages: &str, edges: &str) -> String {
@ -81,24 +78,6 @@ fn admit(workflow: &str, settings: &str) -> AdmittedGraphs {
}
}
/// A reachable Docker daemon. CI requires the backend instead of skipping.
fn docker_available() -> bool {
let daemon = std::process::Command::new("docker")
.args(["version", "--format", "{{.Server.Version}}"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|status| status.success());
if !daemon {
assert!(
env::var_os(REQUIRE_ENV).is_none(),
"{REQUIRE_ENV} is set, but no Docker daemon answers"
);
eprintln!("skipping: no Docker daemon answers");
}
daemon
}
/// One run's pieces: the store, its platform records, where it ran.
struct Harness {
run_id: RunId,
@ -900,7 +879,7 @@ async fn parallel_branches_checkpoint_the_shared_workspace_in_turn() {
/// its ref, with the platform records naming the same commits.
#[tokio::test]
async fn a_docker_run_commits_inside_the_container_and_publishes_every_checkpoint() {
if !docker_available() {
if !fabro_test::docker_available() {
return;
}
assert_sandbox_run_publishes_every_checkpoint(SandboxProviderKind::DOCKER).await;

View file

@ -49,29 +49,16 @@ impl EnvVars {
pub const FABRO_WEB_URL: &'static str = "FABRO_WEB_URL";
pub const FABRO_WORKER_TOKEN: &'static str = "FABRO_WORKER_TOKEN";
// Petri's sandbox-driver plugins: where each provider's plugin executable
// is, its checksum override, dev mode for unpinned plugins, and how a
// remote Docker daemon's containers reach this machine. A run's worker
// resolves the plugins, so these cross into the worker process.
pub const PETRI_SANDBOX_HOST_PLUGIN: &'static str = "PETRI_SANDBOX_HOST_PLUGIN";
pub const PETRI_SANDBOX_HOST_SHA256: &'static str = "PETRI_SANDBOX_HOST_SHA256";
pub const PETRI_SANDBOX_DOCKER_PLUGIN: &'static str = "PETRI_SANDBOX_DOCKER_PLUGIN";
pub const PETRI_SANDBOX_DOCKER_SHA256: &'static str = "PETRI_SANDBOX_DOCKER_SHA256";
pub const PETRI_SANDBOX_DAYTONA_PLUGIN: &'static str = "PETRI_SANDBOX_DAYTONA_PLUGIN";
pub const PETRI_SANDBOX_DAYTONA_SHA256: &'static str = "PETRI_SANDBOX_DAYTONA_SHA256";
// Petri's sandbox settings: dev mode for unpinned third-party plugins,
// how a remote Docker daemon's containers reach this machine, and the
// action-host image. These cross into a run's worker process.
pub const PETRI_SANDBOX_PLUGIN_DEV: &'static str = "PETRI_SANDBOX_PLUGIN_DEV";
pub const PETRI_SANDBOX_DOCKER_HOST_ADDRESS: &'static str = "PETRI_SANDBOX_DOCKER_HOST_ADDRESS";
pub const PETRI_SANDBOX_ACTION_HOST_IMAGE: &'static str = "PETRI_SANDBOX_ACTION_HOST_IMAGE";
/// Every Petri plugin variable, in one list for the process boundaries
/// Every Petri sandbox variable, in one list for the process boundaries
/// that forward them.
pub const PETRI_SANDBOX_PLUGIN_VARS: &'static [&'static str] = &[
Self::PETRI_SANDBOX_HOST_PLUGIN,
Self::PETRI_SANDBOX_HOST_SHA256,
Self::PETRI_SANDBOX_DOCKER_PLUGIN,
Self::PETRI_SANDBOX_DOCKER_SHA256,
Self::PETRI_SANDBOX_DAYTONA_PLUGIN,
Self::PETRI_SANDBOX_DAYTONA_SHA256,
Self::PETRI_SANDBOX_PLUGIN_DEV,
Self::PETRI_SANDBOX_DOCKER_HOST_ADDRESS,
Self::PETRI_SANDBOX_ACTION_HOST_IMAGE,
@ -257,12 +244,6 @@ mod tests {
EnvVars::FABRO_VERBOSE,
EnvVars::FABRO_WEB_URL,
EnvVars::FABRO_WORKER_TOKEN,
EnvVars::PETRI_SANDBOX_HOST_PLUGIN,
EnvVars::PETRI_SANDBOX_HOST_SHA256,
EnvVars::PETRI_SANDBOX_DOCKER_PLUGIN,
EnvVars::PETRI_SANDBOX_DOCKER_SHA256,
EnvVars::PETRI_SANDBOX_DAYTONA_PLUGIN,
EnvVars::PETRI_SANDBOX_DAYTONA_SHA256,
EnvVars::PETRI_SANDBOX_PLUGIN_DEV,
EnvVars::PETRI_SANDBOX_DOCKER_HOST_ADDRESS,
EnvVars::PETRI_SANDBOX_ACTION_HOST_IMAGE,

View file

@ -155,6 +155,34 @@ pub fn require_env(name: &str) -> Option<String> {
}
}
/// Set in CI so a missing sandbox backend (a Docker daemon or image) fails
/// the test instead of skipping it.
pub const REQUIRE_SANDBOX_BACKENDS: &str = "FABRO_REQUIRE_SANDBOX_BACKENDS";
/// A reachable Docker daemon. When [`REQUIRE_SANDBOX_BACKENDS`] is set, a
/// missing daemon fails the test instead of skipping it.
#[must_use]
#[allow(
clippy::print_stderr,
reason = "Skip notices go to stderr so stdout stays assertable."
)]
pub fn docker_available() -> bool {
let daemon = std::process::Command::new("docker")
.args(["version", "--format", "{{.Server.Version}}"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|status| status.success());
if !daemon {
assert!(
std::env::var_os(REQUIRE_SANDBOX_BACKENDS).is_none(),
"{REQUIRE_SANDBOX_BACKENDS} is set, but no Docker daemon answers"
);
eprintln!("skipping: no Docker daemon answers");
}
daemon
}
/// Apply baseline environment isolation to a `Command` that spawns the
/// `fabro` binary (or a helper that will act like it).
///
@ -228,10 +256,10 @@ fn apply_test_isolation_with_lookup(
if let Some(path) = lookup(EnvVars::PATH) {
cmd.env(EnvVars::PATH, path);
}
// Petri resolves its sandbox-driver plugins from these, in the server a
// test starts and in the workers that server launches; a developer's
// plugin override reaches them like `PATH` does, and so does the Docker
// daemon selection the Docker plugin needs.
// Petri reads its sandbox settings from these, in the server a test
// starts and in the workers that server launches; a developer's
// override reaches them like `PATH` does, and so does the Docker daemon
// selection the Docker provider needs.
for name in EnvVars::PETRI_SANDBOX_PLUGIN_VARS
.iter()
.chain(EnvVars::DOCKER_VARS)

View file

@ -147,12 +147,13 @@ impl ServerSandboxProvidersSettings {
.map(|(kind, _)| kind)
}
/// Enabled kinds that are served by a plugin executable.
/// Enabled third-party kinds that are served by a plugin executable.
/// Bundled kinds run in process, so plugin settings on them are ignored.
pub fn enabled_plugins(
&self,
) -> impl Iterator<Item = (&SandboxProviderKind, &SandboxPluginSettings)> {
self.entries.iter().filter_map(|(kind, entry)| {
(entry.enabled)
(entry.enabled && kind.bundled().is_none())
.then_some(entry.plugin.as_ref())
.flatten()
.map(|plugin| (kind, plugin))