Skip to content

Commit e4484aa

Browse files
author
Marco Napetti
committed
feat: sandbox shim injection and firma-secret-shim binary
1 parent 6b78e4b commit e4484aa

5 files changed

Lines changed: 632 additions & 100 deletions

File tree

crates/firma-run/src/runtime/mod.rs

Lines changed: 122 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
use std::collections::BTreeMap;
2-
use std::path::Path;
3-
use std::path::PathBuf;
4-
use std::time::Duration;
1+
use std::{
2+
collections::BTreeMap,
3+
env,
4+
ffi::{OsStr, OsString},
5+
fs,
6+
path::{Path, PathBuf},
7+
time::Duration,
8+
};
59

610
use firma_runtime_state::RuntimeLayout;
711
use firma_secret_provider::IntegrationSpec;
@@ -24,6 +28,10 @@ const DEFAULT_SIDECAR_STARTUP_TIMEOUT_SECS: u64 = 10;
2428
#[doc(hidden)]
2529
pub mod vscode;
2630

31+
/// Secret-mediation shim injection (Unix/bwrap). Wires the broker + shim mounts
32+
/// into a launch when the profile lists `secret_providers`.
33+
mod secret_shims;
34+
2735
/// Lib-level input for [`execute_run`]. The CLI layer (in the `firma`
2836
/// host crate) builds this from its `clap`-derived args struct.
2937
#[derive(Debug, Clone)]
@@ -226,8 +234,14 @@ pub fn execute_run(args: &RunInput, hooks: &LaunchHooks<'_>) -> Result<i32, RunE
226234
if let CapabilitySource::File { ref path } = profile.capability.source {
227235
flags.capability_seed_path = Some(path.clone());
228236
}
229-
let firma_exe = std::env::current_exe()
237+
let firma_exe = env::current_exe()
230238
.map_err(|e| RunError::Internal(format!("resolve current_exe: {e}")))?;
239+
// Start the secret services before the Sidecar so its gateway address
240+
// is available while the Sidecar config is synthesized.
241+
let gateway_binding = secret_shims::pre_bind_gateway(handle_ref, &profile)?;
242+
if let Some(binding) = &gateway_binding {
243+
flags.secret_gateway_addr = Some(binding.addr.clone());
244+
}
231245
let mut prompt = crate::authority::StdAuthorityPrompt;
232246
let authority = crate::routing::resolve_authority(
233247
ResolveAuthorityRequest {
@@ -320,6 +334,14 @@ pub fn execute_run(args: &RunInput, hooks: &LaunchHooks<'_>) -> Result<i32, RunE
320334
.ok_or_else(|| RunError::Internal("sandbox handle missing".to_string()))?;
321335
vscode::ensure_vscode_state_mount(handle_mut, &state_dir);
322336
}
337+
secret_shims::prepare(
338+
&mut handle,
339+
&profile,
340+
&mut env,
341+
&firma_exe,
342+
env::var_os("PATH").as_deref(),
343+
gateway_binding.as_ref(),
344+
)?;
323345
let launch = LaunchSpec {
324346
executable,
325347
args: launch_args,
@@ -386,7 +408,7 @@ fn log_run_start(identity: &RunIdentity, profile: &ResolvedProfile) {
386408
}
387409

388410
fn resolve_working_dir() -> Result<PathBuf, RunError> {
389-
std::env::current_dir()
411+
env::current_dir()
390412
.map_err(|error| RunError::Internal(format!("failed to read current directory: {error}")))
391413
}
392414

@@ -404,7 +426,7 @@ fn combine_run_and_teardown_results(
404426
}
405427

406428
fn ensure_required_session_identity() -> Result<(), RunError> {
407-
let require = std::env::var("FIRMA_RUN_REQUIRE_SESSION_ID")
429+
let require = env::var("FIRMA_RUN_REQUIRE_SESSION_ID")
408430
.ok()
409431
.is_some_and(|v| {
410432
let v = v.trim().to_ascii_lowercase();
@@ -413,7 +435,7 @@ fn ensure_required_session_identity() -> Result<(), RunError> {
413435
if !require {
414436
return Ok(());
415437
}
416-
let has_session = std::env::var("FIRMA_RUN_SESSION_ID")
438+
let has_session = env::var("FIRMA_RUN_SESSION_ID")
417439
.ok()
418440
.is_some_and(|v| !v.trim().is_empty());
419441
if has_session {
@@ -429,9 +451,9 @@ fn maybe_apply_executable_policy(
429451
executable: &str,
430452
args: Vec<String>,
431453
) -> Vec<String> {
432-
let executable = std::path::Path::new(executable)
454+
let executable = Path::new(executable)
433455
.file_name()
434-
.and_then(std::ffi::OsStr::to_str)
456+
.and_then(OsStr::to_str)
435457
.unwrap_or_default()
436458
.to_string();
437459
let Some(policy) = profile.executable_policies.get(&executable) else {
@@ -507,6 +529,75 @@ fn config_item_matches_key(item: &str, key: &str) -> bool {
507529
item.split_once('=').is_some_and(|(k, _)| k.trim() == key)
508530
}
509531

532+
fn resolve_host_executable(
533+
executable: &str,
534+
host_path: Option<&OsStr>,
535+
) -> Result<PathBuf, RunError> {
536+
let candidate = PathBuf::from(executable);
537+
if candidate
538+
.parent()
539+
.is_some_and(|parent| !parent.as_os_str().is_empty())
540+
{
541+
return require_file(candidate, executable);
542+
}
543+
544+
let path_value = host_path
545+
.map(OsString::from)
546+
.or_else(|| env::var_os("PATH"))
547+
.ok_or_else(|| {
548+
RunError::ConfigValidation(format!(
549+
"cannot resolve executable '{executable}' because host PATH is not set"
550+
))
551+
})?;
552+
for dir in env::split_paths(&path_value) {
553+
for candidate in executable_search_candidates(&dir, executable) {
554+
if candidate.is_file() {
555+
return Ok(candidate);
556+
}
557+
}
558+
}
559+
Err(RunError::ConfigValidation(format!(
560+
"cannot resolve executable '{executable}' on host PATH"
561+
)))
562+
}
563+
564+
fn require_file(candidate: PathBuf, executable: &str) -> Result<PathBuf, RunError> {
565+
if candidate.is_file() {
566+
Ok(candidate)
567+
} else {
568+
Err(RunError::ConfigValidation(format!(
569+
"cannot resolve executable '{executable}' at {}",
570+
candidate.display()
571+
)))
572+
}
573+
}
574+
575+
#[cfg(windows)]
576+
fn executable_search_candidates(dir: &Path, executable: &str) -> Vec<PathBuf> {
577+
let direct = dir.join(executable);
578+
if Path::new(executable).extension().is_some() {
579+
return vec![direct];
580+
}
581+
582+
let mut candidates = vec![direct];
583+
let path_ext = env::var_os("PATHEXT").map_or_else(
584+
|| ".COM;.EXE;.BAT;.CMD".to_string(),
585+
|value| value.to_string_lossy().into_owned(),
586+
);
587+
candidates.extend(
588+
path_ext
589+
.split(';')
590+
.filter(|extension| !extension.is_empty())
591+
.map(|extension| dir.join(format!("{executable}{extension}"))),
592+
);
593+
candidates
594+
}
595+
596+
#[cfg(not(windows))]
597+
fn executable_search_candidates(dir: &Path, executable: &str) -> Vec<PathBuf> {
598+
vec![dir.join(executable)]
599+
}
600+
510601
/// Resolve `executable` to its canonical UTF-8 path, enforce the configured
511602
/// allowlist policy, and return the canonical string.
512603
///
@@ -517,7 +608,7 @@ fn resolve_governed_executable(
517608
mediator: &crate::config::CommandMediatorConfig,
518609
executable: &str,
519610
) -> Result<String, RunError> {
520-
let canonical_path = std::fs::canonicalize(executable).map_err(|error| {
611+
let canonical_path = fs::canonicalize(executable).map_err(|error| {
521612
RunError::Governance(format!(
522613
"executable '{executable}' could not be resolved (fail-closed): {error}"
523614
))
@@ -566,7 +657,7 @@ fn build_execution_env(
566657
let mut env = BTreeMap::new();
567658

568659
for key in &profile.env_passthrough {
569-
if let Ok(value) = std::env::var(key) {
660+
if let Ok(value) = env::var(key) {
570661
env.insert(key.clone(), value);
571662
}
572663
}
@@ -640,9 +731,9 @@ fn maybe_apply_claude_settings(
640731
return Ok(args);
641732
}
642733

643-
let executable = std::path::Path::new(executable)
734+
let executable = Path::new(executable)
644735
.file_name()
645-
.and_then(std::ffi::OsStr::to_str)
736+
.and_then(OsStr::to_str)
646737
.unwrap_or_default();
647738
if executable != "claude" {
648739
return Ok(args);
@@ -664,7 +755,7 @@ fn maybe_apply_claude_settings(
664755
"failed to serialize Claude settings payload: {error}"
665756
))
666757
})?;
667-
std::fs::write(&settings_path, serialized).map_err(|error| {
758+
fs::write(&settings_path, serialized).map_err(|error| {
668759
RunError::Internal(format!(
669760
"failed to write Claude settings file {}: {error}",
670761
settings_path.display()
@@ -703,14 +794,14 @@ fn build_appended_ca_bundle(firma_ca_path: &Path) -> Option<PathBuf> {
703794
/// paths and concatenates the first existing one with the firma CA.
704795
fn build_appended_ca_bundle_with_roots(firma_ca_path: &Path, roots: &[PathBuf]) -> Option<PathBuf> {
705796
let system_roots = roots.iter().find(|p| p.is_file())?;
706-
let mut bundle = match std::fs::read(system_roots) {
797+
let mut bundle = match fs::read(system_roots) {
707798
Ok(bytes) => bytes,
708799
Err(error) => {
709800
tracing::warn!(%error, path = %system_roots.display(), "failed to read system CA bundle; using sole firma-ca");
710801
return None;
711802
}
712803
};
713-
let firma_ca = match std::fs::read(firma_ca_path) {
804+
let firma_ca = match fs::read(firma_ca_path) {
714805
Ok(bytes) => bytes,
715806
Err(error) => {
716807
tracing::warn!(%error, path = %firma_ca_path.display(), "failed to read firma-ca; using sole firma-ca");
@@ -722,7 +813,7 @@ fn build_appended_ca_bundle_with_roots(firma_ca_path: &Path, roots: &[PathBuf])
722813
}
723814
bundle.extend_from_slice(&firma_ca);
724815
let bundle_path = firma_ca_path.with_file_name("firma-ca-bundle.crt");
725-
if let Err(error) = std::fs::write(&bundle_path, &bundle) {
816+
if let Err(error) = fs::write(&bundle_path, &bundle) {
726817
tracing::warn!(%error, path = %bundle_path.display(), "failed to write combined CA bundle; using sole firma-ca");
727818
return None;
728819
}
@@ -761,7 +852,7 @@ fn resolve_sidecar_ca_cert_path(network_overrides: &BTreeMap<String, String>) ->
761852
}
762853
}
763854

764-
if let Ok(explicit) = std::env::var("FIRMA_SIDECAR_CA_CERT_PATH")
855+
if let Ok(explicit) = env::var("FIRMA_SIDECAR_CA_CERT_PATH")
765856
&& !explicit.trim().is_empty()
766857
{
767858
let path = PathBuf::from(explicit);
@@ -770,7 +861,7 @@ fn resolve_sidecar_ca_cert_path(network_overrides: &BTreeMap<String, String>) ->
770861
}
771862
}
772863

773-
if let Ok(ca_dir) = std::env::var("FIRMA_SIDECAR_CA_DIR")
864+
if let Ok(ca_dir) = env::var("FIRMA_SIDECAR_CA_DIR")
774865
&& !ca_dir.trim().is_empty()
775866
{
776867
let path = PathBuf::from(ca_dir).join("firma-ca.crt");
@@ -779,7 +870,7 @@ fn resolve_sidecar_ca_cert_path(network_overrides: &BTreeMap<String, String>) ->
779870
}
780871
}
781872

782-
let cwd_candidate = std::env::current_dir()
873+
let cwd_candidate = env::current_dir()
783874
.ok()
784875
.map(|cwd| cwd.join("firma-ca").join("firma-ca.crt"));
785876
let default_candidates = [
@@ -810,7 +901,7 @@ fn print_effective_config(
810901
agent_id: &identity.agent_id,
811902
execution_profile: &identity.execution_profile,
812903
profile,
813-
working_dir: std::env::current_dir().map_err(|error| {
904+
working_dir: env::current_dir().map_err(|error| {
814905
RunError::Internal(format!(
815906
"failed to resolve working dir for snapshot: {error}"
816907
))
@@ -827,7 +918,7 @@ fn print_effective_config(
827918
mod tests {
828919
use std::collections::{BTreeMap, BTreeSet};
829920
use std::fs;
830-
use std::path::PathBuf;
921+
use std::path::{Path, PathBuf};
831922
use std::time::Duration;
832923

833924
use firma_config_loader::CONFIG_FILE_NAME;
@@ -857,15 +948,15 @@ mod tests {
857948
fn appended_ca_bundle_concatenates_system_roots_and_firma_ca() {
858949
let dir = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
859950
let system = dir.path().join("system-roots.pem");
860-
std::fs::write(&system, b"-----SYSTEM ROOT-----\n").unwrap_or_else(|e| panic!("{e}"));
951+
fs::write(&system, b"-----SYSTEM ROOT-----\n").unwrap_or_else(|e| panic!("{e}"));
861952
let firma_ca = dir.path().join("firma-ca.crt");
862-
std::fs::write(&firma_ca, b"-----FIRMA CA-----\n").unwrap_or_else(|e| panic!("{e}"));
953+
fs::write(&firma_ca, b"-----FIRMA CA-----\n").unwrap_or_else(|e| panic!("{e}"));
863954

864955
let bundle =
865956
super::build_appended_ca_bundle_with_roots(&firma_ca, std::slice::from_ref(&system))
866957
.unwrap_or_else(|| panic!("bundle should be built"));
867958
assert_eq!(bundle, dir.path().join("firma-ca-bundle.crt"));
868-
let body = std::fs::read_to_string(&bundle).unwrap_or_else(|e| panic!("{e}"));
959+
let body = fs::read_to_string(&bundle).unwrap_or_else(|e| panic!("{e}"));
869960
assert!(body.contains("SYSTEM ROOT"));
870961
assert!(body.contains("FIRMA CA"));
871962
}
@@ -874,7 +965,7 @@ mod tests {
874965
fn appended_ca_bundle_falls_back_when_no_system_roots() {
875966
let dir = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
876967
let firma_ca = dir.path().join("firma-ca.crt");
877-
std::fs::write(&firma_ca, b"-----FIRMA CA-----\n").unwrap_or_else(|e| panic!("{e}"));
968+
fs::write(&firma_ca, b"-----FIRMA CA-----\n").unwrap_or_else(|e| panic!("{e}"));
878969
let missing = dir.path().join("does-not-exist.pem");
879970
assert!(super::build_appended_ca_bundle_with_roots(&firma_ca, &[missing]).is_none());
880971
}
@@ -1074,7 +1165,7 @@ mod tests {
10741165
fn ca_trust_mode_selects_appended_bundle_at_injection_site() {
10751166
let dir = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
10761167
let ca_cert = dir.path().join("firma-ca.crt");
1077-
std::fs::write(&ca_cert, b"-----FIRMA CA-----\n").unwrap_or_else(|e| panic!("{e}"));
1168+
fs::write(&ca_cert, b"-----FIRMA CA-----\n").unwrap_or_else(|e| panic!("{e}"));
10781169

10791170
let make_profile = |mode: crate::config::CaTrustMode| ResolvedProfile {
10801171
id: "copilot".to_string(),
@@ -1151,15 +1242,15 @@ mod tests {
11511242
let bundle_path = ca_cert.with_file_name("firma-ca-bundle.crt");
11521243
let system_roots_present = super::SYSTEM_CA_BUNDLE_CANDIDATES
11531244
.iter()
1154-
.any(|p| std::path::Path::new(p).is_file());
1245+
.any(|p| Path::new(p).is_file());
11551246
if system_roots_present {
11561247
// A system bundle exists: AppendSystemRoots must point at the
11571248
// generated sibling bundle, and it must contain the firma CA.
11581249
assert_eq!(
11591250
append_env.get("SSL_CERT_FILE"),
11601251
Some(&bundle_path.display().to_string())
11611252
);
1162-
let body = std::fs::read_to_string(&bundle_path).unwrap_or_else(|e| panic!("{e}"));
1253+
let body = fs::read_to_string(&bundle_path).unwrap_or_else(|e| panic!("{e}"));
11631254
assert!(body.contains("FIRMA CA"));
11641255
assert_ne!(
11651256
append_env.get("SSL_CERT_FILE"),
@@ -1722,7 +1813,7 @@ mod tests {
17221813
let real_code = host_bin.join("code");
17231814
fs::write(&real_code, "#!/bin/sh\nexit 0\n").unwrap_or_else(|e| panic!("{e}"));
17241815

1725-
let resolved = super::vscode::resolve_host_executable("code", Some(host_bin.as_os_str()))
1816+
let resolved = super::resolve_host_executable("code", Some(host_bin.as_os_str()))
17261817
.unwrap_or_else(|e| panic!("{e}"));
17271818
assert_eq!(resolved, real_code);
17281819
}

0 commit comments

Comments
 (0)