use std::{
collections::BTreeMap,
ffi::OsString,
fs,
io::Write,
os::unix::fs::{OpenOptionsExt, PermissionsExt},
path::{Path, PathBuf},
time::Duration,
};
use uuid::Uuid;
use crate::{
dependencies::command::{CommandRunner, CommandSpec},
domain::errors::{AgentError, AgentResult, ErrorCode},
};
const COMMAND_TIMEOUT: Duration = Duration::from_secs(2 * 60);
const MAX_SETTINGS_BYTES: u64 = 1024 * 1024;
pub(crate) const CLAUDE_AUTH_EXTENSION: PiPackageSource =
PiPackageSource::new("@cgaravitoq/pi-claude-code-auth", "2.3.0");
pub(crate) const ANTIGRAVITY_EXTENSION: PiPackageSource =
PiPackageSource::new("@raquezha/antigravity", "0.0.9");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct PiPackageSource {
pub(crate) name: &'static str,
pub(crate) version: &'static str,
}
impl PiPackageSource {
pub(crate) const fn new(name: &'static str, version: &'static str) -> Self {
Self { name, version }
}
pub(crate) fn exact(self) -> String {
format!("npm:{}@{}", self.name, self.version)
}
fn is_reviewed(self) -> bool {
self == CLAUDE_AUTH_EXTENSION || self == ANTIGRAVITY_EXTENSION
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ManagedPiRuntime {
pub(crate) pi: PathBuf,
pub(crate) node: PathBuf,
pub(crate) npm: PathBuf,
pub(crate) tools: PathBuf,
pub(crate) settings: PathBuf,
}
impl ManagedPiRuntime {
pub(crate) fn new(
pi: impl Into<PathBuf>,
node: impl Into<PathBuf>,
npm: impl Into<PathBuf>,
tools: impl Into<PathBuf>,
settings: impl Into<PathBuf>,
) -> Self {
Self {
pi: pi.into(),
node: node.into(),
npm: npm.into(),
tools: tools.into(),
settings: settings.into(),
}
}
}
pub(crate) struct ManagedPi<'a> {
runtime: ManagedPiRuntime,
runner: &'a dyn CommandRunner,
}
impl<'a> ManagedPi<'a> {
pub(crate) fn new(runtime: ManagedPiRuntime, runner: &'a dyn CommandRunner) -> Self {
Self { runtime, runner }
}
pub(crate) async fn install_extension(&self, source: &PiPackageSource) -> AgentResult<()> {
let source = *source;
if !source.is_reviewed() {
return Err(extension_failed());
}
let runtime = validate_runtime(&self.runtime)?;
let snapshot = SettingsSnapshot::capture(&self.runtime.settings)?;
configure_managed_npm(
&self.runtime.settings,
&runtime.node,
&runtime.npm,
&snapshot,
)?;
let install = CommandSpec {
program: runtime.node.clone(),
args: vec![
runtime.pi.clone().into_os_string(),
OsString::from("install"),
OsString::from(source.exact()),
],
env: BTreeMap::new(),
cwd: Some(runtime.cwd.clone()),
timeout: COMMAND_TIMEOUT,
};
if let Err(error) = self.runner.output(&install).await {
snapshot.restore(&self.runtime.settings)?;
return Err(extension_command_failed(error));
}
let list = CommandSpec {
program: runtime.node,
args: vec![runtime.pi.into_os_string(), OsString::from("list")],
env: BTreeMap::new(),
cwd: Some(runtime.cwd),
timeout: COMMAND_TIMEOUT,
};
let output = match self.runner.output(&list).await {
Ok(output) => output,
Err(error) => {
snapshot.restore(&self.runtime.settings)?;
return Err(extension_confirmation_command_failed(error));
}
};
if !registered_exactly(&output.stdout, &source.exact()) {
snapshot.restore(&self.runtime.settings)?;
return Err(extension_confirmation_failed());
}
Ok(())
}
}
struct ValidatedRuntime {
pi: PathBuf,
node: PathBuf,
npm: PathBuf,
cwd: PathBuf,
}
fn validate_runtime(runtime: &ManagedPiRuntime) -> AgentResult<ValidatedRuntime> {
let tools = fs::canonicalize(&runtime.tools).map_err(|_| extension_filesystem_failed())?;
if fs::symlink_metadata(&runtime.tools)
.is_ok_and(|metadata| metadata.file_type().is_symlink() || !metadata.is_dir())
{
return Err(extension_filesystem_failed());
}
let pi = contained_executable(&runtime.pi, &tools)?;
let node = contained_executable(&runtime.node, &tools)?;
let npm = contained_executable(&runtime.npm, &tools)?;
let cwd = settings_cwd(&runtime.settings)?;
Ok(ValidatedRuntime { pi, node, npm, cwd })
}
fn settings_cwd(settings: &Path) -> AgentResult<PathBuf> {
let parent = settings.parent().ok_or_else(extension_filesystem_failed)?;
if !parent.is_absolute() {
return Err(extension_filesystem_failed());
}
let metadata = fs::symlink_metadata(parent).map_err(|_| extension_filesystem_failed())?;
if metadata.file_type().is_symlink() || !metadata.is_dir() {
return Err(extension_filesystem_failed());
}
let canonical = fs::canonicalize(parent).map_err(|_| extension_filesystem_failed())?;
let canonical_metadata = fs::metadata(&canonical).map_err(|_| extension_filesystem_failed())?;
if !canonical_metadata.is_dir() {
return Err(extension_filesystem_failed());
}
Ok(canonical)
}
fn contained_executable(path: &Path, tools: &Path) -> AgentResult<PathBuf> {
let canonical = fs::canonicalize(path).map_err(|_| extension_filesystem_failed())?;
let metadata = fs::metadata(&canonical).map_err(|_| extension_filesystem_failed())?;
if !canonical.starts_with(tools)
|| !metadata.is_file()
|| metadata.permissions().mode() & 0o111 == 0
{
return Err(extension_filesystem_failed());
}
Ok(canonical)
}
enum SettingsSnapshot {
Missing,
File { bytes: Vec<u8>, mode: u32 },
}
impl SettingsSnapshot {
fn capture(path: &Path) -> AgentResult<Self> {
match fs::symlink_metadata(path) {
Ok(metadata) => {
if metadata.file_type().is_symlink()
|| !metadata.is_file()
|| metadata.len() > MAX_SETTINGS_BYTES
{
return Err(extension_filesystem_failed());
}
let bytes = fs::read(path).map_err(|_| extension_filesystem_failed())?;
Ok(Self::File {
bytes,
mode: metadata.permissions().mode() & 0o777,
})
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(Self::Missing),
Err(_) => Err(extension_filesystem_failed()),
}
}
fn restore(&self, path: &Path) -> AgentResult<()> {
match self {
Self::Missing => {
if path.exists() {
fs::remove_file(path).map_err(|_| extension_filesystem_failed())?;
sync_parent(path)?;
}
Ok(())
}
Self::File { bytes, mode } => write_settings(path, bytes, *mode),
}
}
}
fn configure_managed_npm(
path: &Path,
node: &Path,
npm: &Path,
snapshot: &SettingsSnapshot,
) -> AgentResult<()> {
if !node.is_absolute() || !npm.is_absolute() {
return Err(extension_filesystem_failed());
}
let node = node.to_str().ok_or_else(extension_filesystem_failed)?;
let npm = npm.to_str().ok_or_else(extension_filesystem_failed)?;
let mut settings = match snapshot {
SettingsSnapshot::Missing => serde_json::Map::new(),
SettingsSnapshot::File { bytes, .. } => {
let value: serde_json::Value =
serde_json::from_slice(bytes).map_err(|_| extension_filesystem_failed())?;
value
.as_object()
.cloned()
.ok_or_else(extension_filesystem_failed)?
}
};
settings.insert(
"npmCommand".to_owned(),
serde_json::Value::Array(vec![
serde_json::Value::String(node.to_owned()),
serde_json::Value::String(npm.to_owned()),
]),
);
let bytes = serde_json::to_vec(&settings).map_err(|_| extension_filesystem_failed())?;
write_settings(path, &bytes, 0o600)
}
fn write_settings(path: &Path, bytes: &[u8], mode: u32) -> AgentResult<()> {
let parent = path.parent().ok_or_else(extension_filesystem_failed)?;
fs::create_dir_all(parent).map_err(|_| extension_filesystem_failed())?;
let parent_metadata =
fs::symlink_metadata(parent).map_err(|_| extension_filesystem_failed())?;
if parent_metadata.file_type().is_symlink() || !parent_metadata.is_dir() {
return Err(extension_filesystem_failed());
}
if fs::symlink_metadata(path)
.is_ok_and(|metadata| metadata.file_type().is_symlink() || !metadata.is_file())
{
return Err(extension_filesystem_failed());
}
let temporary = parent.join(format!(".settings-{}.tmp", Uuid::new_v4()));
let mut file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(mode)
.open(&temporary)
.map_err(|_| extension_filesystem_failed())?;
let result = (|| {
file.write_all(bytes)?;
file.set_permissions(fs::Permissions::from_mode(mode))?;
file.sync_all()?;
fs::rename(&temporary, path)?;
fs::File::open(parent)?.sync_all()
})();
if result.is_err() {
let _ = fs::remove_file(&temporary);
return Err(extension_filesystem_failed());
}
Ok(())
}
fn sync_parent(path: &Path) -> AgentResult<()> {
fs::File::open(path.parent().ok_or_else(extension_filesystem_failed)?)
.and_then(|directory| directory.sync_all())
.map_err(|_| extension_filesystem_failed())
}
fn registered_exactly(stdout: &[u8], expected: &str) -> bool {
std::str::from_utf8(stdout)
.ok()
.is_some_and(|output| output.lines().any(|line| line.trim() == expected))
}
fn extension_failed() -> AgentError {
AgentError::new(
ErrorCode::SkillDownloadFailed,
"Pi extension install failed",
)
}
fn extension_command_failed(error: AgentError) -> AgentError {
AgentError::new(
ErrorCode::SkillDownloadFailed,
format!("Pi extension install failed: {}", error.message()),
)
}
fn extension_confirmation_failed() -> AgentError {
AgentError::new(ErrorCode::SkillInvalid, "Pi extension confirmation failed")
}
fn extension_confirmation_command_failed(error: AgentError) -> AgentError {
AgentError::new(
ErrorCode::SkillInvalid,
format!("Pi extension confirmation failed: {}", error.message()),
)
}
fn extension_filesystem_failed() -> AgentError {
AgentError::new(
ErrorCode::SkillFilesystemFailed,
"Pi extension settings operation failed",
)
}