feat(agent): support a trusted launcher credential broker

Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com>
This commit is contained in:
Alex Rosenzweig
2026-09-28 09:45:48 -05:00
parent 1815161d22
commit 1de2628d64
3 changed files with 186 additions and 4 deletions

View File

@@ -143,6 +143,7 @@ pub async fn discover_databricks_models_with_cache_dir(
) -> Result<Vec<ModelEntry>, AgentError> {
let token_source = if matches!(cfg.provider, Provider::Databricks | Provider::DatabricksV2)
&& cfg.api_key.is_empty()
&& std::env::var_os(crate::sandbox_runtime::BROKER_ENV).is_none()
{
crate::auth::PkceOAuthTokenSource::new(crate::llm::databricks_pkce_config(
&cfg.base_url,

View File

@@ -2096,6 +2096,9 @@ pub(crate) fn databricks_pkce_config(
/// discovery URL. First request without a cached token triggers a browser
/// flow; subsequent requests use the cache + refresh transparently.
pub(crate) fn build_token_source(cfg: &Config) -> Result<Arc<dyn TokenSource>, AgentError> {
if let Some(source) = crate::sandbox_runtime::token_source(cfg)? {
return Ok(source);
}
match cfg.provider {
Provider::Anthropic | Provider::OpenAi | Provider::OpenRouter => {
Ok(Arc::new(StaticTokenSource::new(cfg.api_key.clone())))

View File

@@ -1,11 +1,50 @@
//! Local TLS verification for agents started by trusted external launchers.
use crate::AgentError;
//! Trusted-launcher services for a confined native agent. No arbitrary URLs or file operations.
use crate::{
auth::TokenSource,
config::{Config, Provider},
AgentError,
};
use async_trait::async_trait;
use axum::{
extract::{DefaultBodyLimit, State},
http::{HeaderMap, StatusCode},
routing::post,
Json, Router,
};
use serde::{Deserialize, Serialize};
#[cfg(test)]
use std::{path::Path, sync::Arc};
use std::path::Path;
use std::{sync::Arc, time::Duration};
/// Launcher-supplied JSON capability for a fixed loopback credential broker.
pub const BROKER_ENV: &str = "BUZZ_SANDBOX_AUTH_BROKER";
/// Path to the immutable certificate snapshot supplied by the launcher.
pub const ROOTS_ENV: &str = "BUZZ_SANDBOX_TLS_ROOTS";
/// Connection capability passed only to the protected agent, never its tools.
#[derive(Clone, Serialize, Deserialize)]
pub struct BrokerConfig {
/// Loopback TCP port selected by the trusted launcher.
pub port: u16,
/// Random bearer capability authenticating requests to the launcher.
pub secret: String,
/// Exact configured model service URL; prevents cross-provider reuse.
pub host: String,
}
#[derive(Serialize, Deserialize)]
struct Request {
rejected: Option<String>,
}
#[derive(Serialize, Deserialize)]
struct Response {
token: String,
}
#[derive(Clone)]
struct BrokerState {
secret: String,
source: Arc<dyn TokenSource>,
}
/// Native trust is captured before confinement. Verification remains enabled,
/// using rustls locally instead of macOS trustd (which can bypass network policy).
pub fn http_builder() -> Result<reqwest::ClientBuilder, AgentError> {
@@ -20,10 +59,149 @@ fn builder_with_roots(
.map_err(|e| AgentError::Llm(e.to_string()))
}
pub(crate) fn token_source(cfg: &Config) -> Result<Option<Arc<dyn TokenSource>>, AgentError> {
let Some(raw) = std::env::var_os(BROKER_ENV) else {
return Ok(None);
};
if !matches!(cfg.provider, Provider::Databricks | Provider::DatabricksV2)
|| !cfg.api_key.is_empty()
{
return Ok(None);
}
let broker: BrokerConfig = serde_json::from_str(&raw.to_string_lossy())
.map_err(|_| AgentError::Llm("invalid sandbox auth broker".into()))?;
if broker.host != cfg.base_url || broker.port == 0 || broker.secret.len() < 32 {
return Err(AgentError::Llm(
"sandbox auth broker provider mismatch".into(),
));
}
let client = reqwest::Client::builder()
.no_proxy()
.timeout(Duration::from_secs(60))
.build()
.map_err(|_| AgentError::Llm("sandbox auth client unavailable".into()))?;
Ok(Some(Arc::new(BrokerClient { broker, client })))
}
struct BrokerClient {
broker: BrokerConfig,
client: reqwest::Client,
}
impl BrokerClient {
async fn request(&self, rejected: Option<&str>) -> Result<String, AgentError> {
let response = self
.client
.post(format!("http://127.0.0.1:{}/token", self.broker.port))
.bearer_auth(&self.broker.secret)
.json(&Request {
rejected: rejected.map(str::to_owned),
})
.send()
.await
.map_err(|_| AgentError::Llm("sandbox sign-in service unavailable".into()))?;
if !response.status().is_success() {
return Err(AgentError::LlmAuth(
"Refresh sign-in from the desktop model picker".into(),
));
}
Ok(response
.json::<Response>()
.await
.map_err(|_| AgentError::Llm("invalid sandbox sign-in response".into()))?
.token)
}
}
#[async_trait]
impl TokenSource for BrokerClient {
async fn bearer(&self) -> Result<String, AgentError> {
self.request(None).await
}
async fn refresh_now(&self, rejected: &str) -> Result<String, AgentError> {
self.request(Some(rejected)).await
}
}
/// The launcher fixes the workspace and cache. The child can only request a
/// bearer or refresh a rejected bearer, never choose a destination or open a browser.
pub fn serve(
listener: tokio::net::TcpListener,
host: &str,
secret: String,
) -> Result<tokio::task::JoinHandle<()>, AgentError> {
let source =
crate::auth::PkceOAuthTokenSource::new(crate::llm::databricks_pkce_config(host, None))?;
Ok(serve_source(listener, secret, source))
}
fn serve_source(
listener: tokio::net::TcpListener,
secret: String,
source: Arc<dyn TokenSource>,
) -> tokio::task::JoinHandle<()> {
let app = Router::new()
.route("/token", post(token))
.layer(DefaultBodyLimit::max(16 * 1024))
.with_state(BrokerState { secret, source });
tokio::spawn(async move {
if let Err(error) = axum::serve(listener, app).await {
tracing::warn!(%error, "sandbox auth broker stopped");
}
})
}
async fn token(
State(state): State<BrokerState>,
headers: HeaderMap,
Json(request): Json<Request>,
) -> Result<Json<Response>, StatusCode> {
if headers.get("authorization").and_then(|v| v.to_str().ok())
!= Some(format!("Bearer {}", state.secret).as_str())
{
return Err(StatusCode::UNAUTHORIZED);
}
let result = tokio::time::timeout(Duration::from_secs(55), async {
match request.rejected {
Some(value) => state.source.refresh_now(&value).await,
None => state.source.bearer_no_browser().await,
}
})
.await
.map_err(|_| StatusCode::GATEWAY_TIMEOUT)?;
let token = result.map_err(|_| StatusCode::UNAUTHORIZED)?;
Ok(Json(Response { token }))
}
#[cfg(test)]
mod tests {
use super::*;
use reqwest::StatusCode;
#[tokio::test]
async fn broker_rejects_unauthorized_callers_and_serves_fixed_source() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let task = serve_source(
listener,
"test-broker-capability".into(),
Arc::new(crate::auth::StaticTokenSource::new("synthetic-bearer")),
);
let client = reqwest::Client::builder().no_proxy().build().unwrap();
let url = format!("http://127.0.0.1:{port}/token");
let denied = client
.post(&url)
.json(&Request { rejected: None })
.send()
.await
.unwrap();
assert_eq!(denied.status(), StatusCode::UNAUTHORIZED);
let allowed = client
.post(&url)
.bearer_auth("test-broker-capability")
.json(&Request { rejected: None })
.send()
.await
.unwrap();
assert_eq!(
allowed.json::<Response>().await.unwrap().token,
"synthetic-bearer"
);
task.abort();
}
#[test]
fn missing_trust_snapshot_fails_closed() {
assert!(builder_with_roots(