Commit Graph

23 Commits

  • [sandbox] Enforce protected workspace metadata paths (#19846)
    ## Summary
    
    Make FileSystemSandboxPolicy the semantic source of truth for project
    root metadata protection. Under writable roots, `.git`, `.codex`, and
    `.agents` stay protected unless user policy grants an explicit write
    rule for that metadata path.
    
    ## Scope
    
    1. Add `protected_metadata_names` to `WritableRoot`.
    2. Teach `FileSystemSandboxPolicy::can_write_path_with_cwd` to reject
    protected metadata writes under writable roots unless explicitly
    allowed.
    3. Default workspace write profiles to protect `.git`, `.codex`, and
    `.agents`.
    4. Add the Linux fallback setup needed before Linux enforcement lands
    later in the stack.
    
    ## Reviewer Focus
    
    1. The policy decision belongs in FileSystemSandboxPolicy, not shell
    command parsing.
    2. Legacy SandboxPolicy remains a compatibility projection, not the
    source of the new rule.
    3. Explicit user write rules can still opt into these metadata paths.
    
    ## Stack
    
    1. Policy primitive: this PR
    2. macOS Seatbelt adapter: #19847
    3. Shell preflight UX: #19848
    4. Runtime profile propagation: #19849
    5. Linux bubblewrap adapter: #19852
    
    ## Validation
    
    1. codex protocol permissions tests
    2. formatting for codex protocol and codex linux sandbox
    3. diff whitespace check
  • permissions: derive config defaults as profiles (#19772)
    ## Why
    
    This continues the permissions migration by making legacy config default
    resolution produce the canonical `PermissionProfile` first. The legacy
    `SandboxPolicy` projection should stay available at compatibility
    boundaries, but config loading should not create a legacy policy just to
    immediately convert it back into a profile.
    
    Specifically, when `default_permissions` is not specified in
    `config.toml`, instead of creating a `SandboxPolicy` in
    `codex-rs/core/src/config/mod.rs` and then trying to derive a
    `PermissionProfile` from it, we use `derive_permission_profile()` to
    create a more faithful `PermissionProfile` using the values of
    `ConfigToml` directly.
    
    This also keeps the existing behavior of `sandbox_workspace_write` and
    extra writable roots after #19841 replaced `:cwd` with `:project_roots`.
    Legacy workspace-write defaults are represented as symbolic
    `:project_roots` write access plus symbolic project-root metadata
    carveouts. Extra absolute writable roots are still added directly and
    continue to get concrete metadata protections for paths that exist under
    those roots.
    
    The platform sandboxes differ when a symbolic project-root subpath does
    not exist yet.
    
    * **Seatbelt** can encode literal/subpath exclusions directly, so macOS
    emits project-root metadata subpath policies even if `.git`, `.agents`,
    or `.codex` do not exist.
    * **bwrap** has to materialize bind-mount targets. Binding `/dev/null`
    to a missing `.git` can create a host-visible placeholder that changes
    Git repo discovery. Binding missing `.agents` would not affect Git
    discovery, but it would still create a host-visible project metadata
    placeholder from an automatic compatibility carveout. Linux therefore
    skips only missing automatic `.git` and `.agents` read-only metadata
    masks; missing `.codex` remains protected so first-time project config
    creation goes through the protected-path approval flow. User-authored
    `read` and `none` subpath rules keep normal bwrap behavior, and `none`
    can still mask the first missing component to prevent creation under
    writable roots.
    
    ## What Changed
    
    - Adds profile-native helpers for legacy workspace-write semantics,
    including `PermissionProfile::workspace_write_with()`,
    `FileSystemSandboxPolicy::workspace_write()`, and
    `FileSystemSandboxPolicy::with_additional_legacy_workspace_writable_roots()`.
    - Makes `FileSystemSandboxPolicy::workspace_write()` the single legacy
    workspace-write constructor so both `from_legacy_sandbox_policy()` and
    `From<&SandboxPolicy>` include the project-root metadata carveouts.
    - Removes the no-carveout `legacy_workspace_write_base_policy()` path
    and the `prune_read_entries_under_writable_roots()` cleanup that was
    only needed by that split construction.
    - Adds `ConfigToml::derive_permission_profile()` for legacy sandbox-mode
    fallback resolution; named `default_permissions` profiles continue
    through the permissions profile pipeline instead of being reconstructed
    from `sandbox_mode`.
    - Updates `Config::load()` to start from the derived profile, validate
    that it still has a legacy compatibility projection, and apply
    additional writable roots directly to managed workspace-write filesystem
    policies.
    - Updates Linux bwrap argument construction so missing automatic
    `.git`/`.agents` symbolic project-root read-only carveouts are skipped
    before emitting bind args; missing `.codex`, user-authored `read`/`none`
    subpath rules, and existing missing writable-root behavior are
    preserved.
    - Adds coverage that legacy workspace-write config produces symbolic
    project-root metadata carveouts, extra legacy workspace writable roots
    still protect existing metadata paths such as `.git`, and bwrap skips
    missing `.git`/`.agents` project-root carveouts while preserving missing
    `.codex` and user-authored missing subpath rules.
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19772).
    * #19776
    * #19775
    * #19774
    * #19773
    * __->__ #19772
  • permissions: remove cwd special path (#19841)
    ## Why
    
    The experimental `PermissionProfile` API had both `:cwd` and
    `:project_roots` special filesystem paths, which made the permission
    root ambiguous. This PR removes the unstable `current_working_directory`
    special path before the permissions API is stabilized, so callers use
    `:project_roots` for symbolic project-root access.
    
    ## What changed
    
    - Removes `FileSystemSpecialPath::CurrentWorkingDirectory` from protocol
    and app-server protocol models, plus regenerated app-server
    JSON/TypeScript schemas.
    - Replaces internal `:cwd` permission entries with `:project_roots`
    entries.
    - Keeps the existing cwd-update behavior for legacy-shaped
    workspace-write profiles, while removing the deleted
    `CurrentWorkingDirectory` case from that compatibility path.
    - Keeps `PermissionProfile::workspace_write()` as the reusable symbolic
    workspace-write helper, with docs noting that `:project_roots` entries
    resolve at enforcement time.
    - Updates app-server docs/examples and approval UI labeling to stop
    advertising `:cwd` as a permission token.
    
    ## Compatibility
    
    Persisted rollout items may contain the old
    `{"kind":"current_working_directory"}` tag from earlier experimental
    `permissionProfile` snapshots. This PR keeps that tag as a
    deserialize-only alias for `ProjectRoots { subpath: None }`, while
    continuing to serialize only the new `project_roots` tag.
    
    ## Follow-up
    
    This PR intentionally does not introduce an explicit project-root set on
    `SessionConfiguration` or runtime sandbox resolution. Today, the
    resolver still uses the active cwd as the single implicit project root.
    A follow-up should model project roots separately from tool cwd so
    `:project_roots` entries can resolve against the configured project
    roots, and resolve to no entries when there are no project roots.
    
    ## Verification
    
    - `cargo test -p codex-protocol permissions:: --lib`
    - `cargo test -p codex-app-server-protocol`
    - `cargo test -p codex-sandboxing -p codex-exec-server --lib`
    - `cargo test -p codex-core session_configuration_apply_ --lib`
    - `cargo test -p codex-app-server
    command_exec_permission_profile_project_roots_use_command_cwd --test
    all`
    - `cargo test -p codex-tui
    thread_read_session_state_does_not_reuse_primary_permission_profile
    --lib`
    - `cargo test -p codex-tui
    preset_matching_accepts_workspace_write_with_extra_roots --lib`
    - `cargo test -p codex-config --lib`
  • permissions: derive compatibility policies from profiles (#19392)
    ## Why
    
    After #19391, `PermissionProfile` and the split filesystem/network
    policies could still be stored in parallel. That creates drift risk: a
    profile can preserve deny globs, external enforcement, or split
    filesystem entries while a cached projection silently loses those
    details. This PR makes the profile the runtime source and derives
    compatibility views from it.
    
    ## What Changed
    
    - Removes stored filesystem/network sandbox projections from
    `Permissions` and `SessionConfiguration`; their accessors now derive
    from the canonical `PermissionProfile`.
    - Derives legacy `SandboxPolicy` snapshots from profiles only where an
    older API still needs that field.
    - Updates MCP connection and elicitation state to track
    `PermissionProfile` instead of `SandboxPolicy` for auto-approval
    decisions.
    - Adds semantic filesystem-policy comparison so cwd changes can preserve
    richer profiles while still recognizing equivalent legacy projections
    independent of entry ordering.
    - Updates config/session tests to assert profile-derived projections
    instead of parallel stored fields.
    
    ## Verification
    
    - `cargo test -p codex-core direct_write_roots`
    - `cargo test -p codex-core runtime_roots_to_legacy_projection`
    - `cargo test -p codex-app-server
    requested_permissions_trust_project_uses_permission_profile_intent`
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19392).
    * #19395
    * #19394
    * #19393
    * __->__ #19392
  • permissions: remove legacy read-only access modes (#19449)
    ## Why
    
    `ReadOnlyAccess` was a transitional legacy shape on `SandboxPolicy`:
    `FullAccess` meant the historical read-only/workspace-write modes could
    read the full filesystem, while `Restricted` tried to carry partial
    readable roots. The partial-read model now belongs in
    `FileSystemSandboxPolicy` and `PermissionProfile`, so keeping it on
    `SandboxPolicy` makes every legacy projection reintroduce lossy
    read-root bookkeeping and creates unnecessary noise in the rest of the
    permissions migration.
    
    This PR makes the legacy policy model narrower and explicit:
    `SandboxPolicy::ReadOnly` and `SandboxPolicy::WorkspaceWrite` represent
    the old full-read sandbox modes only. Split readable roots, deny-read
    globs, and platform-default/minimal read behavior stay in the runtime
    permissions model.
    
    ## What changed
    
    - Removes `ReadOnlyAccess` from
    `codex_protocol::protocol::SandboxPolicy`, including the generated
    `access` and `readOnlyAccess` API fields.
    - Updates legacy policy/profile conversions so restricted filesystem
    reads are represented only by `FileSystemSandboxPolicy` /
    `PermissionProfile` entries.
    - Keeps app-server v2 compatible with legacy `fullAccess` read-access
    payloads by accepting and ignoring that no-op shape, while rejecting
    legacy `restricted` read-access payloads instead of silently widening
    them to full-read legacy policies.
    - Carries Windows sandbox platform-default read behavior with an
    explicit override flag instead of depending on
    `ReadOnlyAccess::Restricted`.
    - Refreshes generated app-server schema/types and updates tests/docs for
    the simplified legacy policy shape.
    
    ## Verification
    
    - `cargo check -p codex-app-server-protocol --tests`
    - `cargo check -p codex-windows-sandbox --tests`
    - `cargo test -p codex-app-server-protocol sandbox_policy_`
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19449).
    * #19395
    * #19394
    * #19393
    * #19392
    * #19391
    * __->__ #19449
  • permissions: make legacy profile conversion cwd-free (#19414)
    ## Why
    
    The profile conversion path still required a `cwd` even when it was only
    translating a legacy `SandboxPolicy` into a `PermissionProfile`. That
    made profile producers invent an ambient `cwd`, which is exactly the
    anchoring we are trying to remove from permission-profile data. A legacy
    workspace-write policy can be represented symbolically instead: `:cwd =
    write` plus read-only `:project_roots` metadata subpaths.
    
    This PR creates that cwd-free base so the rest of the stack can stop
    threading cwd through profile construction. Callers that actually need a
    concrete runtime filesystem policy for a specific cwd still have an
    explicitly named cwd-bound conversion.
    
    ## What Changed
    
    - `PermissionProfile::from_legacy_sandbox_policy` now takes only
    `&SandboxPolicy`.
    - `FileSystemSandboxPolicy::from_legacy_sandbox_policy` is now the
    symbolic, cwd-free projection for profiles.
    - The old concrete projection is retained as
    `FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd` for
    runtime/boundary code that must materialize legacy cwd behavior.
    - Workspace-write profiles preserve `CurrentWorkingDirectory` and
    `ProjectRoots` special entries instead of materializing cwd into
    absolute paths.
    
    ## Verification
    
    - `cargo check -p codex-protocol -p codex-core -p
    codex-app-server-protocol -p codex-app-server -p codex-exec -p
    codex-exec-server -p codex-tui -p codex-sandboxing -p
    codex-linux-sandbox -p codex-analytics --tests`
    - `just fix -p codex-protocol -p codex-core -p codex-app-server-protocol
    -p codex-app-server -p codex-exec -p codex-exec-server -p codex-tui -p
    codex-sandboxing -p codex-linux-sandbox -p codex-analytics`
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19414).
    * #19395
    * #19394
    * #19393
    * #19392
    * #19391
    * __->__ #19414
  • permissions: make profiles represent enforcement (#19231)
    ## Why
    
    `PermissionProfile` is becoming the canonical permissions abstraction,
    but the old shape only carried optional filesystem and network fields.
    It could describe allowed access, but not who is responsible for
    enforcing it. That made `DangerFullAccess` and `ExternalSandbox` lossy
    when profiles were exported, cached, or round-tripped through app-server
    APIs.
    
    The important model change is that active permissions are now a disjoint
    union over the enforcement mode. Conceptually:
    
    ```rust
    pub enum PermissionProfile {
        Managed {
            file_system: FileSystemSandboxPolicy,
            network: NetworkSandboxPolicy,
        },
        Disabled,
        External {
            network: NetworkSandboxPolicy,
        },
    }
    ```
    
    This distinction matters because `Disabled` means Codex should apply no
    outer sandbox at all, while `External` means filesystem isolation is
    owned by an outside caller. Those are not equivalent to a broad managed
    sandbox. For example, macOS cannot nest Seatbelt inside Seatbelt, so an
    inner sandbox may require the outer Codex layer to use no sandbox rather
    than a permissive one.
    
    ## How Existing Modeling Maps
    
    Legacy `SandboxPolicy` remains a boundary projection, but it now maps
    into the higher-fidelity profile model:
    
    - `ReadOnly` and `WorkspaceWrite` map to `PermissionProfile::Managed`
    with restricted filesystem entries plus the corresponding network
    policy.
    - `DangerFullAccess` maps to `PermissionProfile::Disabled`, preserving
    the “no outer sandbox” intent instead of treating it as a lax managed
    sandbox.
    - `ExternalSandbox { network_access }` maps to
    `PermissionProfile::External { network }`, preserving external
    filesystem enforcement while still carrying the active network policy.
    - Split runtime policies that legacy `SandboxPolicy` cannot faithfully
    express, such as managed unrestricted filesystem plus restricted
    network, stay `Managed` instead of being collapsed into
    `ExternalSandbox`.
    - Per-command/session/turn grants remain partial overlays via
    `AdditionalPermissionProfile`; full `PermissionProfile` is reserved for
    complete active runtime permissions.
    
    ## What Changed
    
    - Change active `PermissionProfile` into a tagged union: `managed`,
    `disabled`, and `external`.
    - Keep partial permission grants separate with
    `AdditionalPermissionProfile` for command/session/turn overlays.
    - Represent managed filesystem permissions as either `restricted`
    entries or `unrestricted`; `glob_scan_max_depth` is non-zero when
    present.
    - Preserve old rollout compatibility by accepting the pre-tagged `{
    network, file_system }` profile shape during deserialization.
    - Preserve fidelity for important edge cases: `DangerFullAccess`
    round-trips as `disabled`, `ExternalSandbox` round-trips as `external`,
    and managed unrestricted filesystem + restricted network stays managed
    instead of being mistaken for external enforcement.
    - Preserve configured deny-read entries and bounded glob scan depth when
    full profiles are projected back into runtime policies, including
    unrestricted replacements that now become `:root = write` plus deny
    entries.
    - Regenerate the experimental app-server v2 JSON/TypeScript schema and
    update the `command/exec` README example for the tagged
    `permissionProfile` shape.
    
    ## Compatibility
    
    Legacy `SandboxPolicy` remains available at config/API boundaries as the
    compatibility projection. Existing rollout lines with the old
    `PermissionProfile` shape continue to load. The app-server
    `permissionProfile` field is experimental, so its v2 wire shape is
    intentionally updated to match the higher-fidelity model.
    
    ## Verification
    
    - `just write-app-server-schema`
    - `cargo check --tests`
    - `cargo test -p codex-protocol permission_profile`
    - `cargo test -p codex-protocol
    preserving_deny_entries_keeps_unrestricted_policy_enforceable`
    - `cargo test -p codex-app-server-protocol
    permission_profile_file_system_permissions`
    - `cargo test -p codex-app-server-protocol serialize_client_response`
    - `cargo test -p codex-core
    session_configured_reports_permission_profile_for_external_sandbox`
    - `just fix`
    - `just fix -p codex-protocol`
    - `just fix -p codex-app-server-protocol`
    - `just fix -p codex-core`
    - `just fix -p codex-app-server`
  • protocol: canonicalize file system permissions (#18274)
    ## Why
    
    `PermissionProfile` needs stable, canonical file-system semantics before
    it can become the primary runtime permissions abstraction. Without a
    canonical form, callers have to keep re-deriving legacy sandbox maps and
    profile comparisons remain lossy or order-dependent.
    
    ## What changed
    
    This adds canonicalization helpers for `FileSystemPermissions` and
    `PermissionProfile`, expands special paths into explicit sandbox
    entries, and updates permission request/conversion paths to consume
    those canonical entries. It also tightens the legacy bridge so root-wide
    write profiles with narrower carveouts are not silently projected as
    full-disk legacy access.
    
    ## Verification
    
    - `cargo test -p codex-protocol
    root_write_with_read_only_child_is_not_full_disk_write -- --nocapture`
    - `cargo test -p codex-sandboxing permission -- --nocapture`
    - `cargo test -p codex-tui permissions -- --nocapture`
  • feat(sandbox): add glob deny-read platform enforcement (#18096)
    ## Summary
    - adds macOS Seatbelt deny rules for unreadable glob patterns
    - expands unreadable glob matches on Linux and masks them in bwrap,
    including canonical symlink targets
    - keeps Linux glob expansion robust when `rg` is unavailable in minimal
    or Bazel test environments
    - adds sandbox integration coverage that runs `shell` and `exec_command`
    with a `**/*.env = none` policy and verifies the secret contents do not
    reach the model
    
    ## Linux glob expansion
    
    ```text
    Prefer:   rg --files --hidden --no-ignore --glob <pattern> -- <search-root>
    Fallback: internal globset walker when rg is not installed
    Failure:  any other rg failure aborts sandbox construction
    ```
    
    ```
    [permissions.workspace.filesystem]
    glob_scan_max_depth = 2
    
    [permissions.workspace.filesystem.":project_roots"]
    "**/*.env" = "none"
    ```
    
    
    This keeps the common path fast without making sandbox construction
    depend on an ambient `rg` binary. If `rg` is present but fails for
    another reason, the sandbox setup fails closed instead of silently
    omitting deny-read masks.
    
    ## Platform support
    - macOS: subprocess sandbox enforcement is handled by Seatbelt regex
    deny rules
    - Linux: subprocess sandbox enforcement is handled by expanding existing
    glob matches and masking them in bwrap
    - Windows: policy/config/direct-tool glob support is already on `main`
    from #15979; Windows subprocess sandbox paths continue to fail closed
    when unreadable split filesystem carveouts require runtime enforcement,
    rather than silently running unsandboxed
    
    ## Stack
    1. #15979 - merged: cross-platform glob deny-read
    policy/config/direct-tool support for macOS, Linux, and Windows
    2. This PR - macOS/Linux subprocess sandbox enforcement plus Windows
    fail-closed clarification
    3. #17740 - managed deny-read requirements
    
    ## Verification
    - Added integration coverage for `shell` and `exec_command` glob
    deny-read enforcement
    - `cargo check -p codex-sandboxing -p codex-linux-sandbox --tests`
    - `cargo check -p codex-core --test all`
    - `cargo clippy -p codex-linux-sandbox -p codex-sandboxing --tests`
    - `just bazel-lock-check`
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • feat(permissions): add glob deny-read policy support (#15979)
    ## Summary
    - adds first-class filesystem policy entries for deny-read glob patterns
    - parses config such as :project_roots { "**/*.env" = "none" } into
    pattern entries
    - enforces deny-read patterns in direct read/list helpers
    - fails closed for sandbox execution until platform backends enforce
    glob patterns in #18096
    - preserves split filesystem policy in turn context only when it cannot
    be reconstructed from legacy sandbox policy
    
    ## Stack
    1. This PR - glob deny-read policy/config/direct-tool support
    2. #18096 - macOS and Linux sandbox enforcement
    3. #17740 - managed deny-read requirements
    
    ## Verification
    - just fmt
    - cargo check -p codex-core -p codex-sandboxing --tests
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • fix(permissions): fix symlinked writable roots in sandbox permissions (#15981)
    ## Summary
    - preserve logical symlink paths during permission normalization and
    config cwd handling
    - bind real targets for symlinked readable/writable roots in bwrap and
    remap carveouts and unreadable roots there
    - add regressions for symlinked carveouts and nested symlink escape
    masking
    
    ## Root cause
    Permission normalization canonicalized symlinked writable roots and cwd
    to their real targets too early. That drifted policy checks away from
    the logical paths the sandboxed process can actually address, while
    bwrap still needed the real targets for mounts. The mismatch caused
    shell and apply_patch failures on symlinked writable roots.
    
    ## Impact
    Fixes #15781.
    
    Also fixes #17079:
    - #17079 is the protected symlinked carveout side: bwrap now binds the
    real symlinked writable-root target and remaps carveouts before masking.
    
    Related to #15157:
    - #15157 is the broader permission-check side of this path-identity
    problem. This PR addresses the shared logical-vs-canonical normalization
    issue, but the reported Darwin prompt behavior should be validated
    separately before auto-closing it.
    
    This should also fix #14672, #14694, #14715, and #15725:
    - #14672, #14694, and #14715 are the same Linux
    symlinked-writable-root/bwrap family as #15781.
    - #15725 is the protected symlinked workspace path variant; the PR
    preserves the protected logical path in policy space while bwrap applies
    read-only or unreadable treatment to the resolved target so
    file-vs-directory bind mismatches do not abort sandbox setup.
    
    ## Notes
    - Added Linux-only regressions for symlinked writable ancestors and
    protected symlinked directory targets, including nested symlink escape
    masking without rebinding the escape target writable.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • [codex] Make AbsolutePathBuf joins infallible (#16981)
    Having to check for errors every time join is called is painful and
    unnecessary.
  • chore: clean up argument-comment lint and roll out all-target CI on macOS (#16054)
    ## Why
    
    `argument-comment-lint` was green in CI even though the repo still had
    many uncommented literal arguments. The main gap was target coverage:
    the repo wrapper did not force Cargo to inspect test-only call sites, so
    examples like the `latest_session_lookup_params(true, ...)` tests in
    `codex-rs/tui_app_server/src/lib.rs` never entered the blocking CI path.
    
    This change cleans up the existing backlog, makes the default repo lint
    path cover all Cargo targets, and starts rolling that stricter CI
    enforcement out on the platform where it is currently validated.
    
    ## What changed
    
    - mechanically fixed existing `argument-comment-lint` violations across
    the `codex-rs` workspace, including tests, examples, and benches
    - updated `tools/argument-comment-lint/run-prebuilt-linter.sh` and
    `tools/argument-comment-lint/run.sh` so non-`--fix` runs default to
    `--all-targets` unless the caller explicitly narrows the target set
    - fixed both wrappers so forwarded cargo arguments after `--` are
    preserved with a single separator
    - documented the new default behavior in
    `tools/argument-comment-lint/README.md`
    - updated `rust-ci` so the macOS lint lane keeps the plain wrapper
    invocation and therefore enforces `--all-targets`, while Linux and
    Windows temporarily pass `-- --lib --bins`
    
    That temporary CI split keeps the stricter all-targets check where it is
    already cleaned up, while leaving room to finish the remaining Linux-
    and Windows-specific target-gated cleanup before enabling
    `--all-targets` on those runners. The Linux and Windows failures on the
    intermediate revision were caused by the wrapper forwarding bug, not by
    additional lint findings in those lanes.
    
    ## Validation
    
    - `bash -n tools/argument-comment-lint/run.sh`
    - `bash -n tools/argument-comment-lint/run-prebuilt-linter.sh`
    - shell-level wrapper forwarding check for `-- --lib --bins`
    - shell-level wrapper forwarding check for `-- --tests`
    - `just argument-comment-lint`
    - `cargo test` in `tools/argument-comment-lint`
    - `cargo test -p codex-terminal-detection`
    
    ## Follow-up
    
    - Clean up remaining Linux-only target-gated callsites, then switch the
    Linux lint lane back to the plain wrapper invocation.
    - Clean up remaining Windows-only target-gated callsites, then switch
    the Windows lint lane back to the plain wrapper invocation.
  • Protect first-time project .codex creation across Linux and macOS sandboxes (#15067)
    ## Problem
    
    Codex already treated an existing top-level project `./.codex` directory
    as protected, but there was a gap on first creation.
    
    If `./.codex` did not exist yet, a turn could create files under it,
    such as `./.codex/config.toml`, without going through the same approval
    path as later modifications. That meant the initial write could bypass
    the intended protection for project-local Codex state.
    
    ## What this changes
    
    This PR closes that first-creation gap in the Unix enforcement layers:
    
    - `codex-protocol`
    - treat the top-level project `./.codex` path as a protected carveout
    even when it does not exist yet
    - avoid injecting the default carveout when the user already has an
    explicit rule for that exact path
    - macOS Seatbelt
    - deny writes to both the exact protected path and anything beneath it,
    so creating `./.codex` itself is blocked in addition to writes inside it
    - Linux bubblewrap
    - preserve the same protected-path behavior for first-time creation
    under `./.codex`
    - tests
    - add protocol regressions for missing `./.codex` and explicit-rule
    collisions
    - add Unix sandbox coverage for blocking first-time `./.codex` creation
      - tighten Seatbelt policy assertions around excluded subpaths
    
    ## Scope
    
    This change is intentionally scoped to protecting the top-level project
    `.codex` subtree from agent writes.
    
    It does not make `.codex` unreadable, and it does not change the product
    behavior around loading project skills from `.codex` when project config
    is untrusted.
    
    ## Why this shape
    
    The fix is pointed rather than broad:
    - it preserves the current model of “project `.codex` is protected from
    writes”
    - it closes the security-relevant first-write hole
    - it avoids folding a larger permissions-model redesign into this PR
    
    ## Validation
    
    - `cargo test -p codex-protocol`
    - `cargo test -p codex-sandboxing seatbelt`
    - `cargo test -p codex-exec --test all
    sandbox_blocks_first_time_dot_codex_creation -- --nocapture`
    
    ---------
    
    Co-authored-by: Michael Bolin <mbolin@openai.com>
  • fix: allow restricted filesystem profiles to read helper executables (#15114)
    ## Summary
    
    This PR fixes restricted filesystem permission profiles so Codex's
    runtime-managed helper executables remain readable without requiring
    explicit user configuration.
    
    - add implicit readable roots for the configured `zsh` helper path and
    the main execve wrapper
    - allowlist the shared `$CODEX_HOME/tmp/arg0` root when the execve
    wrapper lives there, so session-specific helper paths keep working
    - dedupe injected paths and avoid adding duplicate read entries to the
    sandbox policy
    - add regression coverage for restricted read mode with helper
    executable overrides
    
    ## Testing 
    before this change: got this error when executing a shell command via
    zsh fork:
    ```
    "sandbox error: sandbox denied exec error, exit code: 127, stdout: , stderr: /etc/zprofile:11: operation not permitted: /usr/libexec/path_helper\nzsh:1: operation not permitted: .codex/skills/proxy-a/scripts/fetch_example.sh\n"
    ```
    
    saw this change went away after this change, meaning the readable roots
    and injected correctly.
  • fix: canonicalize symlinked Linux sandbox cwd (#14849)
    ## Problem
    On Linux, Codex can be launched from a workspace path that is a symlink
    (for example, a symlinked checkout or a symlinked parent directory).
    
    Our sandbox policy intentionally canonicalizes writable/readable roots
    to the real filesystem path before building the bubblewrap mounts. That
    part is correct and needed for safety.
    
    The remaining bug was that bubblewrap could still inherit the helper
    process's logical cwd, which might be the symlinked alias instead of the
    mounted canonical path. In that case, the sandbox starts in a cwd that
    does not exist inside the sandbox namespace even though the real
    workspace is mounted. This can cause sandboxed commands to fail in
    symlinked workspaces.
    
    ## Fix
    This PR keeps the sandbox policy behavior the same, but separates two
    concepts that were previously conflated:
    
    - the canonical cwd used to define sandbox mounts and permissions
    - the caller's logical cwd used when launching the command
    
    On the Linux bubblewrap path, we now thread the logical command cwd
    through the helper explicitly and only add `--chdir <canonical path>`
    when the logical cwd differs from the mounted canonical path.
    
    That means:
    - permissions are still computed from canonical paths
    - bubblewrap starts the command from a cwd that definitely exists inside
    the sandbox
    - we do not widen filesystem access or undo the earlier symlink
    hardening
    
    ## Why This Is Safe
    This is a narrow Linux-only launch fix, not a policy change.
    
    - Writable/readable root canonicalization stays intact.
    - Protected metadata carveouts still operate on canonical roots.
    - We only override bubblewrap's inherited cwd when the logical path
    would otherwise point at a symlink alias that is not mounted in the
    sandbox.
    
    ## Tests
    - kept the existing protocol/core regression coverage for symlink
    canonicalization
    - added regression coverage for symlinked cwd handling in the Linux
    bubblewrap builder/helper path
    
    Local validation:
    - `just fmt`
    - `cargo test -p codex-protocol`
    - `cargo test -p codex-core
    normalize_additional_permissions_canonicalizes_symlinked_write_paths`
    - `cargo clippy -p codex-linux-sandbox -p codex-protocol -p codex-core
    --tests -- -D warnings`
    - `cargo build --bin codex`
    
    ## Context
    This is related to #14694. The earlier writable-root symlink fix
    addressed the mount/permission side; this PR fixes the remaining
    symlinked-cwd launch mismatch in the Linux sandbox path.
  • fix: fix symlinked writable roots in sandbox policies (#14674)
    ## Summary
    - normalize effective readable, writable, and unreadable sandbox roots
    after resolving special paths so symlinked roots use canonical runtime
    paths
    - add a protocol regression test for a symlinked writable root with a
    denied child and update protocol expectations to canonicalized effective
    paths
    - update macOS seatbelt tests to assert against effective normalized
    roots produced by the shared policy helpers
    
    ## Testing
    - just fmt
    - cargo test -p codex-protocol
    - cargo test -p codex-core explicit_unreadable_paths_are_excluded_
    - cargo clippy -p codex-protocol -p codex-core --tests -- -D warnings
    
    ## Notes
    - This is intended to fix the symlinked TMPDIR bind failure in
    bubblewrap described in #14672.
    Fixes #14672
  • refactor: centralize filesystem permissions precedence (#14174)
    ## Stack
    
       fix: fail closed for unsupported split windows sandboxing #14172
       fix: preserve split filesystem semantics in linux sandbox #14173
       fix: align core approvals with split sandbox policies #14171
    -> refactor: centralize filesystem permissions precedence #14174
    
    ## Summary
    - add a shared per-path split filesystem precedence helper in
    `FileSystemSandboxPolicy`
    - derive readable, writable, and unreadable roots from the same
    most-specific resolution rules
    - add regression coverage for nested `write` / `read` / `none` carveouts
    and legacy bridge enforcement detection
    
    ## Testing
    - cargo test -p codex-protocol
    - cargo clippy -p codex-protocol --tests -- -D warnings
  • fix: keep permissions profiles forward compatible (#14107)
    ## Summary
    - preserve unknown `:special_path` tokens, including nested entries, so
    older Codex builds warn and ignore instead of failing config load
    - fail closed with a startup warning when a permissions profile has
    missing or empty filesystem entries instead of aborting profile
    compilation
    - normalize Windows verbatim paths like `\?\C:\...` before absolute-path
    validation while keeping explicit errors for truly invalid paths
    
    ## Testing
    - just fmt
    - cargo test -p codex-core permissions_profiles_allow
    - cargo test -p codex-core
    normalize_absolute_path_for_platform_simplifies_windows_verbatim_paths
    - cargo test -p codex-protocol
    unknown_special_paths_are_ignored_by_legacy_bridge
    - cargo clippy -p codex-core -p codex-protocol --all-targets -- -D
    warnings
    - cargo clean
  • fix(protocol): preserve legacy workspace-write semantics (#13957)
    ## Summary
    This is a fast follow to the initial `[permissions]` structure.
    
    - keep the new split-policy carveout behavior for narrower non-write
    entries under broader writable roots
    - preserve legacy `WorkspaceWrite` semantics by using a cwd-aware bridge
    that drops only redundant nested readable roots when projecting from
    `SandboxPolicy`
    - route the legacy macOS seatbelt adapter through that same legacy
    bridge so redundant nested readable roots do not become read-only
    carveouts on macOS
    - derive the legacy bridge for `command_exec` using the sandbox root cwd
    rather than the request cwd so policy derivation matches later sandbox
    enforcement
    - add regression coverage for the legacy macOS nested-readable-root case
    
    ## Examples
    ### Legacy `workspace-write` on macOS
    A legacy `workspace-write` policy can redundantly list a nested readable
    root under an already-writable workspace root.
    
    For example, legacy config can effectively mean:
    - workspace root (`.` / `cwd`) is writable
    - `docs/` is also listed in `readable_roots`
    
    The new shared split-policy helper intentionally treats a narrower
    non-write entry under a broader writable root as a carveout for real
    `[permissions]` configs. Without this fast follow, the unchanged macOS
    seatbelt legacy adapter could project that legacy shape into a
    `FileSystemSandboxPolicy` that treated `docs/` like a read-only carveout
    under the writable workspace root. In practice, legacy callers on macOS
    could unexpectedly lose write access inside `docs/`, even though that
    path was writable before the `[permissions]` migration work.
    
    This change fixes that by routing the legacy seatbelt path through the
    cwd-aware legacy bridge, so:
    - legacy `workspace-write` keeps `docs/` writable when `docs/` was only
    a redundant readable root
    - explicit `[permissions]` entries like `'.' = 'write'` and `'docs' =
    'read'` still make `docs/` read-only, which is the new intended
    split-policy behavior
    
    ### Legacy `command_exec` with a subdirectory cwd
    `command_exec` can run a command from a request cwd that is narrower
    than the sandbox root cwd.
    
    For example:
    - sandbox root cwd is `/repo`
    - request cwd is `/repo/subdir`
    - legacy policy is still `workspace-write` rooted at `/repo`
    
    Before this fast follow, `command_exec` derived the legacy bridge using
    the request cwd, but the sandbox was later built using the sandbox root
    cwd. That mismatch could miss redundant legacy readable roots during
    projection and accidentally reintroduce read-only carveouts for paths
    that should still be writable under the legacy model.
    
    This change fixes that by deriving the legacy bridge with the same
    sandbox root cwd that sandbox enforcement later uses.
    
    ## Verification
    - `just fmt`
    - `cargo test -p codex-core
    seatbelt_legacy_workspace_write_nested_readable_root_stays_writable`
    - `cargo test -p codex-core test_sandbox_config_parsing`
    - `cargo clippy -p codex-core -p codex-app-server --all-targets -- -D
    warnings`
    - `cargo clean`
  • protocol: keep root carveouts sandboxed (#13452)
    ## Why
    
    A restricted filesystem policy that grants `:root` read or write access
    but also carries explicit deny entries should still behave like scoped
    access with carveouts, not like unrestricted disk access.
    
    Without that distinction, later platform backends cannot preserve
    blocked subpaths under root-level permissions because the protocol layer
    reports the policy as fully unrestricted.
    
    ## What changed
    
    - taught `FileSystemSandboxPolicy` to treat root access plus explicit
    deny entries as scoped access rather than full-disk access
    - derived readable and writable roots from the filesystem root when root
    access is combined with carveouts, while preserving the denied paths as
    read-only subpaths
    - added protocol coverage for root-write policies with carveouts and a
    core sandboxing regression so those policies still require platform
    sandboxing
    
    ## Verification
    
    - added protocol coverage in `protocol/src/permissions.rs` and
    `protocol/src/protocol.rs` for root access with explicit carveouts
    - added platform-sandbox regression coverage in
    `core/src/sandboxing/mod.rs`
    - verified the current PR state with `just clippy`
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/13452).
    * #13453
    * __->__ #13452
    * #13451
    * #13449
    * #13448
    * #13445
    * #13440
    * #13439
    
    ---------
    
    Co-authored-by: viyatb-oai <viyatb@openai.com>
  • protocol: derive effective file access from filesystem policies (#13440)
    ## Why
    
    `#13434` and `#13439` introduce split filesystem and network policies,
    but the only code that could answer basic filesystem questions like "is
    access effectively unrestricted?" or "which roots are readable and
    writable for this cwd?" still lived on the legacy `SandboxPolicy` path.
    
    That would force later backends to either keep projecting through
    `SandboxPolicy` or duplicate path-resolution logic. This PR moves those
    queries onto `FileSystemSandboxPolicy` itself so later runtime and
    platform changes can consume the split policy directly.
    
    ## What changed
    
    - added `FileSystemSandboxPolicy` helpers for full-read/full-write
    checks, platform-default reads, readable roots, writable roots, and
    explicit unreadable roots resolved against a cwd
    - added a shared helper for the default read-only carveouts under
    writable roots so the legacy and split-policy paths stay aligned
    - added protocol coverage for full-access detection and derived
    readable, writable, and unreadable roots
    
    ## Verification
    
    - added protocol coverage in `protocol/src/protocol.rs` and
    `protocol/src/permissions.rs` for full-root access and derived
    filesystem roots
    - verified the current PR state with `just clippy`
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/13440).
    * #13453
    * #13452
    * #13451
    * #13449
    * #13448
    * #13445
    * __->__ #13440
    * #13439
    
    ---------
    
    Co-authored-by: viyatb-oai <viyatb@openai.com>
  • config: add initial support for the new permission profile config language in config.toml (#13434)
    ## Why
    
    `SandboxPolicy` currently mixes together three separate concerns:
    
    - parsing layered config from `config.toml`
    - representing filesystem sandbox state
    - carrying basic network policy alongside filesystem choices
    
    That makes the existing config awkward to extend and blocks the new TOML
    proposal where `[permissions]` becomes a table of named permission
    profiles selected by `default_permissions`. (The idea is that if
    `default_permissions` is not specified, we assume the user is opting
    into the "traditional" way to configure the sandbox.)
    
    This PR adds the config-side plumbing for those profiles while still
    projecting back to the legacy `SandboxPolicy` shape that the current
    macOS and Linux sandbox backends consume.
    
    It also tightens the filesystem profile model so scoped entries only
    exist for `:project_roots`, and so nested keys must stay within a
    project root instead of using `.` or `..` traversal.
    
    This drops support for the short-lived `[permissions.network]` in
    `config.toml` because now that would be interpreted as a profile named
    `network` within `[permissions]`.
    
    ## What Changed
    
    - added `PermissionsToml`, `PermissionProfileToml`,
    `FilesystemPermissionsToml`, and `FilesystemPermissionToml` so config
    can parse named profiles under `[permissions.<profile>.filesystem]`
    - added top-level `default_permissions` selection, validation for
    missing or unknown profiles, and compilation from a named profile into
    split `FileSystemSandboxPolicy` and `NetworkSandboxPolicy` values
    - taught config loading to choose between the legacy `sandbox_mode` path
    and the profile-based path without breaking legacy users
    - introduced `codex-protocol::permissions` for the split filesystem and
    network sandbox types, and stored those alongside the legacy projected
    `sandbox_policy` in runtime `Permissions`
    - modeled `FileSystemSpecialPath` so only `ProjectRoots` can carry a
    nested `subpath`, matching the intended config syntax instead of
    allowing invalid states for other special paths
    - restricted scoped filesystem maps to `:project_roots`, with validation
    that nested entries are non-empty descendant paths and cannot use `.` or
    `..` to escape the project root
    - kept existing runtime consumers working by projecting
    `FileSystemSandboxPolicy` back into `SandboxPolicy`, with an explicit
    error for profiles that request writes outside the workspace root
    - loaded proxy settings from top-level `[network]`
    - regenerated `core/config.schema.json`
    
    ## Verification
    
    - added config coverage for profile deserialization,
    `default_permissions` selection, top-level `[network]` loading, network
    enablement, rejection of writes outside the workspace root, rejection of
    nested entries for non-`:project_roots` special paths, and rejection of
    parent-directory traversal in `:project_roots` maps
    - added protocol coverage for the legacy bridge rejecting non-workspace
    writes
    
    ## Docs
    
    - update the Codex config docs on developers.openai.com/codex to
    document named `[permissions.<profile>]` entries, `default_permissions`,
    scoped `:project_roots` syntax, the descendant-path restriction for
    nested `:project_roots` entries, and top-level `[network]` proxy
    configuration
    
    
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/13434).
    * #13453
    * #13452
    * #13451
    * #13449
    * #13448
    * #13445
    * #13440
    * #13439
    * __->__ #13434