Skip to content

Commit 65c13f1

Browse files
zhao-oaibolinfest
andauthored
execpolicy2 core integration (#6641)
This PR threads execpolicy2 into codex-core. activated via feature flag: exec_policy (on by default) reads and parses all .codexpolicy files in `codex_home/codex` refactored tool runtime API to integrate execpolicy logic --------- Co-authored-by: Michael Bolin <[email protected]>
1 parent b00a7cf commit 65c13f1

File tree

21 files changed

+690
-93
lines changed

21 files changed

+690
-93
lines changed

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,39 @@ Codex can access MCP servers. To configure them, refer to the [config docs](./do
6969

7070
Codex CLI supports a rich set of configuration options, with preferences stored in `~/.codex/config.toml`. For full configuration options, see [Configuration](./docs/config.md).
7171

72+
### Execpolicy quickstart
73+
74+
Codex can enforce your own rules-based execution policy before it runs shell commands.
75+
76+
1. Create a policy directory: `mkdir -p ~/.codex/policy`.
77+
2. Create one or more `.codexpolicy` files into that folder. Codex automatically loads every `.codexpolicy` file in there on startup.
78+
3. Write `prefix_rule` entries to describe the commands you want to allow, prompt, or block:
79+
80+
```starlark
81+
prefix_rule(
82+
pattern = ["git", ["push", "fetch"]],
83+
decision = "prompt", # allow | prompt | forbidden
84+
match = [["git", "push", "origin", "main"]], # examples that must match
85+
not_match = [["git", "status"]], # examples that must not match
86+
)
87+
```
88+
89+
- `pattern` is a list of shell tokens, evaluated from left to right; wrap tokens in a nested list to express alternatives (e.g., match both `push` and `fetch`).
90+
- `decision` sets the severity; Codex picks the strictest decision when multiple rules match.
91+
- `match` and `not_match` act as (optional) unit tests. Codex validates them when it loads your policy, so you get feedback if an example has unexpected behavior.
92+
93+
In this example rule, if Codex wants to run commands with the prefix `git push` or `git fetch`, it will first ask for user approval.
94+
95+
Note: If Codex wants to run a command that matches with multiple rules, it will use the strictest decision among the matched rules (forbidden > prompt > allow).
96+
97+
Use the [`execpolicy2` CLI](./codex-rs/execpolicy2/README.md) to preview decisions before you save a rule:
98+
99+
```shell
100+
cargo run -p codex-execpolicy2 -- check --policy ~/.codex/policy/default.codexpolicy git push origin main
101+
```
102+
103+
Pass multiple `--policy` flags to test how several files combine. See the [`codex-rs/execpolicy2` README](./codex-rs/execpolicy2/README.md) for a more detailed walkthrough of the available syntax.
104+
72105
---
73106

74107
### Docs & FAQ

codex-rs/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ codex-chatgpt = { path = "chatgpt" }
6767
codex-common = { path = "common" }
6868
codex-core = { path = "core" }
6969
codex-exec = { path = "exec" }
70+
codex-execpolicy2 = { path = "execpolicy2" }
7071
codex-feedback = { path = "feedback" }
7172
codex-file-search = { path = "file-search" }
7273
codex-git = { path = "utils/git" }

codex-rs/core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ chrono = { workspace = true, features = ["serde"] }
2222
codex-app-server-protocol = { workspace = true }
2323
codex-apply-patch = { workspace = true }
2424
codex-async-utils = { workspace = true }
25+
codex-execpolicy2 = { workspace = true }
2526
codex-file-search = { workspace = true }
2627
codex-git = { workspace = true }
2728
codex-keyring-store = { workspace = true }

codex-rs/core/src/codex.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ use crate::user_instructions::UserInstructions;
121121
use crate::user_notification::UserNotification;
122122
use crate::util::backoff;
123123
use codex_async_utils::OrCancelExt;
124+
use codex_execpolicy2::Policy as ExecPolicy;
124125
use codex_otel::otel_event_manager::OtelEventManager;
125126
use codex_protocol::config_types::ReasoningEffort as ReasoningEffortConfig;
126127
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
@@ -167,6 +168,10 @@ impl Codex {
167168

168169
let user_instructions = get_user_instructions(&config).await;
169170

171+
let exec_policy = crate::exec_policy::exec_policy_for(&config.features, &config.codex_home)
172+
.await
173+
.map_err(|err| CodexErr::Fatal(format!("failed to load execpolicy: {err}")))?;
174+
170175
let config = Arc::new(config);
171176

172177
let session_configuration = SessionConfiguration {
@@ -183,6 +188,7 @@ impl Codex {
183188
cwd: config.cwd.clone(),
184189
original_config_do_not_use: Arc::clone(&config),
185190
features: config.features.clone(),
191+
exec_policy,
186192
session_source,
187193
};
188194

@@ -280,6 +286,7 @@ pub(crate) struct TurnContext {
280286
pub(crate) final_output_json_schema: Option<Value>,
281287
pub(crate) codex_linux_sandbox_exe: Option<PathBuf>,
282288
pub(crate) tool_call_gate: Arc<ReadinessFlag>,
289+
pub(crate) exec_policy: Arc<ExecPolicy>,
283290
pub(crate) truncation_policy: TruncationPolicy,
284291
}
285292

@@ -336,6 +343,8 @@ pub(crate) struct SessionConfiguration {
336343

337344
/// Set of feature flags for this session
338345
features: Features,
346+
/// Execpolicy policy, applied only when enabled by feature flag.
347+
exec_policy: Arc<ExecPolicy>,
339348

340349
// TODO(pakrym): Remove config from here
341350
original_config_do_not_use: Arc<Config>,
@@ -436,6 +445,7 @@ impl Session {
436445
final_output_json_schema: None,
437446
codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(),
438447
tool_call_gate: Arc::new(ReadinessFlag::new()),
448+
exec_policy: session_configuration.exec_policy.clone(),
439449
truncation_policy: TruncationPolicy::new(&per_turn_config),
440450
}
441451
}
@@ -1789,6 +1799,7 @@ async fn spawn_review_thread(
17891799
final_output_json_schema: None,
17901800
codex_linux_sandbox_exe: parent_turn_context.codex_linux_sandbox_exe.clone(),
17911801
tool_call_gate: Arc::new(ReadinessFlag::new()),
1802+
exec_policy: parent_turn_context.exec_policy.clone(),
17921803
truncation_policy: TruncationPolicy::new(&per_turn_config),
17931804
};
17941805

@@ -2608,6 +2619,7 @@ mod tests {
26082619
cwd: config.cwd.clone(),
26092620
original_config_do_not_use: Arc::clone(&config),
26102621
features: Features::default(),
2622+
exec_policy: Arc::new(codex_execpolicy2::Policy::empty()),
26112623
session_source: SessionSource::Exec,
26122624
};
26132625

@@ -2685,6 +2697,7 @@ mod tests {
26852697
cwd: config.cwd.clone(),
26862698
original_config_do_not_use: Arc::clone(&config),
26872699
features: Features::default(),
2700+
exec_policy: Arc::new(codex_execpolicy2::Policy::empty()),
26882701
session_source: SessionSource::Exec,
26892702
};
26902703

codex-rs/core/src/command_safety/is_dangerous_command.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
use codex_protocol::protocol::AskForApproval;
22
use codex_protocol::protocol::SandboxPolicy;
33

4+
use crate::sandboxing::SandboxPermissions;
5+
46
use crate::bash::parse_shell_lc_plain_commands;
57
use crate::is_safe_command::is_known_safe_command;
68

79
pub fn requires_initial_appoval(
810
policy: AskForApproval,
911
sandbox_policy: &SandboxPolicy,
1012
command: &[String],
11-
with_escalated_permissions: bool,
13+
sandbox_permissions: SandboxPermissions,
1214
) -> bool {
1315
if is_known_safe_command(command) {
1416
return false;
@@ -24,8 +26,7 @@ pub fn requires_initial_appoval(
2426
// In restricted sandboxes (ReadOnly/WorkspaceWrite), do not prompt for
2527
// non‑escalated, non‑dangerous commands — let the sandbox enforce
2628
// restrictions (e.g., block network/write) without a user prompt.
27-
let wants_escalation: bool = with_escalated_permissions;
28-
if wants_escalation {
29+
if sandbox_permissions.requires_escalated_permissions() {
2930
return true;
3031
}
3132
command_might_be_dangerous(command)

0 commit comments

Comments
 (0)