use super::child;
use super::dispatch::EnvLookup;
use crate::prompt::dispatch::door::caller::{self, Caller};
use crate::prompt::dispatch::tools::{read_description, read_schema};
use crate::prompt::tool::{ENV_TOOL_ID, STEP_TOOLS_SUBDIR};
use serde::Deserialize;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
use stub::ToolDef;
use thiserror::Error;
pub(crate) mod stub;
const INTERPRETER: &str = "python3";
const MODULE: &str = "litany_tools.py";
const NOT_FOUND: i32 = 127;
const PYTHONPATH: &str = "PYTHONPATH";
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct Input {
program: String,
}
#[derive(Debug, Error)]
pub enum Error {
#[error("invalid input JSON: {0}")]
InvalidJson(#[source] serde_json::Error),
#[error("read input from stdin: {0}")]
StdinRead(#[source] io::Error),
#[error("missing env var {0:?} (set by the harness per ARCH §3.3)")]
MissingEnv(&'static str),
#[error(transparent)]
Caller(#[from] caller::Error),
#[error(transparent)]
Definitions(#[from] crate::prompt::Error),
#[error("write {path}: {source}")]
Module {
path: PathBuf,
#[source]
source: io::Error,
},
#[error(transparent)]
Child(#[from] child::Error),
#[error("write to stdout: {0}")]
Stdout(#[source] io::Error),
#[error("write to stderr: {0}")]
Stderr(#[source] io::Error),
}
pub fn run<R: Read, W: Write, E: Write>(
stdin: &mut R,
stdout: &mut W,
stderr: &mut E,
bindings: &super::Bindings<'_>,
env: &dyn EnvLookup,
) -> Result<i32, Error> {
child::install_sigterm_handler();
run_with(stdin, stdout, stderr, bindings, env, INTERPRETER)
}
pub(crate) fn run_with<R: Read, W: Write, E: Write>(
stdin: &mut R,
stdout: &mut W,
stderr: &mut E,
bindings: &super::Bindings<'_>,
env: &dyn EnvLookup,
interpreter: &str,
) -> Result<i32, Error> {
let mut buf = Vec::new();
stdin.read_to_end(&mut buf).map_err(Error::StdinRead)?;
let input: Input = serde_json::from_slice(&buf).map_err(Error::InvalidJson)?;
let tool_id = env
.get(ENV_TOOL_ID)
.ok_or(Error::MissingEnv(ENV_TOOL_ID))?
.into_string()
.map_err(|_| Error::MissingEnv(ENV_TOOL_ID))?;
let caller = caller::resolve(
env,
bindings.driver_target,
bindings.adapter_target,
bindings.stop,
bindings.injection,
)?;
let record = caller.step_dir.join(STEP_TOOLS_SUBDIR).join(&tool_id);
let module = record.join(MODULE);
let source = stub::module(&toolset(&caller)?, bindings.driver_target, &tool_id);
write(&record, &module, &source)?;
let mut cmd = Command::new(interpreter);
cmd.arg("-").env(PYTHONPATH, path_with(env, &record));
let src = Some(input.program.as_bytes());
let (stop, grace) = (bindings.stop, child::CASCADE_DEADLINE);
let spawned = child::run(&mut cmd, src, stop, grace);
let done = match spawned {
Ok(done) => done,
Err(child::Error::Spawn(e)) if e.kind() == io::ErrorKind::NotFound => {
let text = format!(
"{interpreter}: not found. The `python` tool runs a program with \
{interpreter} on this machine's PATH (ARCH §3.3); this deployment \
has none, so the tool should not be granted here.\n"
);
stderr.write_all(text.as_bytes()).map_err(Error::Stderr)?;
return Ok(NOT_FOUND);
}
Err(e) => return Err(Error::Child(e)),
};
stdout.write_all(&done.stdout).map_err(Error::Stdout)?;
stderr.write_all(&done.stderr).map_err(Error::Stderr)?;
Ok(done.code)
}
fn toolset(caller: &Caller) -> Result<Vec<ToolDef>, crate::prompt::Error> {
let worktree = crate::workspace::agent_worktree(&caller.workspace, &caller.agent);
let mut out = Vec::new();
for name in caller.grant.iter().filter(|n| n.as_str() != super::PYTHON) {
if let Some(input_schema) = read_schema(&worktree, name)? {
out.push(ToolDef {
name: name.clone(),
description: read_description(&worktree, name)?,
input_schema,
});
}
}
for tool in caller.injected.iter().filter(|t| t.name != super::PYTHON) {
out.push(ToolDef {
name: tool.name.clone(),
description: tool.description.clone(),
input_schema: tool.input_schema.clone(),
});
}
Ok(out)
}
fn write(record: &Path, module: &Path, source: &str) -> Result<(), Error> {
std::fs::create_dir_all(record)
.and_then(|()| std::fs::write(module, source))
.map_err(|source| Error::Module {
path: module.to_path_buf(),
source,
})
}
fn path_with(env: &dyn EnvLookup, record: &Path) -> std::ffi::OsString {
let mut path = record.as_os_str().to_owned();
if let Some(existing) = env.get(PYTHONPATH) {
path.push(":");
path.push(existing);
}
path
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod tests_faults;