Skip to content

Commit 63bfa4a

Browse files
committed
feat: support permissions-scoped exec rules
1 parent adccb46 commit 63bfa4a

16 files changed

Lines changed: 699 additions & 43 deletions

File tree

codex-rs/config/src/config_toml.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::path::Path;
77
use crate::HooksToml;
88
use crate::permissions_toml::PermissionsToml;
99
use crate::profile_toml::ConfigProfile;
10+
use crate::requirements_exec_policy::RequirementsExecPolicyToml;
1011
use crate::types::AnalyticsConfigToml;
1112
use crate::types::ApprovalsReviewer;
1213
use crate::types::AppsConfigToml;
@@ -182,6 +183,10 @@ pub struct ConfigToml {
182183
#[serde(default)]
183184
pub auto_review: Option<AutoReviewToml>,
184185

186+
/// Optional command execution policy rules for this config layer.
187+
#[serde(default)]
188+
pub rules: Option<RequirementsExecPolicyToml>,
189+
185190
#[serde(default)]
186191
pub shell_environment_policy: ShellEnvironmentPolicyToml,
187192

codex-rs/config/src/requirements_exec_policy.rs

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ use codex_execpolicy::rule::PatternToken;
55
use codex_execpolicy::rule::PrefixPattern;
66
use codex_execpolicy::rule::PrefixRule;
77
use multimap::MultiMap;
8+
use schemars::JsonSchema;
89
use serde::Deserialize;
10+
use serde::Serialize;
911
use std::sync::Arc;
1012
use thiserror::Error;
1113

@@ -46,33 +48,34 @@ fn policy_fingerprint(policy: &Policy) -> Vec<String> {
4648
}
4749

4850
/// TOML representation of `[rules]` within `requirements.toml`.
49-
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
51+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
5052
pub struct RequirementsExecPolicyToml {
5153
pub prefix_rules: Vec<RequirementsExecPolicyPrefixRuleToml>,
5254
}
5355

5456
/// A TOML representation of the `prefix_rule(...)` Starlark builtin.
5557
///
5658
/// This mirrors the builtin defined in `execpolicy/src/parser.rs`.
57-
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
59+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
5860
pub struct RequirementsExecPolicyPrefixRuleToml {
5961
pub pattern: Vec<RequirementsExecPolicyPatternTokenToml>,
6062
pub decision: Option<RequirementsExecPolicyDecisionToml>,
6163
pub justification: Option<String>,
64+
pub permissions: Option<String>,
6265
}
6366

6467
/// TOML-friendly representation of a pattern token.
6568
///
6669
/// Starlark supports either a string token or a list of alternative tokens at
6770
/// each position, but TOML arrays cannot mix strings and arrays. Using an
6871
/// array of tables sidesteps that restriction.
69-
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
72+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
7073
pub struct RequirementsExecPolicyPatternTokenToml {
7174
pub token: Option<String>,
7275
pub any_of: Option<Vec<String>>,
7376
}
7477

75-
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
78+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
7679
#[serde(rename_all = "kebab-case")]
7780
pub enum RequirementsExecPolicyDecisionToml {
7881
Allow,
@@ -110,6 +113,9 @@ pub enum RequirementsExecPolicyParseError {
110113
#[error("rules prefix_rule at index {rule_index} has an empty justification")]
111114
EmptyJustification { rule_index: usize },
112115

116+
#[error("rules prefix_rule at index {rule_index} has an empty permissions value")]
117+
EmptyPermissions { rule_index: usize },
118+
113119
#[error("rules prefix_rule at index {rule_index} is missing a decision")]
114120
MissingDecision { rule_index: usize },
115121

@@ -123,6 +129,22 @@ impl RequirementsExecPolicyToml {
123129
/// Convert requirements TOML rules into the internal `.rules`
124130
/// representation used by `codex-execpolicy`.
125131
pub fn to_policy(&self) -> Result<Policy, RequirementsExecPolicyParseError> {
132+
self.to_policy_with_allow_rules(/*allow_allow_decision*/ false)
133+
}
134+
135+
/// Convert config TOML rules into the internal `.rules` representation.
136+
///
137+
/// Unlike `requirements.toml`, regular config layers may contain allow
138+
/// rules because they are user/project policy rather than managed
139+
/// restrictions.
140+
pub fn to_config_policy(&self) -> Result<Policy, RequirementsExecPolicyParseError> {
141+
self.to_policy_with_allow_rules(/*allow_allow_decision*/ true)
142+
}
143+
144+
fn to_policy_with_allow_rules(
145+
&self,
146+
allow_allow_decision: bool,
147+
) -> Result<Policy, RequirementsExecPolicyParseError> {
126148
if self.prefix_rules.is_empty() {
127149
return Err(RequirementsExecPolicyParseError::EmptyPrefixRules);
128150
}
@@ -135,6 +157,11 @@ impl RequirementsExecPolicyToml {
135157
{
136158
return Err(RequirementsExecPolicyParseError::EmptyJustification { rule_index });
137159
}
160+
if let Some(permissions) = &rule.permissions
161+
&& permissions.trim().is_empty()
162+
{
163+
return Err(RequirementsExecPolicyParseError::EmptyPermissions { rule_index });
164+
}
138165

139166
if rule.pattern.is_empty() {
140167
return Err(RequirementsExecPolicyParseError::EmptyPattern { rule_index });
@@ -148,7 +175,7 @@ impl RequirementsExecPolicyToml {
148175
.collect::<Result<Vec<_>, _>>()?;
149176

150177
let decision = match rule.decision {
151-
Some(RequirementsExecPolicyDecisionToml::Allow) => {
178+
Some(RequirementsExecPolicyDecisionToml::Allow) if !allow_allow_decision => {
152179
return Err(RequirementsExecPolicyParseError::AllowDecisionNotAllowed {
153180
rule_index,
154181
});
@@ -159,6 +186,7 @@ impl RequirementsExecPolicyToml {
159186
}
160187
};
161188
let justification = rule.justification.clone();
189+
let permissions = rule.permissions.clone();
162190

163191
let (first_token, remaining_tokens) = pattern_tokens
164192
.split_first()
@@ -174,6 +202,7 @@ impl RequirementsExecPolicyToml {
174202
},
175203
decision,
176204
justification: justification.clone(),
205+
permissions: permissions.clone(),
177206
});
178207
rules_by_program.insert(head.clone(), rule);
179208
}

codex-rs/core/config.schema.json

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2708,6 +2708,68 @@
27082708
}
27092709
]
27102710
},
2711+
"RequirementsExecPolicyDecisionToml": {
2712+
"enum": [
2713+
"allow",
2714+
"prompt",
2715+
"forbidden"
2716+
],
2717+
"type": "string"
2718+
},
2719+
"RequirementsExecPolicyPatternTokenToml": {
2720+
"description": "TOML-friendly representation of a pattern token.\n\nStarlark supports either a string token or a list of alternative tokens at each position, but TOML arrays cannot mix strings and arrays. Using an array of tables sidesteps that restriction.",
2721+
"properties": {
2722+
"any_of": {
2723+
"items": {
2724+
"type": "string"
2725+
},
2726+
"type": "array"
2727+
},
2728+
"token": {
2729+
"type": "string"
2730+
}
2731+
},
2732+
"type": "object"
2733+
},
2734+
"RequirementsExecPolicyPrefixRuleToml": {
2735+
"description": "A TOML representation of the `prefix_rule(...)` Starlark builtin.\n\nThis mirrors the builtin defined in `execpolicy/src/parser.rs`.",
2736+
"properties": {
2737+
"decision": {
2738+
"$ref": "#/definitions/RequirementsExecPolicyDecisionToml"
2739+
},
2740+
"justification": {
2741+
"type": "string"
2742+
},
2743+
"pattern": {
2744+
"items": {
2745+
"$ref": "#/definitions/RequirementsExecPolicyPatternTokenToml"
2746+
},
2747+
"type": "array"
2748+
},
2749+
"permissions": {
2750+
"type": "string"
2751+
}
2752+
},
2753+
"required": [
2754+
"pattern"
2755+
],
2756+
"type": "object"
2757+
},
2758+
"RequirementsExecPolicyToml": {
2759+
"description": "TOML representation of `[rules]` within `requirements.toml`.",
2760+
"properties": {
2761+
"prefix_rules": {
2762+
"items": {
2763+
"$ref": "#/definitions/RequirementsExecPolicyPrefixRuleToml"
2764+
},
2765+
"type": "array"
2766+
}
2767+
},
2768+
"required": [
2769+
"prefix_rules"
2770+
],
2771+
"type": "object"
2772+
},
27112773
"RolloutBudgetConfigToml": {
27122774
"additionalProperties": false,
27132775
"properties": {
@@ -5410,6 +5472,15 @@
54105472
"description": "Review model override used by the `/review` feature.",
54115473
"type": "string"
54125474
},
5475+
"rules": {
5476+
"allOf": [
5477+
{
5478+
"$ref": "#/definitions/RequirementsExecPolicyToml"
5479+
}
5480+
],
5481+
"default": null,
5482+
"description": "Optional command execution policy rules for this config layer."
5483+
},
54135484
"sandbox_mode": {
54145485
"allOf": [
54155486
{

codex-rs/core/src/config/config_loader_tests.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3515,6 +3515,7 @@ mod requirements_exec_policy_tests {
35153515
use codex_config::RequirementsExecPolicyToml;
35163516
use codex_execpolicy::Decision;
35173517
use codex_execpolicy::Evaluation;
3518+
use codex_execpolicy::MatchOptions;
35183519
use codex_execpolicy::RuleMatch;
35193520
use codex_utils_absolute_path::AbsolutePathBuf;
35203521
use pretty_assertions::assert_eq;
@@ -3572,6 +3573,7 @@ prefix_rules = [
35723573
}],
35733574
decision: Some(RequirementsExecPolicyDecisionToml::Forbidden),
35743575
justification: None,
3576+
permissions: None,
35753577
}],
35763578
}
35773579
);
@@ -3601,6 +3603,7 @@ prefix_rules = [
36013603
}],
36023604
decision: Some(RequirementsExecPolicyDecisionToml::Forbidden),
36033605
justification: None,
3606+
permissions: None,
36043607
},
36053608
RequirementsExecPolicyPrefixRuleToml {
36063609
pattern: vec![
@@ -3615,6 +3618,7 @@ prefix_rules = [
36153618
],
36163619
decision: Some(RequirementsExecPolicyDecisionToml::Prompt),
36173620
justification: Some("review changes before push or commit".to_string()),
3621+
permissions: None,
36183622
},
36193623
],
36203624
}
@@ -3650,6 +3654,58 @@ prefix_rules = [
36503654
Ok(())
36513655
}
36523656

3657+
#[test]
3658+
fn converts_rules_toml_permissions_scope() -> anyhow::Result<()> {
3659+
let toml_str = r#"
3660+
prefix_rules = [
3661+
{ pattern = [{ token = "git" }, { token = "status" }], decision = "prompt", permissions = "strict" },
3662+
]
3663+
"#;
3664+
3665+
let parsed: RequirementsExecPolicyToml = from_str(toml_str)?;
3666+
let policy = parsed.to_policy()?;
3667+
let command = tokens(&["git", "status"]);
3668+
3669+
assert_eq!(
3670+
policy.check_with_options(
3671+
&command,
3672+
&|_| Decision::Allow,
3673+
&MatchOptions {
3674+
active_permission_profile: Some("strict".to_string()),
3675+
..MatchOptions::default()
3676+
}
3677+
),
3678+
Evaluation {
3679+
decision: Decision::Prompt,
3680+
matched_rules: vec![RuleMatch::PrefixRuleMatch {
3681+
matched_prefix: command.clone(),
3682+
decision: Decision::Prompt,
3683+
resolved_program: None,
3684+
justification: None,
3685+
}],
3686+
}
3687+
);
3688+
assert_eq!(
3689+
policy.check_with_options(
3690+
&command,
3691+
&|_| Decision::Allow,
3692+
&MatchOptions {
3693+
active_permission_profile: Some("relaxed".to_string()),
3694+
..MatchOptions::default()
3695+
}
3696+
),
3697+
Evaluation {
3698+
decision: Decision::Allow,
3699+
matched_rules: vec![RuleMatch::HeuristicsRuleMatch {
3700+
command,
3701+
decision: Decision::Allow,
3702+
}],
3703+
}
3704+
);
3705+
3706+
Ok(())
3707+
}
3708+
36533709
#[test]
36543710
fn head_any_of_expands_into_multiple_program_rules() -> anyhow::Result<()> {
36553711
let toml_str = r#"

0 commit comments

Comments
 (0)