use std::{
collections::BTreeMap,
ffi::OsString,
fs,
os::unix::fs::{MetadataExt, PermissionsExt},
path::{Path, PathBuf},
time::Duration,
};
use serde::Deserialize;
use uuid::Uuid;
use crate::{
dependencies::{
command::{CommandRunner, CommandSpec},
installer::{
InstallerFilesystem, SyncPoint, SystemInstallerFilesystem,
activate_directory_transactionally,
},
},
domain::errors::{AgentError, AgentResult, ErrorCode},
};
const INSTALL_TIMEOUT: Duration = Duration::from_secs(5 * 60);
const PROBE_TIMEOUT: Duration = Duration::from_secs(5);
const MAX_PACKAGE_JSON_BYTES: u64 = 64 * 1024;
const MAX_TREE_ENTRIES: usize = 100_000;
pub(crate) const PI_PACKAGE: ManagedPackage =
ManagedPackage::new("@earendil-works/pi-coding-agent", "0.80.10", "dist/cli.js");
pub(crate) const CLAUDE_PACKAGE: ManagedPackage =
ManagedPackage::new("@anthropic-ai/claude-code", "2.1.212", "bin/claude.js");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ManagedPackage {
pub(crate) name: &'static str,
pub(crate) version: &'static str,
pub(crate) executable: &'static str,
}
impl ManagedPackage {
pub(crate) const fn new(
name: &'static str,
version: &'static str,
executable: &'static str,
) -> Self {
Self {
name,
version,
executable,
}
}
fn tool_name(self) -> AgentResult<&'static str> {
match self.name {
"@earendil-works/pi-coding-agent" => Ok("pi"),
"@anthropic-ai/claude-code" => Ok("claude"),
_ => Err(package_metadata_failed()),
}
}
fn package_spec(self) -> String {
format!("{}@{}", self.name, self.version)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ManagedNodeRuntime {
pub(crate) node: PathBuf,
pub(crate) npm: PathBuf,
pub(crate) tools: PathBuf,
}
impl ManagedNodeRuntime {
pub(crate) fn new(
node: impl Into<PathBuf>,
npm: impl Into<PathBuf>,
tools: impl Into<PathBuf>,
) -> Self {
Self {
node: node.into(),
npm: npm.into(),
tools: tools.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct InstalledManagedPackage {
pub(crate) root: PathBuf,
pub(crate) current: PathBuf,
pub(crate) executable: PathBuf,
pub(crate) version: String,
}
pub(crate) struct ManagedNpm<'a> {
runtime: ManagedNodeRuntime,
runner: &'a dyn CommandRunner,
filesystem: &'a dyn InstallerFilesystem,
}
static SYSTEM_FILESYSTEM: SystemInstallerFilesystem = SystemInstallerFilesystem;
impl<'a> ManagedNpm<'a> {
pub(crate) fn new(runtime: ManagedNodeRuntime, runner: &'a dyn CommandRunner) -> Self {
Self {
runtime,
runner,
filesystem: &SYSTEM_FILESYSTEM,
}
}
#[cfg(test)]
pub(crate) fn with_filesystem(
runtime: ManagedNodeRuntime,
runner: &'a dyn CommandRunner,
filesystem: &'a dyn InstallerFilesystem,
) -> Self {
Self {
runtime,
runner,
filesystem,
}
}
pub(crate) async fn install(
&self,
package: &ManagedPackage,
) -> AgentResult<InstalledManagedPackage> {
let package = *package;
let tool = package.tool_name()?;
let runtime = validate_runtime(&self.runtime)?;
let staging = runtime.tools.join(format!(
".{tool}-{}-stage-{}",
package.version,
Uuid::new_v4()
));
fs::create_dir(&staging).map_err(|_| package_filesystem_failed())?;
fs::set_permissions(&staging, fs::Permissions::from_mode(0o700))
.map_err(|_| package_filesystem_failed())?;
let staging_identity = DirectoryIdentity::capture(&staging)?;
let result = self
.install_staged(package, tool, &runtime, &staging, staging_identity)
.await;
if result.is_err() {
remove_staging(&staging);
}
result
}
async fn install_staged(
&self,
package: ManagedPackage,
tool: &str,
runtime: &ValidatedRuntime,
staging: &Path,
staging_identity: DirectoryIdentity,
) -> AgentResult<InstalledManagedPackage> {
let mut args = vec![
runtime.npm.clone().into_os_string(),
OsString::from("install"),
OsString::from("--global"),
];
if package == PI_PACKAGE {
args.push(OsString::from("--ignore-scripts"));
}
args.extend([
OsString::from("--prefix"),
staging.as_os_str().to_os_string(),
OsString::from(package.package_spec()),
]);
let install = CommandSpec {
program: runtime.node.clone(),
args,
env: BTreeMap::new(),
cwd: Some(staging.to_path_buf()),
timeout: INSTALL_TIMEOUT,
};
self.runner
.output(&install)
.await
.map_err(|_| package_install_failed())?;
staging_identity.verify(staging, &runtime.canonical_tools)?;
let package_root = package_root(staging, package.name);
validate_metadata(&package_root, package)?;
let staged_executable = validate_entrypoint(staging, &package_root, package.executable)?;
let probe = CommandSpec {
program: runtime.node.clone(),
args: vec![
staged_executable.clone().into_os_string(),
OsString::from("--version"),
],
env: if package == CLAUDE_PACKAGE {
BTreeMap::from([(OsString::from("DISABLE_AUTOUPDATER"), OsString::from("1"))])
} else {
BTreeMap::new()
},
cwd: Some(staging.to_path_buf()),
timeout: PROBE_TIMEOUT,
};
let output = self
.runner
.output(&probe)
.await
.map_err(|_| package_entrypoint_failed())?;
if !version_matches(&output.stdout, package.version) {
return Err(package_entrypoint_failed());
}
staging_identity.verify(staging, &runtime.canonical_tools)?;
harden_and_sync_tree(staging, self.filesystem)?;
staging_identity.verify(staging, &runtime.canonical_tools)?;
let canonical_staging =
fs::canonicalize(staging).map_err(|_| package_entrypoint_failed())?;
let relative_executable = staged_executable
.strip_prefix(canonical_staging)
.map_err(|_| package_entrypoint_failed())?
.to_path_buf();
let target_name = format!("{tool}-{}", package.version);
let target = runtime.tools.join(&target_name);
let current = runtime.tools.join(format!("{tool}-current"));
activate_directory_transactionally(
staging,
&target,
¤t,
&target_name,
&runtime.tools,
self.filesystem,
)?;
let executable = current.join(relative_executable);
Ok(InstalledManagedPackage {
executable,
root: target,
current,
version: package.version.to_owned(),
})
}
}
struct ValidatedRuntime {
node: PathBuf,
npm: PathBuf,
tools: PathBuf,
canonical_tools: PathBuf,
}
#[derive(Clone, Copy)]
struct DirectoryIdentity {
device: u64,
inode: u64,
}
impl DirectoryIdentity {
fn capture(path: &Path) -> AgentResult<Self> {
let metadata = fs::symlink_metadata(path).map_err(|_| package_filesystem_failed())?;
if metadata.file_type().is_symlink() || !metadata.is_dir() {
return Err(package_filesystem_failed());
}
Ok(Self {
device: metadata.dev(),
inode: metadata.ino(),
})
}
fn verify(self, path: &Path, tools: &Path) -> AgentResult<()> {
let metadata = fs::symlink_metadata(path).map_err(|_| package_filesystem_failed())?;
let canonical = fs::canonicalize(path).map_err(|_| package_filesystem_failed())?;
if metadata.file_type().is_symlink()
|| !metadata.is_dir()
|| metadata.dev() != self.device
|| metadata.ino() != self.inode
|| !canonical.starts_with(tools)
{
return Err(package_filesystem_failed());
}
Ok(())
}
}
fn remove_staging(path: &Path) {
match fs::symlink_metadata(path) {
Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => {
let _ = fs::remove_dir_all(path);
}
Ok(_) => {
let _ = fs::remove_file(path);
}
Err(_) => {}
}
}
fn validate_runtime(runtime: &ManagedNodeRuntime) -> AgentResult<ValidatedRuntime> {
let canonical_tools =
fs::canonicalize(&runtime.tools).map_err(|_| package_filesystem_failed())?;
if fs::symlink_metadata(&runtime.tools)
.is_ok_and(|metadata| metadata.file_type().is_symlink() || !metadata.is_dir())
{
return Err(package_filesystem_failed());
}
let node = canonical_executable(&runtime.node, &canonical_tools)?;
let npm = canonical_executable(&runtime.npm, &canonical_tools)?;
Ok(ValidatedRuntime {
node,
npm,
tools: runtime.tools.clone(),
canonical_tools,
})
}
fn canonical_executable(path: &Path, tools: &Path) -> AgentResult<PathBuf> {
let canonical = fs::canonicalize(path).map_err(|_| package_entrypoint_failed())?;
let metadata = fs::metadata(&canonical).map_err(|_| package_entrypoint_failed())?;
if !canonical.starts_with(tools)
|| !metadata.is_file()
|| metadata.permissions().mode() & 0o111 == 0
{
return Err(package_entrypoint_failed());
}
Ok(canonical)
}
fn package_root(staging: &Path, package_name: &str) -> PathBuf {
staging.join("lib/node_modules").join(package_name)
}
#[derive(Deserialize)]
struct PackageJson {
name: String,
version: String,
}
fn validate_metadata(root: &Path, package: ManagedPackage) -> AgentResult<()> {
let root_metadata = fs::symlink_metadata(root).map_err(|_| package_metadata_failed())?;
if root_metadata.file_type().is_symlink() || !root_metadata.is_dir() {
return Err(package_metadata_failed());
}
let path = root.join("package.json");
let metadata = fs::symlink_metadata(&path).map_err(|_| package_metadata_failed())?;
if metadata.file_type().is_symlink()
|| !metadata.is_file()
|| metadata.len() > MAX_PACKAGE_JSON_BYTES
{
return Err(package_metadata_failed());
}
let bytes = fs::read(path).map_err(|_| package_metadata_failed())?;
let metadata: PackageJson =
serde_json::from_slice(&bytes).map_err(|_| package_metadata_failed())?;
if metadata.name != package.name || metadata.version != package.version {
return Err(package_metadata_failed());
}
Ok(())
}
fn validate_entrypoint(
staging: &Path,
package_root: &Path,
relative: &str,
) -> AgentResult<PathBuf> {
let path = package_root.join(relative);
let raw_metadata = fs::symlink_metadata(&path).map_err(|_| package_entrypoint_failed())?;
if raw_metadata.file_type().is_symlink() {
return Err(package_entrypoint_failed());
}
let canonical_staging = fs::canonicalize(staging).map_err(|_| package_entrypoint_failed())?;
let canonical_package =
fs::canonicalize(package_root).map_err(|_| package_entrypoint_failed())?;
let canonical = fs::canonicalize(&path).map_err(|_| package_entrypoint_failed())?;
let metadata = fs::metadata(&canonical).map_err(|_| package_entrypoint_failed())?;
if !canonical_package.starts_with(&canonical_staging)
|| !canonical.starts_with(&canonical_package)
|| !metadata.is_file()
|| metadata.permissions().mode() & 0o111 == 0
{
return Err(package_entrypoint_failed());
}
Ok(canonical)
}
fn harden_and_sync_tree(root: &Path, filesystem: &dyn InstallerFilesystem) -> AgentResult<()> {
harden_and_sync_tree_with_limit(root, filesystem, MAX_TREE_ENTRIES)
}
pub(crate) fn harden_and_sync_tree_with_limit(
root: &Path,
filesystem: &dyn InstallerFilesystem,
max_entries: usize,
) -> AgentResult<()> {
let canonical_root = fs::canonicalize(root).map_err(|_| package_filesystem_failed())?;
let mut pending = vec![root.to_path_buf()];
let mut directories = Vec::new();
let mut entries = 1_usize;
if entries > max_entries {
return Err(package_filesystem_failed());
}
while let Some(directory) = pending.pop() {
directories.push(directory.clone());
for entry in fs::read_dir(&directory).map_err(|_| package_filesystem_failed())? {
let path = entry.map_err(|_| package_filesystem_failed())?.path();
entries = entries
.checked_add(1)
.filter(|count| *count <= max_entries)
.ok_or_else(package_filesystem_failed)?;
let metadata = fs::symlink_metadata(&path).map_err(|_| package_filesystem_failed())?;
if metadata.file_type().is_symlink() {
let target = fs::canonicalize(&path).map_err(|_| package_filesystem_failed())?;
if !target.starts_with(&canonical_root) {
return Err(package_filesystem_failed());
}
} else if metadata.is_dir() {
fs::set_permissions(&path, fs::Permissions::from_mode(0o700))
.map_err(|_| package_filesystem_failed())?;
pending.push(path);
} else if metadata.is_file() {
let mode = if metadata.permissions().mode() & 0o111 != 0 {
0o700
} else {
0o600
};
fs::set_permissions(&path, fs::Permissions::from_mode(mode))
.map_err(|_| package_filesystem_failed())?;
fs::File::open(path)
.and_then(|file| file.sync_all())
.map_err(|_| package_filesystem_failed())?;
} else {
return Err(package_filesystem_failed());
}
}
}
fs::set_permissions(root, fs::Permissions::from_mode(0o700))
.map_err(|_| package_filesystem_failed())?;
directories.sort_by_key(|path| std::cmp::Reverse(path.components().count()));
for directory in directories {
filesystem
.sync_directory(&directory, SyncPoint::ExtractedDirectory)
.map_err(|_| package_filesystem_failed())?;
}
Ok(())
}
fn version_matches(stdout: &[u8], expected: &str) -> bool {
std::str::from_utf8(stdout)
.ok()
.and_then(|output| output.lines().next())
.is_some_and(|version| version.trim().trim_start_matches('v') == expected)
}
fn package_install_failed() -> AgentError {
AgentError::new(ErrorCode::SkillDownloadFailed, "managed npm install failed")
}
fn package_metadata_failed() -> AgentError {
AgentError::new(
ErrorCode::SkillInvalid,
"managed package metadata validation failed",
)
}
fn package_entrypoint_failed() -> AgentError {
AgentError::new(
ErrorCode::SkillInvalid,
"managed package entrypoint validation failed",
)
}
fn package_filesystem_failed() -> AgentError {
AgentError::new(
ErrorCode::SkillFilesystemFailed,
"managed package filesystem operation failed",
)
}