use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use car_engine::{agent_basics, CommandOutput, LocalSubstrate, Substrate, ToolExecutor};
use car_policy::InspectorChain;
use serde_json::{json, Value};
use super::policy::{
coder_inspector_chain, coder_inspector_chain_with_project_policies, stays_under,
};
pub(crate) const DEFAULT_SHELL_TIMEOUT_SECS: u64 = 120;
pub(crate) const MAX_SHELL_TIMEOUT_SECS: u64 = 600;
pub(crate) const MAX_OUTPUT_BYTES: usize = 64 * 1024;
pub(crate) fn tail(s: &str, cap: usize) -> String {
if s.len() <= cap {
return s.to_string();
}
let mut start = s.len() - cap;
while !s.is_char_boundary(start) {
start += 1;
}
format!("…[truncated]…{}", &s[start..])
}
fn shell_result(stdout: &str, stderr: &str, exit_code: i32) -> Value {
let mut combined = stdout.to_string();
if !stderr.is_empty() {
if !combined.is_empty() && !combined.ends_with('\n') {
combined.push('\n');
}
combined.push_str(stderr);
}
json!({
"exit_code": exit_code,
"output": tail(&combined, MAX_OUTPUT_BYTES),
"timed_out": false,
})
}
pub(crate) fn clamp_paths_to(
root: &std::path::Path,
tool: &str,
params: &Value,
root_noun: &str,
clamp_reads: bool,
) -> Result<Value, String> {
let mut params = params.clone();
let Some(obj) = params.as_object_mut() else {
return Ok(params);
};
if let Some(Value::String(p)) = obj.get("path") {
let pinned = matches!(tool, "write_file" | "edit_file")
|| (clamp_reads
&& matches!(tool, "read_file" | "list_dir" | "find_files" | "grep_files"));
if !stays_under(root, p) && pinned {
return Err(format!("path '{p}' resolves outside the {root_noun}"));
}
if Path::new(p).is_relative() {
let abs = root.join(p);
obj.insert("path".into(), json!(abs.to_string_lossy()));
}
} else if matches!(tool, "list_dir" | "find_files" | "grep_files") {
obj.entry("path")
.or_insert_with(|| json!(root.to_string_lossy()));
}
Ok(params)
}
fn sh_single_quote(s: &str) -> String {
format!("'{}'", s.replace('\'', r"'\''"))
}
fn withhold_forge_credentials(cmd: &mut tokio::process::Command) {
for var in [
"GH_TOKEN",
"GITHUB_TOKEN",
"GH_ENTERPRISE_TOKEN",
"GITHUB_ENTERPRISE_TOKEN",
] {
cmd.env_remove(var);
}
cmd.env("GH_CONFIG_DIR", empty_config_dir());
cmd.env("GIT_CONFIG_COUNT", "1")
.env("GIT_CONFIG_KEY_0", "credential.helper")
.env("GIT_CONFIG_VALUE_0", "")
.env("GIT_TERMINAL_PROMPT", "0")
.env("GIT_ASKPASS", "")
.env("SSH_ASKPASS", "")
.env("SSH_ASKPASS_REQUIRE", "never");
}
fn empty_config_dir() -> std::path::PathBuf {
let dir = car_home::root_or_relative()
.join("run")
.join("no-forge-config");
if std::fs::create_dir_all(&dir).is_ok() {
return dir;
}
let fallback = std::env::temp_dir().join("car-no-forge-config");
let _ = std::fs::create_dir_all(&fallback);
fallback
}
fn prepend_inherited_path(command: &str) -> String {
match std::env::var("PATH") {
Ok(p) if !p.trim().is_empty() => {
format!("export PATH={}:\"$PATH\"; {}", sh_single_quote(&p), command)
}
_ => command.to_string(),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ForgeCredentials {
Inherit,
Withhold,
}
pub(crate) async fn run_shell_on(
substrate: &Arc<dyn Substrate>,
cwd: Option<&Path>,
inspectors: &InspectorChain,
command: &str,
timeout_secs: Option<u64>,
max_timeout_secs: u64,
forge_credentials: ForgeCredentials,
) -> Result<Value, String> {
if let Some(reason) = inspectors.check("shell", &json!({ "command": command })) {
return Err(format!("denied by policy: {reason}"));
}
let secs = timeout_secs
.unwrap_or(DEFAULT_SHELL_TIMEOUT_SECS)
.clamp(1, max_timeout_secs.max(1));
if !substrate.is_local() {
let CommandOutput {
stdout,
stderr,
exit_code,
} = substrate.run_command(command, Some(secs as f64)).await?;
return Ok(shell_result(&stdout, &stderr, exit_code));
}
let timeout = Duration::from_secs(secs);
let mut cmd = if cfg!(target_os = "windows") {
let mut c = tokio::process::Command::new("cmd");
c.arg("/C").arg(command);
if let Some(path) = car_engine::win_env::cmd_path_override() {
c.env("PATH", path);
}
c
} else {
let mut c = tokio::process::Command::new("/bin/sh");
c.arg("-lc").arg(prepend_inherited_path(command));
c
};
if forge_credentials == ForgeCredentials::Withhold {
withhold_forge_credentials(&mut cmd);
}
cmd.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true);
if let Some(dir) = cwd {
cmd.current_dir(dir);
}
#[cfg(unix)]
cmd.process_group(0);
let child = cmd
.spawn()
.map_err(|e| format!("failed to spawn shell: {e}"))?;
#[cfg(unix)]
let pgid = child.id();
#[cfg(windows)]
let job = match car_registry::supervisor::JobObject::new() {
Ok(j) => {
if let Some(pid) = child.id() {
let _ = j.assign(pid);
}
Some(j)
}
Err(_) => None,
};
match tokio::time::timeout(timeout, child.wait_with_output()).await {
Ok(Ok(out)) => Ok(shell_result(
&String::from_utf8_lossy(&out.stdout),
&String::from_utf8_lossy(&out.stderr),
out.status.code().unwrap_or(-1),
)),
Ok(Err(e)) => Err(format!("shell wait failed: {e}")),
Err(_elapsed) => {
#[cfg(unix)]
if let Some(pid) = pgid {
unsafe {
libc::killpg(pid as i32, libc::SIGKILL);
}
}
#[cfg(windows)]
if let Some(job) = &job {
let _ = job.terminate(1);
}
Ok(json!({
"exit_code": Value::Null,
"output": format!("command timed out after {}s and was killed", timeout.as_secs()),
"timed_out": true,
}))
}
}
}
fn recall_only_memory_defs() -> Vec<Value> {
crate::assistant::memory::MemoryTools::tool_defs()
.into_iter()
.filter(|d| d["name"] == "recall")
.collect()
}
struct Delegate {
executor: Arc<dyn ToolExecutor>,
defs: Vec<Value>,
}
impl Delegate {
fn tool_names(&self) -> impl Iterator<Item = String> + '_ {
self.defs
.iter()
.filter_map(|d| d["name"].as_str().map(String::from))
}
fn advertises(&self, tool: &str) -> bool {
self.defs.iter().any(|d| d["name"] == tool)
}
}
pub struct WorktreeExecutor {
worktree: PathBuf,
inspectors: InspectorChain,
delegates: Vec<Delegate>,
agent_id: Option<String>,
read_ledgers: agent_basics::SessionReadLedgers,
mutations: Arc<super::no_change::MutationLedger>,
delegates_reachable: Arc<std::sync::atomic::AtomicBool>,
check_timeout_ceiling: u64,
}
fn enforce_agent_permission(
agent_id: &str,
tool: &str,
tier: car_policy::PermissionTier,
mode: car_policy::ApprovalMode,
) -> Result<(), String> {
match mode {
car_policy::ApprovalMode::AlwaysAllow => Ok(()),
car_policy::ApprovalMode::Deny => Err(format!(
"denied for agent '{agent_id}' by your Agent Permissions settings: \
'{tool}' is a {}-tier action this agent may not perform",
tier.as_str()
)),
car_policy::ApprovalMode::RequireApproval
if tier == car_policy::PermissionTier::FullAccess =>
{
Err(format!(
"approval required for agent '{agent_id}' by your Agent Permissions \
settings: '{tool}' is a {}-tier action, but this runner has no \
interactive approval channel",
tier.as_str()
))
}
car_policy::ApprovalMode::RequireApproval => Ok(()),
}
}
impl WorktreeExecutor {
pub fn new(worktree: impl Into<PathBuf>) -> Self {
let worktree: PathBuf = worktree.into();
let worktree = worktree.canonicalize().unwrap_or(worktree);
let inspectors = coder_inspector_chain(&worktree);
Self {
worktree,
inspectors,
delegates: Vec::new(),
agent_id: None,
read_ledgers: agent_basics::SessionReadLedgers::new(),
mutations: Arc::new(super::no_change::MutationLedger::new()),
delegates_reachable: Arc::new(std::sync::atomic::AtomicBool::new(false)),
check_timeout_ceiling: MAX_SHELL_TIMEOUT_SECS,
}
}
pub fn with_check_timeout_ceiling(mut self, secs: u64) -> Self {
self.check_timeout_ceiling = secs.max(1);
self
}
pub fn check_timeout_ceiling(&self) -> u64 {
self.check_timeout_ceiling
}
pub fn has_mutated(&self) -> bool {
self.mutations.has_mutated()
}
pub fn advertise_delegates(&self) {
self.delegates_reachable
.store(true, std::sync::atomic::Ordering::SeqCst);
}
pub fn delegates_reachable(&self) -> bool {
self.delegates_reachable
.load(std::sync::atomic::Ordering::SeqCst)
}
pub fn permits_full_access(&self) -> bool {
let Some(agent_id) = &self.agent_id else {
return true;
};
matches!(
crate::agent_permissions::resolve(agent_id, car_policy::PermissionTier::FullAccess),
car_policy::ApprovalMode::AlwaysAllow
)
}
pub fn for_coder_session(worktree: impl Into<PathBuf>) -> Result<Self, String> {
let base = Self::new(worktree);
let inspectors =
coder_inspector_chain_with_project_policies(&base.worktree).map_err(|e| {
format!(
"refusing to start coder session with unreadable operator policy rules: {e}. \
Fix or remove the file — a deny rule that fails to load is a security \
control that would silently not exist"
)
})?;
Ok(base
.with_chain(inspectors)
.with_delegate(
Arc::new(crate::parslee_tools::ParsleeToolExecutor),
crate::parslee_tools::ParsleeToolExecutor::tool_defs(),
)
.with_delegate(
Arc::new(crate::assistant::memory::MemoryTools::open(
crate::assistant::default_memory_path(),
)),
recall_only_memory_defs(),
)
.with_delegate(
Arc::new(crate::assistant::net_tools::NetTools::new()),
crate::assistant::net_tools::net_tool_defs(),
)
.with_agent_permissions("car-coder"))
}
pub fn with_agent_permissions(mut self, agent_id: impl Into<String>) -> Self {
self.agent_id = Some(agent_id.into());
self
}
pub fn with_chain(mut self, chain: InspectorChain) -> Self {
self.inspectors = chain;
self
}
pub fn with_delegate(mut self, delegate: Arc<dyn ToolExecutor>, defs: Vec<Value>) -> Self {
self.delegates.push(Delegate {
executor: delegate,
defs,
});
self
}
pub fn delegate_defs_named(&self, name: &str) -> Vec<Value> {
self.delegates
.iter()
.flat_map(|d| d.defs.iter())
.filter(|d| d["name"] == name)
.cloned()
.collect()
}
pub fn delegate_name_collisions(&self) -> Vec<String> {
let mut seen: std::collections::BTreeMap<String, usize> = std::collections::BTreeMap::new();
for delegate in &self.delegates {
for name in delegate.tool_names() {
*seen.entry(name).or_default() += 1;
}
}
seen.into_iter()
.filter(|(_, n)| *n > 1)
.map(|(name, _)| name)
.collect()
}
fn delegate_for(&self, tool: &str) -> Option<&Delegate> {
self.delegates.iter().find(|d| d.advertises(tool))
}
fn all_delegate_defs(&self) -> Vec<Value> {
self.delegates
.iter()
.flat_map(|d| d.defs.iter().cloned())
.collect()
}
pub fn all_tool_defs(&self) -> Vec<Value> {
let mut defs = Self::tool_defs();
for delegate in &self.delegates {
defs.extend(delegate.defs.iter().cloned());
}
defs
}
pub fn worktree(&self) -> &Path {
&self.worktree
}
pub fn tool_defs() -> Vec<Value> {
let mut defs: Vec<Value> = agent_basics::entries()
.iter()
.map(|e| {
json!({
"name": e.schema.name,
"description": e.schema.description,
"parameters": e.schema.parameters,
})
})
.filter(|d| d["name"] != "calculate") .collect();
defs.push(json!({
"name": "shell",
"description": "Run a shell command at the repository root (the worktree). \
Use for builds, tests, and anything the file tools can't do. \
Output is the combined stdout+stderr tail. Publishing and \
privilege-escalating commands are denied by policy: `git \
push`, `gh`/`glab` writes (`pr create`, `release create`, \
non-GET `api`), `npm`/`cargo publish`, `docker push`, \
`sudo`, and destructive operations outside the repo. \
Reading the forge is allowed (`gh pr view`, `gh run view`, \
`gh api` GET) so you can watch CI. Do not try to route \
around these — the runtime opens the pull request itself \
after it has verified your work.",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "Command executed via sh -c at the repository root"
},
"timeout_secs": {
"type": "integer",
"description": "Wall-clock limit (default 120, max 600)"
}
},
"required": ["command"]
}
}));
defs
}
fn clamp_paths(&self, tool: &str, params: &Value) -> Result<Value, String> {
clamp_paths_to(&self.worktree, tool, params, "worktree", false)
}
pub async fn run_shell(
&self,
command: &str,
timeout_secs: Option<u64>,
) -> Result<Value, String> {
let substrate: Arc<dyn Substrate> = Arc::new(LocalSubstrate::new());
run_shell_on(
&substrate,
Some(&self.worktree),
&self.inspectors,
command,
timeout_secs,
MAX_SHELL_TIMEOUT_SECS,
ForgeCredentials::Withhold,
)
.await
}
pub(crate) async fn run_check_shell(
&self,
command: &str,
timeout_secs: Option<u64>,
) -> Result<Value, String> {
let substrate: Arc<dyn Substrate> = Arc::new(LocalSubstrate::new());
run_shell_on(
&substrate,
Some(&self.worktree),
&self.inspectors,
command,
timeout_secs,
self.check_timeout_ceiling,
ForgeCredentials::Inherit,
)
.await
}
async fn execute_in_session(
&self,
tool: &str,
params: &Value,
session_id: Option<&str>,
) -> Result<Value, String> {
if let Some(agent_id) = &self.agent_id {
let tier = crate::agent_permissions::classify_tool_tier_with_defs(
tool,
params,
&self.all_delegate_defs(),
);
let mode = crate::agent_permissions::resolve(agent_id, tier);
enforce_agent_permission(agent_id, tool, tier, mode)?;
}
if tool == "shell" {
let command = params
.get("command")
.and_then(Value::as_str)
.ok_or("missing 'command' parameter")?;
let timeout_secs = params.get("timeout_secs").and_then(Value::as_u64);
let before = super::no_change::worktree_fingerprint(&self.worktree);
let result = self.run_shell(command, timeout_secs).await;
let after = super::no_change::worktree_fingerprint(&self.worktree);
match (&before, &after) {
(Some(a), Some(b)) if a == b => {}
_ => self.mutations.record_mutation(),
}
return result;
}
let is_mutating_tool = matches!(tool, "write_file" | "edit_file");
if self.delegates_reachable() {
if let Some(delegate) = self.delegate_for(tool) {
if let Some(reason) = self.inspectors.check(tool, params) {
return Err(format!("denied by policy: {reason}"));
}
return delegate.executor.execute(tool, params).await;
}
}
let clamped = self.clamp_paths(tool, params)?;
if let Some(reason) = self.inspectors.check(tool, &clamped) {
return Err(format!("denied by policy: {reason}"));
}
let substrate: Arc<dyn Substrate> = Arc::new(LocalSubstrate::new());
let ledger = self.read_ledgers.ledger_for(session_id);
match agent_basics::execute_with_ledger(&substrate, &ledger, tool, &clamped).await {
Some(result) => {
if is_mutating_tool && result.is_ok() {
self.mutations.record_mutation();
}
result
}
None => Err(format!("unknown tool: {tool}")),
}
}
}
#[async_trait]
impl ToolExecutor for WorktreeExecutor {
async fn execute(&self, tool: &str, params: &Value) -> Result<Value, String> {
self.execute_in_session(tool, params, None).await
}
async fn execute_with_action_in_session(
&self,
tool: &str,
params: &Value,
_action_id: &str,
_timeout_ms: Option<u64>,
session_id: Option<&str>,
_attempt: u32,
) -> Result<Value, String> {
self.execute_in_session(tool, params, session_id).await
}
}
#[cfg(test)]
mod tests {
fn credential_probe() -> tokio::process::Command {
#[cfg(unix)]
{
let mut cmd = tokio::process::Command::new("/bin/sh");
cmd.arg("-c").arg(
"printf 'TOK=%s|GHT=%s|CFG=%s|HELPER=%s' \
\"${GH_TOKEN:-EMPTY}\" \"${GITHUB_TOKEN:-EMPTY}\" \
\"${GH_CONFIG_DIR:-UNSET}\" \"${GIT_CONFIG_COUNT:-UNSET}\"",
);
cmd
}
#[cfg(windows)]
{
let mut cmd = tokio::process::Command::new("cmd");
cmd.arg("/C").arg(
"(if defined GH_TOKEN (echo TOK=%GH_TOKEN%) else (echo TOK=EMPTY)) & \
(if defined GITHUB_TOKEN (echo GHT=%GITHUB_TOKEN%) else (echo GHT=EMPTY)) & \
(if defined GH_CONFIG_DIR (echo CFG=%GH_CONFIG_DIR%) else (echo CFG=UNSET)) & \
(if defined GIT_CONFIG_COUNT (echo HELPER=%GIT_CONFIG_COUNT%) else (echo HELPER=UNSET))",
);
cmd
}
}
fn probe_field(text: &str, name: &str) -> Option<String> {
let prefix = format!("{name}=");
text.split(['|', '\n', '\r'])
.find_map(|field| field.trim().strip_prefix(&prefix))
.map(str::to_string)
}
#[tokio::test]
async fn withholding_removes_every_route_to_a_forge_credential() {
let mut cmd = credential_probe();
cmd.env("GH_TOKEN", "ghp_secret_do_not_leak")
.env("GITHUB_TOKEN", "gho_secret_do_not_leak");
withhold_forge_credentials(&mut cmd);
let out = cmd.output().await.expect("child ran");
let text = String::from_utf8_lossy(&out.stdout).to_string();
assert_eq!(
probe_field(&text, "TOK").as_deref(),
Some("EMPTY"),
"GH_TOKEN survived: {text}"
);
assert_eq!(
probe_field(&text, "GHT").as_deref(),
Some("EMPTY"),
"GITHUB_TOKEN survived: {text}"
);
assert!(
!text.contains("ghp_secret_do_not_leak") && !text.contains("gho_secret_do_not_leak"),
"a credential leaked: {text}"
);
assert!(
!text.contains("CFG=UNSET"),
"GH_CONFIG_DIR not pinned: {text}"
);
assert_eq!(
probe_field(&text, "HELPER").as_deref(),
Some("1"),
"git config override not applied: {text}"
);
let mut git = tokio::process::Command::new("git");
git.arg("config").arg("--get").arg("credential.helper");
withhold_forge_credentials(&mut git);
let helper = git.output().await.expect("git ran");
let resolved = String::from_utf8_lossy(&helper.stdout).trim().to_string();
assert!(
resolved.is_empty(),
"a credential helper survived into the child: {resolved}"
);
}
#[tokio::test]
async fn an_untouched_child_still_sees_the_credential() {
let mut cmd = credential_probe();
cmd.env("GH_TOKEN", "ghp_inherit_me")
.env("GITHUB_TOKEN", "gho_not_read_back");
let out = cmd.output().await.expect("child ran");
let text = String::from_utf8_lossy(&out.stdout).to_string();
assert_eq!(
probe_field(&text, "TOK").as_deref(),
Some("ghp_inherit_me"),
"control case failed — the strip test would pass vacuously"
);
}
#[test]
fn the_empty_config_dir_exists_and_is_empty_of_forge_config() {
let dir = empty_config_dir();
assert!(
dir.is_dir(),
"gh falls back to the real config if this is absent"
);
assert!(!dir.join("hosts.yml").exists());
}
#[cfg(unix)]
#[tokio::test]
async fn login_shell_keeps_the_inherited_path_ahead_of_the_profiles() {
let dir = tempfile::tempdir().unwrap();
let bin = dir.path().join("car-path-probe");
std::fs::write(&bin, "#!/bin/sh\necho WINNER\n").unwrap();
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)).unwrap();
let orig = std::env::var("PATH").unwrap_or_default();
std::env::set_var("PATH", format!("{}:{}", dir.path().display(), orig));
let script =
prepend_inherited_path("command -v car-path-probe >/dev/null && car-path-probe");
std::env::set_var("PATH", &orig);
let out = tokio::process::Command::new("/bin/sh")
.arg("-lc")
.arg(&script)
.output()
.await
.unwrap();
assert_eq!(
String::from_utf8_lossy(&out.stdout).trim(),
"WINNER",
"the daemon's PATH must survive the login shell's profile"
);
}
#[test]
fn path_prepend_is_a_no_op_without_a_path() {
let orig = std::env::var("PATH").ok();
std::env::remove_var("PATH");
assert_eq!(prepend_inherited_path("echo hi"), "echo hi");
if let Some(p) = orig {
std::env::set_var("PATH", p);
}
}
#[cfg(unix)]
#[test]
fn path_prepend_quotes_hostile_paths() {
assert_eq!(
sh_single_quote("/a b/bin:/it's/bin"),
r#"'/a b/bin:/it'\''s/bin'"#
);
let out = std::process::Command::new("/bin/sh")
.arg("-c")
.arg(format!("printf %s {}", sh_single_quote("/a b/x:/it's/y")))
.output()
.unwrap();
assert_eq!(String::from_utf8_lossy(&out.stdout), "/a b/x:/it's/y");
}
use super::*;
fn executor() -> (tempfile::TempDir, WorktreeExecutor) {
let dir = tempfile::tempdir().unwrap();
let exec = WorktreeExecutor::new(dir.path());
(dir, exec)
}
#[cfg(unix)]
#[tokio::test]
async fn shell_runs_at_worktree_root() {
let (dir, exec) = executor();
let out = exec.run_shell("pwd", Some(10)).await.unwrap();
let cwd = out["output"].as_str().unwrap().trim();
assert_eq!(
PathBuf::from(cwd).canonicalize().unwrap(),
dir.path().canonicalize().unwrap()
);
assert_eq!(out["exit_code"], 0);
}
#[cfg(windows)]
#[tokio::test]
async fn shell_runs_at_worktree_root() {
let (dir, exec) = executor();
let out = exec.run_shell("cd", Some(10)).await.unwrap();
let cwd = out["output"].as_str().unwrap().trim();
assert_eq!(
PathBuf::from(cwd).canonicalize().unwrap(),
dir.path().canonicalize().unwrap()
);
assert_eq!(out["exit_code"], 0);
}
#[tokio::test]
async fn shell_reports_nonzero_exit_as_value() {
let (_dir, exec) = executor();
let out = exec.run_shell("exit 3", Some(10)).await.unwrap();
assert_eq!(out["exit_code"], 3);
assert_eq!(out["timed_out"], false);
}
#[cfg(unix)]
#[tokio::test]
async fn shell_captures_stderr() {
let (_dir, exec) = executor();
let out = exec
.run_shell("echo to-out; echo to-err 1>&2", Some(10))
.await
.unwrap();
let text = out["output"].as_str().unwrap();
assert!(text.contains("to-out") && text.contains("to-err"));
}
#[cfg(windows)]
#[tokio::test]
async fn shell_captures_stderr() {
let (_dir, exec) = executor();
let out = exec
.run_shell("echo to-out & echo to-err 1>&2", Some(10))
.await
.unwrap();
let text = out["output"].as_str().unwrap();
assert!(text.contains("to-out") && text.contains("to-err"), "{text}");
}
#[cfg(unix)]
#[tokio::test]
async fn shell_timeout_kills_and_reports() {
let (_dir, exec) = executor();
let started = std::time::Instant::now();
let out = exec.run_shell("sleep 30", Some(1)).await.unwrap();
assert!(
started.elapsed() < Duration::from_secs(10),
"did not wait out the sleep"
);
assert_eq!(out["timed_out"], true);
assert!(out["exit_code"].is_null());
}
#[cfg(windows)]
#[tokio::test]
async fn shell_timeout_kills_and_reports() {
let (_dir, exec) = executor();
let started = std::time::Instant::now();
let out = exec
.run_shell("for /L %i in () do @rem", Some(1))
.await
.unwrap();
assert!(
started.elapsed() < Duration::from_secs(10),
"did not enforce the timeout"
);
assert_eq!(out["timed_out"], true);
assert!(out["exit_code"].is_null());
}
#[tokio::test]
async fn shell_denied_by_policy() {
let (_dir, exec) = executor();
let err = exec
.run_shell("git push origin main", Some(5))
.await
.unwrap_err();
assert!(err.contains("denied by policy"), "{err}");
}
#[test]
fn noninteractive_agent_permissions_fail_closed_for_full_access_approval() {
assert!(
enforce_agent_permission(
"writer",
"shell",
car_policy::PermissionTier::SandboxEdit,
car_policy::ApprovalMode::RequireApproval,
)
.is_ok(),
"sandbox edits remain usable under the Balanced default"
);
let err = enforce_agent_permission(
"writer",
"shell",
car_policy::PermissionTier::FullAccess,
car_policy::ApprovalMode::RequireApproval,
)
.unwrap_err();
assert!(err.contains("approval required"), "{err}");
assert!(err.contains("no interactive approval channel"), "{err}");
let err = enforce_agent_permission(
"writer",
"shell",
car_policy::PermissionTier::ReadOnly,
car_policy::ApprovalMode::Deny,
)
.unwrap_err();
assert!(err.contains("denied for agent"), "{err}");
}
#[tokio::test]
async fn relative_file_writes_land_in_worktree() {
let (dir, exec) = executor();
exec.execute(
"write_file",
&json!({"path": "sub/out.txt", "content": "hi"}),
)
.await
.unwrap();
assert_eq!(
std::fs::read_to_string(dir.path().join("sub/out.txt")).unwrap(),
"hi"
);
}
#[tokio::test]
async fn edit_requires_prior_read_through_worktree_executor() {
let (dir, exec) = executor();
std::fs::write(dir.path().join("f.txt"), "hello world").unwrap();
let err = exec
.execute(
"edit_file",
&json!({ "path": "f.txt", "old_text": "hello", "new_text": "hi" }),
)
.await
.unwrap_err();
assert!(err.contains("before editing it"), "{err}");
}
#[tokio::test]
async fn escaping_writes_are_rejected_in_code() {
let (_dir, exec) = executor();
let err = exec
.execute(
"write_file",
&json!({"path": "../escape.txt", "content": "x"}),
)
.await
.unwrap_err();
assert!(err.contains("outside the worktree"), "{err}");
let err = exec
.execute(
"write_file",
&json!({"path": "/tmp/abs-escape.txt", "content": "x"}),
)
.await
.unwrap_err();
assert!(err.contains("outside the worktree"), "{err}");
}
#[tokio::test]
async fn list_dir_defaults_to_worktree_not_process_cwd() {
let (dir, exec) = executor();
std::fs::write(dir.path().join("marker.txt"), "x").unwrap();
let out = exec.execute("list_dir", &json!({})).await.unwrap();
assert!(
out.to_string().contains("marker.txt"),
"expected worktree listing, got: {out}"
);
}
#[cfg(unix)]
#[tokio::test]
async fn output_is_tail_capped() {
let (_dir, exec) = executor();
let out = exec
.run_shell("i=0; while [ $i -lt 5000 ]; do echo 'line of output 40 bytes long....'; i=$((i+1)); done", Some(30))
.await
.unwrap();
let text = out["output"].as_str().unwrap();
assert!(text.len() <= MAX_OUTPUT_BYTES + 32, "len={}", text.len());
assert!(text.starts_with("…[truncated]…"));
}
#[cfg(windows)]
#[tokio::test]
async fn output_is_tail_capped() {
let (_dir, exec) = executor();
let out = exec
.run_shell(
"for /L %i in (1,1,5000) do @echo line of output 40 bytes long....",
Some(60),
)
.await
.unwrap();
let text = out["output"].as_str().unwrap();
assert!(text.len() <= MAX_OUTPUT_BYTES + 32, "len={}", text.len());
assert!(text.starts_with("…[truncated]…"));
}
#[tokio::test]
async fn unknown_tool_errors() {
let (_dir, exec) = executor();
assert!(exec.execute("teleport", &json!({})).await.is_err());
}
struct StubDelegate;
#[async_trait]
impl ToolExecutor for StubDelegate {
async fn execute(&self, tool: &str, params: &Value) -> Result<Value, String> {
Ok(json!({ "via": "delegate", "tool": tool, "echo": params.clone() }))
}
}
#[test]
fn a_coder_session_carries_graph_memory_recall() {
let dir = tempfile::tempdir().unwrap();
let exec = WorktreeExecutor::for_coder_session(dir.path()).unwrap();
let names: Vec<String> = exec
.all_tool_defs()
.iter()
.filter_map(|d| d["name"].as_str().map(String::from))
.collect();
assert!(
names.iter().any(|n| n == "recall"),
"the coder must be able to recall stored project facts"
);
}
#[test]
fn a_coder_session_cannot_write_to_graph_memory() {
let dir = tempfile::tempdir().unwrap();
let exec = WorktreeExecutor::for_coder_session(dir.path()).unwrap();
let names: Vec<String> = exec
.all_tool_defs()
.iter()
.filter_map(|d| d["name"].as_str().map(String::from))
.collect();
assert!(
!names.iter().any(|n| n == "remember"),
"a coder must not write durable memory later sessions will trust"
);
let attached = recall_only_memory_defs();
assert_eq!(attached.len(), 1);
assert_eq!(attached[0]["name"], "recall");
}
#[tokio::test]
async fn a_second_delegate_does_not_evict_the_first() {
let dir = tempfile::tempdir().unwrap();
let first = vec![json!({
"name": "alpha_tool",
"description": "first",
"parameters": { "type": "object", "properties": {} }
})];
let second = vec![json!({
"name": "beta_tool",
"description": "second",
"parameters": { "type": "object", "properties": {} }
})];
let exec = WorktreeExecutor::new(dir.path())
.with_delegate(Arc::new(StubDelegate), first)
.with_delegate(Arc::new(StubDelegate), second);
let names: Vec<String> = exec
.all_tool_defs()
.iter()
.filter_map(|d| d["name"].as_str().map(String::from))
.collect();
assert!(
names.iter().any(|n| n == "alpha_tool"),
"first delegate evicted"
);
assert!(
names.iter().any(|n| n == "beta_tool"),
"second delegate missing"
);
assert!(names.iter().any(|n| n == "read_file"), "built-ins lost");
exec.advertise_delegates();
for tool in ["alpha_tool", "beta_tool"] {
let out = exec.execute(tool, &json!({ "x": 1 })).await.unwrap();
assert_eq!(out["via"], "delegate", "{tool} did not route to a delegate");
assert_eq!(out["tool"], tool, "{tool} routed to the wrong delegate");
}
}
#[tokio::test]
async fn a_name_collision_resolves_to_the_first_delegate_and_is_reportable() {
let dir = tempfile::tempdir().unwrap();
let def = |name: &str| {
vec![json!({
"name": name,
"description": "x",
"parameters": { "type": "object", "properties": {} }
})]
};
let exec = WorktreeExecutor::new(dir.path())
.with_delegate(Arc::new(StubDelegate), def("shared_name"))
.with_delegate(Arc::new(StubDelegate), def("shared_name"));
assert_eq!(
exec.delegate_name_collisions(),
vec!["shared_name".to_string()],
"an overlap a call site could assert on must be visible"
);
exec.advertise_delegates();
let out = exec.execute("shared_name", &json!({})).await.unwrap();
assert_eq!(out["via"], "delegate");
}
#[test]
fn a_coder_session_has_no_delegate_name_collisions() {
let dir = tempfile::tempdir().unwrap();
let exec = WorktreeExecutor::for_coder_session(dir.path()).unwrap();
assert!(
exec.delegate_name_collisions().is_empty(),
"two attached delegates advertise the same tool name"
);
}
#[tokio::test]
async fn delegate_tool_routes_through_delegate_and_is_advertised() {
let dir = tempfile::tempdir().unwrap();
let defs = vec![json!({
"name": "ext_tool",
"description": "external",
"parameters": { "type": "object", "properties": {} }
})];
let exec = WorktreeExecutor::new(dir.path()).with_delegate(Arc::new(StubDelegate), defs);
let names: Vec<String> = exec
.all_tool_defs()
.iter()
.filter_map(|d| d["name"].as_str().map(String::from))
.collect();
assert!(names.iter().any(|n| n == "ext_tool"));
assert!(names.iter().any(|n| n == "read_file"));
exec.advertise_delegates();
let out = exec.execute("ext_tool", &json!({ "x": 1 })).await.unwrap();
assert_eq!(out["via"], "delegate");
assert_eq!(out["tool"], "ext_tool");
assert_eq!(out["echo"]["x"], 1);
assert!(exec.execute("teleport", &json!({})).await.is_err());
}
#[tokio::test]
async fn project_policy_denies_shell_file_and_advertised_delegate_calls() {
let dir = tempfile::tempdir().unwrap();
let policies = dir.path().join(".car").join("policies");
std::fs::create_dir_all(&policies).unwrap();
std::fs::write(
policies.join("rules.toml"),
"deny_tool = [\"write_file\", \"ext_tool\"]\ndeny_keyword = [\"BLOCKED CHECK\"]\n",
)
.unwrap();
let defs = vec![json!({
"name": "ext_tool",
"description": "external",
"parameters": { "type": "object", "properties": {} }
})];
let mut exec = WorktreeExecutor::for_coder_session(dir.path()).unwrap();
exec = exec.with_delegate(Arc::new(StubDelegate), defs);
let file_err = exec
.execute(
"write_file",
&json!({"path": "blocked.txt", "content": "x"}),
)
.await
.expect_err("project deny_tool must govern coder file tools");
assert!(file_err.contains("operator policy"), "{file_err}");
let check_err = exec
.run_check_shell("echo BLOCKED CHECK", Some(5))
.await
.expect_err("contract checks use the same governed chain");
assert!(check_err.contains("operator policy"), "{check_err}");
exec.advertise_delegates();
let delegate_err = exec
.execute("ext_tool", &json!({}))
.await
.expect_err("advertised delegates remain governed by operator policy");
assert!(delegate_err.contains("operator policy"), "{delegate_err}");
}
#[test]
fn malformed_project_policy_refuses_coder_session() {
let dir = tempfile::tempdir().unwrap();
let policies = dir.path().join(".car").join("policies");
std::fs::create_dir_all(&policies).unwrap();
std::fs::write(
policies.join("broken.toml"),
"deny_tool = [this is not TOML\n",
)
.unwrap();
let err = WorktreeExecutor::for_coder_session(dir.path())
.err()
.expect("a session must not start with silently missing denies");
assert!(err.contains("refusing to start coder session"), "{err}");
assert!(err.contains("operator policy"), "{err}");
assert!(err.contains("broken.toml"), "{err}");
}
#[tokio::test]
async fn an_unadvertised_delegate_tool_is_not_reachable() {
let dir = tempfile::tempdir().unwrap();
let exec = WorktreeExecutor::for_coder_session(dir.path()).unwrap();
assert!(
!exec.delegates_reachable(),
"delegates must be closed until a run advertises them"
);
let delegate_name = crate::parslee_tools::ParsleeToolExecutor::tool_defs()
.first()
.and_then(|d| d["name"].as_str().map(String::from))
.expect("the parslee delegate advertises at least one tool");
let err = exec
.execute(&delegate_name, &json!({}))
.await
.expect_err("an unadvertised delegate name must not dispatch");
assert!(
err.contains("unknown tool"),
"expected it to fall through to the ordinary path, got: {err}"
);
exec.advertise_delegates();
assert!(exec.delegates_reachable());
}
#[tokio::test]
async fn for_coder_session_carries_the_parslee_delegate_and_policy_subject() {
let dir = tempfile::tempdir().unwrap();
let exec = WorktreeExecutor::for_coder_session(dir.path()).unwrap();
let names: Vec<String> = exec
.all_tool_defs()
.iter()
.filter_map(|d| d["name"].as_str().map(String::from))
.collect();
for parslee in crate::parslee_tools::ParsleeToolExecutor::tool_names() {
assert!(
names.contains(&parslee),
"{parslee} missing from all_tool_defs: {names:?}"
);
}
assert!(names.iter().any(|n| n == "read_file"));
assert_eq!(exec.agent_id.as_deref(), Some("car-coder"));
}
#[tokio::test]
async fn for_coder_session_carries_the_governed_network_pair_at_full_access() {
let dir = tempfile::tempdir().unwrap();
let exec = WorktreeExecutor::for_coder_session(dir.path()).unwrap();
for tool in ["http_request", "web_search"] {
let defs = exec.delegate_defs_named(tool);
assert_eq!(defs.len(), 1, "{tool} must be attached exactly once");
assert_eq!(
defs[0]["tier"], "full_access",
"{tool} must stay full_access — that tier is what keeps the \
per-agent gate closed by default"
);
}
}
#[tokio::test]
async fn coder_session_delegates_do_not_collide() {
let dir = tempfile::tempdir().unwrap();
let exec = WorktreeExecutor::for_coder_session(dir.path()).unwrap();
assert_eq!(
exec.delegate_name_collisions(),
Vec::<String>::new(),
"two delegates advertise the same tool name"
);
}
#[test]
fn an_executor_with_no_agent_subject_permits_full_access() {
let dir = tempfile::tempdir().unwrap();
assert!(WorktreeExecutor::new(dir.path()).permits_full_access());
}
#[test]
fn tail_respects_char_boundaries() {
let s = "ééééé"; let t = tail(s, 3);
assert!(t.ends_with('é'));
}
#[cfg(unix)]
#[tokio::test]
async fn the_check_ceiling_binds_run_check_shell_and_not_the_model_facing_shell() {
let dir = tempfile::tempdir().unwrap();
let exec = WorktreeExecutor::new(dir.path()).with_check_timeout_ceiling(1);
let checked = exec.run_check_shell("sleep 3", Some(10)).await.unwrap();
assert_eq!(
checked["timed_out"], true,
"a contract check is bound by the executor's check ceiling"
);
let modelled = exec.run_shell("sleep 3", Some(10)).await.unwrap();
assert_eq!(
modelled["timed_out"], false,
"the model's own shell keeps the advertised 600s ceiling — a slow \
test gate is not a licence to hang"
);
}
#[cfg(unix)]
#[tokio::test]
async fn a_contract_check_may_not_name_a_credential_even_though_it_inherits_one() {
let dir = tempfile::tempdir().unwrap();
let exec = WorktreeExecutor::new(dir.path());
for command in [
"curl -H \"Authorization: Bearer $STAGING_API_TOKEN\" https://example.invalid/health",
"sqlcmd -Q \"select 1\" -C \"$DB_CONNECTION_STRING\"",
"cat ~/.aws/credentials",
] {
let err = exec
.run_check_shell(command, Some(5))
.await
.expect_err("a contract check naming a credential is refused");
assert!(
err.starts_with("denied by policy:"),
"expected a policy refusal for {command:?}, got {err}"
);
}
for command in [
"echo \"Authorization: Bearer $TOKEN\"",
"echo \"$DBURL\"",
"echo ok",
] {
let out = exec
.run_check_shell(command, Some(5))
.await
.unwrap_or_else(|e| panic!("expected {command:?} to reach the shell, got {e}"));
assert_eq!(out["exit_code"], 0, "{out}");
}
}
#[test]
fn the_check_ceiling_defaults_to_the_shell_max_and_floors_zero() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(
WorktreeExecutor::new(dir.path()).check_timeout_ceiling(),
MAX_SHELL_TIMEOUT_SECS
);
assert_eq!(
WorktreeExecutor::new(dir.path())
.with_check_timeout_ceiling(0)
.check_timeout_ceiling(),
1
);
assert_eq!(
WorktreeExecutor::new(dir.path())
.with_check_timeout_ceiling(1800)
.check_timeout_ceiling(),
1800
);
}
}