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.
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	16 Oct 2026 23:38:54 -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 24f45496473ddbde171f1ba97897c2470ac26fed ./rust-openbsd
 
 MAINTAINER =		Kirill A. Korinsky <[email protected]>
 
@@ -39,6 +42,7 @@ HOMEPAGE =		https://github.com/openai/co
 # Apache 2.0
 PERMIT_PACKAGE =	Yes
 
+# uses pledge() and unveil()
 WANTLIB += ${MODCARGO_WANTLIB} bz2 crypto dbus-1 execinfo lzma
 WANTLIB += m onig ssl util
 
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	16 Oct 2026 23:38:54 -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-24f45496473ddbde171f1ba97897c2470ac26fed.tar.gz) = xxx+mH3lTBrj3A8zPLFfDPKjYR4JdfVZOWgYZmZxTVI=
 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-24f45496473ddbde171f1ba97897c2470ac26fed.tar.gz) = 71978
 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	16 Oct 2026 23:38:54 -0000
@@ -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,8 +448,9 @@ regex-automata = { version = "0.4.13", default-feature
  regex-lite = "0.1.8"
  regex-syntax = "0.8.8"
  reqwest = { version = "0.12", features = ["cookies"] }
@@ -35,9 +35,10 @@ 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
+@@ -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	16 Oct 2026 23:38:54 -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	16 Oct 2026 23:38:54 -0000
@@ -2,6 +2,39 @@
 | Running ${PKGSTEM} on OpenBSD
 +-----------------------------------------------------------------------
 
+Codex restricts the commands it runs with pledge(2) and unveil(2): a sandboxed
+command is started through the same executable, re-invoked as
+"codex-openbsd-sandbox", which installs the unveiled paths, applies the promise
+set and then execs the target. The backend is selected automatically for the
+workspace-write and read-only profiles; the danger-full-access profile still
+runs commands without a sandbox. The helper resolves its own executable with
+getexecpath(3), so this package requires OpenBSD 8.0 or later.
+
+To run a local command with the same permission-profile resolution as Codex:
+
+	codex sandbox --permission-profile :read-only -- /bin/sh -c 'echo ok'
+	codex sandbox --permission-profile :workspace -- /usr/bin/make
+
+The target receives the pledge "error" promise so native OpenBSD programs
+can pledge themselves without increasing their permitted operations. A
+forbidden system call returns ENOSYS while that promise remains active;
+programs that drop it can still abort on a pledge violation. Writable
+profiles include fattr for build-tool metadata operations, including Cargo's
+same-owner fchown; arbitrary ownership changes are not granted.
+
+Filesystem paths, including temporary directories and protected metadata,
+come from the selected permission profile. If a profile excludes /tmp and
+$TMPDIR, build tools need a TMPDIR inside an authorized writable root.
+Arbitrary narrow read policies, glob read denials, managed-proxy containment,
+and AF_UNIX sockets are not supported by this backend. Unsupported policies
+fail before command execution; socket operations remain blocked by pledge.
+
+unveil restricts pathname operations, not access through existing file
+descriptors. Writable profiles need fattr, which also permits descriptor
+metadata operations such as fchmod. These kernel semantics differ from
+path-based metadata protection; the backend does not provide full parity
+with every other platform's sandbox.
+
 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
+++ devel/codex/patches/patch-codex-rs_Cargo_lock
@@ -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
+++ devel/codex/patches/patch-codex-rs_arg0_src_lib_rs
@@ -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 pledge(2)/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
+++ devel/codex/patches/patch-codex-rs_cli_src_debug_sandbox_rs
@@ -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
+++ devel/codex/patches/patch-codex-rs_cli_src_lib_rs
@@ -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
+++ devel/codex/patches/patch-codex-rs_exec-server-protocol_src_protocol_rs
@@ -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
+++ devel/codex/patches/patch-codex-rs_exec-server_src_local_process_rs
@@ -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
+++ devel/codex/patches/patch-codex-rs_exec-server_src_process_rs
@@ -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
+++ devel/codex/patches/patch-codex-rs_exec-server_tests_exec_process_rs
@@ -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
+++ devel/codex/patches/patch-codex-rs_exec-server_tests_file_system_windows_rs
@@ -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
+++ devel/codex/patches/patch-codex-rs_protocol_src_sandbox_rs
@@ -0,0 +1,21 @@
+Add the OpenBsdPledgeUnveil sandbox type and its metric tag.
+
+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
+++ devel/codex/patches/patch-codex-rs_sandboxing_Cargo_toml
@@ -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
+++ devel/codex/patches/patch-codex-rs_sandboxing_src_lib_rs
@@ -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 pledge/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
+++ devel/codex/patches/patch-codex-rs_sandboxing_src_manager_rs
@@ -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 pledge/unveil sandbox: {err}")
++            }
++            #[cfg(not(target_os = "openbsd"))]
++            Self::OpenBsdUnavailable => {
++                write!(
++                    f,
++                    "openbsd pledge/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
+++ devel/codex/patches/patch-codex-rs_sandboxing_src_openbsd_policy_rs
@@ -0,0 +1,583 @@
+Translate Codex permission profiles into OpenBSD unveil rules.
+Fail closed for unsupported read policies, deny globs and managed proxy containment.
+
+Index: codex-rs/sandboxing/src/openbsd_policy.rs
+--- /dev/null
++++ codex-rs/sandboxing/src/openbsd_policy.rs
+@@ -0,0 +1,576 @@
++//! Translation of Codex permission profiles into an OpenBSD
++//! [`pledge(2)`](https://man.openbsd.org/pledge.2) /
++//! [`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`, `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`); the helper then applies only
++    /// the pledge restrictions.
++    pub restrict_filesystem: bool,
++    /// Whether the executed program may modify the filesystem.
++    pub writable: bool,
++    /// Whether the executed program may use the network (`inet`, `dns`).
++    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 `pledge(2)`/`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` is set when the caller requires all egress to go
++/// through the managed network proxy. `pledge(2)` can either grant the `inet`
++/// promise (any outbound connection) or withhold it (no sockets at all); it
++/// cannot restrict egress to a proxy, so any active managed proxy is rejected
++/// rather than silently permitting direct connections alongside the proxy.
++///
++/// # 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 pledge(2) cannot \
++             express: granting `inet` would allow arbitrary outbound connections"
++                .to_string(),
++        ));
++    }
++
++    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());
++        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());
++    Ok(OpenBsdSandboxPolicy {
++        restrict_filesystem: true,
++        writable,
++        network,
++        rules,
++    })
++}
++
++/// 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> {
++    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(())
++}
++
++/// 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")
++    }
++
++    #[test]
++    fn read_only_profile_reads_everything_and_writes_nothing() {
++        let profile = PermissionProfile::read_only();
++        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(), 1);
++        assert_eq!(policy.rules[0].path, PathBuf::from("/"));
++        assert_eq!(
++            policy.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::Restricted,
++            /*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::Restricted,
++            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::Restricted,
++        );
++        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::Restricted,
++        );
++        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::Restricted,
++        );
++        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::Restricted,
++        );
++        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::Restricted,
++        );
++        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(&PermissionProfile::read_only(), &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(&PermissionProfile::read_only(), &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: false,
++            rules: Vec::new(),
++        };
++        assert!(matches!(
++            validate_policy(&policy),
++            Err(OpenBsdPolicyError::Invalid(_))
++        ));
++    }
++
++    #[test]
++    fn validation_accepts_translated_policies() {
++        for profile in [
++            PermissionProfile::read_only(),
++            PermissionProfile::workspace_write(),
++        ] {
++            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
+++ devel/codex/patches/patch-codex-rs_sandboxing_src_openbsd_rs
@@ -0,0 +1,386 @@
+Apply the OpenBSD pledge/unveil sandbox in a fresh helper process.
+Use an exec promise ceiling compatible with native tools that pledge themselves.
+
+Index: codex-rs/sandboxing/src/openbsd.rs
+--- /dev/null
++++ codex-rs/sandboxing/src/openbsd.rs
+@@ -0,0 +1,379 @@
++//! OpenBSD sandbox backend: `pledge(2)` promises and `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.
++//!
++//! This boundary is a fresh, single-threaded process, which matters:
++//!
++//! * The helper is created by `execve(2)` from the (possibly multithreaded)
++//!   controller, so it never performs nontrivial Rust work in a `fork(2)` child.
++//! * `pledge(2)` reductions are applied to a process that has exactly one
++//!   thread, avoiding the kernel's sibling-thread unwind during unveil cleanup.
++//!
++//! # Order of operations
++//!
++//! 1. Validate the policy and the execution arguments.
++//! 2. `chdir(2)` into the command working directory (before any restriction).
++//! 3. Install every `unveil(2)` rule.
++//! 4. Lock the unveil configuration with `unveil(NULL, NULL)`.
++//! 5. Set the current and execution promise sets with a single
++//!    `pledge(promises, execpromises)` call.
++//! 6. `execve(2)` the target program.
++//!
++//! Step 5 is where execution promises matter: the promises that restrict the
++//! *target* are the second `pledge(2)` argument. A successful `pledge(2)` in
++//! the helper does not by itself restrict the executed program, so the helper
++//! always sets `execpromises` explicitly.
++//!
++//! The helper's own current promises include `exec`, which is a path promise;
++//! this keeps the kernel from discarding the unveil state while the helper
++//! transitions. Its promises are deliberately not taken from the policy, so a
++//! malformed policy cannot widen the helper itself.
++
++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 rust_openbsd::Promise;
++use rust_openbsd::Promises;
++
++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,
++    )?;
++    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())?;
++
++    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)?;
++    }
++
++    // The current promise set must include a path promise (`exec`) so that the
++    // kernel does not tear down the unveil configuration as the helper
++    // transitions, and the execution promise set carries the restrictions that
++    // apply once the target image is loaded.
++    let helper_promises = Promises::new([Promise::Stdio, Promise::Rpath, Promise::Exec]);
++    let exec_promises = exec_promises(&arguments.policy);
++    pledge_both_with_context(&helper_promises, &exec_promises)?;
++
++    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}"))
++}
++
++/// 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> {
++    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
++}
++
++/// Builds the target's syscall ceiling, independently of its unveil path rules.
++///
++/// Native OpenBSD tools pledge themselves: sh and make request write promises
++/// even for read-only work. `error` makes the kernel intersect such requests
++/// with the current set instead of rejecting startup with EPERM. It never
++/// grants a missing promise; forbidden syscalls return ENOSYS (or abort after
++/// the target voluntarily drops `error`). Exec promises retain the ceiling
++/// across subsequent execs even when a shell reduces its current promises.
++///
++/// `getpw` supports account lookup; `flock` permits shared read locks too.
++/// `fattr` is needed for build outputs and SQLite's same-owner fchown in Cargo.
++/// The kernel's `fattr` promise includes that operation without `chown`, which
++/// would additionally permit arbitrary ownership changes. Path-based metadata
++/// writes still require unveil `w`; descriptor-based metadata operations follow
++/// pledge's descriptor semantics (see the port's sandbox maintenance notes).
++fn exec_promises(policy: &OpenBsdSandboxPolicy) -> Promises {
++    let mut promises = vec![
++        Promise::Stdio,
++        Promise::Rpath,
++        Promise::ProtExec,
++        Promise::Exec,
++        Promise::Proc,
++        Promise::Tty,
++        Promise::Getpw,
++        Promise::Flock,
++        Promise::Error,
++    ];
++    if policy.writable {
++        promises.extend([Promise::Wpath, Promise::Cpath, Promise::Fattr]);
++    }
++    if policy.network {
++        promises.extend([Promise::Inet, Promise::Dns]);
++    }
++    Promises::new(promises)
++}
++
++/// Sets the current and execution promise sets in one `pledge(2)` call.
++fn pledge_both_with_context(current: &Promises, exec: &Promises) -> Result<(), String> {
++    rust_openbsd::pledge_both(current, exec).map_err(|err| format!("pledge(2): {err}"))
++}
++
++#[cfg(test)]
++mod tests {
++    use super::*;
++
++    fn policy(writable: bool, network: bool) -> OpenBsdSandboxPolicy {
++        OpenBsdSandboxPolicy {
++            restrict_filesystem: true,
++            writable,
++            network,
++            rules: Vec::new(),
++        }
++    }
++
++    #[test]
++    fn read_only_allows_native_self_pledge_without_granting_writes() {
++        let promises = exec_promises(&policy(false, false));
++        for promise in [
++            Promise::Error,
++            Promise::Getpw,
++            Promise::Flock,
++            Promise::Exec,
++        ] {
++            assert!(promises.contains(promise));
++        }
++        for promise in [
++            Promise::Wpath,
++            Promise::Cpath,
++            Promise::Fattr,
++            Promise::Inet,
++            Promise::Dns,
++        ] {
++            assert!(!promises.contains(promise));
++        }
++    }
++
++    #[test]
++    fn writable_build_tools_get_fattr_without_arbitrary_chown() {
++        let promises = exec_promises(&policy(true, false));
++        for promise in [
++            Promise::Wpath,
++            Promise::Cpath,
++            Promise::Fattr,
++            Promise::Flock,
++        ] {
++            assert!(promises.contains(promise));
++        }
++        for promise in [
++            Promise::Chown,
++            Promise::Unix,
++            Promise::Inet,
++            Promise::Dns,
++            Promise::Unveil,
++        ] {
++            assert!(!promises.contains(promise));
++        }
++    }
++
++    #[test]
++    fn network_promises_follow_policy() {
++        for writable in [false, true] {
++            for network in [false, true] {
++                let promises = exec_promises(&policy(writable, network));
++                assert_eq!(promises.contains(Promise::Inet), network);
++                assert_eq!(promises.contains(Promise::Dns), network);
++            }
++        }
++    }
++}
Index: devel/codex/patches/patch-codex-rs_sandboxing_src_violation_rs
===================================================================
--- /dev/null
+++ devel/codex/patches/patch-codex-rs_sandboxing_src_violation_rs
@@ -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,
+     };

Attachment: openbsd-sandbox-regress.pl
Description: Perl program

Reply via email to