use std::{
collections::BTreeMap,
ffi::OsString,
fs,
path::{Path, PathBuf},
time::Duration,
};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use crate::{
config::app::ProviderId,
dependencies::command::{CommandRunner, CommandSpec},
domain::errors::{AgentError, AgentResult, ErrorCode},
providers::descriptor::{ProviderDescriptor, descriptor},
};
const PROBE_TIMEOUT: Duration = Duration::from_secs(10);
#[derive(Clone, PartialEq, Eq)]
pub(crate) enum ToolLaunch {
Direct { executable: PathBuf },
ManagedNode { node: PathBuf, script: PathBuf },
}
impl ToolLaunch {
pub(crate) fn direct(executable: impl Into<PathBuf>) -> Self {
Self::Direct {
executable: executable.into(),
}
}
pub(crate) fn managed_node(node: impl Into<PathBuf>, script: impl Into<PathBuf>) -> Self {
Self::ManagedNode {
node: node.into(),
script: script.into(),
}
}
pub(crate) fn command(
&self,
args: &[&str],
managed_claude: bool,
cwd: &Path,
) -> AgentResult<CommandSpec> {
self.validate()?;
let (program, mut command_args) = match self {
Self::Direct { executable } => (executable.clone(), Vec::new()),
Self::ManagedNode { node, script } => {
(node.clone(), vec![script.clone().into_os_string()])
}
};
command_args.extend(args.iter().map(OsString::from));
let env = if managed_claude && matches!(self, Self::ManagedNode { .. }) {
BTreeMap::from([(OsString::from("DISABLE_AUTOUPDATER"), OsString::from("1"))])
} else {
BTreeMap::new()
};
Ok(CommandSpec {
program,
args: command_args,
env,
cwd: Some(cwd.to_path_buf()),
timeout: PROBE_TIMEOUT,
})
}
fn validate(&self) -> AgentResult<()> {
let paths: &[&Path] = match self {
Self::Direct { executable } => &[executable],
Self::ManagedNode { node, script } => &[node, script],
};
if paths.iter().any(|path| !path.is_absolute()) {
return Err(invariant_failed());
}
Ok(())
}
}
#[derive(Clone)]
pub(crate) struct ProviderProbeRuntime {
pub(crate) pi: ToolLaunch,
pub(crate) claude: Option<ToolLaunch>,
neutral_cwd: PathBuf,
}
impl ProviderProbeRuntime {
pub(crate) fn new(
pi: ToolLaunch,
claude: Option<ToolLaunch>,
neutral_cwd: impl Into<PathBuf>,
) -> Self {
Self {
pi,
claude,
neutral_cwd: neutral_cwd.into(),
}
}
pub(crate) fn validated_cwd(&self) -> AgentResult<PathBuf> {
self.pi.validate()?;
if let Some(claude) = &self.claude {
claude.validate()?;
}
canonical_neutral_cwd(&self.neutral_cwd)
}
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub(crate) struct ProviderProbeResult {
pub(crate) id: ProviderId,
pub(crate) executable_ready: bool,
pub(crate) extension_ready: bool,
pub(crate) authenticated: bool,
pub(crate) action: Option<String>,
}
impl ProviderProbeResult {
pub(crate) fn ready(&self) -> bool {
self.executable_ready && self.extension_ready && self.authenticated
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PiAuthState {
Present,
Absent,
Expired,
}
#[async_trait]
pub(crate) trait PiAuthMetadata: Send + Sync {
async fn state(&self, provider: ProviderId) -> AgentResult<PiAuthState>;
}
pub(crate) struct ProviderProbe<'a> {
runtime: ProviderProbeRuntime,
runner: &'a dyn CommandRunner,
pi_auth: &'a dyn PiAuthMetadata,
}
impl<'a> ProviderProbe<'a> {
pub(crate) fn new(
runtime: ProviderProbeRuntime,
runner: &'a dyn CommandRunner,
pi_auth: &'a dyn PiAuthMetadata,
) -> Self {
Self {
runtime,
runner,
pi_auth,
}
}
pub(crate) async fn probe(&self, id: ProviderId) -> AgentResult<ProviderProbeResult> {
let cwd = self.runtime.validated_cwd()?;
let provider = descriptor(id);
let executable_ready = provider.executable.is_none() || self.runtime.claude.is_some();
let extension_ready = self.extension_ready(&provider, &cwd).await;
let authenticated = if id == ProviderId::ClaudeCode {
self.claude_authenticated(&cwd).await
} else {
matches!(self.pi_auth.state(id).await, Ok(PiAuthState::Present))
};
let action = action_for(id, executable_ready, extension_ready, authenticated);
Ok(ProviderProbeResult {
id,
executable_ready,
extension_ready,
authenticated,
action: action.map(str::to_owned),
})
}
async fn extension_ready(&self, provider: &ProviderDescriptor, cwd: &Path) -> bool {
let Some(requirement) = provider.extension else {
return true;
};
let Ok(spec) = self.runtime.pi.command(&["list"], false, cwd) else {
return false;
};
let Ok(output) = self.runner.output(&spec).await else {
return false;
};
let expected = requirement.package.exact();
exact_list_field(&output.stdout, &expected)
}
async fn claude_authenticated(&self, cwd: &Path) -> bool {
let Some(claude) = &self.runtime.claude else {
return false;
};
let Ok(spec) = claude.command(&["auth", "status", "--json"], true, cwd) else {
return false;
};
let Ok(output) = self.runner.output(&spec).await else {
return false;
};
let parsed = serde_json::from_slice::<ClaudeAuthStatus>(&output.stdout)
.map(|status| status.logged_in)
.unwrap_or(false);
drop(output);
parsed
}
}
#[derive(Deserialize)]
struct ClaudeAuthStatus {
#[serde(rename = "loggedIn", alias = "logged_in")]
logged_in: bool,
}
fn exact_list_field(output: &[u8], expected: &str) -> bool {
std::str::from_utf8(output).ok().is_some_and(|output| {
output
.lines()
.flat_map(|line| line.split_whitespace())
.any(|field| field == expected)
})
}
fn action_for(
id: ProviderId,
executable_ready: bool,
extension_ready: bool,
authenticated: bool,
) -> Option<&'static str> {
if !executable_ready {
return Some("Install the Claude Code executable.");
}
if !extension_ready {
return Some(match id {
ProviderId::ClaudeCode => "Install the reviewed Claude Code Pi extension.",
ProviderId::Antigravity => "Install the reviewed Antigravity Pi extension.",
ProviderId::OpenaiCodex => "Configure the OpenAI Codex provider.",
});
}
if !authenticated {
return Some(match id {
ProviderId::OpenaiCodex => "Log in to OpenAI Codex through Pi.",
ProviderId::ClaudeCode => "Log in to Claude Code.",
ProviderId::Antigravity => "Log in to Antigravity through Pi.",
});
}
None
}
fn invariant_failed() -> AgentError {
AgentError::new(
ErrorCode::InvalidMessage,
"provider executable paths must be absolute",
)
}
fn canonical_neutral_cwd(path: &Path) -> AgentResult<PathBuf> {
if !path.is_absolute() {
return Err(neutral_cwd_invariant_failed());
}
let metadata = fs::symlink_metadata(path).map_err(|_| neutral_cwd_invariant_failed())?;
if metadata.file_type().is_symlink() || !metadata.is_dir() {
return Err(neutral_cwd_invariant_failed());
}
let canonical = fs::canonicalize(path).map_err(|_| neutral_cwd_invariant_failed())?;
if !fs::metadata(&canonical)
.map_err(|_| neutral_cwd_invariant_failed())?
.is_dir()
{
return Err(neutral_cwd_invariant_failed());
}
Ok(canonical)
}
fn neutral_cwd_invariant_failed() -> AgentError {
AgentError::new(
ErrorCode::InvalidMessage,
"provider neutral cwd must be an existing absolute non-symlink directory",
)
}