From 11b641afe75bcc0c802de8cbd0e37e551f21a9ce Mon Sep 17 00:00:00 2001 From: Ollie Matthews Date: Tue, 26 May 2026 13:47:18 -0700 Subject: [PATCH] [codex] support home-relative host executable paths --- codex-rs/execpolicy/README.md | 10 +++++ codex-rs/execpolicy/src/parser.rs | 18 ++++----- codex-rs/execpolicy/tests/basic.rs | 65 ++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 10 deletions(-) diff --git a/codex-rs/execpolicy/README.md b/codex-rs/execpolicy/README.md index 5d1179362b..4f95798f0e 100644 --- a/codex-rs/execpolicy/README.md +++ b/codex-rs/execpolicy/README.md @@ -37,6 +37,16 @@ host_executable( ) ``` +- Paths may use `~/...` to identify executables installed beneath the current + user's home directory without hard-coding a username: + +```starlark +host_executable( + name = "az", + paths = ["~/.openai/bin/az"], +) +``` + - Matching semantics: - execpolicy always tries exact first-token matches first. - With host-executable resolution disabled, `/usr/bin/git status` only matches a rule whose first token is `/usr/bin/git`. diff --git a/codex-rs/execpolicy/src/parser.rs b/codex-rs/execpolicy/src/parser.rs index 5d01df18bb..1dffc6b38f 100644 --- a/codex-rs/execpolicy/src/parser.rs +++ b/codex-rs/execpolicy/src/parser.rs @@ -219,15 +219,13 @@ fn parse_examples<'v>(examples: UnpackList>) -> Result examples.items.into_iter().map(parse_example).collect() } -fn parse_literal_absolute_path(raw: &str) -> Result { - if !Path::new(raw).is_absolute() { - return Err(Error::InvalidRule(format!( - "host_executable paths must be absolute (got {raw})" - ))); - } - - AbsolutePathBuf::try_from(raw.to_string()) - .map_err(|error| Error::InvalidRule(format!("invalid absolute path `{raw}`: {error}"))) +fn parse_host_executable_path(raw: &str) -> Result { + AbsolutePathBuf::from_absolute_path_checked(raw).map_err(|error| { + Error::InvalidRule(format!( + "host_executable paths must be absolute or use `~/...` home-relative syntax \ + (got {raw}): {error}" + )) + }) } fn validate_host_executable_name(name: &str) -> Result<()> { @@ -448,7 +446,7 @@ fn policy_builtins(builder: &mut GlobalsBuilder) { value.get_type() )) })?; - let path = parse_literal_absolute_path(raw)?; + let path = parse_host_executable_path(raw)?; let Some(path_name) = executable_path_lookup_key(path.as_path()) else { return Err(Error::InvalidRule(format!( "host_executable path `{raw}` must have basename `{name}`" diff --git a/codex-rs/execpolicy/tests/basic.rs b/codex-rs/execpolicy/tests/basic.rs index 50c3f5361f..70de1fe4f2 100644 --- a/codex-rs/execpolicy/tests/basic.rs +++ b/codex-rs/execpolicy/tests/basic.rs @@ -695,6 +695,56 @@ host_executable( Ok(()) } +#[test] +fn home_relative_host_executable_path_is_expanded_and_used_for_resolution() -> Result<()> { + let executable_name = host_executable_name("az"); + let home_relative_path = format!("~/.openai/bin/{executable_name}"); + let expected_path = AbsolutePathBuf::from_absolute_path_checked(&home_relative_path)?; + let policy_src = format!( + r#" +prefix_rule(pattern = ["az", "account", "show"], decision = "prompt") +host_executable(name = "az", paths = ["{home_relative_path}"]) +"# + ); + let mut parser = PolicyParser::new(); + parser.parse("test.rules", &policy_src)?; + let policy = parser.build(); + + assert_eq!( + policy + .host_executables() + .get("az") + .expect("missing az host executable") + .as_ref(), + [expected_path.clone()] + ); + + let evaluation = policy.check_with_options( + &[ + expected_path.to_string_lossy().into_owned(), + "account".to_string(), + "show".to_string(), + ], + &allow_all, + &MatchOptions { + resolve_host_executables: true, + }, + ); + assert_eq!( + evaluation, + Evaluation { + decision: Decision::Prompt, + matched_rules: vec![RuleMatch::PrefixRuleMatch { + matched_prefix: tokens(&["az", "account", "show"]), + decision: Decision::Prompt, + resolved_program: Some(expected_path), + justification: None, + }], + } + ); + Ok(()) +} + #[test] fn host_executable_rejects_non_absolute_path() { let policy_src = r#" @@ -710,6 +760,21 @@ host_executable(name = "git", paths = ["git"]) ); } +#[test] +fn host_executable_rejects_named_user_home_path() { + let policy_src = r#" +host_executable(name = "git", paths = ["~someone/bin/git"]) +"#; + let mut parser = PolicyParser::new(); + let err = parser + .parse("test.rules", policy_src) + .expect_err("expected parse error"); + assert!( + err.to_string() + .contains("host_executable paths must be absolute") + ); +} + #[test] fn host_executable_rejects_name_with_path_separator() { let git_path = host_absolute_path(&["usr", "bin", "git"]);