Skip to content

feat: load ExecPolicyManager from ConfigLayerStack#8453

Merged
bolinfest merged 1 commit intomainfrom
pr8453
Dec 23, 2025
Merged

feat: load ExecPolicyManager from ConfigLayerStack#8453
bolinfest merged 1 commit intomainfrom
pr8453

Conversation

@bolinfest
Copy link
Collaborator

@bolinfest bolinfest commented Dec 22, 2025

#8354 added support for in-repo .config/ files, so this PR updates the logic for loading *.rules files to load *.rules files from all relevant layers. The main change to the business logic is load_exec_policy() in codex-rs/core/src/exec_policy.rs.

Note this adds a config_folder() method to ConfigLayerSource that returns Option<AbsolutePathBuf> so that it is straightforward to iterate over the sources and get the associated config folder, if any.

bolinfest added a commit that referenced this pull request Dec 23, 2025
…empty (#8456)

This is necessary so that `$CODEX_HOME/skills` and `$CODEX_HOME/rules`
still get loaded even if `$CODEX_HOME/config.toml` does not exist. See
#8453.

For now, it is possible to omit this layer when creating a dummy
`ConfigLayerStack` in a test. We can revisit that later, if it turns out
to be the right thing to do.
@bolinfest bolinfest force-pushed the pr8453 branch 3 times, most recently from d78ca23 to 870cf22 Compare December 23, 2025 00:45
Copy link
Collaborator

@joshka-oai joshka-oai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking.

for layer in config_stack.get_layers(ConfigLayerStackOrdering::LowestPrecedenceFirst) {
if let Some(config_folder) = layer.config_folder() {
#[expect(clippy::expect_used)]
let policy_dir = config_folder.join(RULES_DIR_NAME).expect("safe join");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(not a blocker - more interested in how this might be avoided in case the assumption is wrong)

So it seems like this would fail if the path can't be resolved against the base path (I'm assuming you've thought through why this happen a bit more than I have here...)

pub fn join<P: AsRef<Path>>(&self, path: P) -> std::io::Result<Self> {
Self::resolve_path_against_base(path, &self.0)
}

Are we missing a small ResolvedAbsolutePath concept here perhaps that would codify this more definitely?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like ResolvedAbsolutePath would avoid the expect(), but given that RULES_DIR_NAME is "rules" and is very, very unlikely to change (since it's now API), I don't see how the join() could fail given that AbsolutePathBuf is already guaranteeing its internal path is a valid absolute path?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems reasonable.

Comment on lines +234 to +235
// TODO(mbolin): At a minimum, `cwd` should be configurable via
// `codex/sandbox-state/update` or some other custom MCP call.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably worth expanding on this a little (I found myself digging for more context about the call here being unfamiliar with the code).
Something like: We currently don't include cwd in the layers to load exec policy rule from because ... In the future we should ... because ...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still an experimental feature that's on me to get over the finish line. It requires a bit of background to have the full context, I admit:

//! This is an MCP that implements an alternative `shell` tool with fine-grained privilege
//! escalation based on a per-exec() policy.
//!
//! We spawn Bash process inside a sandbox. The Bash we spawn is patched to allow us to intercept
//! every exec() call it makes by invoking a wrapper program and passing in the arguments it would
//! have passed to exec(). The Bash process (and its descendants) inherit a communication socket
//! from us, and we give its fd number in the CODEX_ESCALATE_SOCKET environment variable.
//!
//! When we intercept an exec() call, we send a message over the socket back to the main
//! MCP process. The MCP process can then decide whether to allow the exec() call to proceed
//! or to escalate privileges and run the requested command with elevated permissions. In the
//! latter case, we send a message back to the child requesting that it forward its open FDs to us.
//! We then execute the requested command on its behalf, patching in the forwarded FDs.
//!
//!
//! ### The privilege escalation flow
//!
//! Child MCP Bash Escalate Helper
//! |
//! o----->o
//! | |
//! | o--(exec)-->o
//! | | |
//! |o<-(EscalateReq)--o
//! || | |
//! |o--(Escalate)---->o
//! || | |
//! |o<---------(fds)--o
//! || | |
//! o<-----o | |
//! | || | |
//! x----->o | |
//! || | |
//! |x--(exit code)--->o
//! | | |
//! | o<--(exit)--x
//! | |
//! o<-----x
//!
//! ### The non-escalation flow
//!
//! MCP Bash Escalate Helper Child
//! |
//! o----->o
//! | |
//! | o--(exec)-->o
//! | | |
//! |o<-(EscalateReq)--o
//! || | |
//! |o-(Run)---------->o
//! | | |
//! | | x--(exec)-->o
//! | | |
//! | o<--------------(exit)--x
//! | |
//! o<-----x
//!

@bolinfest bolinfest merged commit 277babb into main Dec 23, 2025
52 checks passed
@bolinfest bolinfest deleted the pr8453 branch December 23, 2025 01:24
@github-actions github-actions bot locked and limited conversation to collaborators Dec 23, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants