use std::collections::HashMap;
use std::path::{Component, Path, PathBuf};
use car_ir::Action;
use car_policy::{InspectionResult, Inspector, InspectorChain, PolicyEngine, PolicyRules};
use car_state::StateStore;
use serde_json::Value;
pub fn coder_inspector_chain(worktree: &Path) -> InspectorChain {
InspectorChain::new()
.with(Box::new(DenyGitRemoteMutation))
.with(Box::new(DenyForgePublication))
.with(Box::new(DenyHistoryRewrite))
.with(Box::new(DenyPrivilegeEscalation))
.with(Box::new(DenyCredentialAccess))
.with(Box::new(DenyEnvironmentRepair))
.with(Box::new(DenyDestructiveOutsideWorktree {
worktree: worktree.to_path_buf(),
}))
.with(Box::new(DenyPathEscape {
worktree: worktree.to_path_buf(),
}))
}
pub fn coder_inspector_chain_with_project_policies(
worktree: &Path,
) -> Result<InspectorChain, car_policy::PolicyLoadError> {
let dirs = [
car_home::root_or_relative().join("policies"),
worktree.join(".car").join("policies"),
];
coder_inspector_chain_from_policy_dirs(worktree, &dirs)
}
fn coder_inspector_chain_from_policy_dirs(
worktree: &Path,
dirs: &[PathBuf],
) -> Result<InspectorChain, car_policy::PolicyLoadError> {
let mut rules = PolicyRules::default();
for dir in dirs {
rules.merge(car_policy::load_policy_dir(dir)?);
}
let mut engine = PolicyEngine::new();
rules.apply(&mut engine);
Ok(
coder_inspector_chain(worktree).with(Box::new(ProjectPolicyInspector {
engine,
state: StateStore::new(),
})),
)
}
struct ProjectPolicyInspector {
engine: PolicyEngine,
state: StateStore,
}
impl Inspector for ProjectPolicyInspector {
fn name(&self) -> &'static str {
"project_policy"
}
fn inspect(&self, tool: &str, params: &Value) -> InspectionResult {
let mut action = Action::tool_call(tool);
action.id = "coder-policy-check".to_string();
action.parameters = params
.as_object()
.map(|m| {
m.iter()
.map(|(key, value)| (key.clone(), value.clone()))
.collect::<HashMap<_, _>>()
})
.unwrap_or_default();
match self.engine.check(&action, &self.state).into_iter().next() {
Some(violation) => InspectionResult::Deny(format!(
"operator policy '{}': {}",
violation.policy_name, violation.reason
)),
None => InspectionResult::Allow,
}
}
}
struct DenyGovernedShellPathEscape {
worktree: PathBuf,
}
const READ_OR_CHDIR_VERBS: &[&str] = &[
"cat", "head", "tail", "less", "more", "grep", "egrep", "fgrep", "rg", "sed", "awk", "find",
"ls", "stat", "wc", "strings", "readlink", "realpath", "cd", "type",
];
impl Inspector for DenyGovernedShellPathEscape {
fn name(&self) -> &'static str {
"governed_host.deny_shell_path_escape"
}
fn inspect(&self, tool: &str, params: &Value) -> InspectionResult {
let Some(cmd) = shell_command(tool, params) else {
return InspectionResult::Allow;
};
for seg in segments(&cmd) {
let Some(v) = verb(&seg) else { continue };
let v = Path::new(v)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(v)
.to_ascii_lowercase();
if !READ_OR_CHDIR_VERBS.contains(&v.as_str()) {
continue;
}
for arg in seg.iter().skip(1).filter(|arg| !arg.starts_with('-')) {
let candidate =
arg.trim_matches(|c: char| matches!(c, '"' | '\'' | '(' | ')' | ',' | ';'));
let names_path = candidate.starts_with('~')
|| is_abs_or_traversal(candidate)
|| self.worktree.join(candidate).exists();
if names_path && !stays_under(&self.worktree, candidate) {
return InspectionResult::Deny(format!(
"'{v}' path '{candidate}' resolves outside the governed repository"
));
}
}
}
InspectionResult::Allow
}
}
struct DenyGovernedFilePathEscape {
worktree: PathBuf,
}
impl Inspector for DenyGovernedFilePathEscape {
fn name(&self) -> &'static str {
"governed_host.deny_file_path_escape"
}
fn inspect(&self, tool: &str, params: &Value) -> InspectionResult {
if !matches!(
tool,
"read_file" | "write_file" | "edit_file" | "grep_files"
) {
return InspectionResult::Allow;
}
let Some(path) = params.get("path").and_then(Value::as_str) else {
return InspectionResult::Allow;
};
if stays_under(&self.worktree, path) {
InspectionResult::Allow
} else {
InspectionResult::Deny(format!(
"file access to '{path}' resolves outside the governed repository"
))
}
}
}
pub fn governed_host_inspector_chain(worktree: &Path) -> InspectorChain {
InspectorChain::new()
.with(Box::new(DenyGuiShellAutomation))
.with(Box::new(DenyForcePushAndRemoteReconfiguration))
.with(Box::new(DenyBroadGitStage))
.with(Box::new(DenyHistoryRewrite))
.with(Box::new(DenyPrivilegeEscalation))
.with(Box::new(DenyCredentialAccess))
.with(Box::new(DenyEnvironmentRepair))
.with(Box::new(DenyDestructiveOutsideWorktree {
worktree: worktree.to_path_buf(),
}))
.with(Box::new(DenyGovernedShellPathEscape {
worktree: worktree.to_path_buf(),
}))
.with(Box::new(DenyGovernedFilePathEscape {
worktree: worktree.to_path_buf(),
}))
}
struct DenyGuiShellAutomation;
impl Inspector for DenyGuiShellAutomation {
fn name(&self) -> &'static str {
"governed_host.deny_gui_shell_automation"
}
fn inspect(&self, tool: &str, _params: &Value) -> InspectionResult {
if matches!(tool, "run_applescript" | "run_powershell") {
InspectionResult::Deny(
"desktop-driven shell execution is not allowed; use the governed shell tool".into(),
)
} else {
InspectionResult::Allow
}
}
}
struct DenyBroadGitStage;
impl Inspector for DenyBroadGitStage {
fn name(&self) -> &'static str {
"governed_host.deny_broad_git_stage"
}
fn inspect(&self, tool: &str, params: &Value) -> InspectionResult {
let Some(cmd) = shell_command(tool, params) else {
return InspectionResult::Allow;
};
for seg in segments(&cmd) {
if verb(&seg) != Some("git") {
continue;
}
let add = seg.iter().position(|token| token == "add");
if let Some(index) = add {
if seg
.iter()
.skip(index + 1)
.any(|token| matches!(token.as_str(), "." | "-A" | "--all" | "-u" | "--update"))
{
return InspectionResult::Deny(
"broad git staging is not allowed; name only the files changed for this task"
.into(),
);
}
}
if seg.iter().any(|token| token == "commit")
&& seg.iter().any(|token| {
token == "--all"
|| token
.strip_prefix('-')
.filter(|short| !short.starts_with('-'))
.is_some_and(|short| short.contains('a'))
})
{
return InspectionResult::Deny(
"git commit -a is not allowed; stage only explicit task files".into(),
);
}
}
InspectionResult::Allow
}
}
pub(crate) fn stays_under(root: &Path, candidate: &str) -> bool {
let p = Path::new(candidate);
let joined = if p.is_absolute() {
p.to_path_buf()
} else {
root.join(p)
};
if joined.exists() {
if let (Ok(real_root), Ok(real_candidate)) = (root.canonicalize(), joined.canonicalize()) {
return path_starts_with(&real_candidate, &real_root);
}
return false;
}
if let Ok(real_root) = root.canonicalize() {
let mut ancestor = joined.as_path();
while !ancestor.exists() {
let Some(parent) = ancestor.parent() else {
return false;
};
ancestor = parent;
}
match ancestor.canonicalize() {
Ok(real_ancestor) if path_starts_with(&real_ancestor, &real_root) => {}
_ => return false,
}
}
let mut stack: Vec<Component> = Vec::new();
for c in joined.components() {
match c {
Component::CurDir => {}
Component::ParentDir => {
if stack.pop().is_none() {
return false;
}
}
other => stack.push(other),
}
}
let normalized: PathBuf = stack.iter().collect();
path_starts_with(&normalized, root)
}
#[cfg(not(windows))]
fn path_starts_with(path: &Path, base: &Path) -> bool {
path.starts_with(base)
}
#[cfg(windows)]
fn path_starts_with(path: &Path, base: &Path) -> bool {
fn key(p: &Path) -> String {
let s = p.to_string_lossy().into_owned();
let s = if let Some(r) = s.strip_prefix(r"\\?\UNC\") {
format!(r"\\{r}")
} else if let Some(r) = s.strip_prefix(r"\\?\") {
r.to_string()
} else {
s
};
s.replace('/', "\\").to_ascii_lowercase()
}
let base_key = key(base);
let base_trim = base_key.trim_end_matches('\\');
let path_key = key(path);
path_key == base_trim || path_key.starts_with(&format!("{base_trim}\\"))
}
fn is_abs_or_traversal(arg: &str) -> bool {
arg.starts_with('/')
|| arg.starts_with('\\')
|| arg.contains("..")
|| Path::new(arg).is_absolute()
}
fn is_windows_switch(arg: &str) -> bool {
#[cfg(not(windows))]
{
let _ = arg;
false
}
#[cfg(windows)]
{
arg.strip_prefix('/')
.map(|rest| {
(1..=2).contains(&rest.len()) && rest.chars().all(|c| c.is_ascii_alphanumeric())
})
.unwrap_or(false)
}
}
fn segments(command: &str) -> Vec<Vec<String>> {
command
.replace("&&", "\n")
.replace("||", "\n")
.replace(['ï¼›', ';', '|'], "\n")
.lines()
.map(|seg| {
seg.split_whitespace()
.map(|t| t.trim_matches(|c| c == '"' || c == '\'').to_string())
.filter(|t| !t.is_empty())
.collect::<Vec<_>>()
})
.filter(|toks: &Vec<String>| !toks.is_empty())
.collect()
}
fn verb(tokens: &[String]) -> Option<&str> {
tokens.iter().map(String::as_str).find(|t| !t.contains('='))
}
fn shell_command(tool: &str, params: &Value) -> Option<String> {
if tool != "shell" {
return None;
}
params
.get("command")
.and_then(Value::as_str)
.map(str::to_string)
}
struct DenyGitRemoteMutation;
impl Inspector for DenyGitRemoteMutation {
fn name(&self) -> &'static str {
"coder.deny_git_remote_mutation"
}
fn inspect(&self, tool: &str, params: &Value) -> InspectionResult {
let Some(cmd) = shell_command(tool, params) else {
return InspectionResult::Allow;
};
for seg in segments(&cmd) {
let is_git = verb(&seg) == Some("git");
if !is_git {
continue;
}
if seg.iter().any(|t| t == "push") {
return InspectionResult::Deny(
"git push is not allowed from a coder session — results are delivered \
via the approved local branch"
.into(),
);
}
if seg.iter().any(|t| t == "remote")
&& seg
.iter()
.any(|t| t == "add" || t == "set-url" || t == "remove")
{
return InspectionResult::Deny("mutating git remotes is not allowed".into());
}
}
InspectionResult::Allow
}
}
struct DenyForgePublication;
const FORGE_VERBS: &[&str] = &["gh", "glab", "hub"];
const FORGE_READS: &[(&str, &[&str])] = &[
("pr", &["view", "list", "diff", "checks", "status"]),
("mr", &["view", "list", "diff", "checks", "status"]),
("issue", &["view", "list"]),
("repo", &["view"]),
("run", &["view", "list", "watch"]),
("release", &["view", "list"]),
("workflow", &["view", "list"]),
("label", &["list"]),
("cache", &["list"]),
("gist", &["view", "list"]),
("auth", &["status"]),
("search", &[]),
("status", &[]),
("version", &[]),
];
const FORGE_VALUE_FLAGS: &[&str] = &["-r", "--repo", "--hostname"];
const PUBLICATION_COMMANDS: &[(&str, &[&str])] = &[
("npm", &["publish"]),
("pnpm", &["publish"]),
("yarn", &["publish"]),
("cargo", &["publish"]),
("gem", &["push"]),
("twine", &["upload"]),
("docker", &["push", "login"]),
];
impl Inspector for DenyForgePublication {
fn name(&self) -> &'static str {
"coder.deny_forge_publication"
}
fn inspect(&self, tool: &str, params: &Value) -> InspectionResult {
let Some(cmd) = shell_command(tool, params) else {
return InspectionResult::Allow;
};
for seg in segments(&cmd) {
let Some(v) = verb(&seg) else { continue };
let v = Path::new(&v.to_ascii_lowercase())
.file_name()
.map(|f| f.to_string_lossy().into_owned())
.unwrap_or_default();
let v = v.strip_suffix(".exe").unwrap_or(&v).to_string();
let args: Vec<String> = seg.iter().skip(1).map(|a| a.to_ascii_lowercase()).collect();
if FORGE_VERBS.contains(&v.as_str()) {
if let Some(reason) = forge_denial(&v, &args) {
return InspectionResult::Deny(reason);
}
continue;
}
for (mgr, subs) in PUBLICATION_COMMANDS {
if v != *mgr {
continue;
}
if leading_operands(&args).iter().any(|sub| subs.contains(sub)) {
return InspectionResult::Deny(format!(
"'{mgr}' publication is not allowed from a coder session — results \
leave the worktree only through the approved merge branch"
));
}
}
}
InspectionResult::Allow
}
}
fn forge_operands(args: &[String]) -> Vec<&str> {
let mut operands = Vec::new();
let mut skip_value = false;
for arg in args {
if std::mem::take(&mut skip_value) {
continue;
}
if FORGE_VALUE_FLAGS.contains(&arg.as_str()) {
skip_value = true;
continue;
}
if arg.starts_with('-') {
continue;
}
operands.push(arg.as_str());
}
operands
}
fn leading_operands(args: &[String]) -> Vec<&str> {
args.iter()
.map(String::as_str)
.filter(|a| !a.starts_with('-') && !a.starts_with('+'))
.take(2)
.collect()
}
fn forge_denial(verb: &str, args: &[String]) -> Option<String> {
const BLOCKED: &str = "publishing from a coder session is not allowed — the runtime opens \
the pull request after `coder.approve_merge`";
if args
.iter()
.any(|a| matches!(a.as_str(), "--version" | "--help"))
{
return None;
}
let operands = forge_operands(args);
let Some(group) = operands.first().copied() else {
return None; };
if group == "api" {
let is_write_method = |v: &str| !v.is_empty() && v != "get";
let explicit_method = args
.windows(2)
.any(|pair| matches!(pair[0].as_str(), "--method" | "-x") && is_write_method(&pair[1]))
|| args.iter().any(|a| {
a.strip_prefix("--method=")
.or_else(|| a.strip_prefix("-x"))
.is_some_and(is_write_method)
});
let field_flag = |a: &String| {
matches!(a.as_str(), "-f" | "--field" | "--raw-field" | "--input")
|| a.starts_with("--field=")
|| a.starts_with("--raw-field=")
|| a.starts_with("--input=")
|| a.starts_with("-f")
};
let graphql = operands.get(1).is_some_and(|o| *o == "graphql");
let mutating_graphql = graphql
&& args
.iter()
.any(|a| a.contains("mutation") || a.contains("deletion"));
let implicit_post = !graphql && args.iter().any(field_flag);
return (explicit_method || implicit_post || mutating_graphql)
.then(|| format!("'{verb} api' with a write method is not allowed — {BLOCKED}"));
}
if group == "auth"
&& args
.iter()
.any(|a| a == "-t" || a == "--show-token" || a.starts_with("--show-token="))
{
return Some(format!(
"'{verb} auth status --show-token' prints the forge credential — {BLOCKED}"
));
}
let Some((_, subs)) = FORGE_READS.iter().find(|(g, _)| *g == group) else {
return Some(format!("'{verb} {group}' is not allowed — {BLOCKED}"));
};
if subs.is_empty() {
return None;
}
match operands.get(1).copied() {
Some(sub) if subs.contains(&sub) => None,
Some(sub) => Some(format!("'{verb} {group} {sub}' is not allowed — {BLOCKED}")),
None => Some(format!(
"'{verb} {group}' without a read-only subcommand is not allowed — {BLOCKED}"
)),
}
}
struct DenyForcePushAndRemoteReconfiguration;
impl Inspector for DenyForcePushAndRemoteReconfiguration {
fn name(&self) -> &'static str {
"governed_host.deny_force_push_and_remote_reconfiguration"
}
fn inspect(&self, tool: &str, params: &Value) -> InspectionResult {
let Some(cmd) = shell_command(tool, params) else {
return InspectionResult::Allow;
};
for seg in segments(&cmd) {
if verb(&seg) != Some("git") {
continue;
}
let push = seg.iter().any(|token| token == "push");
let forced = seg.iter().any(|token| {
token == "--force"
|| token == "-f"
|| token.starts_with("--force-with-lease")
|| token.starts_with('+')
});
if push && forced {
return InspectionResult::Deny("force-push is never allowed".into());
}
if seg.iter().any(|token| token == "remote")
&& seg.iter().any(|token| {
token == "add" || token == "set-url" || token == "remove" || token == "rename"
})
{
return InspectionResult::Deny(
"mutating git remote configuration is not allowed".into(),
);
}
}
InspectionResult::Allow
}
}
struct DenyHistoryRewrite;
impl Inspector for DenyHistoryRewrite {
fn name(&self) -> &'static str {
"coder.deny_history_rewrite"
}
fn inspect(&self, tool: &str, params: &Value) -> InspectionResult {
let Some(cmd) = shell_command(tool, params) else {
return InspectionResult::Allow;
};
for seg in segments(&cmd) {
if verb(&seg) != Some("git") {
continue;
}
if seg.iter().any(|t| t == "rebase" || t == "filter-branch") {
return InspectionResult::Deny("git history rewrite is not allowed".into());
}
if seg.iter().any(|t| t == "reset") && seg.iter().any(|t| t == "--hard") {
return InspectionResult::Deny("git reset --hard is not allowed".into());
}
if seg.iter().any(|t| t == "worktree") && seg.iter().any(|t| t == "remove") {
return InspectionResult::Deny(
"removing worktrees is the runtime's job, not the agent's".into(),
);
}
}
InspectionResult::Allow
}
}
struct DenyPrivilegeEscalation;
const PRIVILEGE_VERBS: &[&str] = &[
"sudo",
"doas",
"su",
"launchctl",
"systemctl", "runas",
"sc",
"psexec",
];
impl Inspector for DenyPrivilegeEscalation {
fn name(&self) -> &'static str {
"coder.deny_privilege_escalation"
}
fn inspect(&self, tool: &str, params: &Value) -> InspectionResult {
let Some(cmd) = shell_command(tool, params) else {
return InspectionResult::Allow;
};
for seg in segments(&cmd) {
if let Some(v) = verb(&seg) {
if PRIVILEGE_VERBS.contains(&v.to_ascii_lowercase().as_str()) {
return InspectionResult::Deny(format!(
"'{v}' is not allowed in a coder session"
));
}
}
}
InspectionResult::Allow
}
}
struct DenyCredentialAccess;
const CREDENTIAL_PATH_MARKERS: [&str; 6] = [
"/.ssh",
"/.aws",
"/.gnupg",
"/.kube",
"/.car/secrets",
"/.netrc",
];
impl Inspector for DenyCredentialAccess {
fn name(&self) -> &'static str {
"coder.deny_credential_access"
}
fn inspect(&self, tool: &str, params: &Value) -> InspectionResult {
let haystacks: Vec<String> = if let Some(cmd) = shell_command(tool, params) {
if cmd.contains("find-generic-password") || cmd.contains("find-internet-password") {
return InspectionResult::Deny("keychain access is not allowed".into());
}
let cmd_lower = cmd.to_ascii_lowercase();
if cmd_lower.contains("cmdkey") || cmd_lower.contains("vaultcmd") {
return InspectionResult::Deny(
"Windows Credential Manager access is not allowed".into(),
);
}
let sensitive_env = [
"_key",
"_token",
"_secret",
"_password",
"openai_",
"anthropic_",
"azure_client_",
"github_token",
"connection_string",
];
if sensitive_env
.iter()
.any(|marker| cmd_lower.contains(marker))
{
return InspectionResult::Deny(
"reading or expanding credential environment variables is not allowed".into(),
);
}
for seg in segments(&cmd) {
let Some(command) = verb(&seg).map(|value| value.to_ascii_lowercase()) else {
continue;
};
if command == "env" && seg.len() == 1
|| command == "printenv"
|| command == "set" && seg.len() == 1
{
return InspectionResult::Deny(
"dumping the process environment is not allowed".into(),
);
}
}
vec![cmd]
} else if matches!(
tool,
"read_file" | "write_file" | "edit_file" | "grep_files"
) {
params
.get("path")
.and_then(Value::as_str)
.map(|p| vec![p.to_string()])
.unwrap_or_default()
} else {
return InspectionResult::Allow;
};
for hay in &haystacks {
let hay = hay.replace('\\', "/");
let hay = hay
.replace("~/", "/HOME/.")
.replace("$HOME/", "/HOME/.")
.replace("%USERPROFILE%/", "/HOME/.")
.replace("%HOMEPATH%/", "/HOME/.");
let hay = hay.replace("/HOME/..", "/."); for marker in CREDENTIAL_PATH_MARKERS {
if hay.contains(marker) {
return InspectionResult::Deny(format!(
"access to credential path matching '{marker}' is not allowed"
));
}
}
}
InspectionResult::Allow
}
}
struct DenyDestructiveOutsideWorktree {
worktree: PathBuf,
}
const DESTRUCTIVE_VERBS: &[&str] = &[
"rm", "rmdir", "mv", "cp", "chmod", "chown", "truncate", "dd", "del", "erase", "rd", "move", "copy", "format", "ren", "rename",
];
impl Inspector for DenyDestructiveOutsideWorktree {
fn name(&self) -> &'static str {
"coder.deny_destructive_outside_worktree"
}
fn inspect(&self, tool: &str, params: &Value) -> InspectionResult {
let Some(cmd) = shell_command(tool, params) else {
return InspectionResult::Allow;
};
for seg in segments(&cmd) {
let Some(v) = verb(&seg) else { continue };
let v_lower = v.to_ascii_lowercase();
if !DESTRUCTIVE_VERBS.contains(&v_lower.as_str()) {
continue;
}
for arg in seg
.iter()
.skip(1)
.filter(|a| !a.starts_with('-') && !is_windows_switch(a))
{
if arg.starts_with('~') {
return InspectionResult::Deny(format!(
"'{v}' on a home-relative path ('{arg}') is not allowed"
));
}
if is_abs_or_traversal(arg) && !stays_under(&self.worktree, arg) {
return InspectionResult::Deny(format!(
"'{v}' outside the worktree ('{arg}') is not allowed"
));
}
}
}
InspectionResult::Allow
}
}
struct DenyEnvironmentRepair;
const PACKAGE_MUTATIONS: &[(&str, &[&str])] = &[
("pip", &["install", "uninstall"]),
("pip3", &["install", "uninstall"]),
("conda", &["install", "remove", "uninstall", "update"]),
("poetry", &["add", "remove", "install", "update"]),
("uv", &["add", "remove", "sync"]),
("easy_install", &[]),
];
const SHIM_FILES: &[&str] = &["sitecustomize.py", "usercustomize.py"];
impl Inspector for DenyEnvironmentRepair {
fn name(&self) -> &'static str {
"coder.deny_environment_repair"
}
fn inspect(&self, tool: &str, params: &Value) -> InspectionResult {
if matches!(tool, "write_file" | "edit_file") {
let path = params.get("path").and_then(Value::as_str).unwrap_or("");
let base = Path::new(path)
.file_name()
.map(|f| f.to_string_lossy().to_ascii_lowercase())
.unwrap_or_default();
if SHIM_FILES.contains(&base.as_str()) {
return InspectionResult::Deny(format!(
"writing '{base}' changes how the interpreter loads, not what your code \
does — the runtime re-runs the contract in the correct environment"
));
}
return InspectionResult::Allow;
}
let Some(cmd) = shell_command(tool, params) else {
return InspectionResult::Allow;
};
for seg in segments(&cmd) {
let Some(v) = verb(&seg) else { continue };
let v = Path::new(&v.to_ascii_lowercase())
.file_name()
.map(|f| f.to_string_lossy().into_owned())
.unwrap_or_default();
let v = v.strip_suffix(".exe").unwrap_or(&v).to_string();
let args: Vec<String> = seg.iter().skip(1).map(|a| a.to_ascii_lowercase()).collect();
let module = args
.iter()
.position(|a| a == "-m")
.and_then(|i| args.get(i + 1))
.cloned();
let (effective, effective_args): (String, Vec<String>) = match module {
Some(m) if v.starts_with("python") || v.starts_with("py") => {
let rest = args
.iter()
.skip_while(|a| **a != m)
.skip(1)
.cloned()
.collect();
(m, rest)
}
_ => (v.clone(), args.clone()),
};
if effective == "venv" || effective == "virtualenv" {
return InspectionResult::Deny(
"creating an interpreter is environment repair, not part of the task — \
the runtime re-runs the contract in the correct environment"
.into(),
);
}
for (mgr, subs) in PACKAGE_MUTATIONS {
if effective != *mgr {
continue;
}
let mutates = subs.is_empty()
|| effective_args.iter().any(|a| subs.contains(&a.as_str()))
|| (effective == "uv" && effective_args.iter().any(|a| a == "install"));
if mutates {
return InspectionResult::Deny(format!(
"'{mgr}' package mutation is environment repair, not part of the task \
— the runtime re-runs the contract in the correct environment"
));
}
}
}
InspectionResult::Allow
}
}
struct DenyPathEscape {
worktree: PathBuf,
}
impl Inspector for DenyPathEscape {
fn name(&self) -> &'static str {
"coder.deny_path_escape"
}
fn inspect(&self, tool: &str, params: &Value) -> InspectionResult {
if !matches!(tool, "write_file" | "edit_file") {
return InspectionResult::Allow;
}
let Some(path) = params.get("path").and_then(Value::as_str) else {
return InspectionResult::Allow; };
if stays_under(&self.worktree, path) {
InspectionResult::Allow
} else {
InspectionResult::Deny(format!("write to '{path}' resolves outside the worktree"))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn chain() -> InspectorChain {
coder_inspector_chain(Path::new("/wt"))
}
fn denied(tool: &str, params: Value) -> bool {
chain().check(tool, ¶ms).is_some()
}
fn sh(cmd: &str) -> Value {
json!({ "command": cmd })
}
fn write_policy(dir: &Path, body: &str) {
std::fs::create_dir_all(dir).unwrap();
std::fs::write(dir.join("rules.toml"), body).unwrap();
}
#[test]
fn coder_chain_merges_machine_and_project_deny_rules() {
let root = tempfile::tempdir().unwrap();
let repo = root.path().join("repo");
let machine = root.path().join("machine-policies");
let project = repo.join(".car").join("policies");
std::fs::create_dir_all(&repo).unwrap();
write_policy(&machine, "deny_tool = [\"write_file\"]\n");
write_policy(&project, "deny_keyword = [\"DO NOT RUN\"]\n");
let chain =
coder_inspector_chain_from_policy_dirs(&repo, &[machine.clone(), project.clone()])
.unwrap();
assert!(chain
.check("write_file", &json!({"path": "x", "content": "ok"}))
.is_some());
assert!(chain
.check("shell", &json!({"command": "echo DO NOT RUN"}))
.is_some());
assert!(chain.check("read_file", &json!({"path": "x"})).is_none());
}
#[test]
fn built_in_denial_reason_wins_before_project_policy() {
let root = tempfile::tempdir().unwrap();
let policies = root.path().join("policies");
write_policy(&policies, "deny_tool = [\"shell\"]\n");
let chain = coder_inspector_chain_from_policy_dirs(root.path(), &[policies]).unwrap();
let reason = chain
.check("shell", &sh("git push origin main"))
.expect("both rules deny");
assert!(
reason.contains("git push"),
"built-in reason must win: {reason}"
);
assert!(
!reason.contains("operator policy"),
"wrong precedence: {reason}"
);
}
#[test]
fn malformed_or_unenforced_policy_refuses_chain_construction() {
let root = tempfile::tempdir().unwrap();
let malformed = root.path().join("malformed");
write_policy(&malformed, "deny_tool = [not valid TOML\n");
assert!(coder_inspector_chain_from_policy_dirs(root.path(), &[malformed]).is_err());
let trace = root.path().join("trace");
write_policy(
&trace,
"[[trace_rule]]\nkind = \"never\"\ntool = \"deploy\"\n",
);
let err = coder_inspector_chain_from_policy_dirs(root.path(), &[trace])
.err()
.expect("trace rules are deliberately unenforced");
assert!(err.to_string().contains("not enforced"), "{err}");
}
#[test]
fn denies_package_mutation_and_interpreter_creation() {
for cmd in [
"pip install requests",
"pip3 uninstall -y six",
"python -m pip install --upgrade pip",
"/usr/bin/python3.11 -m pip install pytest",
"conda install numpy",
"poetry add httpx",
"uv pip install ruff",
"python -m venv .venv",
"virtualenv env",
"easy_install foo",
"cd /wt && pip install -e .",
] {
assert!(denied("shell", sh(cmd)), "should be denied: {cmd}");
}
}
#[test]
fn allows_read_only_package_queries_and_real_test_runs() {
for cmd in [
"pip list",
"pip show pytest",
"python -m pytest -q tests/test_x.py",
"/wt/.venv/bin/python -m pytest -q tests/test_x.py",
"cargo test -p car-engine",
"npm test",
] {
assert!(!denied("shell", sh(cmd)), "should be allowed: {cmd}");
}
}
#[test]
fn denies_interpreter_shims_but_not_ordinary_test_config() {
assert!(denied(
"write_file",
json!({ "path": "sitecustomize.py", "content": "x" })
));
assert!(denied(
"write_file",
json!({ "path": "src/usercustomize.py", "content": "x" })
));
for path in ["conftest.py", "pyproject.toml", "tox.ini", "setup.cfg"] {
assert!(
!denied("write_file", json!({ "path": path, "content": "x" })),
"must stay allowed: {path}"
);
}
}
#[test]
fn git_push_and_remote_mutation_denied() {
assert!(denied("shell", sh("git push origin main")));
assert!(denied("shell", sh("cargo test && git push --force")));
assert!(denied("shell", sh("git remote add evil https://x")));
assert!(denied("shell", sh("git remote set-url origin https://x")));
assert!(!denied("shell", sh("git remote -v")));
assert!(!denied("shell", sh("git commit -m 'x'")));
assert!(!denied("shell", sh("git status && git diff")));
assert!(!denied("shell", sh("echo push")));
}
#[test]
fn forge_publication_denied_but_reads_allowed() {
for cmd in [
"gh pr create --fill",
"gh pr merge --admin",
"gh api --method DELETE /repos/o/r/branches/main/protection",
"gh api -X POST /repos/o/r/issues",
"gh api repos/o/r/issues -f title=x",
"gh release create v9.9.9 ./x",
"gh auth token",
"gh repo fork",
"glab mr create",
"npm publish",
"cargo publish",
"docker push img",
"docker login ghcr.io",
"cargo test && gh pr create",
"/opt/homebrew/bin/gh pr create --fill",
"gh release create v9.9.9 --notes -h",
"gh pr create --title -h --body b --head mybranch --base main",
"gh auth status -t",
"gh auth status --show-token",
"gh api -XPOST repos/o/r/pulls --input=-",
"gh api -XPOST repos/o/r/pulls --field=title=x",
"gh api --method=post repos/o/r/pulls",
"gh api repos/o/r/issues --raw-field=title=x",
"gh api graphql --field=query=mutation{createpullrequest}",
"cargo +stable publish",
"docker image push img",
"npm --workspace x publish",
] {
assert!(denied("shell", sh(cmd)), "should be denied: {cmd}");
}
for cmd in [
"gh pr view 12",
"gh pr checks",
"gh pr diff 12",
"gh issue list",
"gh run view 5",
"gh run watch 5",
"gh api repos/o/r",
"gh api --method GET /repos/o/r",
"gh --repo o/r pr view 12",
"gh auth status",
"gh --version",
"/opt/homebrew/bin/gh pr list",
"echo gh pr create",
"cargo test -p car-engine",
"npm run build",
"docker build -t img .",
"gh api graphql -f query=query{viewer{login}}",
] {
assert!(!denied("shell", sh(cmd)), "should be allowed: {cmd}");
}
}
#[test]
fn governed_host_still_allows_ci_reads_and_approved_push() {
let chain = governed_host_inspector_chain(Path::new("/wt"));
for command in [
"gh run list",
"az pipelines runs list",
"git push origin HEAD:main",
] {
assert!(
chain.check("shell", &sh(command)).is_none(),
"governed host must still allow {command}"
);
}
}
#[test]
fn governed_host_allows_only_normal_push_shape() {
let chain = governed_host_inspector_chain(Path::new("/wt"));
assert!(chain
.check("shell", &sh("git push origin HEAD:main"))
.is_none());
for command in [
"git push --force origin main",
"git push --force-with-lease origin main",
"git push origin +HEAD:main",
"git remote set-url origin https://evil",
"git rebase -i HEAD~2",
"git add .",
"git add -A",
"git commit -am fix",
] {
assert!(
chain.check("shell", &sh(command)).is_some(),
"must deny {command}"
);
}
}
#[test]
fn governed_host_denies_direct_reads_outside_repository() {
let temp = tempfile::tempdir().unwrap();
let repo = temp.path().join("repo");
let outside = temp.path().join("outside.txt");
std::fs::create_dir(&repo).unwrap();
std::fs::write(&outside, "secret").unwrap();
let chain = governed_host_inspector_chain(&repo);
assert!(chain
.check("read_file", &json!({"path": outside}))
.is_some());
assert!(chain
.check("shell", &sh(&format!("cat {}", outside.display())))
.is_some());
assert!(chain.check("shell", &sh("cd ..")).is_some());
assert!(chain.check("shell", &sh("cat src/lib.rs")).is_none());
#[cfg(unix)]
{
std::os::unix::fs::symlink(&outside, repo.join("escape")).unwrap();
assert!(chain
.check("read_file", &json!({"path": "escape"}))
.is_some());
assert!(chain.check("shell", &sh("cat escape")).is_some());
}
}
#[test]
fn governed_host_denies_gui_shell_automation() {
let chain = governed_host_inspector_chain(Path::new("/wt"));
assert!(chain
.check(
"run_applescript",
&json!({"script": "tell application \"Terminal\" to do script \"az deploy\""})
)
.is_some());
assert!(chain
.check("run_powershell", &json!({"script": "az deploy"}))
.is_some());
}
#[test]
fn history_rewrite_denied() {
assert!(denied("shell", sh("git rebase -i HEAD~3")));
assert!(denied("shell", sh("git reset --hard HEAD~1")));
assert!(denied("shell", sh("git filter-branch --all")));
assert!(denied("shell", sh("git worktree remove /wt")));
assert!(!denied("shell", sh("git reset HEAD file.txt"))); }
#[test]
fn privilege_escalation_denied() {
assert!(denied("shell", sh("sudo rm -rf /tmp/x")));
assert!(denied("shell", sh("doas pkg_add x")));
assert!(denied("shell", sh("FOO=1 sudo make install")));
assert!(denied("shell", sh("launchctl unload foo")));
assert!(!denied("shell", sh("echo sudo"))); }
#[test]
fn credential_access_denied_for_shell_and_file_tools() {
assert!(denied("shell", sh("cat ~/.ssh/id_rsa")));
assert!(denied("shell", sh("cat $HOME/.aws/credentials")));
assert!(denied("shell", sh("security find-generic-password -s x")));
assert!(denied("read_file", json!({"path": "/Users/u/.ssh/id_rsa"})));
assert!(denied("read_file", json!({"path": "~/.netrc"})));
assert!(!denied("read_file", json!({"path": "src/main.rs"})));
}
#[test]
fn destructive_ops_scoped_to_worktree() {
assert!(denied("shell", sh("rm -rf /etc")));
assert!(denied("shell", sh("rm -rf ../other-checkout")));
assert!(denied("shell", sh("mv target ~/elsewhere")));
assert!(denied("shell", sh("chmod 777 /usr/local/bin/x")));
assert!(!denied("shell", sh("rm -rf target/debug")));
assert!(!denied("shell", sh("rm /wt/scratch.txt")));
assert!(!denied("shell", sh("cp a.txt b.txt")));
}
#[test]
fn write_path_escape_denied_but_reads_allowed() {
assert!(denied(
"write_file",
json!({"path": "/etc/hosts", "content": "x"})
));
assert!(denied("edit_file", json!({"path": "../outside.txt"})));
assert!(!denied(
"write_file",
json!({"path": "src/new.rs", "content": "x"})
));
assert!(!denied(
"write_file",
json!({"path": "/wt/src/new.rs", "content": "x"})
));
assert!(!denied(
"read_file",
json!({"path": "/usr/include/stdio.h"})
));
}
#[test]
fn stays_under_is_lexical_and_strict() {
let root = Path::new("/wt");
assert!(stays_under(root, "src/x.rs"));
assert!(stays_under(root, "a/../b.txt"));
assert!(stays_under(root, "/wt/deep/file"));
assert!(!stays_under(root, "../escape"));
assert!(!stays_under(root, "a/../../escape"));
assert!(!stays_under(root, "/etc/passwd"));
assert!(!stays_under(root, "/wtevil/file")); }
#[cfg(windows)]
#[test]
fn windows_destructive_and_privilege_denied() {
let chain = coder_inspector_chain(Path::new(r"C:\wt"));
let denied = |cmd: &str| chain.check("shell", &sh(cmd)).is_some();
assert!(denied(r"del C:\Windows\System32\drivers\etc\hosts"));
assert!(denied(r"rd /s /q C:\Windows"));
assert!(denied(r"del /q C:\Users\victim\file")); assert!(denied(r"move C:\wt\keep.txt C:\Users\public\stolen.txt"));
assert!(denied("runas /user:Administrator cmd"));
assert!(denied("sc stop windefend"));
assert!(!denied(r"del C:\wt\target\debug\app.exe"));
assert!(!denied(r"del build\out.txt"));
assert!(!denied("dir")); }
#[cfg(windows)]
#[test]
fn windows_credential_access_denied() {
let chain = coder_inspector_chain(Path::new(r"C:\wt"));
assert!(chain
.check("shell", &sh(r"type %USERPROFILE%\.ssh\id_rsa"))
.is_some());
assert!(chain.check("shell", &sh("cmdkey /list")).is_some());
assert!(chain
.check(
"read_file",
&json!({"path": r"C:\Users\u\.aws\credentials"})
)
.is_some());
assert!(chain
.check("read_file", &json!({"path": r"C:\wt\src\main.rs"}))
.is_none());
}
#[cfg(windows)]
#[test]
fn stays_under_handles_verbatim_prefix_and_case() {
let root = Path::new(r"\\?\C:\wt");
assert!(stays_under(root, r"C:\WT\src\main.rs"));
assert!(stays_under(root, r"c:\wt\src\main.rs"));
assert!(!stays_under(root, r"C:\other\x"));
assert!(!stays_under(root, r"C:\wtevil\x")); }
}