mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
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:
parent
3e881d073b
commit
acd89fb235
19 changed files with 64 additions and 356 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ pub(crate) async fn create_run(
|
|||
.root()
|
||||
.to_path_buf(),
|
||||
),
|
||||
ServerTarget::HttpUrl { .. } => None,
|
||||
ServerTarget::HttpUrl(_) => None,
|
||||
};
|
||||
|
||||
Ok(CreatedRun {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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!(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue