Commit Graph

6 Commits

  • core: expose permission profile to shell tools (#29941)
    ## tl;dr
    
    Inject a `CODEX_PERMISSION_PROFILE` environment variable with the name
    of the current permission profile when invoking a shell tool.
    
    ## Why
    
    Shell tool owners may need to launch nested commands under the same
    named permission profile, including through `codex sandbox -P PROFILE
    --include-managed-config`. Until now, child processes could observe
    sandbox and network metadata but could not identify the active named
    permission profile.
    
    The `--include-managed-config` flag is essential when a helper
    reconstructs the sandbox from a profile name: it ensures the nested
    sandbox also loads managed enterprise requirements. Without it, using
    the inherited profile could unintentionally create a sandbox that does
    not enforce the organization's managed restrictions.
    
    The new environment value is intentionally informational and **must not
    be treated as trusted input**. Any process in the ancestry can overwrite
    an environment variable, so a consumer that passes this value to `codex
    sandbox -P` must first validate it against the profiles that helper is
    authorized to use.
    
    ## Example Use Case
    
    Suppose an organization provides a trusted `remote-bash` wrapper that
    lets Codex run a command on an approved build host. The local shell
    command uses the named `:workspace` permission profile:
    
    ```toml
    default_permissions = ":workspace"
    ```
    
    The command exposed to the model is a small zsh wrapper. It deliberately
    delegates with `exec`, preserving the original arguments and process
    environment:
    
    ```zsh
    #!/usr/bin/env zsh
    exec /opt/codex-tools/remote_bash.py "$@"
    ```
    
    The model invokes the public wrapper, not its Python implementation:
    
    ```sh
    /opt/codex-tools/remote-bash \
      --host builder.example.com \
      -- printf '%s' 'hello world'
    ```
    
    Only the inner implementation is authorized to escape the local sandbox:
    
    ```starlark
    prefix_rule(
        pattern=["/opt/codex-tools/remote_bash.py"],
        decision="allow",
    )
    ```
    
    With zsh-fork, execution begins with `remote-bash` inside the
    `:workspace` sandbox. When the wrapper calls `exec`, the exact prefix
    rule matches `remote_bash.py`, so that inner script is restarted
    unsandboxed. The escalated process inherits:
    
    ```text
    CODEX_PERMISSION_PROFILE=:workspace
    ```
    
    Inheritance does not make the value trustworthy. `remote_bash.py`
    independently allowlists both the remote host and the permission profile
    before using either value. In particular, a forged value such as
    `:danger-full-access` is rejected before it can reach `codex sandbox
    -P`:
    
    ```python
    import argparse
    import os
    import shlex
    import sys
    
    ALLOWED_HOSTS = {"builder.example.com"}
    ALLOWED_PROFILES = {":workspace"}
    
    parser = argparse.ArgumentParser()
    parser.add_argument("--host", required=True)
    separator = sys.argv.index("--")
    args = parser.parse_args(sys.argv[1:separator])
    command = sys.argv[separator + 1:]
    
    if args.host not in ALLOWED_HOSTS:
        parser.error("host is not allowlisted")
    if not command:
        parser.error("the remote command must not be empty")
    
    profile = os.environ.get("CODEX_PERMISSION_PROFILE")
    if not profile:
        raise SystemExit("CODEX_PERMISSION_PROFILE must not be empty")
    if profile not in ALLOWED_PROFILES:
        raise SystemExit("CODEX_PERMISSION_PROFILE is not allowlisted")
    
    remote_command = shlex.join(command)
    sandbox_command = shlex.join([
        "codex", "sandbox", "-P", profile,
        "--include-managed-config", "--",
        "bash", "-lc", remote_command,
    ])
    print(shlex.join(["ssh", args.host, sandbox_command]))
    ```
    
    This builds each command layer as an argument vector and uses
    `shlex.join()` at the boundary, rather than interpolating untrusted
    shell text. After validation and parsing, the nested command has this
    structure:
    
    ```text
    ssh argv:
      ["ssh", "builder.example.com", SANDBOX_COMMAND]
    
    SANDBOX_COMMAND argv:
      ["codex", "sandbox", "-P", ":workspace",
       "--include-managed-config", "--",
       "bash", "-lc", "printf %s 'hello world'"]
    
    bash -lc payload argv:
      ["printf", "%s", "hello world"]
    ```
    
    A production implementation could execute that SSH command. The
    integration fixture prints it and parses the result back into arguments,
    verifying the complete flow:
    
    ```text
    model invokes outer wrapper
      -> zsh-fork starts wrapper under :workspace
      -> wrapper execs allowlisted Python script
      -> prefix rule restarts Python script unsandboxed
      -> Python script inherits CODEX_PERMISSION_PROFILE=:workspace
      -> Python script verifies :workspace is allowlisted
      -> remote command runs codex sandbox -P :workspace
         with --include-managed-config
      -> nested sandbox honors managed enterprise requirements
    ```
    
    This gives the trusted helper access to resources outside the local
    sandbox—such as SSH credentials—while ensuring that it can select only
    an explicitly authorized profile and that work on the remote host
    remains subject to the organization's managed requirements.
    
    ## What changed
    
    - Inject `CODEX_PERMISSION_PROFILE` after shell environment policy
    evaluation so the active profile wins over inherited or configured stale
    values.
    - Apply the variable to both `shell_command` and unified `exec_command`,
    including local, zsh-fork, and remote exec-server paths.
    - Remove stale values when the session has no active named profile.
    - Preserve the current profile value when loading a shell snapshot so a
    parent snapshot cannot restore an older profile.
    
    ## Testing
    
    - Added classic-shell integration coverage proving an exact prefix rule
    can run a `require_escalated` script outside the `:workspace` sandbox
    while preserving `CODEX_PERMISSION_PROFILE=:workspace`.
    - Added zsh-fork integration coverage in which the model invokes an
    outer zsh wrapper, an inner allowlisted `remote_bash.py` runs
    unsandboxed, and its printed SSH command reconstructs the inherited
    `:workspace` sandbox with `--include-managed-config` while preserving
    every argument after `--`.
    - The example helper treats `CODEX_PERMISSION_PROFILE` as untrusted and
    validates it against `ALLOWED_PROFILES` before constructing the nested
    command.
    - Assert that the reconstructed sandbox command includes
    `--include-managed-config` so nested use of the inherited profile cannot
    bypass managed enterprise requirements.
    - Added coverage for overriding and removing stale profile values.
    - Verified `shell_command` receives the selected active profile.
    - Added shell snapshot coverage using `printenv
    CODEX_PERMISSION_PROFILE`.
  • [codex] Move config loading into codex-config (#19487)
    ## Why
    
    Config loading had become split across crates: `codex-config` owned the
    config types and merge logic, while `codex-core` still owned the loader
    that assembled the layer stack. This change consolidates that
    responsibility in `codex-config`, so the crate that defines config
    behavior also owns how configs are discovered and loaded.
    
    To make that move possible without reintroducing the old dependency
    cycle, the shell-environment policy types and helpers that
    `codex-exec-server` needs now live in `codex-protocol` instead of
    flowing through `codex-config`.
    
    This also makes the migrated loader tests more deterministic on machines
    that already have managed or system Codex config installed by letting
    tests override the system config and requirements paths instead of
    reading the host's `/etc/codex`.
    
    ## What Changed
    
    - moved the config loader implementation from `codex-core` into
    `codex-config::loader` and deleted the old `core::config_loader` module
    instead of leaving a compatibility shim
    - moved shell-environment policy types and helpers into
    `codex-protocol`, then updated `codex-exec-server` and other downstream
    crates to import them from their new home
    - updated downstream callers to use loader/config APIs from
    `codex-config`
    - added test-only loader overrides for system config and requirements
    paths so loader-focused tests do not depend on host-managed config state
    - cleaned up now-unused dependency entries and platform-specific cfgs
    that were surfaced by post-push CI
    
    ## Testing
    
    - `cargo test -p codex-config`
    - `cargo test -p codex-core config_loader_tests::`
    - `cargo test -p codex-protocol -p codex-exec-server -p
    codex-cloud-requirements -p codex-rmcp-client --lib`
    - `cargo test --lib -p codex-app-server-client -p codex-exec`
    - `cargo test --no-run --lib -p codex-app-server`
    - `cargo test -p codex-linux-sandbox --lib`
    - `cargo shear`
    - `just bazel-lock-check`
    
    ## Notes
    
    - I did not chase unrelated full-suite failures outside the migrated
    loader surface.
    - `cargo test -p codex-core --lib` still hits unrelated proxy-sensitive
    failures on this machine, and Windows CI still shows unrelated
    long-running/timeouting test noise outside the loader migration itself.
  • fix: preserve platform-specific core shell env vars (#16707)
    ## Why
    
    We were seeing failures in the following tests as part of trying to get
    all the tests running under Bazel on Windows in CI
    (https://github.com/openai/codex/pull/16528):
    
    ```
    suite::shell_command::unicode_output::with_login
    suite::shell_command::unicode_output::without_login
    ```
    
    Certainly `PATHEXT` should have been included in the extra `CORE_VARS`
    list, so we fix that up here, but also take things a step further for
    now by forcibly ensuring it is set on Windows in the return value of
    `create_env()`. Once we get the Windows Bazel build working reliably
    (i.e., after #16528 is merged), we should come back to this and confirm
    we can remove the special case in `create_env()`.
    
    ## What
    
    - Split core env inheritance into `COMMON_CORE_VARS` plus
    platform-specific allowlists for Windows and Unix in
    [`exec_env.rs`](https://github.com/openai/codex/blob/1b55c88fbf585b32cd553cb9d02ec817f2ad6ebc/codex-rs/core/src/exec_env.rs#L45-L81).
    - Preserve `PATHEXT`, `USERNAME`, and `USERPROFILE` on Windows, and
    `HOME` / locale vars on Unix.
    - Backfill a default `PATHEXT` in `create_env()` on Windows if the
    parent env does not provide one, so child process launch still works in
    stripped-down Bazel environments.
    - Extend the Windows exec-env test to assert mixed-case `PathExt`
    survives case-insensitive core filtering, and document why the
    shell-command Unicode test goes through a child process.
    
    ## Verification
    
    - `cargo test -p codex-core exec_env::tests`
  • [codex] Remove codex-core config type shim (#16529)
    ## Why
    
    This finishes the config-type move out of `codex-core` by removing the
    temporary compatibility shim in `codex_core::config::types`. Callers now
    depend on `codex-config` directly, which keeps these config model types
    owned by the config crate instead of re-expanding `codex-core` as a
    transitive API surface.
    
    ## What Changed
    
    - Removed the `codex-rs/core/src/config/types.rs` re-export shim and the
    `core::config::ApprovalsReviewer` re-export.
    - Updated `codex-core`, `codex-cli`, `codex-tui`, `codex-app-server`,
    `codex-mcp-server`, and `codex-linux-sandbox` call sites to import
    `codex_config::types` directly.
    - Added explicit `codex-config` dependencies to downstream crates that
    previously relied on the `codex-core` re-export.
    - Regenerated `codex-rs/core/config.schema.json` after updating the
    config docs path reference.
  • 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.
  • fix: move inline codex-rs/core unit tests into sibling files (#14444)
    ## Why
    PR #13783 moved the `codex.rs` unit tests into `codex_tests.rs`. This
    applies the same extraction pattern across the rest of `codex-rs/core`
    so the production modules stay focused on runtime code instead of large
    inline test blocks.
    
    Keeping the tests in sibling files also makes follow-up edits easier to
    review because product changes no longer have to share a file with
    hundreds or thousands of lines of test scaffolding.
    
    ## What changed
    - replaced each inline `mod tests { ... }` in `codex-rs/core/src/**`
    with a path-based module declaration
    - moved each extracted unit test module into a sibling `*_tests.rs`
    file, using `mod_tests.rs` for `mod.rs` modules
    - preserved the existing `cfg(...)` guards and module-local structure so
    the refactor remains structural rather than behavioral
    
    ## Testing
    - `cargo test -p codex-core --lib` (`1653 passed; 0 failed; 5 ignored`)
    - `just fix -p codex-core`
    - `cargo fmt --check`
    - `cargo shear`