Commit Graph

1568 Commits

  • Fixed a hole in token refresh logic for app server (#11802)
    We've continued to receive reports from users that they're seeing the
    error message "Your access token could not be refreshed because your
    refresh token was already used. Please log out and sign in again." This
    PR fixes two holes in the token refresh logic that lead to this
    condition.
    
    Background: A previous change in token refresh introduced the
    `UnauthorizedRecovery` object. It implements a state machine in the core
    agent loop that first performs a load of the on-disk auth information
    guarded by a check for matching account ID. If it finds that the on-disk
    version has been updated by another instance of codex, it uses the
    reloaded auth tokens. If the on-disk version hasn't been updated, it
    issues a refresh request from the token authority.
    
    There are two problems that this PR addresses:
    
    Problem 1: We weren't doing the same thing for the code path used by the
    app server interface. This PR effectively replicates the
    `UnauthorizedRecovery` logic for that code path.
    
    Problem 2: The `UnauthorizedRecovery` logic contained a hole in the
    `ReloadOutcome::Skipped` case. Here's the scenario. A user starts two
    instances of the CLI. Instance 1 is active (working on a task), instance
    2 is idle. Both instances have the same in-memory cached tokens. The
    user then runs `codex logout` or `codex login` to log in to a separate
    account, which overwrites the `auth.json` file. Instance 1 receives a
    401 and refreshes its token, but it doesn't write the new token to the
    `auth.json` file because the account ID doesn't match. Instance 2 is
    later activated and presented with a new task. It immediately hits a 401
    and attempts to refresh its token but fails because its cached refresh
    token is now invalid. To avoid this situation, I've changed the logic to
    immediately fail a token refresh if the user has since logged out or
    logged in to another account. This will still be seen as an error by the
    user, but the cause will be clearer.
    
    I also took this opportunity to clean up the names of existing functions
    to make their roles clearer.
    * `try_refresh_token` is renamed `request_chatgpt_token_refresh`
    * the existing `refresh_token` is renamed `refresh_token_from_authority`
    (there's a new higher-level function named `refresh_token` now)
    * `refresh_tokens` is renamed `refresh_and_persist_chatgpt_token`, and
    it now implicitly reloads
    * `update_tokens` is renamed `persist_tokens`
  • Disable collab tools during review delegation (#12157)
    Summary
    - prevent delegated review agents from re-enabling blocked tools by
    explicitly disabling the Collab feature alongside web search and view
    image controls
    
    Testing
    - Not run (not requested)
  • Stop filtering model tools in js_repl_tools_only mode (#12069)
    ## Summary
    This change removes tool-list filtering in `js_repl_tools_only` mode and
    relies on the normal model tool descriptions, while still enforcing that
    tool execution must go through `js_repl` + `codex.tool(...)`.
    
    ## Motivation
    The previous `js_repl_tools_only` filtering hid most tools from the
    model request, which diverged from standard tool-list behavior and made
    signatures less discoverable. I tested that this filtering is not
    needed, and the model can follow the prompt to only call tools via
    `js_repl`.
    
    ## What Changed
    - `filter_tools_for_model(...)` in `core/src/tools/spec.rs` is now a
    pass-through (no filtering when `js_repl_tools_only` is enabled).
    - Updated tests to assert that model tools are not filtered in
    `js_repl_tools_only` mode.
    - Updated dynamic-tool test to assert dynamic tools remain visible in
    model tool specs.
    - Removed obsolete test helper used only by the old filtering
    assertions.
    
    ## Safety / Behavior
    - This commit does **not** relax execution policy.
    - Direct model tool calls remain blocked in `js_repl_tools_only` mode
    (except internal `js_repl` tools), and callers are instructed to use
    `js_repl` + `codex.tool(...)`.
    
    ## Testing
    - `cargo test -p codex-core js_repl_tools_only`
    - Manual rollout validation showed the model can follow the `js_repl`
    routing instructions without needing filtered tool lists.
    
    
    
    #### [git stack](https://github.com/magus/git-stack-cli)
    - 👉 `1` https://github.com/openai/codex/pull/12069
    -  `2` https://github.com/openai/codex/pull/10673
    -  `3` https://github.com/openai/codex/pull/10670
  • Enable default status line indicators in TUI config (#12015)
    Default statusline to something
    <img width="307" height="83" alt="Screenshot 2026-02-17 at 18 16 12"
    src="https://github.com/user-attachments/assets/44e16153-0aa2-4c1a-9b4a-02e2feb8b7f6"
    />
  • fix: Restricted Read: /System is too permissive for macOS platform de… (#11798)
    …fault
    
    Update the list of platform defaults included for `ReadOnlyAccess`.
    
    When `ReadOnlyAccess::Restricted::include_platform_defaults` is `true`,
    the policy defined in
    `codex-rs/core/src/seatbelt_platform_defaults.sbpl` is appended to
    enable macOS programs to function properly.
  • [js_repl] paths for node module resolution can be specified for js_repl (#11944)
    # External (non-OpenAI) Pull Request Requirements
    
    In `js_repl` mode, module resolution currently starts from
    `js_repl_kernel.js`, which is written to a per-kernel temp dir. This
    effectively means that bare imports will not resolve.
    
    This PR adds a new config option, `js_repl_node_module_dirs`, which is a
    list of dirs that are used (in order) to resolve a bare import. If none
    of those work, the current working directory of the thread is used.
    
    For example:
    ```toml
    js_repl_node_module_dirs = [
        "/path/to/node_modules/",
        "/other/path/to/node_modules/",
    ]
    ```
  • feat(core): zsh exec bridge (#12052)
    zsh fork PR stack:
    - https://github.com/openai/codex/pull/12051 
    - https://github.com/openai/codex/pull/12052 👈 
    
    ### Summary
    This PR introduces a feature-gated native shell runtime path that routes
    shell execution through a patched zsh exec bridge, removing MCP-specific
    behavior from the shell hot path while preserving existing
    CommandExecution lifecycle semantics.
    
    When shell_zsh_fork is enabled, shell commands run via patched zsh with
    per-`execve` interception through EXEC_WRAPPER. Core receives wrapper
    IPC requests over a Unix socket, applies existing approval policy, and
    returns allow/deny before the subcommand executes.
    
    ### What’s included
    **1) New zsh exec bridge runtime in core**
    - Wrapper-mode entrypoint (maybe_run_zsh_exec_wrapper_mode) for
    EXEC_WRAPPER invocations.
    - Per-execution Unix-socket IPC handling for wrapper requests/responses.
    - Approval callback integration using existing core approval
    orchestration.
    - Streaming stdout/stderr deltas to existing command output event
    pipeline.
    - Error handling for malformed IPC, denial/abort, and execution
    failures.
    
    **2) Session lifecycle integration**
    SessionServices now owns a `ZshExecBridge`.
    Session startup initializes bridge state; shutdown tears it down
    cleanly.
    
    **3) Shell runtime routing (feature-gated)**
    When `shell_zsh_fork` is enabled:
    - Build execution env/spec as usual.
    - Add wrapper socket env wiring.
    - Execute via `zsh_exec_bridge.execute_shell_request(...)` instead of
    the regular shell path.
    - Non-zsh-fork behavior remains unchanged.
    
    **4) Config + feature wiring**
    - Added `Feature::ShellZshFork` (under development).
    - Added config support for `zsh_path` (optional absolute path to patched
    zsh):
    - `Config`, `ConfigToml`, `ConfigProfile`, overrides, and schema.
    - Session startup validates that `zsh_path` exists/usable when zsh-fork
    is enabled.
    - Added startup test for missing `zsh_path` failure mode.
    
    **5) Seatbelt/sandbox updates for wrapper IPC**
    - Extended seatbelt policy generation to optionally allow outbound
    connection to explicitly permitted Unix sockets.
    - Wired sandboxing path to pass wrapper socket path through to seatbelt
    policy generation.
    - Added/updated seatbelt tests for explicit socket allow rule and
    argument emission.
    
    **6) Runtime entrypoint hooks**
    - This allows the same binary to act as the zsh wrapper subprocess when
    invoked via `EXEC_WRAPPER`.
    
    **7) Tool selection behavior**
    - ToolsConfig now prefers ShellCommand type when shell_zsh_fork is
    enabled.
    - Added test coverage for precedence with unified-exec enabled.
  • feat(core): plumb distinct approval ids for command approvals (#12051)
    zsh fork PR stack:
    - https://github.com/openai/codex/pull/12051 👈 
    - https://github.com/openai/codex/pull/12052
    
    With upcoming support for a fork of zsh that allows us to intercept
    `execve` and run execpolicy checks for each subcommand as part of a
    `CommandExecution`, it will be possible for there to be multiple
    approval requests for a shell command like `/path/to/zsh -lc 'git status
    && rg \"TODO\" src && make test'`.
    
    To support that, this PR introduces a new `approval_id` field across
    core, protocol, and app-server so that we can associate approvals
    properly for subcommands.
  • Chore: remove response model check and rely on header model for downgrade (#12061)
    ### Summary
    Ensure that we use the model value from the response header only so that
    we are guaranteed with the correct slug name. We are no longer checking
    against the model value from response so that we are less likely to have
    false positive.
    
    There are two different treatments - for SSE we use the header from the
    response and for websocket we check top-level events.
  • [apps] Expose more fields from apps listing endpoints. (#11706)
    - [x] Expose app_metadata, branding, and labels in AppInfo.
  • chore: rm remote models fflag (#11699)
    rm `remote_models` feature flag.
    
    We see issues like #11527 when a user has `remote_models` disabled, as
    we always use the default fallback `ModelInfo`. This causes issues with
    model performance.
    
    Builds on #11690, which helps by warning the user when they are using
    the default fallback. This PR will make that happen much less frequently
    as an accidental consequence of disabling `remote_models`.
  • Add remote skill scope/product_surface/enabled params and cleanup (#11801)
    skills/remote/list: params=hazelnutScope, productSurface, enabled;
    returns=data: { id, name, description }[]
    skills/remote/export: params=hazelnutId; returns={ id, path }
  • Feat: add model reroute notification (#12001)
    ### Summary
    Builiding off
    https://github.com/openai/codex/pull/11964/files/5c75aa7b89a70bc2cc410a6fd238749306ec4c5e#diff-058ae8f109a8b84b4b79bbfa45f522c2233b9d9e139696044ae374d50b6196e0,
    we have created a `model/rerouted` notification that captures the event
    so that consumers can render as expected. Keep the `EventMsg::Warning`
    path in core so that this does not affect TUI rendering.
    
    `model/rerouted` is meant to be generic to account for future usage
    including capacity planning etc.
  • chore: clarify web_search deprecation notices and consolidate tests (#11224)
    follow up to #10406, clarify default-enablement of web_search.
    
    also consolidate pseudo-redundant tests
    
    Tests pass
  • Centralize context update diffing logic (#11807)
    ## Summary
    This PR centralizes model-visible state diffing for turn context updates
    into one module, while keeping existing behavior and call sites stable.
    
    ### What changed
    - Added `core/src/context_updates.rs` with the consolidated diffing
    logic for:
      - environment context updates
      - permissions/policy updates
      - collaboration mode updates
      - model-instruction switch updates
      - personality updates
    - Added `BuildSettingsUpdateItemsParams` so required dependencies are
    passed explicitly.
    - Updated `Session::build_settings_update_items` in `core/src/codex.rs`
    to delegate to the centralized module.
    - Reused the same centralized `personality_message_for` helper from
    initial-context assembly to avoid duplicated logic.
    - Registered the new module in `core/src/lib.rs`.
    
    ## Why
    This is a minimal, shippable step toward the model-visible-state design:
    all state diff decisions for turn-context update items now live in one
    place, improving reviewability and reducing drift risk without expanding
    scope.
    
    ## Behavior
    - Intended to be behavior-preserving.
    - No protocol/schema changes.
    - No call-site behavior changes beyond routing through the new
    centralized logic.
    
    ## Testing
    Ran targeted tests in this worktree:
    - `cargo test -p codex-core
    build_settings_update_items_emits_environment_item_for_network_changes`
    - `cargo test -p codex-core collaboration_instructions --test all`
    
    Both passed.
    
    ## Codex author
    `codex resume 019c540f-3951-7352-a3fa-6f07b834d4ce`
  • Don't allow model_supports_reasoning_summaries to disable reasoning (#11833)
    The `model_supports_reasoning_summaries` config option was originally
    added so users could enable reasoning for custom models (models that
    codex doesn't know about). This is how it was documented in the source,
    but its implementation didn't match. It was implemented such that it can
    also be used to disable reasoning for models that otherwise support
    reasoning. This leads to bad behavior for some reasoning models like
    `gpt-5.3-codex`. Diagnosing this is difficult, and it has led to many
    support issues.
    
    This PR changes the handling of `model_supports_reasoning_summaries` so
    it matches its original documented behavior. If it is set to false, it
    is a no-op. That is, it never disables reasoning for models that are
    known to support reasoning. It can still be used for its intended
    purpose -- to enable reasoning for unknown models.
  • fix: race in js repl (#11922)
    js_repl_reset previously raced with in-flight/new js_repl executions
    because reset() could clear exec_tool_calls without synchronizing with
    execute(). In that window, a running exec could lose its per-exec
    tool-call context, and subsequent kernel RunTool messages would fail
    with js_repl exec context not found. The fix serializes reset and
    execute on the same exec_lock, so reset cannot run concurrently with
    exec setup/teardown. We also keep the timeout path safe by performing
    reset steps inline while execute() already holds the lock, avoiding
    re-entrant lock acquisition. A regression test now verifies that reset
    waits for the exec lock and does not clear tool-call state early.
  • fix: js_repl reset hang by clearing exec tool calls without waiting (#11932)
    Remove the waiting loop in `reset` so it no longer blocks on potentially
    hanging exec tool calls + add `clear_all_exec_tool_calls_map` to drain
    the map and notify waiters so `reset` completes immediately
  • fix(core) exec_policy parsing fixes (#11951)
    ## Summary
    Fixes a few things in our exec_policy handling of prefix_rules:
    1. Correctly match redirects specifically for exec_policy parsing. i.e.
    if you have `prefix_rule(["echo"], decision="allow")` then `echo hello >
    output.txt` should match - this should fix #10321
    2. If there already exists any rule that would match our prefix rule
    (not just a prompt), then drop it, since it won't do anything.
    
    
    ## Testing
    - [x] Updated unit tests, added approvals ScenarioSpecs
  • add(core): safety check downgrade warning (#11964)
    Add per-turn notice when a request is downgraded to a fallback model due
    to cyber safety checks.
    
    **Changes**
    
    - codex-api: Emit a ServerModel event based on the openai-model response
    header and/or response payload (SSE + WebSocket), including when the
    model changes mid-stream.
    - core: When the server-reported model differs from the requested model,
    emit a single per-turn warning explaining the reroute to gpt-5.2 and
    directing users to Trusted
        Access verification and the cyber safety explainer.
    - app-server (v2): Surface these cyber model-routing warnings as
    synthetic userMessage items with text prefixed by Warning: (and document
    this behavior).
  • chore(core) rm Feature::RequestRule (#11866)
    ## Summary
    This feature is now reasonably stable, let's remove it so we can
    simplify our upcoming iterations here.
    
    ## Testing 
    - [x] Existing tests pass
  • [apps] Fix app mention syntax. (#11894)
    - [x] Fix app mention syntax.
  • Rename collab modules to multi agents (#11939)
    Summary
    - rename the `collab` handlers and UI files to `multi_agents` to match
    the new naming
    - update module references and specs so the handlers and TUI widgets
    consistently use the renamed files
    - keep the existing functionality while aligning file and module names
    with the multi-agent terminology
  • feat: add customizable roles for multi-agents (#11917)
    The idea is to have 2 family of agents.
    
    1. Built-in that we packaged directly with Codex
    2. User defined that are defined using the `agents_config.toml` file. It
    can reference config files that will override the agent config. This
    looks like this:
    ```
    version = 1
    
    [agents.explorer]
    description = """Use `explorer` for all codebase questions.
    Explorers are fast and authoritative.
    Always prefer them over manual search or file reading.
    Rules:
    - Ask explorers first and precisely.
    - Do not re-read or re-search code they cover.
    - Trust explorer results without verification.
    - Run explorers in parallel when useful.
    - Reuse existing explorers for related questions."""
    config_file = "explorer.toml"
    ```
  • chore: rename collab feature flag key to multi_agent (#11918)
    Summary
    - rename the collab feature key to multi_agent while keeping the Feature
    enum unchanged
    - add legacy alias support so both "multi_agent" and "collab" map to the
    same feature
    - cover the alias behavior with a new unit test
  • Allow hooks to error (#11615)
    Allow hooks to return errors. 
    
    We should do this before introducing more hook types, or we'll have to
    migrate them all.
  • feat: use shell policy in shell snapshot (#11759)
    Honor `shell_environment_policy.set` even after a shell snapshot
  • fix: only emit unknown model warning on user turns (#11884)
    ###### Context
    unknown model warning added in #11690 has
    [issues](https://github.com/openai/codex/actions/runs/22047424710/job/63700733887)
    on ubuntu runners because we potentially emit it on all new turns,
    including ones with intentionally fake models (i.e., `mock-model` in a
    test).
    
    ###### Fix
    change the warning to only emit on user turns/review turns.
    
    ###### Tests
    CI now passes on ubuntu, still passes locally
  • feat: persist and restore codex app's tools after search (#11780)
    ### What changed
    1. Removed per-turn MCP selection reset in `core/src/tasks/mod.rs`.
    2. Added `SessionState::set_mcp_tool_selection(Vec<String>)` in
    `core/src/state/session.rs` for authoritative restore behavior (deduped,
    order-preserving, empty clears).
    3. Added rollout parsing in `core/src/codex.rs` to recover
    `active_selected_tools` from prior `search_tool_bm25` outputs:
       - tracks matching `call_id`s
       - parses function output text JSON
       - extracts `active_selected_tools`
       - latest valid payload wins
       - malformed/non-matching payloads are ignored
    4. Applied restore logic to resumed and forked startup paths in
    `core/src/codex.rs`.
    5. Updated instruction text to session/thread scope in
    `core/templates/search_tool/tool_description.md`.
    6. Expanded tests in `core/tests/suite/search_tool.rs`, plus unit
    coverage in:
       - `core/src/codex.rs`
       - `core/src/state/session.rs`
    
    ### Behavior after change
    1. Search activates matched tools.
    2. Additional searches union into active selection.
    3. Selection survives new turns in the same thread.
    4. Resume/fork restores selection from rollout history.
    5. Separate threads do not inherit selection unless forked.
  • fix: show user warning when using default fallback metadata (#11690)
    ### What
    It's currently unclear when the harness falls back to the default,
    generic `ModelInfo`. This happens when the `remote_models` feature is
    disabled or the model is truly unknown, and can lead to bad performance
    and issues in the harness.
    
    Add a user-facing warning when this happens so they are aware when their
    setup is broken.
    
    ### Tests
    Added tests, tested locally.
  • fix(core): add linux bubblewrap sandbox tag (#11767)
    ## Summary
    - add a distinct `linux_bubblewrap` sandbox tag when the Linux
    bubblewrap pipeline feature is enabled
    - thread the bubblewrap feature flag into sandbox tag generation for:
      - turn metadata header emission
      - tool telemetry metric tags and after-tool-use hooks
    - add focused unit tests for `sandbox_tag` precedence and Linux
    bubblewrap behavior
    
    ## Validation
    - `just fmt`
    - `cargo clippy -p codex-core --all-targets`
    - `cargo test -p codex-core sandbox_tags::tests`
    - started `cargo test -p codex-core` and stopped it per request
    
    Co-authored-by: Codex <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
  • feat(core): add structured network approval plumbing and policy decision model (#11672)
    ### Description
    #### Summary
    Introduces the core plumbing required for structured network approvals
    
    #### What changed
    - Added structured network policy decision modeling in core.
    - Added approval payload/context types needed for network approval
    semantics.
    - Wired shell/unified-exec runtime plumbing to consume structured
    decisions.
    - Updated related core error/event surfaces for structured handling.
    - Updated protocol plumbing used by core approval flow.
    - Included small CLI debug sandbox compatibility updates needed by this
    layer.
    
    #### Why
    establishes the minimal backend foundation for network approvals without
    yet changing high-level orchestration or TUI behavior.
    
    #### Notes
    - Behavior remains constrained by existing requirements/config gating.
    - Follow-up PRs in the stack handle orchestration, UX, and app-server
    integration.
    
    ---------
    
    Co-authored-by: Codex <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
  • Handle model-switch base instructions after compaction (#11659)
    Strip trailing <model_switch> during model-switch compaction request,
    and append <model_switch> after model switch compaction
  • feat(skills): add permission profiles from openai.yaml metadata (#11658)
    ## Summary
    
    This PR adds support for skill-level permissions in .codex/openai.yaml
    and wires that through the skill loading pipeline.
    
      ## What’s included
    
    1. Added a new permissions section for skills (network, filesystem, and
    macOS-related access).
    2. Implemented permission parsing/normalization and translation into
    runtime permission profiles.
    3. Threaded the new permission profile through SkillMetadata and loader
    flow.
    
      ## Follow-up
    
    A follow-up PR will connect these permission profiles to actual sandbox
    enforcement and add user approval prompts for executing binaries/scripts
    from skill directories.
    
    
     ## Example 
    `openai.yaml` snippet:
    ```
      permissions:
        network: true
        fs_read:
          - "./data"
          - "./data"
        fs_write:
          - "./output"
        macos_preferences: "readwrite"
        macos_automation:
          - "com.apple.Notes"
        macos_accessibility: true
        macos_calendar: true
    ```
    
    compiled skill permission profile metadata (macOS): 
    ```
    SkillPermissionProfile {
          sandbox_policy: SandboxPolicy::WorkspaceWrite {
              writable_roots: vec![
                  AbsolutePathBuf::try_from("/ABS/PATH/TO/SKILL/output").unwrap(),
              ],
              read_only_access: ReadOnlyAccess::Restricted {
                  include_platform_defaults: true,
                  readable_roots: vec![
                      AbsolutePathBuf::try_from("/ABS/PATH/TO/SKILL/data").unwrap(),
                  ],
              },
              network_access: true,
              exclude_tmpdir_env_var: false,
              exclude_slash_tmp: false,
          },
          // Truncated for readability; actual generated profile is longer.
          macos_seatbelt_permission_file: r#"
      (allow user-preference-write)
      (allow appleevent-send
          (appleevent-destination "com.apple.Notes"))
      (allow mach-lookup (global-name "com.apple.axserver"))
      (allow mach-lookup (global-name "com.apple.CalendarAgent"))
      ...
      "#.to_string(),
    ```
  • Fix js_repl in-flight tool-call waiter race (#11800)
    ## Summary
    
    This PR fixes a race in `js_repl` tool-call draining that could leave an
    exec waiting indefinitely for in-flight tool calls to finish.
    
    The fix is in:
    
    -
    `/Users/fjord/code/codex-jsrepl-seq/codex-rs/core/src/tools/js_repl/mod.rs`
    
    ## Problem
    
    `js_repl` tracks in-flight tool calls per exec and waits for them to
    drain on completion/timeout/cancel paths.
    The previous wait logic used a check-then-wait pattern with `Notify`
    that could miss a wakeup:
    
    1. Observe `in_flight > 0`
    2. Drop lock
    3. Register wait (`notified().await`)
    
    If `notify_waiters()` happened between (2) and (3), the waiter could
    sleep until another notification that never comes.
    
    ## What changed
    
    - Updated all exec-tool-call wait loops to create an owned notification
    future while holding the lock:
    - use `Arc<Notify>::notified_owned()` instead of cloning notify and
    awaiting later.
    - Applied this consistently to:
      - `wait_for_exec_tool_calls`
      - `wait_for_all_exec_tool_calls`
      - `wait_for_exec_tool_calls_map`
    
    This preserves existing behavior while eliminating the lost-wakeup
    window.
    
    ## Test coverage
    
    Added a regression test:
    
    - `wait_for_exec_tool_calls_map_drains_inflight_calls_without_hanging`
    
    The test repeatedly races waiter/finisher tasks and asserts bounded
    completion to catch hangs.
    
    ## Impact
    
    - No API changes.
    - No user-facing behavior changes intended.
    - Improves reliability of exec lifecycle boundaries when tool calls are
    still in flight.
    
    
    #### [git stack](https://github.com/magus/git-stack-cli)
    -  `1` https://github.com/openai/codex/pull/11796
    - 👉 `2` https://github.com/openai/codex/pull/11800
    -  `3` https://github.com/openai/codex/pull/10673
    -  `4` https://github.com/openai/codex/pull/10670
  • Fix js_repl view_image test runtime panic (#11796)
    ## Summary
    Fixes a flaky/panicking `js_repl` image-path test by running it on a
    multi-thread Tokio runtime and tightening assertions to focus on real
    behavior.
    
    ## Problem
    `js_repl_can_attach_image_via_view_image_tool` in  
    
    `/Users/fjord/code/codex-jsrepl-seq/codex-rs/core/src/tools/js_repl/mod.rs`
    can panic under single-thread test runtime with:
    
    `can call blocking only when running on the multi-threaded runtime`
    
    It also asserted a brittle user-facing text string.
    
    ## Changes
    1. Updated the test runtime to:
       `#[tokio::test(flavor = "multi_thread", worker_threads = 2)]`
    2. Removed the brittle `"attached local image path"` string assertion.
    3. Kept the concrete side-effect assertions:
       - tool call succeeds
    - image is actually injected into pending input (`InputImage` with
    `data:image/png;base64,...`)
    
    ## Why this is safe
    This is test-only behavior. No production runtime code paths are
    changed.
    
    ## Validation
    - Ran:
    `cargo test -p codex-core
    tools::js_repl::tests::js_repl_can_attach_image_via_view_image_tool --
    --nocapture`
    - Result: pass
    
    
    #### [git stack](https://github.com/magus/git-stack-cli)
    - 👉 `1` https://github.com/openai/codex/pull/11796
    -  `2` https://github.com/openai/codex/pull/11800
    -  `3` https://github.com/openai/codex/pull/10673
    -  `4` https://github.com/openai/codex/pull/10670