Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions crates/firma-run/src/authority/supervisor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,11 @@ impl AuthoritySupervisor {
let mut tee_handle: Option<JoinHandle<()>> = None;
let mut last_error: Option<RunError> = None;
for attempt in 0..MAX_BIND_ATTEMPTS {
let listen_addr = select_loopback_v6_port()?;
authority_config.listen_addr = listen_addr.to_string();
let authority_conf_str = toml::to_string_pretty(&authority_config).map_err(|err| {
let inner = toml::to_string_pretty(&authority_config).map_err(|err| {
RunError::Internal(format!("invalid synthetic authority config: {err}"))
})?;
std::fs::write(&authority_toml, authority_conf_str).map_err(|e| {
let authority_conf_str = format!("[authority]\n{inner}");
std::fs::write(&authority_toml, &authority_conf_str).map_err(|e| {
RunError::Internal(format!("write {}: {e}", authority_toml.display()))
})?;

Expand Down Expand Up @@ -229,6 +228,8 @@ impl AuthoritySupervisor {
if attempt + 1 < MAX_BIND_ATTEMPTS {
std::thread::sleep(Duration::from_millis(120));
}
let listen_addr = select_loopback_v6_port()?;
authority_config.listen_addr = listen_addr.to_string();
}
let capture = capture.ok_or_else(|| {
last_error.unwrap_or_else(|| RunError::AuthorityStartupFailed {
Expand Down Expand Up @@ -359,6 +360,12 @@ fn resolve_persisted_paths(user_config: &std::path::Path) -> Result<AuthorityCon
.map_err(|e| RunError::Internal(format!("parse authority config: {e}")))?;
cfg.rebase_defaults(&config_dir);

// Per-run authority always runs plaintext on loopback — strip any TLS
// config from the user's persisted settings, and pick an ephemeral port
// so we never conflict with a long-running authority on the configured addr.
cfg.tls = firma_authority::AuthorityTlsConfig::default();
cfg.listen_addr = select_loopback_v6_port()?.to_string();

Ok(cfg)
}

Expand Down
28 changes: 28 additions & 0 deletions crates/firma-run/src/sidecar/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ pub fn synthesize(req: SynthesizeRequest<'_>) -> Result<TemplateSource, RunError
rebase_template_resource_paths(&mut value, dir)?;
}
override_interceptor(&mut value, req.socket_path, req.listen_addr)?;
// Pin ca.dir to the marker dir so the MITM CA cert lands where
// sidecar_trust_env_overrides expects it (<marker_dir>/firma-ca/).
// The default "./firma-ca/" is CWD-relative and would diverge when
// firma run's CWD differs from the marker dir.
override_ca_dir(&mut value, req.out_path)?;
if let Some(url) = req.authority_url {
override_authority_url(&mut value, url)?;
}
Expand Down Expand Up @@ -528,6 +533,29 @@ fn override_sidecar_mode(value: &mut toml::Value, mode: &str) -> Result<(), RunE
Ok(())
}

fn override_ca_dir(value: &mut toml::Value, out_path: &Path) -> Result<(), RunError> {
let marker_dir = out_path.parent().ok_or_else(|| {
RunError::Internal(format!(
"cannot resolve marker dir from synthesized config path {}",
out_path.display()
))
})?;
let ca_dir = marker_dir.join("firma-ca");
let root = value
.as_table_mut()
.ok_or_else(|| RunError::Internal("sidecar template root is not a table".into()))?;
let ca_table = root
.entry("ca".to_string())
.or_insert_with(|| toml::Value::Table(toml::value::Table::new()))
.as_table_mut()
.ok_or_else(|| RunError::Internal("[ca] is not a table".into()))?;
ca_table.insert(
"dir".to_string(),
toml::Value::String(ca_dir.display().to_string()),
);
Ok(())
}

/// Default the audit sink to a file at `audit_path` when the template did not
/// configure one. The per-run sidecar is spawned with a null stdout, so the
/// default `stdout` audit sink would silently discard every decision and
Expand Down
71 changes: 71 additions & 0 deletions tests/e2e/agent.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AgentKind {
ClaudeCode,
Codex,
}

/// An agent the harness can run, optionally carrying extra CLI flags.
///
/// Flags passed via `.args()` are inserted before the subcommand so they are
/// treated as global flags by the agent binary.
#[derive(Debug, Clone)]
pub struct Agent {
pub kind: AgentKind,
args: Vec<String>,
}

impl Agent {
#[must_use]
pub fn claude() -> Self {
Self {
kind: AgentKind::ClaudeCode,
args: Vec::new(),
}
}

#[must_use]
pub fn codex() -> Self {
Self {
kind: AgentKind::Codex,
args: Vec::new(),
}
}

/// Attach CLI flags inserted before the subcommand / prompt flag.
#[must_use]
pub fn args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.args = args.into_iter().map(Into::into).collect();
self
}

#[must_use]
pub fn command(&self) -> &'static str {
match self.kind {
AgentKind::ClaudeCode => "claude",
AgentKind::Codex => "codex",
}
}

#[must_use]
pub fn profile(&self) -> &'static str {
match self.kind {
AgentKind::ClaudeCode => "claude-code",
AgentKind::Codex => "codex",
}
}

pub fn prompt_args(&self, prompt: &str) -> Vec<String> {
let mut result = self.args.clone();
match self.kind {
AgentKind::ClaudeCode => {
result.push("-p".to_string());
result.push(prompt.to_string());
}
AgentKind::Codex => {
result.push("exec".to_string());
result.push(prompt.to_string());
}
}
result
}
}
Loading
Loading