support pi

This commit is contained in:
Franklin 2026-02-05 17:06:53 -05:00
parent cc5a9e0d73
commit 843498e9db
41 changed files with 2654 additions and 102 deletions

View file

@ -0,0 +1,30 @@
use std::env;
use reqwest::blocking::ClientBuilder as BlockingClientBuilder;
use reqwest::ClientBuilder;
const NO_SYSTEM_PROXY_ENV: &str = "SANDBOX_AGENT_NO_SYSTEM_PROXY";
fn disable_system_proxy() -> bool {
env::var(NO_SYSTEM_PROXY_ENV)
.map(|value| matches!(value.as_str(), "1" | "true" | "TRUE" | "yes" | "YES"))
.unwrap_or(false)
}
pub fn client_builder() -> ClientBuilder {
let builder = reqwest::Client::builder();
if disable_system_proxy() {
builder.no_proxy()
} else {
builder
}
}
pub fn blocking_client_builder() -> BlockingClientBuilder {
let builder = reqwest::blocking::Client::builder();
if disable_system_proxy() {
builder.no_proxy()
} else {
builder
}
}

View file

@ -2,6 +2,7 @@
mod agent_server_logs;
pub mod credentials;
pub mod http_client;
pub mod router;
pub mod telemetry;
pub mod ui;

View file

@ -11,6 +11,7 @@ mod build_version {
}
use reqwest::blocking::Client as HttpClient;
use reqwest::Method;
use sandbox_agent::http_client;
use sandbox_agent::router::{build_router_with_state, shutdown_servers};
use sandbox_agent::router::{
AgentInstallRequest, AppState, AuthConfig, CreateSessionRequest, MessageRequest,
@ -687,6 +688,7 @@ enum CredentialAgent {
Codex,
Opencode,
Amp,
Pi,
}
fn credentials_to_output(credentials: ExtractedCredentials, reveal: bool) -> CredentialsOutput {
@ -806,6 +808,31 @@ fn select_token_for_agent(
)))
}
}
CredentialAgent::Pi => {
if let Some(provider) = provider {
return select_token_for_provider(credentials, provider);
}
if let Some(openai) = credentials.openai.as_ref() {
return Ok(openai.api_key.clone());
}
if let Some(anthropic) = credentials.anthropic.as_ref() {
return Ok(anthropic.api_key.clone());
}
if credentials.other.len() == 1 {
if let Some((_, cred)) = credentials.other.iter().next() {
return Ok(cred.api_key.clone());
}
}
let available = available_providers(credentials);
if available.is_empty() {
Err(CliError::Server("no credentials found for pi".to_string()))
} else {
Err(CliError::Server(format!(
"multiple providers available for pi: {} (use --provider)",
available.join(", ")
)))
}
}
}
}
@ -919,7 +946,7 @@ impl ClientContext {
} else {
cli.token.clone()
};
let client = HttpClient::builder().build()?;
let client = http_client::blocking_client_builder().build()?;
Ok(Self {
endpoint,
token,

File diff suppressed because it is too large Load diff

View file

@ -10,6 +10,8 @@ use serde::Serialize;
use time::OffsetDateTime;
use tokio::time::Instant;
use crate::http_client;
const TELEMETRY_URL: &str = "https://tc.rivet.dev";
const TELEMETRY_ENV_DEBUG: &str = "SANDBOX_AGENT_TELEMETRY_DEBUG";
const TELEMETRY_ID_FILE: &str = "telemetry_id";
@ -77,7 +79,7 @@ pub fn log_enabled_message() {
pub fn spawn_telemetry_task() {
tokio::spawn(async move {
let client = match Client::builder()
let client = match http_client::client_builder()
.timeout(Duration::from_millis(TELEMETRY_TIMEOUT_MS))
.build()
{

View file

@ -5,3 +5,4 @@ mod agent_permission_flow;
mod agent_question_flow;
mod agent_termination;
mod agent_tool_flow;
mod pi_rpc_integration;

View file

@ -0,0 +1,61 @@
// Pi RPC integration tests (gated via SANDBOX_TEST_PI + PATH).
include!("../common/http.rs");
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn pi_rpc_session_and_stream() {
let configs = match test_agents_from_env() {
Ok(configs) => configs,
Err(err) => {
eprintln!("Skipping Pi RPC integration test: {err}");
return;
}
};
let Some(config) = configs.iter().find(|config| config.agent == AgentId::Pi) else {
return;
};
let app = TestApp::new();
let _guard = apply_credentials(&config.credentials);
install_agent(&app.app, config.agent).await;
let session_id = "pi-rpc-session".to_string();
let (status, payload) = send_json(
&app.app,
Method::POST,
&format!("/v1/sessions/{session_id}"),
Some(json!({
"agent": "pi",
"permissionMode": test_permission_mode(AgentId::Pi),
})),
)
.await;
assert_eq!(status, StatusCode::OK, "create pi session");
let native_session_id = payload
.get("native_session_id")
.and_then(Value::as_str)
.unwrap_or("");
assert!(
!native_session_id.is_empty(),
"expected native_session_id for pi session"
);
let events = read_turn_stream_events(&app.app, &session_id, Duration::from_secs(120)).await;
assert!(!events.is_empty(), "no events from pi stream");
assert!(
!events.iter().any(is_unparsed_event),
"agent.unparsed event encountered"
);
let mut last_sequence = 0u64;
for event in events {
let sequence = event
.get("sequence")
.and_then(Value::as_u64)
.expect("missing sequence");
assert!(
sequence > last_sequence,
"sequence did not increase (prev {last_sequence}, next {sequence})"
);
last_sequence = sequence;
}
}

View file

@ -1,4 +1,5 @@
use std::collections::HashMap;
use std::env;
use sandbox_agent_agent_management::agents::{
AgentError, AgentId, AgentManager, InstallOptions, SpawnOptions,
@ -29,6 +30,29 @@ fn prompt_ok(label: &str) -> String {
format!("Respond with exactly the text {label} and nothing else.")
}
fn pi_tests_enabled() -> bool {
env::var("SANDBOX_TEST_PI")
.map(|value| {
let value = value.trim().to_ascii_lowercase();
value == "1" || value == "true" || value == "yes"
})
.unwrap_or(false)
}
fn pi_on_path() -> bool {
let binary = AgentId::Pi.binary_name();
let path_var = match env::var_os("PATH") {
Some(path) => path,
None => return false,
};
for path in env::split_paths(&path_var) {
if path.join(binary).exists() {
return true;
}
}
false
}
#[test]
fn test_agents_install_version_spawn() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = tempfile::tempdir()?;
@ -36,12 +60,15 @@ fn test_agents_install_version_spawn() -> Result<(), Box<dyn std::error::Error>>
let env = build_env();
assert!(!env.is_empty(), "expected credentials to be available");
let agents = [
let mut agents = vec![
AgentId::Claude,
AgentId::Codex,
AgentId::Opencode,
AgentId::Amp,
];
if pi_tests_enabled() && pi_on_path() {
agents.push(AgentId::Pi);
}
for agent in agents {
let install = manager.install(agent, InstallOptions::default())?;
assert!(install.path.exists(), "expected install for {agent}");

View file

@ -178,7 +178,7 @@ async fn install_agent(app: &Router, agent: AgentId) {
/// while other agents support "bypass" which skips tool approval.
fn test_permission_mode(agent: AgentId) -> &'static str {
match agent {
AgentId::Opencode => "default",
AgentId::Opencode | AgentId::Pi => "default",
_ => "bypass",
}
}

View file

@ -182,7 +182,7 @@ pub async fn create_session_with_mode(
pub fn test_permission_mode(agent: AgentId) -> &'static str {
match agent {
AgentId::Opencode => "default",
AgentId::Opencode | AgentId::Pi => "default",
_ => "bypass",
}
}