Commit Graph

7 Commits

  • Move models.rs to protocol (#2595)
    Moving models.rs to protocol so we can use them in `Codex` operations
  • feat: introduce TurnContext (#2343)
    This PR introduces `TurnContext`, which is designed to hold a set of
    fields that should be constant for a turn of a conversation. Note that
    the fields of `TurnContext` were previously governed by `Session`.
    
    Ultimately, we want to enable users to change these values between turns
    (changing model, approval policy, etc.), though in the current
    implementation, the `TurnContext` is constant for the entire
    conversation.
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/2345).
    * #2345
    * #2329
    * __->__ #2343
    * #2340
    * #2338
  • fix: tighten up checks against writable folders for SandboxPolicy (#2338)
    I was looking at the implementation of `Session::get_writable_roots()`,
    which did not seem right, as it was a copy of writable roots, which is
    not guaranteed to be in sync with the `sandbox_policy` field.
    
    I looked at who was calling `get_writable_roots()` and its only call
    site was `apply_patch()` in `codex-rs/core/src/apply_patch.rs`, which
    took the roots and forwarded them to `assess_patch_safety()` in
    `safety.rs`. I updated `assess_patch_safety()` to take `sandbox_policy:
    &SandboxPolicy` instead of `writable_roots: &[PathBuf]` (and replaced
    `Session::get_writable_roots()` with `Session::get_sandbox_policy()`).
    
    Within `safety.rs`, it was fairly easy to update
    `is_write_patch_constrained_to_writable_paths()` to work with
    `SandboxPolicy`, and in particular, it is far more accurate because, for
    better or worse, `SandboxPolicy::get_writable_roots_with_cwd()` _returns
    an empty vec_ for `SandboxPolicy::DangerFullAccess`, suggesting that
    _nothing_ is writable when in reality _everything_ is writable. With
    this PR, `is_write_patch_constrained_to_writable_paths()` now does the
    right thing for each variant of `SandboxPolicy`.
    
    I thought this would be the end of the story, but it turned out that
    `test_writable_roots_constraint()` in `safety.rs` needed to be updated,
    as well. In particular, the test was writing to
    `std::env::current_dir()` instead of a `TempDir`, which I suspect was a
    holdover from earlier when `SandboxPolicy::WorkspaceWrite` would always
    make `TMPDIR` writable on macOS, which made it hard to write tests to
    verify `SandboxPolicy` in `TMPDIR`. Fortunately, we now have
    `exclude_tmpdir_env_var` as an option on
    `SandboxPolicy::WorkspaceWrite`, so I was able to update the test to
    preserve the existing behavior, but to no longer write to
    `std::env::current_dir()`.
    
    
    
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/2338).
    * #2345
    * #2329
    * #2343
    * #2340
    * __->__ #2338
  • fix: make all fields of Session private (#2285)
    As `Session` needs a bit of work, it will make things easier to move
    around if we can start by reducing the extent of its public API. This
    makes all the fields private, though adds three `pub(crate)` getters.
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/2285).
    * #2287
    * #2286
    * __->__ #2285
  • fix: ensure PatchApplyBeginEvent and PatchApplyEndEvent are dispatched reliably (#1760)
    This is a follow-up to https://github.com/openai/codex/pull/1705, as
    that PR inadvertently lost the logic where `PatchApplyBeginEvent` and
    `PatchApplyEndEvent` events were sent when patches were auto-approved.
    
    Though as part of this fix, I believe this also makes an important
    safety fix to `assess_patch_safety()`, as there was a case that returned
    `SandboxType::None`, which arguably is the thing we were trying to avoid
    in #1705.
    
    On a high level, we want there to be only one codepath where
    `apply_patch` happens, which should be unified with the patch to run
    `exec`, in general, so that sandboxing is applied consistently for both
    cases.
    
    Prior to this change, `apply_patch()` in `core` would either:
    
    * exit early, delegating to `exec()` to shell out to `apply_patch` using
    the appropriate sandbox
    * proceed to run the logic for `apply_patch` in memory
    
    
    https://github.com/openai/codex/blob/549846b29ad52f6cb4f8560365a731966054a9b3/codex-rs/core/src/apply_patch.rs#L61-L63
    
    In this implementation, only the latter would dispatch
    `PatchApplyBeginEvent` and `PatchApplyEndEvent`, though the former would
    dispatch `ExecCommandBeginEvent` and `ExecCommandEndEvent` for the
    `apply_patch` call (or, more specifically, the `codex
    --codex-run-as-apply-patch PATCH` call).
    
    To unify things in this PR, we:
    
    * Eliminate the back half of the `apply_patch()` function, and instead
    have it also return with `DelegateToExec`, though we add an extra field
    to the return value, `user_explicitly_approved_this_action`.
    * In `codex.rs` where we process `DelegateToExec`, we use
    `SandboxType::None` when `user_explicitly_approved_this_action` is
    `true`. This means **we no longer run the apply_patch logic in memory**,
    as we always `exec()`. (Note this is what allowed us to delete so much
    code in `apply_patch.rs`.)
    * In `codex.rs`, we further update `notify_exec_command_begin()` and
    `notify_exec_command_end()` to take additional fields to determine what
    type of notification to send: `ExecCommand` or `PatchApply`.
    
    Admittedly, this PR also drops some of the functionality about giving
    the user the opportunity to expand the set of writable roots as part of
    approving the `apply_patch` command. I'm not sure how much that was
    used, and we should probably rethink how that works as we are currently
    tidying up the protocol to the TUI, in general.
  • fix: run apply_patch calls through the sandbox (#1705)
    Building on the work of https://github.com/openai/codex/pull/1702, this
    changes how a shell call to `apply_patch` is handled.
    
    Previously, a shell call to `apply_patch` was always handled in-process,
    never leveraging a sandbox. To determine whether the `apply_patch`
    operation could be auto-approved, the
    `is_write_patch_constrained_to_writable_paths()` function would check if
    all the paths listed in the paths were writable. If so, the agent would
    apply the changes listed in the patch.
    
    Unfortunately, this approach afforded a loophole: symlinks!
    
    * For a soft link, we could fix this issue by tracing the link and
    checking whether the target is in the set of writable paths, however...
    * ...For a hard link, things are not as simple. We can run `stat FILE`
    to see if the number of links is greater than 1, but then we would have
    to do something potentially expensive like `find . -inum <inode_number>`
    to find the other paths for `FILE`. Further, even if this worked, this
    approach runs the risk of a
    [TOCTOU](https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use)
    race condition, so it is not robust.
    
    The solution, implemented in this PR, is to take the virtual execution
    of the `apply_patch` CLI into an _actual_ execution using `codex
    --codex-run-as-apply-patch PATCH`, which we can run under the sandbox
    the user specified, just like any other `shell` call.
    
    This, of course, assumes that the sandbox prevents writing through
    symlinks as a mechanism to write to folders that are not in the writable
    set configured by the sandbox. I verified this by testing the following
    on both Mac and Linux:
    
    ```shell
    #!/usr/bin/env bash
    set -euo pipefail
    
    # Can running a command in SANDBOX_DIR write a file in EXPLOIT_DIR?
    
    # Codex is run in SANDBOX_DIR, so writes should be constrianed to this directory.
    SANDBOX_DIR=$(mktemp -d -p "$HOME" sandboxtesttemp.XXXXXX)
    # EXPLOIT_DIR is outside of SANDBOX_DIR, so let's see if we can write to it.
    EXPLOIT_DIR=$(mktemp -d -p "$HOME" sandboxtesttemp.XXXXXX)
    
    echo "SANDBOX_DIR: $SANDBOX_DIR"
    echo "EXPLOIT_DIR: $EXPLOIT_DIR"
    
    cleanup() {
      # Only remove if it looks sane and still exists
      [[ -n "${SANDBOX_DIR:-}" && -d "$SANDBOX_DIR" ]] && rm -rf -- "$SANDBOX_DIR"
      [[ -n "${EXPLOIT_DIR:-}" && -d "$EXPLOIT_DIR" ]] && rm -rf -- "$EXPLOIT_DIR"
    }
    
    trap cleanup EXIT
    
    echo "I am the original content" > "${EXPLOIT_DIR}/original.txt"
    
    # Drop the -s to test hard links.
    ln -s "${EXPLOIT_DIR}/original.txt" "${SANDBOX_DIR}/link-to-original.txt"
    
    cat "${SANDBOX_DIR}/link-to-original.txt"
    
    if [[ "$(uname)" == "Linux" ]]; then
        SANDBOX_SUBCOMMAND=landlock
    else
        SANDBOX_SUBCOMMAND=seatbelt
    fi
    
    # Attempt the exploit
    cd "${SANDBOX_DIR}"
    
    codex debug "${SANDBOX_SUBCOMMAND}" bash -lc "echo pwned > ./link-to-original.txt" || true
    
    cat "${EXPLOIT_DIR}/original.txt"
    ```
    
    Admittedly, this change merits a proper integration test, but I think I
    will have to do that in a follow-up PR.
  • chore: split apply_patch logic out of codex.rs and into apply_patch.rs (#1703)
    This is a straight refactor, moving apply-patch-related code from
    `codex.rs` and into the new `apply_patch.rs` file. The only "logical"
    change is inlining `#[allow(clippy::unwrap_used)]` instead of declaring
    `#![allow(clippy::unwrap_used)]` at the top of the file (which is
    currently the case in `codex.rs`).
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/1703).
    * #1705
    * __->__ #1703
    * #1702
    * #1698
    * #1697