Hold Daytona credentials as the SDK's configuration

DaytonaCredentials mirrored DaytonaConfig field for field and was copied
into one at connect time. It is now a newtype over the SDK configuration
with the API key always present and a Debug that never prints it; the
driver's Daytona provider connects with the configuration as it is.
Callers build it from an API key, a settings lookup, and the optional
control-plane URL, organization, and HTTP client.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-09-10 16:53:20 -06:00
parent 91d905813f
commit 054a37f830
No known key found for this signature in database
7 changed files with 80 additions and 92 deletions

View file

@ -1005,13 +1005,12 @@ async fn check_install_daytona_api_key(
state: &InstallAppState,
api_key: String,
) -> anyhow::Result<daytona::DaytonaKeyCheck> {
let credentials = DaytonaCredentials {
api_key,
api_url: state.upstreams.daytona_api_base_url.clone(),
organization_id: state.upstreams.daytona_organization_id.clone(),
target: None,
http_client: Some(fabro_http::http_client().context("failed to build HTTP client")?),
};
let credentials = DaytonaCredentials::new(api_key)
.with_api_url(state.upstreams.daytona_api_base_url.clone())
.with_organization_id(state.upstreams.daytona_organization_id.clone())
.with_http_client(Some(
fabro_http::http_client().context("failed to build HTTP client")?,
));
daytona::check_daytona_api_key(&credentials, daytona::DAYTONA_CREDENTIAL_PROBE_TIMEOUT).await
}

View file

@ -1470,15 +1470,8 @@ impl AppState {
/// the server's HTTP client. The process environment is consulted only
/// through the configured lookup.
pub(crate) fn daytona_credentials(&self, api_key: String) -> DaytonaCredentials {
DaytonaCredentials {
api_key,
api_url: self
.config_env_lookup(EnvVars::DAYTONA_API_URL)
.or_else(|| self.config_env_lookup(EnvVars::DAYTONA_SERVER_URL)),
organization_id: self.config_env_lookup(EnvVars::DAYTONA_ORGANIZATION_ID),
target: None,
http_client: self.http_client().ok(),
}
DaytonaCredentials::from_api_key(api_key, |name| self.config_env_lookup(name))
.with_http_client(self.http_client().ok())
}
/// Everything a reconnect needs to reach a run's provider: the server's
@ -2358,14 +2351,8 @@ fn build_sandbox_inventory(
if let Some(daytona) = provider_settings.get(&SandboxProviderKind::DAYTONA) {
if let Some(api_key) = daytona_api_key.filter(|_| daytona.enabled) {
let credentials = DaytonaCredentials {
api_key,
api_url: env_lookup(EnvVars::DAYTONA_API_URL)
.or_else(|| env_lookup(EnvVars::DAYTONA_SERVER_URL)),
organization_id: env_lookup(EnvVars::DAYTONA_ORGANIZATION_ID),
target: None,
http_client,
};
let credentials = DaytonaCredentials::from_api_key(api_key, |name| env_lookup(name))
.with_http_client(http_client);
inventory = inventory.with_lazy(
SandboxProviderKind::DAYTONA,
daytona.clone(),

View file

@ -689,14 +689,9 @@ mod tests {
#[tokio::test]
async fn credential_probe_reports_configured_timeout() {
let credentials = DaytonaCredentials {
api_key: "dtn_test".to_string(),
// A non-routable address: the probe cannot finish within the budget.
api_url: Some("http://10.255.255.1:1/api".to_string()),
organization_id: None,
target: None,
http_client: None,
};
// A non-routable address: the probe cannot finish within the budget.
let credentials = DaytonaCredentials::new("dtn_test".to_string())
.with_api_url(Some("http://10.255.255.1:1/api".to_string()));
let err = check_daytona_api_key(&credentials, Duration::from_millis(1))
.await
.expect_err("probe should time out");
@ -737,15 +732,9 @@ mod wire_gate {
)]
fn live_credentials() -> Option<DaytonaCredentials> {
let api_key = std::env::var(EnvVars::DAYTONA_API_KEY).ok()?;
Some(DaytonaCredentials {
api_key,
api_url: std::env::var(EnvVars::DAYTONA_API_URL)
.or_else(|_| std::env::var(EnvVars::DAYTONA_SERVER_URL))
.ok(),
organization_id: std::env::var(EnvVars::DAYTONA_ORGANIZATION_ID).ok(),
target: None,
http_client: None,
})
Some(DaytonaCredentials::from_api_key(api_key, |name| {
std::env::var(name).ok()
}))
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View file

@ -38,39 +38,74 @@ pub const PLUGIN_BINARY_PREFIX: &str = "fabro-sandbox";
/// `User-Agent` fabro presents to remote sandbox control planes.
pub const USER_AGENT: &str = concat!("fabro-sandbox/", env!("CARGO_PKG_VERSION"));
/// Explicit Daytona credentials. The process environment is never consulted.
/// Explicit Daytona credentials: the SDK's configuration with the API key
/// always present and a `Debug` that never prints it. The process
/// environment is never consulted.
#[derive(Clone)]
pub struct DaytonaCredentials {
pub api_key: String,
pub api_url: Option<String>,
pub organization_id: Option<String>,
pub target: Option<String>,
/// Shared HTTP client; tests pass a no-proxy client here.
pub http_client: Option<reqwest::Client>,
}
pub struct DaytonaCredentials(DaytonaConfig);
impl DaytonaCredentials {
/// Credentials for `api_key` against Daytona's public control plane,
/// presenting fabro's `User-Agent`.
#[must_use]
pub fn new(api_key: String) -> Self {
Self(DaytonaConfig {
api_key: Some(api_key),
user_agent: Some(USER_AGENT.to_string()),
..DaytonaConfig::default()
})
}
/// Credentials for a vault API key, with the control-plane URL and
/// organization taken from `lookup` (server configuration, or the
/// process environment in a CLI worker). Nothing is read implicitly.
pub fn from_api_key(api_key: String, lookup: impl Fn(&str) -> Option<String>) -> Self {
Self {
api_key,
api_url: lookup(EnvVars::DAYTONA_API_URL)
.or_else(|| lookup(EnvVars::DAYTONA_SERVER_URL)),
organization_id: lookup(EnvVars::DAYTONA_ORGANIZATION_ID),
target: None,
http_client: None,
}
Self::new(api_key)
.with_api_url(
lookup(EnvVars::DAYTONA_API_URL).or_else(|| lookup(EnvVars::DAYTONA_SERVER_URL)),
)
.with_organization_id(lookup(EnvVars::DAYTONA_ORGANIZATION_ID))
}
/// The control-plane URL; Daytona's public API when `None`.
#[must_use]
pub fn with_api_url(mut self, api_url: Option<String>) -> Self {
self.0.api_url = api_url;
self
}
#[must_use]
pub fn with_organization_id(mut self, organization_id: Option<String>) -> Self {
self.0.organization_id = organization_id;
self
}
/// A shared HTTP client; tests pass a no-proxy client here.
#[must_use]
pub fn with_http_client(mut self, http_client: Option<reqwest::Client>) -> Self {
self.0.http_client = http_client;
self
}
/// The API key, which every constructor sets.
#[must_use]
pub fn api_key(&self) -> &str {
self.0.api_key.as_deref().unwrap_or_default()
}
/// The SDK configuration the driver's Daytona provider connects with.
#[must_use]
pub fn config(&self) -> &DaytonaConfig {
&self.0
}
}
impl std::fmt::Debug for DaytonaCredentials {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DaytonaCredentials")
.field("api_url", &self.api_url)
.field("organization_id", &self.organization_id)
.field("target", &self.target)
.field("api_url", &self.0.api_url)
.field("organization_id", &self.0.organization_id)
.field("target", &self.0.target)
.finish_non_exhaustive()
}
}
@ -187,17 +222,8 @@ pub async fn connect_provider(
.daytona
.as_ref()
.ok_or(ConnectError::MissingDaytonaCredentials)?;
let config = DaytonaConfig {
api_key: Some(credentials.api_key.clone()),
jwt_token: None,
organization_id: credentials.organization_id.clone(),
api_url: credentials.api_url.clone(),
target: credentials.target.clone(),
http_client: credentials.http_client.clone(),
user_agent: Some(USER_AGENT.to_string()),
};
Arc::new(
DaytonaProvider::connect_explicit(config)
DaytonaProvider::connect_explicit(credentials.config().clone())
.await
.map_err(driver)?,
)

View file

@ -52,7 +52,7 @@ pub async fn provider_sandbox(
.ok_or_else(|| crate::Error::message(MISSING_DAYTONA_CREDENTIALS))?;
let plan = daytona::create_plan(
Arc::clone(&provider),
credentials.api_key.clone(),
credentials.api_key().to_string(),
spec,
run_id,
);

View file

@ -533,16 +533,11 @@ mod daytona_streaming_live {
reason = "live smoke tests take Daytona credentials from the developer's environment"
)]
fn live_credentials() -> Result<DaytonaCredentials> {
Ok(DaytonaCredentials {
api_key: std::env::var(EnvVars::DAYTONA_API_KEY)
.context("DAYTONA_API_KEY must be set")?,
api_url: std::env::var(EnvVars::DAYTONA_API_URL)
.or_else(|_| std::env::var(EnvVars::DAYTONA_SERVER_URL))
.ok(),
organization_id: std::env::var(EnvVars::DAYTONA_ORGANIZATION_ID).ok(),
target: None,
http_client: None,
})
let api_key =
std::env::var(EnvVars::DAYTONA_API_KEY).context("DAYTONA_API_KEY must be set")?;
Ok(DaytonaCredentials::from_api_key(api_key, |name| {
std::env::var(name).ok()
}))
}
fn daytona_access(credentials: DaytonaCredentials) -> ProviderAccess {

View file

@ -194,16 +194,8 @@ fn daytona_access(credentials: DaytonaCredentials) -> ProviderAccess {
}
fn live_daytona_credentials() -> DaytonaCredentials {
DaytonaCredentials {
api_key: std::env::var(EnvVars::DAYTONA_API_KEY)
.expect("DAYTONA_API_KEY must be set"),
api_url: std::env::var(EnvVars::DAYTONA_API_URL)
.or_else(|_| std::env::var(EnvVars::DAYTONA_SERVER_URL))
.ok(),
organization_id: std::env::var(EnvVars::DAYTONA_ORGANIZATION_ID).ok(),
target: None,
http_client: None,
}
let api_key = std::env::var(EnvVars::DAYTONA_API_KEY).expect("DAYTONA_API_KEY must be set");
DaytonaCredentials::from_api_key(api_key, |name| std::env::var(name).ok())
}
async fn create_env() -> RunSandbox {