refactor: remove CLI client mTLS

Delete the CliTargetTls settings, ClientTlsSettings struct, rustls-pemfile
dep, and the fabro-http wrapper methods (use_rustls_tls/identity/
add_root_certificate) that only the CLI client-auth path used. The server
no longer terminates TLS in-process and the CliAuthStrategy::Mtls variant
had no construction or match sites.

Also simplify ServerTarget::HttpUrl to a tuple variant (HttpUrl(String))
now that tls is gone, removing the struct-variant ceremony across 22
construction and destructure sites.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-04-20 17:37:24 -04:00
parent 3e881d073b
commit acd89fb235
No known key found for this signature in database
19 changed files with 64 additions and 356 deletions

10
Cargo.lock generated
View file

@ -1662,7 +1662,6 @@ dependencies = [
"reqwest 0.13.2",
"ring",
"rustls",
"rustls-pemfile",
"scopeguard",
"semver",
"serde",
@ -5525,15 +5524,6 @@ dependencies = [
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.14.0"

View file

@ -63,11 +63,6 @@ _version = 1
type = "http"
url = "https://fabro.example.com/api/v1"
[cli.target.tls]
cert = "~/.fabro/tls/client.crt"
key = "~/.fabro/tls/client.key"
ca = "~/.fabro/tls/ca.crt"
[cli.exec]
prevent_idle_sleep = true
@ -264,25 +259,6 @@ An explicit `http(s)://...` target is always remote-by-contract. Fabro does not
`fabro exec` does not automatically use `[cli.target]`. It only routes model traffic through a Fabro server when you pass `--server` for that invocation.
### `[cli.target.tls]` section
Optional client-certificate configuration for authenticating with an HTTP target. When present, the CLI presents a client certificate during the TLS handshake with your external HTTPS endpoint or reverse proxy.
```toml title="settings.toml"
[cli.target.tls]
cert = "~/.fabro/tls/client.crt"
key = "~/.fabro/tls/client.key"
ca = "~/.fabro/tls/ca.crt"
```
| Key | Description |
|---|---|
| `cert` | Path to client certificate PEM file |
| `key` | Path to client private key PEM file |
| `ca` | Path to CA certificate PEM file (to verify the server) |
Paths support `~/` expansion.
## `[run.pull_request]`
Enable auto-PR globally so workflows open a GitHub pull request on successful completion.

View file

@ -75,7 +75,6 @@ ulid.workspace = true
scopeguard = "1"
rustls = { version = "0.23", default-features = false, features = ["std", "ring"] }
ring = "0.17"
rustls-pemfile = "2"
x509-parser = "0.16"
rand.workspace = true
dialoguer.workspace = true

View file

@ -47,7 +47,7 @@ pub(crate) struct ServerTargetKey(String);
impl ServerTargetKey {
pub(crate) fn new(target: &ServerTarget) -> Result<Self, AuthStoreError> {
match target {
ServerTarget::HttpUrl { api_url, .. } => canonical_http_target(api_url).map(Self),
ServerTarget::HttpUrl(api_url) => canonical_http_target(api_url).map(Self),
ServerTarget::UnixSocket(path) => Ok(Self(format!(
"unix://{}",
canonical_socket_path(path)?.display()
@ -460,10 +460,7 @@ mod tests {
}
fn https_target(value: &str) -> ServerTarget {
ServerTarget::HttpUrl {
api_url: value.to_string(),
tls: None,
}
ServerTarget::HttpUrl(value.to_string())
}
#[cfg(unix)]

View file

@ -343,10 +343,7 @@ mod tests {
#[test]
fn token_transport_accepts_only_https_loopback_or_unix() {
let target = ServerTarget::HttpUrl {
api_url: "https://fabro.example.com".to_string(),
tls: None,
};
let target = ServerTarget::HttpUrl("https://fabro.example.com".to_string());
assert_eq!(
is_loopback_or_unix_socket(&target).unwrap(),
LoopbackClassification::Https

View file

@ -85,10 +85,7 @@ fn server_target_from_key(key: &ServerTargetKey) -> Result<ServerTarget> {
return Ok(ServerTarget::UnixSocket(path.into()));
}
if value.starts_with("http://") || value.starts_with("https://") {
return Ok(ServerTarget::HttpUrl {
api_url: value,
tls: None,
});
return Ok(ServerTarget::HttpUrl(value));
}
bail!("invalid auth store server key `{value}`")
}
@ -109,18 +106,14 @@ mod tests {
#[test]
fn rebuilds_server_target_from_http_key() {
let key = ServerTargetKey::new(&ServerTarget::HttpUrl {
api_url: "https://fabro.example.com/api/v1".to_string(),
tls: None,
})
let key = ServerTargetKey::new(&ServerTarget::HttpUrl(
"https://fabro.example.com/api/v1".to_string(),
))
.unwrap();
assert_eq!(
server_target_from_key(&key).unwrap(),
ServerTarget::HttpUrl {
api_url: "https://fabro.example.com".to_string(),
tls: None,
}
ServerTarget::HttpUrl("https://fabro.example.com".to_string())
);
}
@ -137,10 +130,9 @@ mod tests {
#[test]
fn warning_mentions_local_removal_and_remote_failure() {
let key = ServerTargetKey::new(&ServerTarget::HttpUrl {
api_url: "https://fabro.example.com".to_string(),
tls: None,
})
let key = ServerTargetKey::new(&ServerTarget::HttpUrl(
"https://fabro.example.com".to_string(),
))
.unwrap();
let warning = format_warning(&key, "request failed with status 500");
assert!(warning.contains("removed local session"));

View file

@ -369,7 +369,7 @@ pub(crate) async fn execute(
permissions,
output_format,
);
let server_target = user_config::exec_server_target(&args.server, &raw_settings)?;
let server_target = user_config::exec_server_target(&args.server)?;
// v2 MCPs live under `cli.exec.agent.mcps` (owner-specific) or
// `run.agent.mcps`. For `fabro exec` we use the cli.exec path, falling
// back to run.agent.mcps if unset.

View file

@ -2113,13 +2113,12 @@ mod tests {
.and_then(|c| c.target.as_ref())
.expect("cli.target should be set");
match target {
CliTargetLayer::Http { url, tls } => {
CliTargetLayer::Http { url } => {
assert_eq!(
url.as_ref()
.map(fabro_types::settings::InterpString::as_source),
Some("http://127.0.0.1:32276".to_string())
);
assert!(tls.is_none());
}
CliTargetLayer::Unix { .. } => panic!("expected http target"),
}

View file

@ -80,7 +80,7 @@ pub(crate) async fn create_run(
.root()
.to_path_buf(),
),
ServerTarget::HttpUrl { .. } => None,
ServerTarget::HttpUrl(_) => None,
};
Ok(CreatedRun {

View file

@ -133,7 +133,7 @@ fn is_non_release_profile(profile: &str) -> bool {
fn format_server_target(target: &ServerTarget) -> String {
match target {
ServerTarget::HttpUrl { api_url, .. } => api_url.clone(),
ServerTarget::HttpUrl(api_url) => api_url.clone(),
ServerTarget::UnixSocket(path) => path.display().to_string(),
}
}

View file

@ -27,7 +27,7 @@ pub(crate) fn is_loopback_or_unix_socket(
) -> Result<LoopbackClassification, TargetSchemeError> {
match target {
ServerTarget::UnixSocket(_) => Ok(LoopbackClassification::UnixSocket),
ServerTarget::HttpUrl { api_url, .. } => classify_http_target(api_url),
ServerTarget::HttpUrl(api_url) => classify_http_target(api_url),
}
}
@ -128,31 +128,19 @@ mod tests {
fn classifies_https_loopback_and_unix_targets() {
let cases = [
(
ServerTarget::HttpUrl {
api_url: "https://fabro.example.com".to_string(),
tls: None,
},
ServerTarget::HttpUrl("https://fabro.example.com".to_string()),
LoopbackClassification::Https,
),
(
ServerTarget::HttpUrl {
api_url: "http://127.0.0.1:3000".to_string(),
tls: None,
},
ServerTarget::HttpUrl("http://127.0.0.1:3000".to_string()),
LoopbackClassification::LoopbackHttp,
),
(
ServerTarget::HttpUrl {
api_url: "http://[::1]:3000".to_string(),
tls: None,
},
ServerTarget::HttpUrl("http://[::1]:3000".to_string()),
LoopbackClassification::LoopbackHttp,
),
(
ServerTarget::HttpUrl {
api_url: "http://[::ffff:127.0.0.1]:3000".to_string(),
tls: None,
},
ServerTarget::HttpUrl("http://[::ffff:127.0.0.1]:3000".to_string()),
LoopbackClassification::LoopbackHttp,
),
(
@ -179,10 +167,7 @@ mod tests {
];
for api_url in cases {
let target = ServerTarget::HttpUrl {
api_url: api_url.to_string(),
tls: None,
};
let target = ServerTarget::HttpUrl(api_url.to_string());
assert_eq!(
is_loopback_or_unix_socket(&target).unwrap(),
LoopbackClassification::Rejected
@ -192,10 +177,7 @@ mod tests {
#[test]
fn rejects_unsupported_schemes() {
let target = ServerTarget::HttpUrl {
api_url: "ftp://fabro.example.com".to_string(),
tls: None,
};
let target = ServerTarget::HttpUrl("ftp://fabro.example.com".to_string());
let error = is_loopback_or_unix_socket(&target).unwrap_err();
assert!(error.to_string().contains("unsupported server URL scheme"));
}

View file

@ -186,11 +186,7 @@ pub(crate) async fn connect_server_target(target: &user_config::ServerTarget) ->
pub(crate) async fn connect_server_target_direct(target: &str) -> Result<Client> {
if target.starts_with("http://") || target.starts_with("https://") {
connect_server_target(&user_config::ServerTarget::HttpUrl {
api_url: target.to_string(),
tls: None,
})
.await
connect_server_target(&user_config::ServerTarget::HttpUrl(target.to_string())).await
} else {
let path = Path::new(target);
if !path.is_absolute() {
@ -301,12 +297,11 @@ pub(crate) async fn connect_api_client(storage_dir: &Path) -> Result<fabro_api::
async fn connect_target_api_client_bundle(target: &user_config::ServerTarget) -> Result<Client> {
match target {
user_config::ServerTarget::HttpUrl { api_url, tls } => {
user_config::ServerTarget::HttpUrl(api_url) => {
let bearer = resolve_target_bearer(target, None, local_dev_token_fallback(target))?;
let refreshable_oauth = refreshable_oauth(target, bearer.as_ref())?;
let bundle = connect_remote_api_client_bundle(
api_url,
tls.as_ref(),
bearer.as_ref().map(ResolvedBearer::bearer_token),
)?;
Ok(Client::from_bundle(
@ -336,11 +331,10 @@ async fn connect_target_api_client_bundle(target: &user_config::ServerTarget) ->
fn connect_remote_api_client_bundle(
api_url: &str,
tls: Option<&user_config::ClientTlsSettings>,
bearer_token: Option<&str>,
) -> Result<ClientBundle> {
let normalized = user_config::normalized_http_base_url(api_url);
let mut builder = user_config::build_server_client_builder(tls)?;
let mut builder = user_config::cli_http_client_builder();
builder = match bearer_token {
Some(token) => apply_bearer_token_auth(builder, token)?,
None => builder,
@ -760,8 +754,8 @@ impl Client {
bearer_token: Option<&str>,
) -> Result<()> {
let bundle = match target {
user_config::ServerTarget::HttpUrl { api_url, tls } => {
connect_remote_api_client_bundle(api_url, tls.as_ref(), bearer_token)?
user_config::ServerTarget::HttpUrl(api_url) => {
connect_remote_api_client_bundle(api_url, bearer_token)?
}
user_config::ServerTarget::UnixSocket(path) => {
connect_unix_socket_api_client_bundle(path, None, bearer_token).await?
@ -1880,10 +1874,8 @@ mod tests {
#[test]
fn explicit_http_targets_do_not_allow_local_dev_token_fallback() {
let target = user_config::ServerTarget::HttpUrl {
api_url: "https://fabro.example.com/api/v1".to_string(),
tls: None,
};
let target =
user_config::ServerTarget::HttpUrl("https://fabro.example.com/api/v1".to_string());
assert!(!local_dev_token_fallback(&target));
}
@ -1916,10 +1908,7 @@ mod tests {
async fn refresh_access_token_rejects_plain_http_non_loopback_targets() {
let temp = tempfile::tempdir().unwrap();
let auth_store = AuthStore::new(temp.path().join("auth.json"));
let target = user_config::ServerTarget::HttpUrl {
api_url: "http://fabro.example.com".to_string(),
tls: None,
};
let target = user_config::ServerTarget::HttpUrl("http://fabro.example.com".to_string());
let key = ServerTargetKey::new(&target).unwrap();
auth_store.put(&key, oauth_entry("octocat")).unwrap();

View file

@ -1,27 +1,13 @@
#![expect(
clippy::disallowed_methods,
reason = "CLI user config: sync file I/O loading user config"
)]
use std::fmt;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use anyhow::{Result, bail};
pub(crate) use fabro_config::user::*;
use fabro_types::settings::cli::CliTargetSettings;
use fabro_types::settings::{CliSettings, SettingsLayer};
use fabro_util::version::FABRO_VERSION;
use serde::{Deserialize, Serialize};
use tracing::debug;
/// Client-side TLS material for the CLI's remote server target.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub(crate) struct ClientTlsSettings {
pub cert: PathBuf,
pub key: PathBuf,
pub ca: PathBuf,
}
use crate::args::ServerTargetArgs;
pub(crate) fn load_settings() -> anyhow::Result<SettingsLayer> {
@ -76,17 +62,14 @@ pub(crate) fn apply_storage_dir_override(
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum ServerTarget {
HttpUrl {
api_url: String,
tls: Option<ClientTlsSettings>,
},
HttpUrl(String),
UnixSocket(PathBuf),
}
impl fmt::Display for ServerTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::HttpUrl { api_url, .. } => f.write_str(api_url),
Self::HttpUrl(api_url) => f.write_str(api_url),
Self::UnixSocket(path) => write!(f, "unix://{}", path.display()),
}
}
@ -101,8 +84,8 @@ pub(crate) fn build_public_http_client(
target: &ServerTarget,
) -> Result<(fabro_http::HttpClient, String)> {
match target {
ServerTarget::HttpUrl { api_url, tls } => {
let http_client = build_server_client_builder(tls.as_ref())?.build()?;
ServerTarget::HttpUrl(api_url) => {
let http_client = cli_http_client_builder().build()?;
Ok((http_client, normalized_http_base_url(api_url).to_string()))
}
ServerTarget::UnixSocket(path) => {
@ -124,29 +107,21 @@ pub(crate) fn build_public_http_client(
}
/// Pull the resolved CLI target configuration out of `[cli.target]`.
/// Returns `(target_string, tls)` where `target_string` is either an
/// http(s) URL or a unix socket path.
fn cli_target_from_settings(settings: &CliSettings) -> Option<(String, Option<ClientTlsSettings>)> {
/// Returns either an http(s) URL or a unix socket path.
fn cli_target_from_settings(settings: &CliSettings) -> Option<String> {
let target = settings.target.as_ref()?;
match target {
CliTargetSettings::Http { url, tls } => {
let tls_settings = tls.as_ref().map(|tls| ClientTlsSettings {
cert: PathBuf::from(tls.cert.as_source()),
key: PathBuf::from(tls.key.as_source()),
ca: PathBuf::from(tls.ca.as_source()),
});
Some((url.as_source(), tls_settings))
}
CliTargetSettings::Unix { path } => Some((path.as_source(), None)),
CliTargetSettings::Http { url } => Some(url.as_source()),
CliTargetSettings::Unix { path } => Some(path.as_source()),
}
}
fn configured_server_target(settings: &SettingsLayer) -> Result<Option<ServerTarget>> {
let cli_settings = resolve_cli_settings(settings)?;
let Some((value, tls)) = cli_target_from_settings(&cli_settings) else {
let Some(value) = cli_target_from_settings(&cli_settings) else {
return Ok(None);
};
parse_server_target(&value, tls).map(Some)
parse_server_target(&value).map(Some)
}
pub(crate) fn default_server_target() -> ServerTarget {
@ -177,12 +152,9 @@ pub(crate) fn storage_dir(settings: &SettingsLayer) -> anyhow::Result<PathBuf> {
Ok(PathBuf::from(resolved_root.value))
}
fn parse_server_target(value: &str, tls: Option<ClientTlsSettings>) -> Result<ServerTarget> {
fn parse_server_target(value: &str) -> Result<ServerTarget> {
if value.starts_with("http://") || value.starts_with("https://") {
return Ok(ServerTarget::HttpUrl {
api_url: value.to_string(),
tls,
});
return Ok(ServerTarget::HttpUrl(value.to_string()));
}
let path = Path::new(value);
@ -193,26 +165,15 @@ fn parse_server_target(value: &str, tls: Option<ClientTlsSettings>) -> Result<Se
bail!("server target must be an http(s) URL or absolute Unix socket path")
}
fn explicit_server_target(
args: &ServerTargetArgs,
settings: &SettingsLayer,
) -> Result<Option<ServerTarget>> {
let cli_settings = resolve_cli_settings(settings)?;
args.as_deref()
.map(|value| {
parse_server_target(
value,
cli_target_from_settings(&cli_settings).and_then(|(_, tls)| tls),
)
})
.transpose()
fn explicit_server_target(args: &ServerTargetArgs) -> Result<Option<ServerTarget>> {
args.as_deref().map(parse_server_target).transpose()
}
pub(crate) fn resolve_nondefault_server_target(
args: &ServerTargetArgs,
settings: &SettingsLayer,
) -> Result<Option<ServerTarget>> {
Ok(explicit_server_target(args, settings)?.or(configured_server_target(settings)?))
Ok(explicit_server_target(args)?.or(configured_server_target(settings)?))
}
pub(crate) fn resolve_server_target(
@ -222,11 +183,8 @@ pub(crate) fn resolve_server_target(
Ok(resolve_nondefault_server_target(args, settings)?.unwrap_or_else(default_server_target))
}
pub(crate) fn exec_server_target(
args: &ServerTargetArgs,
settings: &SettingsLayer,
) -> Result<Option<ServerTarget>> {
let target = explicit_server_target(args, settings)?;
pub(crate) fn exec_server_target(args: &ServerTargetArgs) -> Result<Option<ServerTarget>> {
let target = explicit_server_target(args)?;
debug!(?target, "Resolved exec server target");
Ok(target)
}
@ -235,37 +193,6 @@ pub(crate) fn cli_http_client_builder() -> fabro_http::HttpClientBuilder {
fabro_http::HttpClientBuilder::new().user_agent(format!("fabro-cli/{FABRO_VERSION}"))
}
pub(crate) fn build_server_client_builder(
tls: Option<&ClientTlsSettings>,
) -> anyhow::Result<fabro_http::HttpClientBuilder> {
let Some(tls) = tls else {
return Ok(cli_http_client_builder());
};
let cert_path = fabro_config::expand_tilde(&tls.cert);
let key_path = fabro_config::expand_tilde(&tls.key);
let ca_path = fabro_config::expand_tilde(&tls.ca);
let cert_pem = std::fs::read(&cert_path)
.with_context(|| format!("reading TLS client certificate {}", cert_path.display()))?;
let key_pem = std::fs::read(&key_path)
.with_context(|| format!("reading TLS client key {}", key_path.display()))?;
let ca_pem = std::fs::read(&ca_path)
.with_context(|| format!("reading TLS CA certificate {}", ca_path.display()))?;
let mut identity_pem = cert_pem;
identity_pem.push(b'\n');
identity_pem.extend_from_slice(&key_pem);
let identity = fabro_http::Identity::from_pem(&identity_pem)?;
let ca_cert = fabro_http::Certificate::from_pem(&ca_pem)?;
Ok(cli_http_client_builder()
.use_rustls_tls()
.identity(identity)
.add_root_certificate(ca_cert))
}
#[cfg(test)]
mod tests {
use fabro_config::parse_settings_layer;
@ -285,53 +212,28 @@ mod tests {
#[test]
fn exec_has_no_server_target_by_default() {
let settings = SettingsLayer::default();
assert_eq!(
exec_server_target(&server_target_args(None), &settings).unwrap(),
None
);
assert_eq!(exec_server_target(&server_target_args(None)).unwrap(), None);
}
#[test]
fn exec_uses_cli_server_target() {
let settings = SettingsLayer::default();
assert_eq!(
exec_server_target(
&server_target_args(Some("https://cli.example.com")),
&settings
)
.unwrap(),
Some(ServerTarget::HttpUrl {
api_url: "https://cli.example.com".to_string(),
tls: None,
})
exec_server_target(&server_target_args(Some("https://cli.example.com"))).unwrap(),
Some(ServerTarget::HttpUrl("https://cli.example.com".to_string()))
);
}
#[test]
fn exec_supports_explicit_unix_socket_target() {
let settings = SettingsLayer::default();
assert_eq!(
exec_server_target(&server_target_args(Some("/tmp/fabro.sock")), &settings).unwrap(),
exec_server_target(&server_target_args(Some("/tmp/fabro.sock"))).unwrap(),
Some(ServerTarget::UnixSocket(PathBuf::from("/tmp/fabro.sock")))
);
}
#[test]
fn exec_ignores_configured_server_target_without_cli_override() {
let settings = parse_v2(
r#"
_version = 1
[cli.target]
type = "http"
url = "https://config.example.com"
"#,
);
assert_eq!(
exec_server_target(&server_target_args(None), &settings).unwrap(),
None
);
assert_eq!(exec_server_target(&server_target_args(None)).unwrap(), None);
}
#[test]
@ -347,10 +249,7 @@ url = "https://config.example.com"
);
assert_eq!(
resolve_server_target(&server_target_args(None), &settings).unwrap(),
ServerTarget::HttpUrl {
api_url: "https://config.example.com".to_string(),
tls: None,
}
ServerTarget::HttpUrl("https://config.example.com".to_string())
);
}
@ -371,10 +270,7 @@ url = "https://config.example.com"
&settings
)
.unwrap(),
ServerTarget::HttpUrl {
api_url: "https://cli.example.com".to_string(),
tls: None,
}
ServerTarget::HttpUrl("https://cli.example.com".to_string())
);
}
@ -404,52 +300,13 @@ url = "https://config.example.com"
&settings
)
.unwrap(),
ServerTarget::HttpUrl {
api_url: "https://cli.example.com".to_string(),
tls: None,
}
);
}
#[test]
fn remote_target_uses_tls_from_config() {
let expected_tls = ClientTlsSettings {
cert: PathBuf::from("cert.pem"),
key: PathBuf::from("key.pem"),
ca: PathBuf::from("ca.pem"),
};
let settings = parse_v2(
r#"
_version = 1
[cli.target]
type = "http"
url = "https://config.example.com"
[cli.target.tls]
cert = "cert.pem"
key = "key.pem"
ca = "ca.pem"
"#,
);
assert_eq!(
exec_server_target(
&server_target_args(Some("https://cli.example.com")),
&settings
)
.unwrap(),
Some(ServerTarget::HttpUrl {
api_url: "https://cli.example.com".to_string(),
tls: Some(expected_tls),
})
ServerTarget::HttpUrl("https://cli.example.com".to_string())
);
}
#[test]
fn invalid_server_target_is_rejected() {
let settings = SettingsLayer::default();
let error =
exec_server_target(&server_target_args(Some("fabro.internal")), &settings).unwrap_err();
let error = exec_server_target(&server_target_args(Some("fabro.internal"))).unwrap_err();
assert_eq!(
error.to_string(),
"server target must be an http(s) URL or absolute Unix socket path"

View file

@ -1,7 +1,7 @@
use fabro_types::settings::cli::{
CliAuthSettings, CliExecAgentSettings, CliExecLayer, CliExecModelSettings, CliExecSettings,
CliLayer, CliLoggingSettings, CliOutputSettings, CliSettings, CliTargetLayer,
CliTargetSettings, CliTargetTlsSettings, CliUpdatesSettings,
CliTargetSettings, CliUpdatesSettings,
};
use super::{ResolveError, require_interp};
@ -46,13 +46,8 @@ fn resolve_target(
errors: &mut Vec<ResolveError>,
) -> Option<CliTargetSettings> {
match target {
Some(CliTargetLayer::Http { url, tls }) => Some(CliTargetSettings::Http {
Some(CliTargetLayer::Http { url }) => Some(CliTargetSettings::Http {
url: require_interp(url.as_ref(), "cli.target.url", errors),
tls: tls.as_ref().map(|tls| CliTargetTlsSettings {
cert: require_interp(tls.cert.as_ref(), "cli.target.tls.cert", errors),
key: require_interp(tls.key.as_ref(), "cli.target.tls.key", errors),
ca: require_interp(tls.ca.as_ref(), "cli.target.tls.ca", errors),
}),
}),
Some(CliTargetLayer::Unix { path }) => Some(CliTargetSettings::Unix {
path: require_interp(path.as_ref(), "cli.target.path", errors),

View file

@ -27,11 +27,6 @@ _version = 1
type = "http"
url = "https://config.example.com"
[cli.target.tls]
cert = "cert.pem"
key = "key.pem"
ca = "ca.pem"
[cli.exec]
prevent_idle_sleep = true
@ -61,14 +56,10 @@ level = "debug"
let cli = resolve_cli_from_file(&settings).expect("cli settings should resolve");
let CliTargetSettings::Http { url, tls } = cli.target.expect("target") else {
let CliTargetSettings::Http { url } = cli.target.expect("target") else {
panic!("expected http target");
};
assert_eq!(url.as_source(), "https://config.example.com");
let tls = tls.expect("tls");
assert_eq!(tls.cert.as_source(), "cert.pem");
assert_eq!(tls.key.as_source(), "key.pem");
assert_eq!(tls.ca.as_source(), "ca.pem");
assert!(cli.exec.prevent_idle_sleep);
assert_eq!(

View file

@ -9,7 +9,6 @@ use std::path::Path;
use std::time::Duration;
pub use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
pub use reqwest::tls::{Certificate, Identity};
pub use reqwest::{
Body, Method, RequestBuilder, Response, StatusCode, Url, header, multipart, tls,
};
@ -143,30 +142,12 @@ macro_rules! define_builder {
self
}
#[must_use]
pub fn use_rustls_tls(mut self) -> Self {
self.inner = self.inner.use_rustls_tls();
self
}
#[must_use]
pub fn danger_accept_invalid_certs(mut self, accept_invalid_certs: bool) -> Self {
self.inner = self.inner.danger_accept_invalid_certs(accept_invalid_certs);
self
}
#[must_use]
pub fn add_root_certificate(mut self, cert: Certificate) -> Self {
self.inner = self.inner.add_root_certificate(cert);
self
}
#[must_use]
pub fn identity(mut self, identity: Identity) -> Self {
self.inner = self.inner.identity(identity);
self
}
#[cfg(unix)]
#[must_use]
pub fn unix_socket<P>(mut self, path: P) -> Self

View file

@ -26,20 +26,8 @@ pub struct CliSettings {
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum CliTargetSettings {
Http {
url: InterpString,
tls: Option<CliTargetTlsSettings>,
},
Unix {
path: InterpString,
},
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct CliTargetTlsSettings {
pub cert: InterpString,
pub key: InterpString,
pub ca: InterpString,
Http { url: InterpString },
Unix { path: InterpString },
}
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
@ -107,8 +95,6 @@ pub enum CliTargetLayer {
Http {
#[serde(default)]
url: Option<InterpString>,
#[serde(default)]
tls: Option<CliTargetTlsLayer>,
},
Unix {
#[serde(default)]
@ -116,17 +102,6 @@ pub enum CliTargetLayer {
},
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CliTargetTlsLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cert: Option<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key: Option<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ca: Option<InterpString>,
}
/// `[cli.auth]` — explicit auth strategy selection.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
@ -141,7 +116,6 @@ pub struct CliAuthLayer {
pub enum CliAuthStrategy {
None,
Jwt,
Mtls,
}
/// `[cli.exec]` — `fabro exec` defaults.

View file

@ -25,8 +25,7 @@ pub mod workflow;
pub use cli::{
CliAuthSettings, CliExecAgentSettings, CliExecModelSettings, CliExecSettings, CliLayer,
CliLoggingSettings, CliOutputSettings, CliSettings, CliTargetSettings, CliTargetTlsSettings,
CliUpdatesSettings,
CliLoggingSettings, CliOutputSettings, CliSettings, CliTargetSettings, CliUpdatesSettings,
};
pub use duration::{Duration, ParseDurationError};
pub use features::{FeaturesLayer, FeaturesSettings};

View file

@ -23,7 +23,7 @@ mod tests {
use serde_json::json;
use super::Settings;
use crate::settings::cli::{CliTargetSettings, CliTargetTlsSettings};
use crate::settings::cli::CliTargetSettings;
use crate::settings::interp::InterpString;
use crate::settings::run::{
DockerfileSource, McpServerSettings, McpTransport, RunAgentSettings, RunGoal, RunSettings,
@ -42,21 +42,11 @@ mod tests {
assert_eq!(
serde_json::to_value(CliTargetSettings::Http {
url: InterpString::parse("https://api.example.com"),
tls: Some(CliTargetTlsSettings {
cert: InterpString::parse("/tmp/client.crt"),
key: InterpString::parse("/tmp/client.key"),
ca: InterpString::parse("/tmp/ca.pem"),
}),
})
.unwrap(),
json!({
"type": "http",
"url": "https://api.example.com",
"tls": {
"cert": "/tmp/client.crt",
"key": "/tmp/client.key",
"ca": "/tmp/ca.pem"
}
})
);