Skip to content

Support Unix socket allowlists in macOS sandbox#17654

Merged
aaronl-openai merged 6 commits intomainfrom
aaronl/codex-sandbox-unix-socket
Apr 15, 2026
Merged

Support Unix socket allowlists in macOS sandbox#17654
aaronl-openai merged 6 commits intomainfrom
aaronl/codex-sandbox-unix-socket

Conversation

@aaronl-openai
Copy link
Copy Markdown
Collaborator

@aaronl-openai aaronl-openai commented Apr 13, 2026

Changes

Allows sandboxes to restrict overall network access while granting access to specific unix sockets on mac.

Details

  • codex sandbox macos: adds a repeatable --allow-unix-socket option.
  • codex-sandboxing: threads explicit Unix socket roots into the macOS Seatbelt profile generation.
  • Preserves restricted network behavior when only Unix socket IPC is requested, and preserves full network behavior when full network is already enabled.

Verification

  • cargo test -p codex-cli -p codex-sandboxing
  • cargo build -p codex-cli --bin codex
  • verified that codex sandbox macos --allow-unix-socket /tmp/test.sock -- test-client grants access as expected

@aaronl-openai aaronl-openai force-pushed the aaronl/codex-sandbox-unix-socket branch 5 times, most recently from 31ffeaf to 80202b0 Compare April 14, 2026 03:57
@aaronl-openai aaronl-openai marked this pull request as ready for review April 14, 2026 04:55
@aaronl-openai aaronl-openai requested a review from bolinfest April 14, 2026 04:55
@aaronl-openai aaronl-openai force-pushed the aaronl/codex-sandbox-unix-socket branch from 671aa26 to 94088b2 Compare April 14, 2026 07:16
@aaronl-openai aaronl-openai changed the title Support MCP sandbox state and Unix socket allowlists Support Unix socket allowlists in macOS sandbox Apr 14, 2026
Comment thread codex-rs/sandboxing/src/seatbelt.rs Outdated
};

let proxy = proxy_policy_inputs(network);
let proxy = proxy_policy_inputs(network).with_extra_unix_sockets(extra_allow_unix_sockets);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

proxy_policy_inputs() has only one callsite. Why not update it to take extra_allow_unix_sockets so that we don't have to introduce with_extra_unix_sockets() which takes mut self?

Comment thread codex-rs/sandboxing/src/seatbelt.rs Outdated
)
}

pub fn create_seatbelt_command_args_for_policies_with_extra_unix_sockets(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maybe this also merits using a builder pattern, but we have these three things calling each other right now, each adding one param:

create_seatbelt_command_args() ->
create_seatbelt_command_args_for_policies() -> create_seatbelt_command_args_for_policies_with_extra_unix_sockets()

This feels a bit much: can we just have one function that takes a struct with the full set of arguments?

If we feel strongly, we could make the struct impl Default so it's easier to elide the arguments with trivial default values when constructing it.

@aaronl-openai aaronl-openai force-pushed the aaronl/codex-sandbox-unix-socket branch from 94088b2 to 4c1665c Compare April 15, 2026 05:09
Comment thread codex-rs/cli/src/lib.rs Outdated
}

fn parse_absolute_path(raw: &str) -> Result<AbsolutePathBuf, String> {
AbsolutePathBuf::from_absolute_path_checked(raw)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
AbsolutePathBuf::from_absolute_path_checked(raw)
AbsolutePathBuf::relative_to_current_dir(raw)

@aaronl-openai aaronl-openai force-pushed the aaronl/codex-sandbox-unix-socket branch from 4c1665c to ba9b37f Compare April 15, 2026 05:37
@aaronl-openai aaronl-openai force-pushed the aaronl/codex-sandbox-unix-socket branch from ba9b37f to e49ceb1 Compare April 15, 2026 05:41
…unix-socket

# Conflicts:
#	codex-rs/core/src/seatbelt.rs
Comment thread codex-rs/cli/src/lib.rs Outdated

fn parse_allow_unix_socket_path(raw: &str) -> Result<AbsolutePathBuf, String> {
AbsolutePathBuf::relative_to_current_dir(raw)
.map_err(|err| format!("invalid path {raw:?}: {err}"))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since raw is &str, you don't need :? to print the Debug version of the value.

Suggested change
.map_err(|err| format!("invalid path {raw:?}: {err}"))
.map_err(|err| format!("invalid path {raw}: {err}"))

Comment thread codex-rs/sandboxing/src/manager.rs Outdated
@@ -12,7 +12,9 @@ use crate::policy_transforms::should_require_platform_sandbox;
#[cfg(target_os = "macos")]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Instead of these top-level imports with #[cfg(target_os = "macos")], you might want to just do the imports inside the SandboxType::MacosSeatbelt case and then you can drop the cfg on each import.

Comment thread codex-rs/sandboxing/src/seatbelt.rs Outdated
Comment on lines +260 to +266
let has_unix_socket_access = matches!(
proxy.unix_domain_socket_policy,
UnixDomainSocketPolicy::AllowAll
) || matches!(
&proxy.unix_domain_socket_policy,
UnixDomainSocketPolicy::Restricted { allowed } if !allowed.is_empty()
);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is better because if a new variant is added to the UnixDomainSocketPolicy enum, this forces the match to be updated:

Suggested change
let has_unix_socket_access = matches!(
proxy.unix_domain_socket_policy,
UnixDomainSocketPolicy::AllowAll
) || matches!(
&proxy.unix_domain_socket_policy,
UnixDomainSocketPolicy::Restricted { allowed } if !allowed.is_empty()
);
let has_unix_socket_access = match proxy.unix_domain_socket_policy {
UnixDomainSocketPolicy::AllowAll => true,
UnixDomainSocketPolicy::Restricted { allowed } => !allowed.is_empty()
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maybe has_some_unix_socket_access is more accurate since it is true when Restricted is non-empty, but that is not quite a strong a statement as AllowAll?

"(allow network-outbound)\n(allow network-inbound)\n{MACOS_SEATBELT_NETWORK_POLICY}"
)
let mut policy = String::from("(allow network-outbound)\n(allow network-inbound)\n");
let unix_socket_policy = unix_socket_policy(proxy);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

So was it an oversight that the result of unix_socket_policy() was not included previously?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

yeah, I think so. I think it mostly wasn’t an issue before because the unix socket policy was exercised through the proxy/restricted-network path.

Comment thread codex-rs/sandboxing/src/seatbelt.rs Outdated
network: Option<&NetworkProxy>,
) -> Vec<String> {
#[derive(Debug)]
pub struct SeatbeltCommandArgs<'a> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: I would name this SeatbeltCommandParams since create_seatbelt_command_args() takes this struct as an argument, it doesn't create it.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Or even CreateSeatbeltCommandArgsParams because it's the "params" for the create_seatbelt_command_args() function?


struct TestConfigReloader;

#[async_trait::async_trait]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We got burned by async_trait recently: #16630. Prefer native async traits, which effectively makes #[async_trait::async_trait] unnecessary.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Oh, I see, we already have:

#[async_trait]
pub trait ConfigReloader: Send + Sync {
    /// Human-readable description of where config is loaded from, for logs.
    fn source_label(&self) -> String;

    /// Return a freshly loaded state if a reload is needed; otherwise, return `None`.
    async fn maybe_reload(&self) -> Result<Option<ConfigState>>;

    /// Force a reload, regardless of whether a change was detected.
    async fn reload_now(&self) -> Result<ConfigState>;
}

OK, we can let this be for now, but I'll go after it in a follow-up...

@bolinfest bolinfest self-requested a review April 15, 2026 06:41
@aaronl-openai aaronl-openai merged commit 2e10037 into main Apr 15, 2026
41 of 47 checks passed
@aaronl-openai aaronl-openai deleted the aaronl/codex-sandbox-unix-socket branch April 15, 2026 07:53
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 15, 2026
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