fix(pi-agent): add credential extraction and env injection for multi-provider support

Co-authored-by: Nathan <github@nathanflurry.com>
This commit is contained in:
tembo[bot] 2026-02-13 05:25:48 +00:00 committed by GitHub
parent 1c381c552a
commit 27dfa37b5a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 125 additions and 9 deletions

View file

@ -45,6 +45,7 @@ OAuth tokens (used when OAuth extraction is enabled):
| Amp | Anthropic |
| Codex | OpenAI |
| OpenCode | Anthropic or OpenAI |
| Pi | Any supported provider API key (Anthropic, OpenAI, Gemini, etc.) |
| Mock | None |
## Error handling behavior

View file

@ -8,6 +8,9 @@ use acp_http_adapter::process::{AdapterError, AdapterRuntime, PostOutcome};
use acp_http_adapter::registry::LaunchSpec;
use axum::response::sse::Event;
use futures::Stream;
use sandbox_agent_agent_credentials::{
extract_all_credentials, AuthType, CredentialExtractionOptions, ExtractedCredentials,
};
use sandbox_agent_agent_management::agents::{AgentId, AgentManager, InstallOptions};
use sandbox_agent_error::SandboxError;
use sandbox_agent_opencode_adapter::{AcpDispatch, AcpDispatchResult, AcpPayloadStream};
@ -303,6 +306,14 @@ impl AcpProxyRuntime {
message: err.to_string(),
})?;
let credentials = tokio::task::spawn_blocking(move || {
extract_all_credentials(&CredentialExtractionOptions::new())
})
.await
.map_err(|err| SandboxError::StreamError {
message: format!("failed to resolve credentials: {err}"),
})?;
tracing::info!(
server_id = server_id,
agent = agent.as_str(),
@ -312,11 +323,14 @@ impl AcpProxyRuntime {
"create_instance: launch spec resolved, spawning"
);
let mut launch_env = launch.env;
merge_credentials_env(&mut launch_env, &credentials);
let runtime = AdapterRuntime::start(
LaunchSpec {
program: launch.program,
args: launch.args,
env: launch.env,
env: launch_env,
},
self.inner.request_timeout,
)
@ -488,6 +502,41 @@ fn annotate_agent_error(agent: AgentId, mut value: Value) -> Value {
value
}
fn merge_credentials_env(env: &mut HashMap<String, String>, credentials: &ExtractedCredentials) {
if let Some(cred) = &credentials.anthropic {
if cred.auth_type == AuthType::ApiKey {
insert_env_if_missing(env, "ANTHROPIC_API_KEY", &cred.api_key);
}
}
if let Some(cred) = &credentials.openai {
if cred.auth_type == AuthType::ApiKey {
insert_env_if_missing(env, "OPENAI_API_KEY", &cred.api_key);
}
}
for (provider, cred) in &credentials.other {
if cred.auth_type != AuthType::ApiKey {
continue;
}
let key = format!("{}_API_KEY", provider.to_uppercase().replace('-', "_"));
insert_env_if_missing(env, &key, &cred.api_key);
}
}
fn insert_env_if_missing(env: &mut HashMap<String, String>, key: &str, value: &str) {
if env.contains_key(key) || env_has_value(key) {
return;
}
env.insert(key.to_string(), value.to_string());
}
fn env_has_value(key: &str) -> bool {
std::env::var(key)
.map(|value| !value.trim().is_empty())
.unwrap_or(false)
}
fn duration_from_env_ms(key: &str, default: Duration) -> Duration {
match std::env::var(key) {
Ok(raw) => raw

View file

@ -17,7 +17,7 @@ use sandbox_agent_agent_management::agents::{
AgentId, AgentManager, InstallOptions, InstallResult, InstallSource, InstalledArtifactKind,
};
use sandbox_agent_agent_management::credentials::{
extract_all_credentials, CredentialExtractionOptions,
extract_all_credentials, AuthType, CredentialExtractionOptions,
};
use sandbox_agent_error::{ErrorType, ProblemDetails, SandboxError};
use sandbox_agent_opencode_adapter::{build_opencode_router, OpenCodeAdapterConfig};
@ -427,6 +427,19 @@ async fn get_v1_agents(
let has_anthropic = credentials.anthropic.is_some();
let has_openai = credentials.openai.is_some();
let has_other = !credentials.other.is_empty();
let has_any_api_key = credentials
.anthropic
.as_ref()
.is_some_and(|cred| cred.auth_type == AuthType::ApiKey)
|| credentials
.openai
.as_ref()
.is_some_and(|cred| cred.auth_type == AuthType::ApiKey)
|| credentials
.other
.values()
.any(|cred| cred.auth_type == AuthType::ApiKey);
let instances = state.acp_proxy().list_instances().await;
let mut active_by_agent = HashMap::<AgentId, Vec<i64>>::new();
@ -444,7 +457,13 @@ async fn get_v1_agents(
for agent_id in AgentId::all().iter().copied() {
let capabilities = agent_capabilities_for(agent_id);
let installed = state.agent_manager().is_installed(agent_id);
let credentials_available = credentials_available_for(agent_id, has_anthropic, has_openai);
let credentials_available = credentials_available_for(
agent_id,
has_anthropic,
has_openai,
has_other,
has_any_api_key,
);
let server_status = active_by_agent.get(&agent_id).map(|created_times| {
let uptime_ms = created_times
@ -569,6 +588,19 @@ async fn get_v1_agent(
let has_anthropic = credentials.anthropic.is_some();
let has_openai = credentials.openai.is_some();
let has_other = !credentials.other.is_empty();
let has_any_api_key = credentials
.anthropic
.as_ref()
.is_some_and(|cred| cred.auth_type == AuthType::ApiKey)
|| credentials
.openai
.as_ref()
.is_some_and(|cred| cred.auth_type == AuthType::ApiKey)
|| credentials
.other
.values()
.any(|cred| cred.auth_type == AuthType::ApiKey);
let instances = state.acp_proxy().list_instances().await;
let created_times: Vec<i64> = instances
@ -579,7 +611,13 @@ async fn get_v1_agent(
let capabilities = agent_capabilities_for(agent_id);
let installed = state.agent_manager().is_installed(agent_id);
let credentials_available = credentials_available_for(agent_id, has_anthropic, has_openai);
let credentials_available = credentials_available_for(
agent_id,
has_anthropic,
has_openai,
has_other,
has_any_api_key,
);
let server_status = if created_times.is_empty() {
None

View file

@ -1,4 +1,7 @@
use super::*;
use sandbox_agent_agent_management::credentials::{
extract_all_credentials, AuthType, CredentialExtractionOptions,
};
pub(super) async fn not_found() -> Response {
let problem = ProblemDetails {
@ -48,12 +51,15 @@ pub(super) fn credentials_available_for(
agent: AgentId,
has_anthropic: bool,
has_openai: bool,
has_other: bool,
has_any_api_key: bool,
) -> bool {
match agent {
AgentId::Claude | AgentId::Amp => has_anthropic,
AgentId::Codex => has_openai,
AgentId::Opencode => has_anthropic || has_openai,
AgentId::Pi | AgentId::Cursor => true,
AgentId::Opencode => has_anthropic || has_openai || has_other,
AgentId::Pi => has_any_api_key,
AgentId::Cursor => true,
AgentId::Mock => true,
}
}
@ -524,8 +530,22 @@ pub(super) fn build_provider_payload_for_opencode(_state: &Arc<AppState>) -> Val
AgentId::Cursor,
];
let has_anthropic = std::env::var("ANTHROPIC_API_KEY").is_ok();
let has_openai = std::env::var("OPENAI_API_KEY").is_ok();
let credentials = extract_all_credentials(&CredentialExtractionOptions::new());
let has_anthropic = credentials.anthropic.is_some();
let has_openai = credentials.openai.is_some();
let has_other = !credentials.other.is_empty();
let has_any_api_key = credentials
.anthropic
.as_ref()
.is_some_and(|cred| cred.auth_type == AuthType::ApiKey)
|| credentials
.openai
.as_ref()
.is_some_and(|cred| cred.auth_type == AuthType::ApiKey)
|| credentials
.other
.values()
.any(|cred| cred.auth_type == AuthType::ApiKey);
let mut all_providers = Vec::new();
let mut defaults = serde_json::Map::new();
@ -579,7 +599,15 @@ pub(super) fn build_provider_payload_for_opencode(_state: &Arc<AppState>) -> Val
defaults.insert(agent_str.to_string(), json!(current_value));
if agent == AgentId::Mock || credentials_available_for(agent, has_anthropic, has_openai) {
if agent == AgentId::Mock
|| credentials_available_for(
agent,
has_anthropic,
has_openai,
has_other,
has_any_api_key,
)
{
connected.push(json!(agent_str));
}