David Uhden Collado wrote:
Hello,

Please find attached a patch adding native OpenBSD sandbox support to
devel/codex 0.163.0 using pledge(2) and unveil(2).

The implementation uses my rust-openbsd bindings and integrates with
Codex's existing sandbox infrastructure.

The main changes are:

- Translate Codex permission profiles into pledge/unveil restrictions.
- Support read-only and workspace-write sandbox modes.
- Execute sandboxed commands through a dedicated arg0 helper.
- Integrate the backend with the CLI and exec-server.
- Add documentation describing the security model and limitations.

The implementation uses getexecpath(3), introduced in OpenBSD 8.0.

I performed runtime testing with an installed Codex 0.163.0p1
package on OpenBSD 8.0 amd64, running the tests as an unprivileged user.

25 sandbox regression and helper checks passed, followed by six
additional shell and make checks. These covered filesystem
restrictions, symlink escapes, network permissions, subprocesses,
workspace writes and command execution.

One limitation remains: redirecting shell output to /dev/null under
read-only mode currently causes an abort. This still needs
investigation.

I would appreciate review of the implementation, particularly the
pledge/unveil policy mapping and its security implications.

Thank you for your time and consideration.

Best regards,
David.

I've prepared a substantially smaller alternative implementation.

This version deliberately avoids pledge(2), execpromises, and the error promise. Since unveil(2) restrictions are discarded across execve(2) without execpromises, applying them in a launcher would provide no protection to the executed program.

Rather than introducing a misleading security boundary, the implementation explicitly rejects unsupported policies:

- Restricted filesystem profiles are rejected before command execution.
- Disabled network access and managed network proxies are unsupported.
- Sandbox preparation failures never trigger an unrestricted fallback.
- The danger-full-access profile remains an explicit opt-out.

This means the proposal does not yet provide a functional restricted sandbox. Instead, it establishes the OpenBSD integration infrastructure, including policy translation, validation, helper dispatch, getexecpath(3), and the rust bindings, without relying on the problematic execution promises.

Would you consider this a more reasonable starting point, or would you prefer to defer integration until we have a viable mechanism for preserving restrictions across execve(2) without execpromises?

Regards,
David.
Index: devel/codex/Makefile
===================================================================
RCS file: /cvs/ports/devel/codex/Makefile,v
diff -u -p -u -p -r1.61 Makefile
--- devel/codex/Makefile	16 Oct 2026 09:32:32 -0000	1.61
+++ devel/codex/Makefile	17 Oct 2026 04:28:59 -0000
@@ -13,6 +13,7 @@ DPB_PROPERTIES =	parallel
 USE_WXNEEDED =		Yes
 
 V =			0.163.0
+REVISION =		0
 GH_ACCOUNT =		openai
 GH_PROJECT =		codex
 GH_TAGNAME =		rust-v${V}
@@ -29,6 +30,8 @@ DIST_TUPLE += github modelcontextprotoco
 DIST_TUPLE += github dzbarsky rules_rust b56cbaa8465e74127f1ea216f813cd377295ad81 ./runfiles
 DIST_TUPLE += github microsoft mxc 6cd3d58f05d3447e67109cfb75e042803b843ca4 ./mxc
 DIST_TUPLE += github hyperium h3 e07e69412876f7e26f026bd75a48b2704d8c8283 ./h3
+# Sandbox bindings; getexecpath(3) requires OpenBSD 8.0.
+DIST_TUPLE += github daviduhden rust-openbsd 1984b9510937cc40f0a483a52311630721aa8dcd ./rust-openbsd
 
 MAINTAINER =		Kirill A. Korinsky <[email protected]>
 
Index: devel/codex/distinfo
===================================================================
RCS file: /cvs/ports/devel/codex/distinfo,v
diff -u -p -u -p -r1.58 distinfo
--- devel/codex/distinfo	16 Oct 2026 09:32:32 -0000	1.58
+++ devel/codex/distinfo	17 Oct 2026 04:28:59 -0000
@@ -1295,6 +1295,7 @@ SHA256 (cargo/zvariant-4.2.0.tar.gz) = I
 SHA256 (cargo/zvariant_derive-4.2.0.tar.gz) = c+K6VGvaaDqQZSusSiebwUatrROG8lN5z3MgDSACxEk=
 SHA256 (cargo/zvariant_utils-2.1.0.tar.gz) = xRvP98w9u1BVOWvPd0dIw9q0JrS4ZZBGljUjzuSAg0A=
 SHA256 (codex-rust-v0.163.0.tar.gz) = WZDdsTMYTOXkcMdB0U8e6ySgts02aF73j7dyz4PKt/w=
+SHA256 (daviduhden-rust-openbsd-1984b9510937cc40f0a483a52311630721aa8dcd.tar.gz) = 8JIIUaGyrlxTBxD5IvbZFga2m20BHnABDg6b2QUfpl4=
 SHA256 (dzbarsky-rules_rust-b56cbaa8465e74127f1ea216f813cd377295ad81.tar.gz) = 3b30DbjfaF32fDJg3+Wc9/+ksZOpMk4Qs1m76JgdueA=
 SHA256 (helix-editor-nucleo-4253de9faabb4e5c6d81d946a5e35a90f87347ee.tar.gz) = 0WdqwzqCxZA//t5oznPJ2SRmaqihArtkmo+5JqemHOE=
 SHA256 (hyperium-h3-e07e69412876f7e26f026bd75a48b2704d8c8283.tar.gz) = O52KnRCEH+J/uGyXoGvs4UEfywrPrcDKhXtjZdTl0po=
@@ -2600,6 +2601,7 @@ SIZE (cargo/zvariant-4.2.0.tar.gz) = 821
 SIZE (cargo/zvariant_derive-4.2.0.tar.gz) = 11057
 SIZE (cargo/zvariant_utils-2.1.0.tar.gz) = 7043
 SIZE (codex-rust-v0.163.0.tar.gz) = 17505520
+SIZE (daviduhden-rust-openbsd-1984b9510937cc40f0a483a52311630721aa8dcd.tar.gz) = 73696
 SIZE (dzbarsky-rules_rust-b56cbaa8465e74127f1ea216f813cd377295ad81.tar.gz) = 3951333
 SIZE (helix-editor-nucleo-4253de9faabb4e5c6d81d946a5e35a90f87347ee.tar.gz) = 86782
 SIZE (hyperium-h3-e07e69412876f7e26f026bd75a48b2704d8c8283.tar.gz) = 244832
Index: devel/codex/patches/patch-codex-rs_Cargo_toml
===================================================================
RCS file: /cvs/ports/devel/codex/patches/patch-codex-rs_Cargo_toml,v
diff -u -p -u -p -r1.45 patch-codex-rs_Cargo_toml
--- devel/codex/patches/patch-codex-rs_Cargo_toml	16 Oct 2026 09:32:32 -0000	1.45
+++ devel/codex/patches/patch-codex-rs_Cargo_toml	17 Oct 2026 04:28:59 -0000
@@ -1,7 +1,7 @@
 Index: codex-rs/Cargo.toml
 --- codex-rs/Cargo.toml.orig
 +++ codex-rs/Cargo.toml
-@@ -320,12 +320,12 @@ codex-windows-sandbox-test-support = { path = "windows
+@@ -320,12 +320,12 @@ codex-windows-sandbox-test-support = { p
  core_test_support = { path = "core/tests/common" }
  
  # External
@@ -26,7 +26,7 @@ Index: codex-rs/Cargo.toml
  once_cell = "1.20.2"
  openssl-sys = "*"
  opentelemetry = "0.31.0"
-@@ -448,8 +448,8 @@ regex-automata = { version = "0.4.13", default-feature
+@@ -448,9 +448,10 @@ regex-automata = { version = "0.4.13", d
  regex-lite = "0.1.8"
  regex-syntax = "0.8.8"
  reqwest = { version = "0.12", features = ["cookies"] }
@@ -35,9 +35,11 @@ Index: codex-rs/Cargo.toml
 +rmcp = { path = "../rmcp/crates/rmcp", version = "=3.5.1", default-features = false }
 +runfiles = { path = "../runfiles/rust/runfiles" }
  rustix = { version = "1.1.4", features = ["net"] }
++rust-openbsd = { path = "../rust-openbsd" }
  rustls = { version = "0.23.45", default-features = false, features = [
      "aws_lc_rs",
-@@ -641,12 +641,6 @@ opt-level = 0
+     "std",
+@@ -641,12 +642,6 @@ opt-level = 0
  [patch.crates-io]
  # Uncomment to debug local changes.
  # ratatui = { path = "../../ratatui" }
Index: devel/codex/patches/patch-codex-rs_cli_src_main_rs
===================================================================
RCS file: /cvs/ports/devel/codex/patches/patch-codex-rs_cli_src_main_rs,v
diff -u -p -u -p -r1.4 patch-codex-rs_cli_src_main_rs
--- devel/codex/patches/patch-codex-rs_cli_src_main_rs	5 Sep 2026 13:04:45 -0000	1.4
+++ devel/codex/patches/patch-codex-rs_cli_src_main_rs	17 Oct 2026 04:28:59 -0000
@@ -7,3 +7,136 @@ Index: codex-rs/cli/src/main.rs
  use clap::Args;
  use clap::CommandFactory;
  use clap::Parser;
+@@ -480,11 +482,23 @@ type HostSandboxArgs = codex_cli::Seatbe
+ type HostSandboxArgs = codex_cli::LandlockCommand;
+ #[cfg(target_os = "windows")]
+ type HostSandboxArgs = codex_cli::WindowsCommand;
++#[cfg(target_os = "openbsd")]
++type HostSandboxArgs = codex_cli::OpenBsdCommand;
+ 
+-#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
++#[cfg(not(any(
++    target_os = "macos",
++    target_os = "linux",
++    target_os = "windows",
++    target_os = "openbsd"
++)))]
+ type HostSandboxArgs = UnsupportedSandboxArgs;
+ 
+-#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
++#[cfg(not(any(
++    target_os = "macos",
++    target_os = "linux",
++    target_os = "windows",
++    target_os = "openbsd"
++)))]
+ #[derive(Debug, Parser)]
+ struct UnsupportedSandboxArgs {
+     /// Layer $CODEX_HOME/<name>.config.toml on top of the base user config.
+@@ -1717,6 +1731,8 @@ async fn cli_main(
+                 loader_overrides,
+             )
+             .await?;
++            #[cfg(target_os = "openbsd")]
++            codex_cli::run_command_under_openbsd(sandbox_cli, loader_overrides).await?;
+             #[cfg(target_os = "windows")]
+             codex_cli::run_command_under_windows_sandbox(
+                 sandbox_cli,
+@@ -1724,7 +1740,12 @@ async fn cli_main(
+                 loader_overrides,
+             )
+             .await?;
+-            #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
++            #[cfg(not(any(
++                target_os = "macos",
++                target_os = "linux",
++                target_os = "windows",
++                target_os = "openbsd"
++            )))]
+             {
+                 let _ = loader_overrides;
+                 anyhow::bail!("`codex sandbox` is not supported on this operating system");
+@@ -3649,7 +3670,12 @@ enabled = true
+         }
+     }
+ 
+-    #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
++    #[cfg(any(
++        target_os = "macos",
++        target_os = "linux",
++        target_os = "windows",
++        target_os = "openbsd"
++    ))]
+     #[test]
+     fn sandbox_parses_permission_profile() {
+         let cli = MultitoolCli::try_parse_from([
+@@ -3670,7 +3696,12 @@ enabled = true
+         assert_eq!(command.command, vec!["echo"]);
+     }
+ 
+-    #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
++    #[cfg(any(
++        target_os = "macos",
++        target_os = "linux",
++        target_os = "windows",
++        target_os = "openbsd"
++    ))]
+     #[test]
+     fn sandbox_parses_legacy_permissions_profile_alias() {
+         let cli = MultitoolCli::try_parse_from([
+@@ -3691,7 +3722,12 @@ enabled = true
+         assert_eq!(command.command, vec!["echo"]);
+     }
+ 
+-    #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
++    #[cfg(any(
++        target_os = "macos",
++        target_os = "linux",
++        target_os = "windows",
++        target_os = "openbsd"
++    ))]
+     #[test]
+     fn sandbox_help_only_shows_singular_permission_profile() {
+         let help = help_from_args(&["codex", "sandbox", "--help"]);
+@@ -3699,7 +3735,12 @@ enabled = true
+         assert!(!help.contains("--permissions-profile"), "{help}");
+     }
+ 
+-    #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
++    #[cfg(any(
++        target_os = "macos",
++        target_os = "linux",
++        target_os = "windows",
++        target_os = "openbsd"
++    ))]
+     #[test]
+     fn sandbox_parses_permissions_profile_short_alias() {
+         let cli =
+@@ -3714,7 +3755,12 @@ enabled = true
+         assert_eq!(command.command, vec!["echo"]);
+     }
+ 
+-    #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
++    #[cfg(any(
++        target_os = "macos",
++        target_os = "linux",
++        target_os = "windows",
++        target_os = "openbsd"
++    ))]
+     #[test]
+     fn sandbox_parses_config_profile() {
+         let cli =
+@@ -3729,7 +3775,12 @@ enabled = true
+         assert_eq!(command.command, vec!["echo"]);
+     }
+ 
+-    #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
++    #[cfg(any(
++        target_os = "macos",
++        target_os = "linux",
++        target_os = "windows",
++        target_os = "openbsd"
++    ))]
+     #[test]
+     fn sandbox_rejects_explicit_profile_controls_without_profile() {
+         let err = MultitoolCli::try_parse_from(["codex", "sandbox", "-C", "/tmp"])
Index: devel/codex/pkg/README
===================================================================
RCS file: /cvs/ports/devel/codex/pkg/README,v
diff -u -p -u -p -r1.2 README
--- devel/codex/pkg/README	24 Aug 2026 16:39:57 -0000	1.2
+++ devel/codex/pkg/README	17 Oct 2026 04:28:59 -0000
@@ -2,6 +2,21 @@
 | Running ${PKGSTEM} on OpenBSD
 +-----------------------------------------------------------------------
 
+The OpenBSD command sandbox is currently unavailable without the legacy
+execution promise. Restricted filesystem profiles are rejected before command
+execution, including profiles with network access enabled explicitly.
+
+The kernel clears unveil rules on execve unless execpromises are installed.
+Using unveil alone in a launcher would therefore run the target with no
+filesystem restrictions. The backend checks this limitation in both the
+controller and helper and fails closed. It does not request exec or error.
+Network isolation and managed-proxy containment are also unsupported.
+
+The danger-full-access profile is an explicit opt-out and continues to run
+commands without a sandbox. This package does not silently select it when a
+restricted profile fails. getexecpath(3) is still used to resolve helper paths.
+The internal OpenBsdPledgeUnveil identifier is retained for compatibility.
+
 You may use OpenAI commercial services or misc/llama.cpp, for the last
 one you should add into ~/.codex/config.toml:
 
Index: devel/codex/patches/patch-codex-rs_Cargo_lock
===================================================================
--- /dev/null	Sat Oct 17 06:28:59 2026
+++ devel/codex/patches/patch-codex-rs_Cargo_lock	Sat Oct 17 06:20:39 2026
@@ -0,0 +1,25 @@
+Record the rust-openbsd path dependency in the lockfile.
+
+Index: codex-rs/Cargo.lock
+--- codex-rs/Cargo.lock.orig
++++ codex-rs/Cargo.lock
+@@ -4539,6 +4539,8 @@ dependencies = [
+  "libc",
+  "pretty_assertions",
+  "regex-lite",
++ "rust-openbsd",
++ "serde",
+  "serde_json",
+  "tempfile",
+  "tokio",
+@@ -13496,6 +13498,10 @@ dependencies = [
+ ]
+ 
+ [[package]]
++name = "rust-openbsd"
++version = "0.2.0"
++
++[[package]]
+ name = "rust-stemmers"
+ version = "1.2.0"
+ source = "registry+https://github.com/rust-lang/crates.io-index";
Index: devel/codex/patches/patch-codex-rs_arg0_src_lib_rs
===================================================================
--- /dev/null	Sat Oct 17 06:28:59 2026
+++ devel/codex/patches/patch-codex-rs_arg0_src_lib_rs	Sat Oct 17 06:20:39 2026
@@ -0,0 +1,41 @@
+Dispatch and materialize the codex-openbsd-sandbox alias.
+Use the canonical OpenBSD executable path for all PATH aliases.
+
+Index: codex-rs/arg0/src/lib.rs
+--- codex-rs/arg0/src/lib.rs.orig
++++ codex-rs/arg0/src/lib.rs
+@@ -11,6 +11,8 @@ use codex_exec_server::CODEX_ARG0_EXEC_H
+ use codex_exec_server::CODEX_FS_HELPER_ARG1;
+ use codex_install_context::InstallContext;
+ use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0;
++#[cfg(target_os = "openbsd")]
++use codex_sandboxing::openbsd::CODEX_OPENBSD_SANDBOX_ARG0;
+ use codex_utils_home_dir::find_codex_home;
+ #[cfg(target_os = "windows")]
+ use codex_windows_sandbox::CODEX_WINDOWS_SANDBOX_ARG1;
+@@ -73,6 +75,12 @@ pub fn arg0_dispatch() -> Option<Arg0Pat
+     } else if exe_name == APPLY_PATCH_ARG0 || exe_name == MISSPELLED_APPLY_PATCH_ARG0 {
+         codex_apply_patch::main();
+     }
++    #[cfg(target_os = "openbsd")]
++    if exe_name == CODEX_OPENBSD_SANDBOX_ARG0 {
++        // The OpenBSD helper applies unveil(2) to itself and then
++        // execs the target; it never returns.
++        codex_sandboxing::openbsd::run_openbsd_sandbox_main();
++    }
+ 
+     let argv1 = args.next().unwrap_or_default();
+     if argv1 == codex_sandboxing::CODEX_WINDOWS_MXC_ARG1 {
+@@ -372,7 +380,12 @@ fn prepare_aliases_in_home(
+         MISSPELLED_APPLY_PATCH_ARG0,
+         #[cfg(target_os = "linux")]
+         CODEX_LINUX_SANDBOX_ARG0,
++        #[cfg(target_os = "openbsd")]
++        CODEX_OPENBSD_SANDBOX_ARG0,
+     ] {
++        #[cfg(target_os = "openbsd")]
++        let exe = codex_sandboxing::openbsd::sandbox_executable()?;
++        #[cfg(not(target_os = "openbsd"))]
+         let exe = std::env::current_exe()?;
+ 
+         #[cfg(unix)]
Index: devel/codex/patches/patch-codex-rs_cli_src_debug_sandbox_rs
===================================================================
--- /dev/null	Sat Oct 17 06:28:59 2026
+++ devel/codex/patches/patch-codex-rs_cli_src_debug_sandbox_rs	Sat Oct 17 06:20:39 2026
@@ -0,0 +1,94 @@
+Run the OpenBSD helper through upstream sandbox config/profile preparation.
+Preserve disabled-profile bypass, environment setup and child exit propagation.
+
+Index: codex-rs/cli/src/debug_sandbox.rs
+--- codex-rs/cli/src/debug_sandbox.rs.orig
++++ codex-rs/cli/src/debug_sandbox.rs
+@@ -98,6 +98,29 @@ pub async fn run_command_under_landlock(
+     codex_linux_sandbox_exe: Option<PathBuf>,
+     loader_overrides: LoaderOverrides,
+ ) -> anyhow::Result<()> {
++    run_command_under_profile_sandbox(
++        command,
++        codex_linux_sandbox_exe,
++        loader_overrides,
++        SandboxType::Landlock,
++    )
++    .await
++}
++
++#[cfg(target_os = "openbsd")]
++pub async fn run_command_under_openbsd(
++    command: crate::OpenBsdCommand,
++    loader_overrides: LoaderOverrides,
++) -> anyhow::Result<()> {
++    run_command_under_profile_sandbox(command, None, loader_overrides, SandboxType::OpenBsd).await
++}
++
++async fn run_command_under_profile_sandbox(
++    command: LandlockCommand,
++    codex_linux_sandbox_exe: Option<PathBuf>,
++    loader_overrides: LoaderOverrides,
++    sandbox_type: SandboxType,
++) -> anyhow::Result<()> {
+     let LandlockCommand {
+         sandbox_state,
+         permissions_profile,
+@@ -122,7 +145,7 @@ pub async fn run_command_under_landlock(
+         command,
+         config_overrides,
+         codex_linux_sandbox_exe,
+-        SandboxType::Landlock,
++        sandbox_type,
+         /*log_denials*/ false,
+         &[],
+     )
+@@ -169,6 +192,8 @@ enum SandboxType {
+     #[cfg(target_os = "macos")]
+     Seatbelt,
+     Landlock,
++    #[cfg(target_os = "openbsd")]
++    OpenBsd,
+     Windows,
+ }
+ 
+@@ -380,7 +405,7 @@ async fn run_command_under_sandbox(
+         .map(codex_core::config::StartedNetworkProxy::proxy);
+     // Proxy containment depends on whether a proxy is active, not whether its
+     // policy came from managed requirements.
+-    #[cfg(any(target_os = "macos", target_os = "windows"))]
++    #[cfg(any(target_os = "macos", target_os = "windows", target_os = "openbsd"))]
+     let enforce_managed_network = network.is_some();
+     let managed_mitm_ca_trust_bundle_path = match network.as_ref() {
+         Some(network) => network.managed_mitm_ca_trust_bundle_path(),
+@@ -438,6 +463,30 @@ async fn run_command_under_sandbox(
+             )
+             .await?
+         }
++        #[cfg(target_os = "openbsd")]
++        SandboxType::OpenBsd => {
++            use codex_sandboxing::openbsd::CODEX_OPENBSD_SANDBOX_ARG0;
++            use codex_sandboxing::openbsd::create_openbsd_sandbox_command_args;
++            use codex_sandboxing::openbsd::sandbox_executable;
++
++            let args = create_openbsd_sandbox_command_args(
++                command,
++                cwd.as_path(),
++                sandbox_policy_cwd.as_path(),
++                &runtime_permission_profile,
++                enforce_managed_network,
++            )?;
++            spawn_debug_sandbox_child(
++                sandbox_executable()?,
++                args,
++                Some(CODEX_OPENBSD_SANDBOX_ARG0),
++                cwd.to_path_buf(),
++                runtime_permission_profile.network_sandbox_policy(),
++                env,
++                |_| {},
++            )
++            .await?
++        }
+         SandboxType::Landlock => {
+             #[expect(clippy::expect_used)]
+             let codex_linux_sandbox_exe = config
Index: devel/codex/patches/patch-codex-rs_cli_src_lib_rs
===================================================================
--- /dev/null	Sat Oct 17 06:28:59 2026
+++ devel/codex/patches/patch-codex-rs_cli_src_lib_rs	Sat Oct 17 06:20:39 2026
@@ -0,0 +1,31 @@
+Expose the OpenBSD sandbox launcher with the common permission/profile options.
+
+Index: codex-rs/cli/src/lib.rs
+--- codex-rs/cli/src/lib.rs.orig
++++ codex-rs/cli/src/lib.rs
+@@ -10,6 +10,8 @@ use codex_utils_cli::ProfileV2Name;
+ use std::path::PathBuf;
+ 
+ pub use debug_sandbox::run_command_under_landlock;
++#[cfg(target_os = "openbsd")]
++pub use debug_sandbox::run_command_under_openbsd;
+ pub use debug_sandbox::run_command_under_seatbelt;
+ pub use debug_sandbox::run_command_under_windows_sandbox;
+ pub use login::read_access_token_from_stdin;
+@@ -141,11 +143,15 @@ pub struct LandlockCommand {
+     #[clap(skip)]
+     pub config_overrides: CliConfigOverrides,
+ 
+-    /// Full command args to run under the Linux sandbox.
++    /// Full command args to run under the host sandbox.
+     #[arg(trailing_var_arg = true)]
+     pub command: Vec<String>,
+ }
+ 
++/// OpenBSD uses the common profile/state options without Linux-specific flags.
++#[cfg(target_os = "openbsd")]
++pub type OpenBsdCommand = LandlockCommand;
++
+ #[derive(Debug, Parser)]
+ pub struct WindowsCommand {
+     #[command(flatten)]
Index: devel/codex/patches/patch-codex-rs_exec-server-protocol_src_protocol_rs
===================================================================
--- /dev/null	Sat Oct 17 06:28:59 2026
+++ devel/codex/patches/patch-codex-rs_exec-server-protocol_src_protocol_rs	Sat Oct 17 06:20:39 2026
@@ -0,0 +1,13 @@
+Carry the OpenBSD sandbox type over the exec-server protocol.
+
+Index: codex-rs/exec-server-protocol/src/protocol.rs
+--- codex-rs/exec-server-protocol/src/protocol.rs.orig
++++ codex-rs/exec-server-protocol/src/protocol.rs
+@@ -466,6 +466,7 @@ pub enum ProcessSandboxType {
+     None,
+     MacosSeatbelt,
+     LinuxSeccomp,
++    OpenBsdPledgeUnveil,
+     WindowsRestrictedToken,
+     WindowsMxc,
+ }
Index: devel/codex/patches/patch-codex-rs_exec-server_src_local_process_rs
===================================================================
--- /dev/null	Sat Oct 17 06:28:59 2026
+++ devel/codex/patches/patch-codex-rs_exec-server_src_local_process_rs	Sat Oct 17 06:20:39 2026
@@ -0,0 +1,13 @@
+Report OpenBsdPledgeUnveil for locally started processes.
+
+Index: codex-rs/exec-server/src/local_process.rs
+--- codex-rs/exec-server/src/local_process.rs.orig
++++ codex-rs/exec-server/src/local_process.rs
+@@ -391,6 +391,7 @@ impl LocalProcess {
+             SandboxType::None => Some(ProcessSandboxType::None),
+             SandboxType::MacosSeatbelt => Some(ProcessSandboxType::MacosSeatbelt),
+             SandboxType::LinuxSeccomp => Some(ProcessSandboxType::LinuxSeccomp),
++            SandboxType::OpenBsdPledgeUnveil => Some(ProcessSandboxType::OpenBsdPledgeUnveil),
+             SandboxType::WindowsRestrictedToken => Some(ProcessSandboxType::WindowsRestrictedToken),
+             SandboxType::WindowsMxc => Some(ProcessSandboxType::WindowsMxc),
+         };
Index: devel/codex/patches/patch-codex-rs_exec-server_src_process_rs
===================================================================
--- /dev/null	Sat Oct 17 06:28:59 2026
+++ devel/codex/patches/patch-codex-rs_exec-server_src_process_rs	Sat Oct 17 06:20:39 2026
@@ -0,0 +1,13 @@
+Map the exec-server protocol sandbox type back to OpenBsdPledgeUnveil.
+
+Index: codex-rs/exec-server/src/process.rs
+--- codex-rs/exec-server/src/process.rs.orig
++++ codex-rs/exec-server/src/process.rs
+@@ -32,6 +32,7 @@ pub(crate) fn sandbox_type_from_protocol(
+         Some(ProcessSandboxType::None) => Some(SandboxType::None),
+         Some(ProcessSandboxType::MacosSeatbelt) => Some(SandboxType::MacosSeatbelt),
+         Some(ProcessSandboxType::LinuxSeccomp) => Some(SandboxType::LinuxSeccomp),
++        Some(ProcessSandboxType::OpenBsdPledgeUnveil) => Some(SandboxType::OpenBsdPledgeUnveil),
+         Some(ProcessSandboxType::WindowsRestrictedToken) => {
+             Some(SandboxType::WindowsRestrictedToken)
+         }
Index: devel/codex/patches/patch-codex-rs_exec-server_tests_exec_process_rs
===================================================================
--- /dev/null	Sat Oct 17 06:28:59 2026
+++ devel/codex/patches/patch-codex-rs_exec-server_tests_exec_process_rs	Sat Oct 17 06:20:39 2026
@@ -0,0 +1,15 @@
+Handle the OpenBSD sandbox type in a Windows-only test assertion.
+
+Index: codex-rs/exec-server/tests/exec_process.rs
+--- codex-rs/exec-server/tests/exec_process.rs.orig
++++ codex-rs/exec-server/tests/exec_process.rs
+@@ -1544,7 +1544,8 @@ async fn assert_remote_windows_sandbox_process_write(
+         }
+         codex_sandboxing::SandboxType::None
+         | codex_sandboxing::SandboxType::MacosSeatbelt
+-        | codex_sandboxing::SandboxType::LinuxSeccomp => {
++        | codex_sandboxing::SandboxType::LinuxSeccomp
++        | codex_sandboxing::SandboxType::OpenBsdPledgeUnveil => {
+             anyhow::bail!("expected a Windows sandbox type")
+         }
+     }
Index: devel/codex/patches/patch-codex-rs_exec-server_tests_file_system_windows_rs
===================================================================
--- /dev/null	Sat Oct 17 06:28:59 2026
+++ devel/codex/patches/patch-codex-rs_exec-server_tests_file_system_windows_rs	Sat Oct 17 06:20:39 2026
@@ -0,0 +1,17 @@
+Handle the OpenBSD sandbox type in a Windows-only test assertion.
+
+Index: codex-rs/exec-server/tests/file_system_windows.rs
+--- codex-rs/exec-server/tests/file_system_windows.rs.orig
++++ codex-rs/exec-server/tests/file_system_windows.rs
+@@ -387,7 +387,10 @@ async fn file_system_remote_fs_helper_respects_windows
+         SandboxType::WindowsMxc => {
+             sandbox.windows_sandbox_selection = WindowsSandboxSelection::Mxc;
+         }
+-        SandboxType::None | SandboxType::MacosSeatbelt | SandboxType::LinuxSeccomp => {
++        SandboxType::None
++        | SandboxType::MacosSeatbelt
++        | SandboxType::LinuxSeccomp
++        | SandboxType::OpenBsdPledgeUnveil => {
+             anyhow::bail!("expected a Windows sandbox type")
+         }
+     }
Index: devel/codex/patches/patch-codex-rs_protocol_src_sandbox_rs
===================================================================
--- /dev/null	Sat Oct 17 06:28:59 2026
+++ devel/codex/patches/patch-codex-rs_protocol_src_sandbox_rs	Sat Oct 17 06:20:39 2026
@@ -0,0 +1,22 @@
+Retain the OpenBsdPledgeUnveil protocol name and metric tag for compatibility.
+The OpenBSD implementation now enforces pathname rules with unveil only.
+
+Index: codex-rs/protocol/src/sandbox.rs
+--- codex-rs/protocol/src/sandbox.rs.orig
++++ codex-rs/protocol/src/sandbox.rs
+@@ -30,6 +30,7 @@ pub enum SandboxType {
+     None,
+     MacosSeatbelt,
+     LinuxSeccomp,
++    OpenBsdPledgeUnveil,
+     WindowsRestrictedToken,
+     WindowsMxc,
+ }
+@@ -40,6 +41,7 @@ impl SandboxType {
+             Self::None => "none",
+             Self::MacosSeatbelt => "seatbelt",
+             Self::LinuxSeccomp => "seccomp",
++            Self::OpenBsdPledgeUnveil => "openbsd_pledge_unveil",
+             Self::WindowsRestrictedToken => "windows_sandbox",
+             Self::WindowsMxc => "windows_mxc",
+         }
Index: devel/codex/patches/patch-codex-rs_sandboxing_Cargo_toml
===================================================================
--- /dev/null	Sat Oct 17 06:28:59 2026
+++ devel/codex/patches/patch-codex-rs_sandboxing_Cargo_toml	Sat Oct 17 06:20:39 2026
@@ -0,0 +1,26 @@
+Depend on rust-openbsd, for OpenBSD only.
+
+The `openbsd-8-0` feature is enabled because the backend resolves the helper
+executable with getexecpath(3), which first appeared in OpenBSD 8.0.
+
+Index: codex-rs/sandboxing/Cargo.toml
+--- codex-rs/sandboxing/Cargo.toml.orig
++++ codex-rs/sandboxing/Cargo.toml
+@@ -24,6 +24,7 @@ codex-windows-sandbox = { workspace = true }
+ codex-windows-sandbox = { workspace = true }
+ dunce = { workspace = true }
+ libc = { workspace = true }
++serde = { workspace = true, features = ["derive"] }
+ serde_json = { workspace = true }
+ regex-lite = { workspace = true }
+ tokio = { workspace = true, features = ["rt", "sync"] }
+@@ -35,6 +36,9 @@ which = { workspace = true }
+ codex-otel = { workspace = true }
+ codex-utils-home-dir = { workspace = true }
+ 
++[target.'cfg(target_os = "openbsd")'.dependencies]
++rust-openbsd = { workspace = true, features = ["openbsd-8-0"] }
++
+ [dev-dependencies]
+ anyhow = { workspace = true }
+ codex-utils-cargo-bin = { workspace = true }
Index: devel/codex/patches/patch-codex-rs_sandboxing_src_lib_rs
===================================================================
--- /dev/null	Sat Oct 17 06:28:59 2026
+++ devel/codex/patches/patch-codex-rs_sandboxing_src_lib_rs	Sat Oct 17 06:20:39 2026
@@ -0,0 +1,37 @@
+Register the OpenBSD backend modules and surface its transform errors.
+
+Index: codex-rs/sandboxing/src/lib.rs
+--- codex-rs/sandboxing/src/lib.rs.orig
++++ codex-rs/sandboxing/src/lib.rs
+@@ -4,6 +4,10 @@ pub mod landlock;
+ pub mod landlock;
+ mod linux_pid_namespace;
+ mod manager;
++#[cfg(target_os = "openbsd")]
++pub mod openbsd;
++#[cfg(any(target_os = "openbsd", test))]
++mod openbsd_policy;
+ pub mod policy_transforms;
+ mod sandbox_integrity;
+ #[cfg(target_os = "macos")]
+@@ -82,6 +86,20 @@ impl From<SandboxTransformError> for CodexErr {
+             SandboxTransformError::MissingLinuxSandboxExecutable => {
+                 CodexErr::LandlockSandboxExecutableNotProvided
+             }
++            #[cfg(target_os = "openbsd")]
++            SandboxTransformError::MissingOpenBsdSandboxExecutable => {
++                CodexErr::UnsupportedOperation(
++                    "missing codex-openbsd-sandbox helper executable path".to_string(),
++                )
++            }
++            #[cfg(target_os = "openbsd")]
++            SandboxTransformError::OpenBsdPreparation(message) => {
++                CodexErr::UnsupportedOperation(message)
++            }
++            #[cfg(not(target_os = "openbsd"))]
++            SandboxTransformError::OpenBsdUnavailable => CodexErr::UnsupportedOperation(
++                "openbsd unveil sandbox is only available on OpenBSD".to_string(),
++            ),
+             SandboxTransformError::EnvironmentNetworkProxy(message) => {
+                 CodexErr::UnsupportedOperation(message)
+             }
Index: devel/codex/patches/patch-codex-rs_sandboxing_src_manager_rs
===================================================================
--- /dev/null	Sat Oct 17 06:28:59 2026
+++ devel/codex/patches/patch-codex-rs_sandboxing_src_manager_rs	Sat Oct 17 06:20:39 2026
@@ -0,0 +1,112 @@
+Select and transform the OpenBSD backend through the arg0 helper.
+Resolve the helper executable with getexecpath(3), never a PATH search.
+
+Index: codex-rs/sandboxing/src/manager.rs
+--- codex-rs/sandboxing/src/manager.rs.orig
++++ codex-rs/sandboxing/src/manager.rs
+@@ -52,6 +52,8 @@ pub fn get_platform_sandbox(windows_sand
+         Some(SandboxType::MacosSeatbelt)
+     } else if cfg!(target_os = "linux") {
+         Some(SandboxType::LinuxSeccomp)
++    } else if cfg!(target_os = "openbsd") {
++        Some(SandboxType::OpenBsdPledgeUnveil)
+     } else if cfg!(target_os = "windows") {
+         if windows_sandbox_enabled {
+             Some(SandboxType::WindowsRestrictedToken)
+@@ -198,6 +200,12 @@ pub enum SandboxTransformError {
+         source: io::Error,
+     },
+     MissingLinuxSandboxExecutable,
++    #[cfg(target_os = "openbsd")]
++    MissingOpenBsdSandboxExecutable,
++    #[cfg(target_os = "openbsd")]
++    OpenBsdPreparation(String),
++    #[cfg(not(target_os = "openbsd"))]
++    OpenBsdUnavailable,
+     WindowsMxcPreparation(String),
+     EnvironmentNetworkProxy(String),
+     #[cfg(target_os = "macos")]
+@@ -226,6 +234,21 @@ impl std::fmt::Display for SandboxTransf
+             Self::MissingLinuxSandboxExecutable => {
+                 write!(f, "missing codex-linux-sandbox executable path")
+             }
++            #[cfg(target_os = "openbsd")]
++            Self::MissingOpenBsdSandboxExecutable => {
++                write!(f, "missing codex-openbsd-sandbox helper executable path")
++            }
++            #[cfg(target_os = "openbsd")]
++            Self::OpenBsdPreparation(err) => {
++                write!(f, "failed to prepare OpenBSD unveil sandbox: {err}")
++            }
++            #[cfg(not(target_os = "openbsd"))]
++            Self::OpenBsdUnavailable => {
++                write!(
++                    f,
++                    "openbsd unveil sandbox is only available on OpenBSD"
++                )
++            }
+             Self::WindowsMxcPreparation(err) => {
+                 write!(f, "failed to prepare MXC sandbox: {err}")
+             }
+@@ -254,6 +277,12 @@ impl std::error::Error for SandboxTransf
+             Self::InvalidCommandCwd { source, .. }
+             | Self::InvalidSandboxPolicyCwd { source, .. } => Some(source),
+             Self::MissingLinuxSandboxExecutable => None,
++            #[cfg(target_os = "openbsd")]
++            Self::MissingOpenBsdSandboxExecutable => None,
++            #[cfg(target_os = "openbsd")]
++            Self::OpenBsdPreparation(_) => None,
++            #[cfg(not(target_os = "openbsd"))]
++            Self::OpenBsdUnavailable => None,
+             Self::WindowsMxcPreparation(_) => None,
+             Self::EnvironmentNetworkProxy(_) => None,
+             #[cfg(target_os = "macos")]
+@@ -474,6 +503,48 @@ impl SandboxManager {
+             }
+             #[cfg(not(target_os = "macos"))]
+             SandboxType::MacosSeatbelt => return Err(SandboxTransformError::SeatbeltUnavailable),
++            #[cfg(target_os = "openbsd")]
++            SandboxType::OpenBsdPledgeUnveil => {
++                let pending = pending_sandboxed_request?;
++                // The helper is the Codex executable itself, re-invoked through
++                // the `codex-openbsd-sandbox` arg0 alias. Prefer the helper path
++                // supplied by the caller; otherwise this process *is* the
++                // helper, so its own executable path is the helper path.
++                let exe = match sandbox_exe {
++                    Some(path) if path.is_absolute() => path.to_path_buf(),
++                    Some(_) => {
++                        return Err(SandboxTransformError::OpenBsdPreparation(
++                            "sandbox helper executable path must be absolute".to_string(),
++                        ));
++                    }
++                    // getexecpath(3), added in OpenBSD 8.0, reports the canonical
++                    // pathname directly. std::env::current_exe() is documented as
++                    // best-effort on OpenBSD: it reports argv[0], which is a bare
++                    // program name when Codex itself was found through PATH.
++                    None => crate::openbsd::sandbox_executable()
++                        .map_err(|_| SandboxTransformError::MissingOpenBsdSandboxExecutable)?,
++                };
++                let mut args = crate::openbsd::create_openbsd_sandbox_command_args(
++                    argv,
++                    pending.native_command_cwd.as_path(),
++                    pending.native_sandbox_policy_cwd.as_path(),
++                    &pending.effective_permission_profile,
++                    enforce_managed_network,
++                )
++                .map_err(|err| SandboxTransformError::OpenBsdPreparation(err.to_string()))?;
++                let mut full_command = Vec::with_capacity(1 + args.len());
++                full_command.push(os_string_to_command_component(exe.into_os_string()));
++                full_command.append(&mut args);
++                (
++                    full_command,
++                    Some(crate::openbsd::CODEX_OPENBSD_SANDBOX_ARG0.to_string()),
++                    Some(pending),
++                )
++            }
++            #[cfg(not(target_os = "openbsd"))]
++            SandboxType::OpenBsdPledgeUnveil => {
++                return Err(SandboxTransformError::OpenBsdUnavailable);
++            }
+             SandboxType::LinuxSeccomp => {
+                 let pending = pending_sandboxed_request?;
+                 let exe =
Index: devel/codex/patches/patch-codex-rs_sandboxing_src_openbsd_policy_rs
===================================================================
--- /dev/null	Sat Oct 17 06:28:59 2026
+++ devel/codex/patches/patch-codex-rs_sandboxing_src_openbsd_policy_rs	Sat Oct 17 06:20:39 2026
@@ -0,0 +1,723 @@
+Translate pathname policies with unveil; reject network containment without approximation.
+
+Index: codex-rs/sandboxing/src/openbsd_policy.rs
+--- /dev/null
++++ codex-rs/sandboxing/src/openbsd_policy.rs
+@@ -0,0 +1,717 @@
++//! Translation of Codex permission profiles into an OpenBSD
++//! [`unveil(2)`](https://man.openbsd.org/unveil.2) policy.
++//!
++//! The translation is intentionally platform independent and is compiled on
++//! every host so that it can be unit tested without an OpenBSD kernel. The
++//! code that actually applies the translated policy — and the only code that
++//! depends on the `rust-openbsd` bindings — lives in [`crate::openbsd`], which
++//! is compiled only for `target_os = "openbsd"`.
++//!
++//! # Why the translation can fail
++//!
++//! `unveil(2)` is a *whitelist*: once the first rule is installed, the entire
++//! filesystem is hidden except for the unveiled paths. A rule that matches a
++//! path is only shadowed by a more specific matching rule at a lower level, so
++//! a writable root can be carved back to read-only for selected descendants.
++//! The standard "read everything, write a set of roots" and "read everything,
++//! write nothing" profiles map to these path rules. This implementation only
++//! supports profiles with a full-disk read grant, optionally narrowed by exact
++//! path denials. Narrow read allowlists and glob denials are rejected rather
++//! than approximated. Descriptor-based operations have separate kernel semantics
++//! (see the port's maintenance notes).
++
++use std::error::Error;
++use std::fmt;
++use std::path::Path;
++use std::path::PathBuf;
++
++use codex_protocol::models::PermissionProfile;
++use codex_protocol::permissions::FileSystemSandboxKind;
++use codex_protocol::permissions::FileSystemSandboxPolicy;
++use serde::Deserialize;
++use serde::Serialize;
++
++/// Access bits of a single `unveil(2)` rule.
++///
++/// These mirror `rust_openbsd::Permissions`. The bindings deliberately do not
++/// implement `serde` for their types, and the policy has to cross a process
++/// boundary, so the helper process converts these bits into
++/// `rust_openbsd::Permissions` immediately before calling `unveil(2)`.
++#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
++pub struct OpenBsdPermissions {
++    /// `r`: read operations.
++    pub read: bool,
++    /// `w`: write operations.
++    pub write: bool,
++    /// `x`: execute operations.
++    pub execute: bool,
++    /// `c`: create and remove operations.
++    pub create: bool,
++}
++
++impl OpenBsdPermissions {
++    /// No access: the path is unveiled but every operation on it is denied.
++    pub const NONE: Self = Self {
++        read: false,
++        write: false,
++        execute: false,
++        create: false,
++    };
++
++    /// `r` and `x`: enough to open and run files, but not to modify them.
++    pub const READ_EXECUTE: Self = Self {
++        read: true,
++        write: false,
++        execute: true,
++        create: false,
++    };
++
++    /// `r` and `w`: the null device, without creation or execution permission.
++    pub const READ_WRITE: Self = Self {
++        read: true,
++        write: true,
++        execute: false,
++        create: false,
++    };
++
++    /// `r`, `w`, `c`, and `x`: full access to a writable root.
++    pub const READ_WRITE_CREATE_EXECUTE: Self = Self {
++        read: true,
++        write: true,
++        execute: true,
++        create: true,
++    };
++
++    /// Returns `true` when the rule grants any write capability.
++    pub const fn is_writable(self) -> bool {
++        self.write || self.create
++    }
++}
++
++/// A single `unveil(2)` rule.
++#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
++pub struct OpenBsdUnveilRule {
++    /// Absolute path to unveil.
++    pub path: PathBuf,
++    /// Permissions granted (or denied, for [`OpenBsdPermissions::NONE`]) below
++    /// the path.
++    pub permissions: OpenBsdPermissions,
++}
++
++/// A translated OpenBSD sandbox policy.
++///
++/// This is the complete, validated description of the restrictions that apply
++/// to one sandboxed command. It is produced by the trusted Codex controller and
++/// consumed by the OpenBSD sandbox helper, which re-validates it before use.
++#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
++pub struct OpenBsdSandboxPolicy {
++    /// Whether `unveil(2)` must be configured and locked.
++    ///
++    /// `false` for profiles that ask Codex *not* to build a filesystem sandbox
++    /// (`unrestricted` and `external-sandbox`); no pathname rules are applied.
++    pub restrict_filesystem: bool,
++    /// Whether the profile grants filesystem writes, excluding the null device.
++    pub writable: bool,
++    /// Whether the profile allows unrestricted network access. Must be true.
++    pub network: bool,
++    /// `unveil(2)` rules in application order.
++    pub rules: Vec<OpenBsdUnveilRule>,
++}
++
++/// Failure to translate or transport an OpenBSD sandbox policy.
++///
++/// The variants distinguish "this policy cannot be enforced" from "the helper
++/// was handed malformed input", so that callers can report the two separately.
++#[derive(Clone, Debug, Eq, PartialEq)]
++pub enum OpenBsdPolicyError {
++    /// The policy cannot be enforced correctly with `unveil(2)`.
++    ///
++    /// The sandbox fails closed: the command is not executed.
++    Unsupported(String),
++    /// The execution request cannot be represented (for example a command
++    /// working directory that is not valid UTF-8).
++    Invalid(String),
++    /// The validated policy could not be serialized for the helper.
++    Serialization(String),
++}
++
++impl fmt::Display for OpenBsdPolicyError {
++    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
++        match self {
++            Self::Unsupported(reason) => {
++                write!(f, "openbsd sandbox policy is not supported: {reason}")
++            }
++            Self::Invalid(reason) => write!(f, "invalid openbsd sandbox request: {reason}"),
++            Self::Serialization(reason) => {
++                write!(f, "failed to serialize openbsd sandbox policy: {reason}")
++            }
++        }
++    }
++}
++
++impl Error for OpenBsdPolicyError {}
++
++/// Translates a Codex permission profile into an OpenBSD sandbox policy.
++///
++/// `sandbox_policy_cwd` is the working directory that relative profile entries
++/// resolve against, exactly as the other platform backends use it.
++///
++/// `enforce_managed_network` requires proxy-only egress. Unveil does not
++/// restrict network syscalls, so both proxy containment and disabled network
++/// access must be rejected rather than silently granting network access.
++///
++/// # Errors
++///
++/// Returns [`OpenBsdPolicyError::Unsupported`] when the profile cannot be
++/// represented faithfully. The sandbox must not fall back to running the
++/// command unrestricted in that case.
++pub fn translate_permission_profile(
++    permission_profile: &PermissionProfile,
++    sandbox_policy_cwd: &Path,
++    enforce_managed_network: bool,
++) -> Result<OpenBsdSandboxPolicy, OpenBsdPolicyError> {
++    let file_system = permission_profile.file_system_sandbox_policy();
++    let network = permission_profile.network_sandbox_policy().is_enabled();
++
++    if enforce_managed_network {
++        return Err(OpenBsdPolicyError::Unsupported(
++            "a managed network proxy requires proxy-only egress, which unveil(2) cannot enforce"
++                .to_string(),
++        ));
++    }
++
++    require_unrestricted_network(network)?;
++
++    if !matches!(file_system.kind, FileSystemSandboxKind::Restricted) {
++        // `unrestricted` and `external-sandbox` profiles do not ask Codex to
++        // build a filesystem sandbox, so Codex must not add one.
++        return Ok(OpenBsdSandboxPolicy {
++            restrict_filesystem: false,
++            writable: true,
++            network,
++            rules: Vec::new(),
++        });
++    }
++
++    if file_system.has_full_disk_write_access() {
++        let mut rules = vec![OpenBsdUnveilRule {
++            path: PathBuf::from("/"),
++            permissions: OpenBsdPermissions::READ_WRITE_CREATE_EXECUTE,
++        }];
++        append_deny_rules(&file_system, sandbox_policy_cwd, &mut rules)?;
++        let writable = rules.iter().any(|rule| rule.permissions.is_writable());
++        append_null_device_rule(&file_system, sandbox_policy_cwd, &mut rules);
++        return Ok(OpenBsdSandboxPolicy {
++            restrict_filesystem: true,
++            writable,
++            network,
++            rules,
++        });
++    }
++
++    let mut readable_policy = file_system.clone();
++    readable_policy
++        .entries
++        .retain(|entry| entry.access != codex_protocol::permissions::FileSystemAccessMode::Deny);
++    // Do not erase a denial of `/`: the unreadable-root adapter deliberately
++    // omits that root to preserve narrow allowlists in other backends.
++    if !readable_policy.has_full_disk_read_access()
++        || !file_system.can_read_local_path_with_cwd(Path::new("/"), sandbox_policy_cwd)
++    {
++        return Err(OpenBsdPolicyError::Unsupported(
++            "this backend requires a full-disk read grant, optionally narrowed by exact \
++             path denials; narrow read allowlists are not implemented"
++                .to_string(),
++        ));
++    }
++
++    // Read everything, then make the configured roots writable and carve the
++    // protected descendants back to read-only. The kernel applies the most
++    // specific matching rule, so a deeper rule shadows the broader one.
++    let mut rules = vec![OpenBsdUnveilRule {
++        path: PathBuf::from("/"),
++        permissions: OpenBsdPermissions::READ_EXECUTE,
++    }];
++    for root in file_system.get_writable_roots_with_cwd(sandbox_policy_cwd) {
++        push_rule(
++            &mut rules,
++            root.root.to_path_buf(),
++            OpenBsdPermissions::READ_WRITE_CREATE_EXECUTE,
++        );
++        for subpath in &root.read_only_subpaths {
++            push_rule(
++                &mut rules,
++                subpath.to_path_buf(),
++                OpenBsdPermissions::READ_EXECUTE,
++            );
++        }
++        for name in &root.protected_metadata_names {
++            push_rule(
++                &mut rules,
++                root.root.join(name.as_str()).to_path_buf(),
++                OpenBsdPermissions::READ_EXECUTE,
++            );
++        }
++    }
++
++    append_deny_rules(&file_system, sandbox_policy_cwd, &mut rules)?;
++
++    let writable = rules.iter().any(|rule| rule.permissions.is_writable());
++    append_null_device_rule(&file_system, sandbox_policy_cwd, &mut rules);
++    Ok(OpenBsdSandboxPolicy {
++        restrict_filesystem: true,
++        writable,
++        network,
++        rules,
++    })
++}
++
++/// Permit shell output redirection to the null device, as upstream's other
++/// backends do. Explicit read denials of this path or its ancestors take
++/// precedence, as do existing rules for this exact path. Never try to widen
++/// an earlier rule, which unveil would reject. The helper checks that the
++/// device is not a regular file or symlink before installing the rules.
++/// No directory is made writable here.
++fn append_null_device_rule(
++    file_system: &FileSystemSandboxPolicy,
++    sandbox_policy_cwd: &Path,
++    rules: &mut Vec<OpenBsdUnveilRule>,
++) {
++    let path = Path::new("/dev/null");
++    if file_system.can_read_local_path_with_cwd(path, sandbox_policy_cwd)
++        && !rules.iter().any(|rule| rule.path == path)
++    {
++        push_rule(rules, path.to_path_buf(), OpenBsdPermissions::READ_WRITE);
++    }
++}
++
++/// Validates a policy before the helper applies it.
++///
++/// The policy is produced by the trusted controller, but it crosses a process
++/// boundary as text, so the helper re-checks it instead of assuming the
++/// controller was correct. A malformed policy is rejected rather than applied.
++///
++/// # Errors
++///
++/// Returns [`OpenBsdPolicyError::Invalid`] for an empty or relative rule path,
++/// or for a restricted policy that carries no rule at all (locking such a
++/// configuration would deny the entire filesystem, including the program about
++/// to be executed).
++pub fn validate_policy(policy: &OpenBsdSandboxPolicy) -> Result<(), OpenBsdPolicyError> {
++    require_unrestricted_network(policy.network)?;
++    if policy.restrict_filesystem && policy.rules.is_empty() {
++        return Err(OpenBsdPolicyError::Invalid(
++            "restricted policy without any unveil rule".to_string(),
++        ));
++    }
++    for rule in &policy.rules {
++        if rule.path.as_os_str().is_empty() {
++            return Err(OpenBsdPolicyError::Invalid(
++                "unveil rule with an empty path".to_string(),
++            ));
++        }
++        if !rule.path.is_absolute() {
++            return Err(OpenBsdPolicyError::Invalid(format!(
++                "unveil rule path is not absolute: {}",
++                rule.path.display()
++            )));
++        }
++    }
++    Ok(())
++}
++
++/// Reject network containment before applying any filesystem restriction.
++fn require_unrestricted_network(network: bool) -> Result<(), OpenBsdPolicyError> {
++    if !network {
++        return Err(OpenBsdPolicyError::Unsupported(
++            "network isolation is unavailable with the OpenBSD unveil backend; \
++             select a profile with network access enabled explicitly"
++                .to_string(),
++        ));
++    }
++    Ok(())
++}
++
++/// Appends `unveil(2)` rules that deny read access to explicit paths.
++///
++/// Glob-based read denials cannot be expressed as unveil rules and are
++/// rejected.
++fn append_deny_rules(
++    file_system: &FileSystemSandboxPolicy,
++    sandbox_policy_cwd: &Path,
++    rules: &mut Vec<OpenBsdUnveilRule>,
++) -> Result<(), OpenBsdPolicyError> {
++    let globs = file_system.get_unreadable_globs_with_cwd(sandbox_policy_cwd);
++    if !globs.is_empty() {
++        return Err(OpenBsdPolicyError::Unsupported(format!(
++            "glob-based read denials cannot be expressed with unveil(2): {globs:?}"
++        )));
++    }
++
++    for path in file_system.get_unreadable_roots_with_cwd(sandbox_policy_cwd) {
++        push_rule(rules, path.to_path_buf(), OpenBsdPermissions::NONE);
++    }
++
++    Ok(())
++}
++
++/// Appends a rule unless an identical one is already present.
++///
++/// Ordering does not affect enforcement (the kernel always picks the most
++/// specific matching rule), but de-duplicating keeps the policy small and makes
++/// the output deterministic for tests.
++fn push_rule(rules: &mut Vec<OpenBsdUnveilRule>, path: PathBuf, permissions: OpenBsdPermissions) {
++    let rule = OpenBsdUnveilRule { path, permissions };
++    if !rules.contains(&rule) {
++        rules.push(rule);
++    }
++}
++
++#[cfg(test)]
++mod tests {
++    use super::*;
++
++    use codex_protocol::permissions::FileSystemAccessMode;
++    use codex_protocol::permissions::FileSystemPath;
++    use codex_protocol::permissions::FileSystemSandboxEntry;
++    use codex_protocol::permissions::NetworkSandboxPolicy;
++    use codex_utils_absolute_path::AbsolutePathBuf;
++    use codex_utils_path_uri::PathUri;
++
++    fn cwd() -> PathBuf {
++        PathBuf::from("/work")
++    }
++
++    fn read_only_with_network() -> PermissionProfile {
++        PermissionProfile::from_runtime_permissions(
++            &FileSystemSandboxPolicy::read_only(),
++            NetworkSandboxPolicy::Enabled,
++        )
++    }
++
++    #[test]
++    fn network_isolation_is_rejected_before_translation() {
++        for profile in [
++            PermissionProfile::read_only(),
++            PermissionProfile::workspace_write(),
++        ] {
++            assert!(matches!(
++                translate_permission_profile(&profile, &cwd(), false),
++                Err(OpenBsdPolicyError::Unsupported(_))
++            ));
++        }
++    }
++
++    #[test]
++    fn helper_rejects_serialized_network_isolation() {
++        let mut policy = translate_permission_profile(&read_only_with_network(), &cwd(), false)
++            .expect("translate");
++        policy.network = false;
++        assert!(matches!(
++            validate_policy(&policy),
++            Err(OpenBsdPolicyError::Unsupported(_))
++        ));
++    }
++
++    #[test]
++    fn read_only_profile_reads_everything_and_writes_nothing() {
++        let profile = read_only_with_network();
++        let policy = translate_permission_profile(&profile, &cwd(), false).expect("translate");
++
++        assert!(policy.restrict_filesystem);
++        assert!(!policy.writable);
++        assert!(policy.network);
++        assert_eq!(policy.rules.len(), 2);
++        assert_eq!(policy.rules[0].path, PathBuf::from("/"));
++        assert_eq!(
++            policy.rules[0].permissions,
++            OpenBsdPermissions::READ_EXECUTE
++        );
++    }
++
++    #[test]
++    fn read_only_null_device_grant_cannot_create_or_execute() {
++        let policy = translate_permission_profile(&read_only_with_network(), &cwd(), false)
++            .expect("translate");
++        let rule = policy
++            .rules
++            .iter()
++            .find(|rule| rule.path == Path::new("/dev/null"))
++            .expect("null device rule");
++        assert_eq!(rule.permissions, OpenBsdPermissions::READ_WRITE);
++        assert!(!rule.permissions.create);
++        assert!(!rule.permissions.execute);
++        assert!(!policy.writable);
++    }
++
++    #[test]
++    fn explicit_null_device_and_parent_denials_are_preserved() {
++        for denied in ["/dev/null", "/dev"] {
++            let path = AbsolutePathBuf::from_absolute_path(denied).expect("absolute");
++            let mut entries = FileSystemSandboxPolicy::read_only().entries;
++            entries.push(FileSystemSandboxEntry::new(
++                FileSystemPath::Path {
++                    path: PathUri::from_abs_path(&path),
++                },
++                FileSystemAccessMode::Deny,
++            ));
++            let profile = PermissionProfile::from_runtime_permissions(
++                &FileSystemSandboxPolicy::restricted(entries),
++                NetworkSandboxPolicy::Enabled,
++            );
++            let policy = translate_permission_profile(&profile, &cwd(), false).expect("translate");
++            assert!(
++                !policy
++                    .rules
++                    .iter()
++                    .any(|rule| rule.path == Path::new("/dev/null") && rule.permissions.write)
++            );
++            assert!(policy.rules.contains(&OpenBsdUnveilRule {
++                path: path.to_path_buf(),
++                permissions: OpenBsdPermissions::NONE,
++            }));
++        }
++    }
++
++    #[test]
++    fn explicit_null_device_carveout_is_not_widened() {
++        let mut entries = FileSystemSandboxPolicy::read_only().entries;
++        for (path, access) in [
++            ("/dev", FileSystemAccessMode::Write),
++            ("/dev/null", FileSystemAccessMode::Read),
++        ] {
++            let path = AbsolutePathBuf::from_absolute_path(path).expect("absolute");
++            entries.push(FileSystemSandboxEntry::new(
++                FileSystemPath::Path {
++                    path: PathUri::from_abs_path(&path),
++                },
++                access,
++            ));
++        }
++        let profile = PermissionProfile::from_runtime_permissions(
++            &FileSystemSandboxPolicy::restricted(entries),
++            NetworkSandboxPolicy::Enabled,
++        );
++        let policy = translate_permission_profile(&profile, &cwd(), false).expect("translate");
++        let rules: Vec<_> = policy
++            .rules
++            .iter()
++            .filter(|rule| rule.path == Path::new("/dev/null"))
++            .collect();
++        assert_eq!(rules.len(), 1);
++        assert_eq!(rules[0].permissions, OpenBsdPermissions::READ_EXECUTE);
++    }
++
++    #[test]
++    fn workspace_write_profile_makes_roots_writable() {
++        let root = AbsolutePathBuf::from_absolute_path("/work/project").expect("absolute");
++        let profile = PermissionProfile::workspace_write_with(
++            std::slice::from_ref(&root),
++            NetworkSandboxPolicy::Enabled,
++            /*exclude_tmpdir_env_var*/ true,
++            /*exclude_slash_tmp*/ true,
++        );
++        let policy = translate_permission_profile(&profile, &cwd(), false).expect("translate");
++
++        assert!(policy.restrict_filesystem);
++        assert!(policy.writable);
++        assert!(policy.network);
++        assert_eq!(policy.rules[0].path, PathBuf::from("/"));
++        assert_eq!(
++            policy.rules[0].permissions,
++            OpenBsdPermissions::READ_EXECUTE
++        );
++
++        let root_rule = policy
++            .rules
++            .iter()
++            .find(|rule| rule.path == PathBuf::from("/work/project"))
++            .expect("writable root rule");
++        assert_eq!(
++            root_rule.permissions,
++            OpenBsdPermissions::READ_WRITE_CREATE_EXECUTE
++        );
++    }
++
++    #[test]
++    fn protected_metadata_stays_read_only_under_a_writable_root() {
++        let root = AbsolutePathBuf::from_absolute_path("/work/project").expect("absolute");
++        let profile = PermissionProfile::workspace_write_with(
++            std::slice::from_ref(&root),
++            NetworkSandboxPolicy::Enabled,
++            true,
++            true,
++        );
++        let policy = translate_permission_profile(&profile, &cwd(), false).expect("translate");
++
++        let git = policy
++            .rules
++            .iter()
++            .find(|rule| rule.path == PathBuf::from("/work/project/.git"))
++            .expect(".git carveout");
++        assert_eq!(git.permissions, OpenBsdPermissions::READ_EXECUTE);
++        assert!(!git.permissions.is_writable());
++    }
++
++    #[test]
++    fn network_enabled_is_carried_through() {
++        let profile =
++            PermissionProfile::workspace_write_with(&[], NetworkSandboxPolicy::Enabled, true, true);
++        let policy = translate_permission_profile(&profile, &cwd(), false).expect("translate");
++        assert!(policy.network);
++    }
++
++    #[test]
++    fn managed_network_over_restricted_policy_is_rejected() {
++        let profile = PermissionProfile::read_only();
++        let error = translate_permission_profile(&profile, &cwd(), true).expect_err("rejected");
++        assert!(matches!(error, OpenBsdPolicyError::Unsupported(_)));
++    }
++
++    #[test]
++    fn managed_network_over_enabled_policy_is_rejected() {
++        let profile =
++            PermissionProfile::workspace_write_with(&[], NetworkSandboxPolicy::Enabled, true, true);
++        let error = translate_permission_profile(&profile, &cwd(), true).expect_err("rejected");
++        assert!(matches!(error, OpenBsdPolicyError::Unsupported(_)));
++    }
++
++    #[test]
++    fn unrestricted_profile_adds_no_unveil_rules() {
++        let profile = PermissionProfile::from_runtime_permissions(
++            &codex_protocol::permissions::FileSystemSandboxPolicy::unrestricted(),
++            NetworkSandboxPolicy::Enabled,
++        );
++        let policy = translate_permission_profile(&profile, &cwd(), false).expect("translate");
++        assert!(!policy.restrict_filesystem);
++        assert!(policy.writable);
++        assert!(policy.rules.is_empty());
++    }
++
++    #[test]
++    fn narrow_read_policy_is_rejected() {
++        let path = AbsolutePathBuf::from_absolute_path("/work/project").expect("absolute");
++        let entry = FileSystemSandboxEntry::new(
++            FileSystemPath::Path {
++                path: PathUri::from_abs_path(&path),
++            },
++            FileSystemAccessMode::Read,
++        );
++        let profile = PermissionProfile::from_runtime_permissions(
++            &FileSystemSandboxPolicy::restricted(vec![entry]),
++            NetworkSandboxPolicy::Enabled,
++        );
++        let error = translate_permission_profile(&profile, &cwd(), false).expect_err("rejected");
++        assert!(matches!(error, OpenBsdPolicyError::Unsupported(_)));
++    }
++
++    #[test]
++    fn glob_read_denial_is_rejected() {
++        let deny = FileSystemSandboxEntry::new(
++            FileSystemPath::GlobPattern {
++                pattern: "/work/**/*.pem".to_string(),
++            },
++            FileSystemAccessMode::Deny,
++        );
++        let mut entries = FileSystemSandboxPolicy::read_only().entries;
++        entries.push(deny);
++        let profile = PermissionProfile::from_runtime_permissions(
++            &FileSystemSandboxPolicy::restricted(entries),
++            NetworkSandboxPolicy::Enabled,
++        );
++        let error = translate_permission_profile(&profile, &cwd(), false).expect_err("rejected");
++        assert!(matches!(error, OpenBsdPolicyError::Unsupported(_)));
++    }
++
++    #[test]
++    fn explicit_read_denial_is_applied_below_full_disk_read() {
++        let path = AbsolutePathBuf::from_absolute_path("/work/secret").expect("absolute");
++        let mut entries = FileSystemSandboxPolicy::read_only().entries;
++        entries.push(FileSystemSandboxEntry::new(
++            FileSystemPath::Path {
++                path: PathUri::from_abs_path(&path),
++            },
++            FileSystemAccessMode::Deny,
++        ));
++        let profile = PermissionProfile::from_runtime_permissions(
++            &FileSystemSandboxPolicy::restricted(entries),
++            NetworkSandboxPolicy::Enabled,
++        );
++        let policy = translate_permission_profile(&profile, &cwd(), false).expect("translate");
++        assert!(policy.rules.contains(&OpenBsdUnveilRule {
++            path: path.to_path_buf(),
++            permissions: OpenBsdPermissions::NONE,
++        }));
++        assert!(!policy.writable);
++    }
++
++    #[test]
++    fn root_read_denial_cannot_turn_into_full_disk_read() {
++        let root = AbsolutePathBuf::from_absolute_path("/").expect("absolute");
++        let mut entries = FileSystemSandboxPolicy::read_only().entries;
++        entries.push(FileSystemSandboxEntry::new(
++            FileSystemPath::Path {
++                path: PathUri::from_abs_path(&root),
++            },
++            FileSystemAccessMode::Deny,
++        ));
++        let profile = PermissionProfile::from_runtime_permissions(
++            &FileSystemSandboxPolicy::restricted(entries),
++            NetworkSandboxPolicy::Enabled,
++        );
++        assert!(matches!(
++            translate_permission_profile(&profile, &cwd(), false),
++            Err(OpenBsdPolicyError::Unsupported(_))
++        ));
++    }
++
++    #[test]
++    fn validation_rejects_relative_and_empty_paths() {
++        let mut policy = translate_permission_profile(&read_only_with_network(), &cwd(), false)
++            .expect("translate");
++        policy.rules.push(OpenBsdUnveilRule {
++            path: PathBuf::from("relative/path"),
++            permissions: OpenBsdPermissions::READ_EXECUTE,
++        });
++        assert!(matches!(
++            validate_policy(&policy),
++            Err(OpenBsdPolicyError::Invalid(_))
++        ));
++
++        let mut policy = translate_permission_profile(&read_only_with_network(), &cwd(), false)
++            .expect("translate");
++        policy.rules.push(OpenBsdUnveilRule {
++            path: PathBuf::new(),
++            permissions: OpenBsdPermissions::READ_EXECUTE,
++        });
++        assert!(matches!(
++            validate_policy(&policy),
++            Err(OpenBsdPolicyError::Invalid(_))
++        ));
++    }
++
++    #[test]
++    fn validation_rejects_restricted_policy_without_rules() {
++        let policy = OpenBsdSandboxPolicy {
++            restrict_filesystem: true,
++            writable: false,
++            network: true,
++            rules: Vec::new(),
++        };
++        assert!(matches!(
++            validate_policy(&policy),
++            Err(OpenBsdPolicyError::Invalid(_))
++        ));
++    }
++
++    #[test]
++    fn validation_accepts_translated_policies() {
++        for profile in [
++            read_only_with_network(),
++            PermissionProfile::workspace_write_with(&[], NetworkSandboxPolicy::Enabled, true, true),
++        ] {
++            let policy = translate_permission_profile(&profile, &cwd(), false).expect("translate");
++            assert!(validate_policy(&policy).is_ok());
++        }
++    }
++}
Index: devel/codex/patches/patch-codex-rs_sandboxing_src_openbsd_rs
===================================================================
--- /dev/null	Sat Oct 17 06:28:59 2026
+++ devel/codex/patches/patch-codex-rs_sandboxing_src_openbsd_rs	Sat Oct 17 06:20:39 2026
@@ -0,0 +1,326 @@
+Fail closed when the kernel cannot preserve the filesystem sandbox across exec.
+
+Index: codex-rs/sandboxing/src/openbsd.rs
+--- /dev/null
++++ codex-rs/sandboxing/src/openbsd.rs
+@@ -0,0 +1,320 @@
++//! OpenBSD pathname sandbox backend using locked `unveil(2)` rules.
++//!
++//! # Architecture
++//!
++//! Codex never restricts the controller process. Instead, an OpenBSD sandboxed
++//! command is launched through a dedicated helper that is the *same* Codex
++//! executable re-invoked through the `arg0` alias `codex-openbsd-sandbox` (see
++//! `codex-arg0`). The trusted controller serializes a validated
++//! [`OpenBsdSandboxPolicy`] onto the helper's command line; the helper applies
++//! it to itself and then `execve(2)`s the target program.
++//!
++//! The helper installs and locks the unveil rules before executing the target.
++//! Unveil rules are inherited by fork, but the current kernel destroys them
++//! on exec unless execution promises were installed. Restricted policies are
++//! therefore rejected: this backend must never claim filesystem containment
++//! that disappears when it executes the target.
++//! It deliberately does not call pledge: without the `exec` promise, pledging
++//! the helper would prevent execution on kernels that require that promise.
++//! Without `error`, native tools' self-pledge calls also cannot safely be
++//! intersected with an inherited syscall ceiling.
++//!
++//! This backend restricts pathname access only. Profiles requiring network
++//! containment are rejected in both the controller and the helper. It does
++//! not restrict descriptor operations, process operations or other syscalls.
++//!
++//! # Order of operations
++//!
++//! 1. Validate the policy and the execution arguments.
++//! 2. `chdir(2)` into the command working directory.
++//! 3. Install every `unveil(2)` rule and lock with `unveil(NULL, NULL)`.
++//! 4. `execve(2)` the target program.
++
++use std::os::unix::fs::FileTypeExt;
++use std::os::unix::process::CommandExt;
++use std::path::Path;
++use std::path::PathBuf;
++
++use codex_protocol::models::PermissionProfile;
++use rust_openbsd::Permission;
++use rust_openbsd::Permissions;
++
++use crate::openbsd_policy::OpenBsdPermissions;
++use crate::openbsd_policy::OpenBsdPolicyError;
++use crate::openbsd_policy::OpenBsdSandboxPolicy;
++use crate::openbsd_policy::translate_permission_profile;
++use crate::openbsd_policy::validate_policy;
++
++/// Basename under which the Codex executable re-invokes itself as the OpenBSD
++/// sandbox helper.
++///
++/// This mirrors `CODEX_LINUX_SANDBOX_ARG0`: the CLI materializes it as a
++/// symlink in a private directory on `PATH` and also passes it as `argv[0]`.
++pub const CODEX_OPENBSD_SANDBOX_ARG0: &str = "codex-openbsd-sandbox";
++
++/// Resolves the running Codex executable for helper launches and PATH aliases.
++/// Rust's `current_exe()` is best-effort on OpenBSD and can return a bare argv[0].
++/// Never turn a failed resolution into a PATH search for a privileged helper.
++pub fn sandbox_executable() -> std::io::Result<PathBuf> {
++    let path = rust_openbsd::execpath()?;
++    if !path.is_absolute() {
++        return Err(std::io::Error::new(
++            std::io::ErrorKind::InvalidData,
++            "getexecpath(3) returned a non-absolute executable path",
++        ));
++    }
++    Ok(path)
++}
++
++/// Exit status used when the helper cannot set up or perform the execution.
++///
++/// `126` mirrors the shell convention for "found but not executable"; it is
++/// distinct from a program's own exit status and from a sandbox denial.
++pub const CODEX_OPENBSD_SANDBOX_SETUP_FAILURE: i32 = 126;
++
++/// Builds the argument vector for a sandboxed OpenBSD command.
++///
++/// The returned vector is appended after the helper executable path. It carries
++/// the validated policy as JSON between the helper and the target command,
++/// separated by `--` so that command arguments cannot be mistaken for helper
++/// options. The policy is produced and serialized by the trusted controller and
++/// is re-validated by the helper before it is applied.
++///
++/// # Errors
++///
++/// Returns an [`OpenBsdPolicyError`] when the profile cannot be enforced, when
++/// the command directory is not representable, or when the policy cannot be
++/// serialized. Callers must treat this as fatal for the command: the sandbox
++/// never degrades to running the command without restrictions.
++pub fn create_openbsd_sandbox_command_args(
++    command: Vec<String>,
++    command_cwd: &Path,
++    sandbox_policy_cwd: &Path,
++    permission_profile: &PermissionProfile,
++    enforce_managed_network: bool,
++) -> Result<Vec<String>, OpenBsdPolicyError> {
++    if !command_cwd.is_absolute() {
++        return Err(OpenBsdPolicyError::Invalid(
++            "command working directory must be absolute".to_string(),
++        ));
++    }
++    let policy = translate_permission_profile(
++        permission_profile,
++        sandbox_policy_cwd,
++        enforce_managed_network,
++    )?;
++    ensure_exec_sandbox_supported(&policy)?;
++    let policy_json = serde_json::to_string(&policy).map_err(|err| {
++        OpenBsdPolicyError::Serialization(format!("failed to serialize openbsd policy: {err}"))
++    })?;
++    let command_cwd = command_cwd.to_str().ok_or_else(|| {
++        OpenBsdPolicyError::Invalid("command working directory is not valid UTF-8".to_string())
++    })?;
++
++    let mut args = Vec::with_capacity(4 + command.len());
++    args.push("--command-cwd".to_string());
++    args.push(command_cwd.to_string());
++    args.push("--openbsd-policy".to_string());
++    args.push(policy_json);
++    args.push("--".to_string());
++    args.extend(command);
++    Ok(args)
++}
++
++/// Entry point of the `codex-openbsd-sandbox` helper. Never returns.
++///
++/// The helper is dispatched by `codex-arg0` from `argv[0]`, so it receives the
++/// target command and the serialized policy directly on its command line.
++pub fn run_openbsd_sandbox_main() -> ! {
++    let message = match run_sandbox_helper() {
++        Ok(()) => {
++            // `execve(2)` replaces the process image and never returns on
++            // success, so reaching this point would be a logic error.
++            "sandbox helper returned without executing the command".to_string()
++        }
++        Err(message) => message,
++    };
++    eprintln!("{CODEX_OPENBSD_SANDBOX_ARG0}: {message}");
++    std::process::exit(CODEX_OPENBSD_SANDBOX_SETUP_FAILURE);
++}
++
++/// Applies the policy and replaces the process image.
++///
++/// Returns the error message on any failure; on success it does not return.
++fn run_sandbox_helper() -> Result<(), String> {
++    let arguments = parse_arguments()?;
++    validate_policy(&arguments.policy).map_err(|err| err.to_string())?;
++    ensure_exec_sandbox_supported(&arguments.policy).map_err(|err| err.to_string())?;
++
++    let program = arguments
++        .command
++        .first()
++        .ok_or_else(|| "no command to execute".to_string())?
++        .clone();
++
++    std::env::set_current_dir(&arguments.command_cwd)
++        .map_err(|err| format!("chdir({}): {err}", arguments.command_cwd.display()))?;
++
++    if arguments.policy.restrict_filesystem {
++        apply_unveil_rules(&arguments.policy)?;
++    }
++
++    let mut command = std::process::Command::new(&program);
++    command.args(&arguments.command[1..]);
++    // `exec` only returns on failure.
++    let error = command.exec();
++    Err(format!("exec({program}): {error}"))
++}
++
++/// Refuse a restriction which the kernel would discard at the exec boundary.
++fn ensure_exec_sandbox_supported(policy: &OpenBsdSandboxPolicy) -> Result<(), OpenBsdPolicyError> {
++    if policy.restrict_filesystem {
++        return Err(OpenBsdPolicyError::Unsupported(
++            "unveil rules are discarded by execve without execution promises; \
++             the OpenBSD backend cannot enforce this filesystem sandbox without \
++             the removed execution promise"
++                .to_string(),
++        ));
++    }
++    Ok(())
++}
++
++/// Parsed helper command line.
++struct HelperArguments {
++    /// Directory to enter before applying the sandbox.
++    command_cwd: PathBuf,
++    /// Validated policy to apply.
++    policy: OpenBsdSandboxPolicy,
++    /// Program and arguments to execute.
++    command: Vec<String>,
++}
++
++/// Parses the helper command line.
++///
++/// The grammar is intentionally small and hand-written: the helper is on the
++/// trusted side of the boundary and must not pull a full argument parser (or
++/// any other machinery) into its single-threaded startup.
++fn parse_arguments() -> Result<HelperArguments, String> {
++    let mut args = std::env::args().skip(1);
++    let mut command_cwd: Option<String> = None;
++    let mut policy_json: Option<String> = None;
++    let mut command: Vec<String> = Vec::new();
++
++    while let Some(arg) = args.next() {
++        match arg.as_str() {
++            "--command-cwd" => {
++                command_cwd = Some(next_value(&mut args, "--command-cwd")?);
++            }
++            "--openbsd-policy" => {
++                policy_json = Some(next_value(&mut args, "--openbsd-policy")?);
++            }
++            "--" => {
++                command.extend(args);
++                break;
++            }
++            other => return Err(format!("unexpected helper argument `{other}`")),
++        }
++    }
++
++    let command_cwd = command_cwd.ok_or_else(|| "missing --command-cwd".to_string())?;
++    let policy_json = policy_json.ok_or_else(|| "missing --openbsd-policy".to_string())?;
++    let policy: OpenBsdSandboxPolicy = serde_json::from_str(&policy_json)
++        .map_err(|err| format!("invalid --openbsd-policy: {err}"))?;
++
++    let command_cwd = PathBuf::from(command_cwd);
++    if !command_cwd.is_absolute() {
++        return Err("command working directory must be absolute".to_string());
++    }
++    Ok(HelperArguments {
++        command_cwd,
++        policy,
++        command,
++    })
++}
++
++/// Returns the value that follows a helper option.
++fn next_value(args: &mut impl Iterator<Item = String>, option: &str) -> Result<String, String> {
++    args.next()
++        .ok_or_else(|| format!("missing value for {option}"))
++}
++
++/// Installs and locks the unveil rules.
++fn apply_unveil_rules(policy: &OpenBsdSandboxPolicy) -> Result<(), String> {
++    // The implicit writable device must not grant writes to a regular file
++    // (or follow a substituted symlink) in a misconfigured filesystem.
++    if policy
++        .rules
++        .iter()
++        .any(|rule| rule.path == Path::new("/dev/null") && rule.permissions.write)
++    {
++        let metadata = std::fs::symlink_metadata("/dev/null")
++            .map_err(|err| format!("lstat(/dev/null): {err}"))?;
++        if !metadata.file_type().is_char_device() {
++            return Err("/dev/null must be a character device, not a file or symlink".to_string());
++        }
++    }
++    for rule in &policy.rules {
++        let permissions = permissions_from_bits(rule.permissions);
++        rust_openbsd::unveil(rule.path.as_path(), permissions)
++            .map_err(|err| format!("unveil({}): {err}", rule.path.display()))?;
++    }
++    // Locking is irreversible; after this the filesystem view is fixed.
++    rust_openbsd::lock_unveil().map_err(|err| format!("unveil(NULL, NULL): {err}"))?;
++    Ok(())
++}
++
++/// Converts the serialized permission bits into the bindings' permission set.
++fn permissions_from_bits(bits: OpenBsdPermissions) -> Permissions {
++    let mut permissions = Permissions::NONE;
++    if bits.read {
++        permissions = permissions.enable(Permission::Read);
++    }
++    if bits.write {
++        permissions = permissions.enable(Permission::Write);
++    }
++    if bits.execute {
++        permissions = permissions.enable(Permission::Execute);
++    }
++    if bits.create {
++        permissions = permissions.enable(Permission::Create);
++    }
++    permissions
++}
++
++#[cfg(test)]
++mod tests {
++    use super::*;
++
++    #[test]
++    fn restricted_execution_fails_closed() {
++        let mut policy = OpenBsdSandboxPolicy {
++            restrict_filesystem: true,
++            writable: false,
++            network: true,
++            rules: Vec::new(),
++        };
++        assert!(ensure_exec_sandbox_supported(&policy).is_err());
++        policy.restrict_filesystem = false;
++        assert!(ensure_exec_sandbox_supported(&policy).is_ok());
++    }
++
++    #[test]
++    fn permission_conversion_preserves_each_bit() {
++        for bits in 0_u8..16 {
++            let translated = permissions_from_bits(OpenBsdPermissions {
++                read: bits & 1 != 0,
++                write: bits & 2 != 0,
++                execute: bits & 4 != 0,
++                create: bits & 8 != 0,
++            });
++            for (bit, permission) in [
++                (1, Permission::Read),
++                (2, Permission::Write),
++                (4, Permission::Execute),
++                (8, Permission::Create),
++            ] {
++                assert_eq!(translated.is_enabled(permission), bits & bit != 0);
++            }
++        }
++    }
++}
Index: devel/codex/patches/patch-codex-rs_sandboxing_src_violation_rs
===================================================================
--- /dev/null	Sat Oct 17 06:28:59 2026
+++ devel/codex/patches/patch-codex-rs_sandboxing_src_violation_rs	Sat Oct 17 06:20:39 2026
@@ -0,0 +1,29 @@
+Report the OpenBSD backend when classifying sandbox denials.
+
+Index: codex-rs/sandboxing/src/violation.rs
+--- codex-rs/sandboxing/src/violation.rs.orig
++++ codex-rs/sandboxing/src/violation.rs
+@@ -48,6 +48,7 @@ pub enum SandboxViolationBackend {
+ pub enum SandboxViolationBackend {
+     LinuxSandbox,
+     ManagedNetworkProxy,
++    OpenBsdPledgeUnveil,
+     Seatbelt,
+     WindowsSandbox,
+     WindowsMxc,
+@@ -58,6 +59,7 @@ impl SandboxViolationBackend {
+         match self {
+             Self::LinuxSandbox => "linux_sandbox",
+             Self::ManagedNetworkProxy => "managed_network_proxy",
++            Self::OpenBsdPledgeUnveil => "openbsd_pledge_unveil",
+             Self::Seatbelt => "seatbelt",
+             Self::WindowsSandbox => "windows_sandbox",
+             Self::WindowsMxc => "windows_mxc",
+@@ -144,6 +146,7 @@ fn classify_filesystem_sandbox_violation(
+         SandboxType::None => return None,
+         SandboxType::MacosSeatbelt => SandboxViolationBackend::Seatbelt,
+         SandboxType::LinuxSeccomp => SandboxViolationBackend::LinuxSandbox,
++        SandboxType::OpenBsdPledgeUnveil => SandboxViolationBackend::OpenBsdPledgeUnveil,
+         SandboxType::WindowsRestrictedToken => SandboxViolationBackend::WindowsSandbox,
+         SandboxType::WindowsMxc => SandboxViolationBackend::WindowsMxc,
+     };

Reply via email to