Commit Graph

862 Commits

  • feat(app-server): list descendant threads by ancestor (#29591)
    ## Why
    
    `thread/list` can filter direct children with `parentThreadId`, but
    clients cannot request an entire spawned subtree. Discovering every
    descendant requires repeated client-side requests and gives up the
    database's existing filtering and pagination path.
    
    ## What changed
    
    Experimental clients can use `ancestorThreadId` to return strict
    descendants at any depth while `parentThreadId` retains its direct-child
    meaning. The filters are mutually exclusive, the ancestor is excluded,
    and every result preserves its immediate `parentThreadId` so callers can
    reconstruct the tree.
    
    ## How it works
    
    - **Explicit relationship:** Internal list parameters distinguish direct
    children from transitive descendants without changing the meaning of
    `parentThreadId`.
    - **Existing graph:** Persisted parent-child spawn edges remain the
    source of truth, so descendant lookup needs no schema migration or
    ancestry cache.
    - **Indexed traversal:** A recursive SQLite query starts from the
    parent-edge index, walks each generation, and applies thread filters,
    sorting, and cursor pagination in the same database request.
    - **Reconstructable results:** The response stays flat and normally
    ordered while carrying each descendant's immediate parent.
    
    ## Verification
    
    Ran 550 tests across the protocol, state, rollout, and thread-store
    crates, then reran the four focused state, store, and app-server
    descendant-listing tests after the final diff reduction. Scoped Clippy
    and formatting checks passed. Stable and experimental schema generation
    was checked; the stable fixtures remain unchanged while the experimental
    schema includes the new field.
  • test: use automatic environments in app-server integration tests (#29789)
    ## Why
    
    Topology-neutral app-server integration tests should exercise automatic
    environment selection so the same setup covers local and remote
    executors.
    
    ## What
    
    Migrate eligible tests to `TestAppServer::new_with_auto_env()` and
    `send_thread_start_request_with_auto_env()`. Leave explicit-topology
    tests unchanged, and skip the request-permissions case on Windows with a
    TODO for cross-platform tool routing.
    
    ## Validation
    
    - `just test -p codex-app-server`
    - `bazel test //codex-rs/app-server:app-server-all-wine-exec-test
    --test_output=errors`
    
    Stacked on #29788.
  • test: run app-server integration tests under Wine (#29788)
    ## Why
    
    Made a mistake when carving #29746 out of my local changes and the test
    was missing from the build graph. Oops!
    
    ## What
    
    Enable the app-server Wine exec test target. Remove the `manual` tag
    from generated Wine-exec test variants so wildcard Bazel test
    invocations select them. Refactor the smoke test to ensure it passes
    with current Windows support.
  • auth: move domain mode below app wire types (#29721)
    ## Why
    
    Authentication mode is a domain concept used by login, model selection,
    telemetry, and transports. Keeping the canonical type in app-server
    protocol forces those lower-level crates to depend on an unrelated wire
    API.
    
    ## What changed
    
    - Added canonical `codex_protocol::auth::AuthMode` domain values.
    - Kept the app-server wire DTO unchanged and added an explicit app-side
    conversion.
    - Removed production app-server-protocol dependencies from login,
    model-provider-info, models-manager, and otel call paths.
    
    ## Stack
    
    This is PR 2 of 6, stacked on [PR
    #29714](https://github.com/openai/codex/pull/29714). Review only the
    delta from `codex/split-json-rpc-protocols`. Next: [PR
    #29722](https://github.com/openai/codex/pull/29722).
    
    ## Validation
    
    - Auth and login coverage passed in the focused protocol/domain test
    run.
    - App-server account and auth conversion coverage passed.
  • [codex] Ignore local curated plugins when remote catalog is active (#29765)
    ## Summary
    
    - suppress configured `openai-curated` plugins when the remote plugin
    feature is enabled and auth uses the Codex backend
    - preserve `openai-api-curated` and non-Codex-backend behavior while
    including remote catalog activation in the plugin load cache key
    - add core plugin coverage and an app-server integration test for
    runtime feature enablement
    
    ## Why
    
    The Codex app enables remote plugins through process-local runtime
    feature enablement, which can happen after app-server startup tasks have
    already observed legacy local plugin state. The existing conflict logic
    only preferred a remote plugin when the same plugin was already
    installed remotely, so a configured legacy-only plugin could continue
    exposing skills and other capabilities from `openai-curated`.
    
    ## Impact
    
    When the remote catalog is active, legacy `openai-curated` plugins no
    longer contribute skills, MCP servers, apps, or hooks. Remote installed
    plugins continue to load normally, and `openai-api-curated` remains
    unaffected. This does not change remote fetch, bundle sync, or uninstall
    behavior.
    
    ## Validation
    
    - `just test -p codex-core-plugins
    remote_global_catalog_ignores_local_curated_plugins
    remote_plugin_feature_keeps_local_curated_without_codex_backend`
    - `just test -p codex-app-server
    runtime_remote_plugin_enablement_excludes_local_curated_plugin_skills`
    - `just fmt`
    - `git diff --check`
  • Let image generation extension hosts control output persistence (#29711)
    ## Why
    
    Some extension hosts need generated images returned without writing them
    to the local filesystem or giving the model a local path.
    
    ## What changed
    
    **tl;dr**: we now conduct all extension operations in the image gen
    extension
    
    - Let hosts provide an optional image save root when installing the
    extension.
    - Save images and return path hints only when a save root is configured.
    - Return image data without saving or adding a path hint when no save
    root is configured.
    - Preserve the extension-provided `saved_path` instead of persisting
    extension images again in core.
    - Leave built-in image generation unchanged.
    
    ## Validation
    
    - `just test -p codex-image-generation-extension`
    - `just test -p codex-app-server
    standalone_image_generation_returns_saved_path_hint_to_model`
    - `just test -p codex-core
    extension_tool_uses_granted_turn_permissions_without_local_persistence`
    - `just test -p codex-core tools::handlers::extension_tools::tests`
    - tested on CODEX CLI on both save_root: CODEX_HOME and None 
    - tested on CODEX APP on both as well
  • test: add app-server auto environment helper (#29746)
    ## Why
    
    Start moving towards app-server tests defaulting to running against
    remote & foreign OS executors. To do so we need a point of indirection
    similar to core integration tests' `build_with_auto_env`, but with the
    flexibility of letting tests control environment registration if they
    need to.
    
    ## What
    
    This adds:
    
    - `TestAppServer::new_with_auto_env()` for constructing an app server
    with a default environment defined by the test runner (e.g. bazel)
    - `TestAppServer::auto_env_params()` for tests to easily acquire turn
    env params tailored to the automatic environment
    - `TestAppServer::send_thread_start_request_with_auto_env()` to make it
    easy for tests to start a thread using the automatic environment
    
    The above methods all fail if the test calling them has set up an
    environment where the automatic environment configuration conflicts with
    test-created state.
    
    ## Validation
    
    Adds a couple of basic smoke tests to the app-server test suite.
    Follow-ups will migrate more tests to use it.
  • Support thread-level originator overrides (#29477)
    ## Why
    
    Work(TPP) threads can be launched from the Desktop app, but if they all
    keep the Desktop app's default originator then downstream attribution
    cannot distinguish local Work launches from cloud-backed Work launches.
    `thread/start.serviceName` already carries that launch signal, while
    `SessionMeta.originator` is the durable thread-level value that survives
    resume and fork.
    
    This change converts the Desktop Work service names into an effective
    originator at thread creation time, persists that originator with the
    thread, and keeps using it for later model requests and memory writes.
    
    ## What changed
    
    - Map `CODEX_WORK_LOCAL` and `CODEX_WORK_CLOUD` service names to
    per-thread originators, while preserving
    `CODEX_INTERNAL_ORIGINATOR_OVERRIDE` as the highest-precedence override.
    - Persist the effective originator in `SessionMeta.originator`, read it
    back on resume/fork, and inherit the parent originator for subagent
    spawns when there is no persisted session metadata.
    - Handle truncated `SpawnAgentForkMode::LastNTurns` forks by falling
    back to the live parent originator when the forked history no longer
    includes `SessionMeta`.
    - Thread the per-thread originator through Responses headers,
    websocket/compaction request paths, thread-store creation, rollout
    metadata, and memory stage-one telemetry.
    
    ## Verification
    
    - `just test -p codex-core
    agent::control::tests::spawn_thread_subagent_inherits_parent_originator_without_fork
    agent::control::tests::spawn_thread_subagent_fork_last_n_turns_inherits_parent_originator_without_session_meta
    thread_manager::tests::originator_override_precedes_service_name_remapping`
    - `just test -p codex-core
    agent::control::tests::resume_thread_subagent_restores_stored_metadata_and_effective_multi_agent_mode`
    - `just test -p codex-memories-write`
    - `just fix -p codex-core -p codex-memories-write`
    - `git diff --check`
  • Make selected plugin roots URI-native (#28918)
    ## Why
    
    Selected capability roots belong to the executor filesystem, not the
    app-server host. Converting their path strings into the host's native
    `Path` breaks whenever the two machines use different path conventions,
    such as a Windows executor behind a Unix app-server.
    
    This PR establishes `PathUri` as the selected-plugin boundary so the
    executor remains authoritative for its paths.
    
    ## What changed
    
    - Require `selectedCapabilityRoots[].location.path` to be a canonical
    `file:` URI and deserialize it directly as `PathUri`; native path
    strings are rejected.
    - Update the app-server schema, generated TypeScript, examples, and
    request coverage for the URI contract.
    - Keep selected roots, resolved plugin locations, manifest paths, and
    manifest resources as `PathUri`.
    - Inspect and read plugin roots and manifests only through the selected
    environment's `ExecutorFileSystem`.
    - Parse executor manifests with the shared URI-native parser from #29620
    instead of projecting them onto the host filesystem.
    - Enforce resource containment lexically and preserve the root URI's
    POSIX or Windows path convention.
    - Cover foreign Windows plugin roots and URI-native manifest resources.
    
    ```text
    thread/start
      selectedCapabilityRoots[].location.path = "file:///C:/plugins/demo"
                                  | PathUri
                                  v
                        ExecutorFileSystem
                                  |
                                  +--> plugin.json
                                  +--> manifest resources
    ```
    
    This PR stops at the shared selected-plugin representation. The next two
    PRs remove the remaining host-path projections in the skill and MCP
    consumers.
    
    ## Stack
    
    1. #29614 — add lexical `PathUri` containment.
    2. #29620 — share URI-native manifest path resolution.
    3. **This PR** — keep selected plugin roots and resources URI-native.
    4. #29626 — load executor skills without host path conversion.
    5. #29628 — resolve executor MCP working directories without host path
    conversion.
  • core: persist initial context window metadata (#29519)
    ## Why
    
    PR #29494 made context-window IDs visible to the model by wrapping the
    token-budget window payload in `<context_window>`, but rollout JSONL
    consumers still could not see the initial window identity by tailing the
    session file. Compacted rollout items carry window IDs only after
    compaction has happened, so a session with no compaction had no durable
    JSONL record for window 0.
    
    This change gives tailing consumers a stable initial-window record at
    session creation time.
    
    ## What Changed
    
    - Added `session_meta.context_window.window_id` for the initial
    context-window identity.
    - `CreateThreadParams` now requires `initial_window_id: String`, so
    thread-store callers cannot accidentally create new threads without
    window-0 metadata.
    - Live thread creation derives the persisted initial window ID from the
    same `AutoCompactWindowIds` used to initialize `SessionState`, keeping
    runtime state and JSONL metadata aligned.
    - Rollout reconstruction uses `session_meta.context_window.window_id` as
    the initial-window fallback and derives `window_number = 0`,
    `first_window_id = window_id`, and `previous_window_id = None`
    internally.
    - Fork reconstruction intentionally uses the same rollout reconstruction
    path; consumers that need to distinguish copied initial-window metadata
    can use the rollout `thread_id`.
    - Legacy compactions without `window_number` still use compaction-count
    fallback accounting instead of being reset to window 0 by the
    initial-window fallback.
    - Compacted rollout metadata still takes precedence once compaction
    records exist, preserving the richer chain fields there.
    
    ## JSONL Shape
    
    Real rollout JSONL is one object per line. This example is expanded for
    readability, but shows the new initial `session_meta.context_window`
    record followed by the existing compacted rollout item shape that also
    carries window IDs:
    
    ```jsonl
    {
      "timestamp": "2026-06-22T12:00:00.000Z",
      "type": "session_meta",
      "payload": {
        "session_id": "<THREAD_ID>",
        "id": "<THREAD_ID>",
        "timestamp": "2026-06-22T12:00:00.000Z",
        "cwd": "/repo",
        "originator": "codex",
        "cli_version": "0.0.0",
        "source": "cli",
        "model_provider": "<MODEL_PROVIDER>",
        "context_window": {
          "window_id": "<INITIAL_WINDOW_ID>"
        }
      }
    }
    ...
    {
      "timestamp": "2026-06-22T12:34:56.000Z",
      "type": "compacted",
      "payload": {
        "message": "<COMPACTION_SUMMARY>",
        "replacement_history": [
          "..."
        ],
        "window_number": 1,
        "first_window_id": "<INITIAL_WINDOW_ID>",
        "previous_window_id": "<INITIAL_WINDOW_ID>",
        "window_id": "<NEXT_WINDOW_ID>"
      }
    }
    ```
    
    The nested `context_window` object is intentional: it gives rollout
    consumers a stable namespace for context-window metadata while only
    writing the non-derivable initial `window_id`. For the initial window,
    `window_number`, `first_window_id`, and `previous_window_id` are derived
    internally instead of being written to the rollout.
    
    ## Verification
    
    - `just test -p codex-protocol`
    - `just test -p codex-rollout
    recorder_materializes_on_flush_with_pending_items`
    - `just test -p codex-core reconstruct_history`
    - `just test -p codex-core
    record_initial_history_reconstructs_forked_transcript`
    - `just test -p codex-thread-store`
    - `just test -p codex-state`
    - `just test -p codex-app-server
    thread_read_returns_summary_without_turns`
    - `just test -p codex-rollout persistence_metrics`
  • test: branch on target OS instead of runner flavor (#29712)
    ## Why
    
    Core tests should branch on the executor's operating system, not on
    runner details such as Docker or Wine. This keeps platform behavior
    stable as new test backends are added and reserves Wine-specific skips
    for actual runner debt.
    
    ## What
    
    - Add `TestTargetOs` and target/host-aware skip helpers while keeping
    `TestEnvironment` internal.
    - Replace topology enum access with remote predicates and a narrow
    Docker accessor.
    - Migrate OS-semantic Wine skips, preserve runner-specific gaps, and
    document the skip taxonomy.
    
    ## Validation
    
    - `just test -p core_test_support`
    - `just test -p codex-core
    remote_test_env_can_connect_and_use_filesystem`
    - `bazel test //codex-rs/core:core-all-wine-exec-test
    --test_output=errors` reached test execution; unrelated existing
    view-image, path, and timing failures remain.
    - `just test -p codex-core` and `just test` reached broad test
    execution; this checkout has unrelated helper, sandbox, and timing
    failures.
  • feat(app-server): thread/turns/items/list -> thread/items/list (#29705)
    ## Description
    
    Rename the experimental app-server item pagination API from
    `thread/turns/items/list` to `thread/items/list` and make `turnId`
    optional. Clients can now page persisted items across a thread, or still
    filter to one turn when needed.
    
    ## What changed
    
    - Rename the request/response protocol types and JSON-RPC method to
    `ThreadItemsList*` / `thread/items/list`.
    - Pass optional `turnId` through to `ThreadStore::list_items`.
    - Update app-server docs and focused protocol/app-server tests.
    
    ## Validation
    
    - `just test -p codex-app-server-protocol thread_items_list_round_trips`
    - `just test -p codex-app-server thread_items_list_returns_unsupported`
  • Separate local and remote plugin analytics IDs (#29495)
    ## Why
    
    Plugin analytics overloaded `plugin_id`: most events used the Codex
    `<plugin>@<marketplace>` identity, while remote install events used the
    backend plugin ID. That makes the same field change meaning across event
    types and complicates downstream identity resolution.
    
    This change makes the contract unambiguous:
    
    - `plugin_id`: the local Codex `<plugin>@<marketplace>` identity, when
    resolved
    - `remote_plugin_id`: the backend plugin identity, when available
    
    For a remote install failure that happens before plugin details resolve,
    `plugin_id` is `null` and `remote_plugin_id` remains populated.
    
    ## What changed
    
    All six plugin analytics events use the same identity contract:
    
    - `codex_plugin_installed`
    - `codex_plugin_install_failed`
    - `codex_plugin_uninstalled`
    - `codex_plugin_enabled`
    - `codex_plugin_disabled`
    - `codex_plugin_used`
    
    Remote identity is resolved from the current installed-plugin snapshot
    first, with persisted install metadata as fallback. The telemetry
    metadata type keeps local identity optional for failures that occur
    before remote details are available.
    
    The app-server test client's manual analytics smokes now find remote
    mutation events through `remote_plugin_id` and validate that `plugin_id`
    remains local.
    
    ## Remote uninstall
    
    Resolve and capture telemetry metadata before removing the local plugin
    cache, then emit `codex_plugin_uninstalled` after the backend confirms
    success. The event is also emitted when backend uninstall succeeds but
    local cache cleanup reports `CacheRemove`.
    
    If a concurrent remote-cache refresh removes the local bundle before
    telemetry capture, the already-fetched remote plugin detail supplies
    fallback capability metadata.
    
    ## Validation
    
    - `just test -p codex-analytics` — 82 passed
    - `just test -p codex-core-plugins` — 271 passed
    - `just test -p codex-app-server-test-client` — 5 passed
    - `just test -p codex-plugin` — 3 passed
    - `just test -p codex-app-server plugin_install` — 37 passed
    - `just test -p codex-app-server plugin_uninstall` — 10 passed
    
    The production app-server install/uninstall flow was also exercised
    against `plugins~Plugin_f1b845ac33888191ac156169c58733c2`
    (`build-ios-apps@openai-curated-remote`), and the plugin's original
    uninstalled state was restored.
  • [core] debounce current-time reminders by elapsed time (#29659)
    ## Summary
    - rename `reminder_interval_model_requests` to
    `reminder_interval_seconds`
    - read the configured time provider before every model request and
    inject a reminder only after the configured number of seconds has
    elapsed
    - preserve immediate first delivery and forced delivery after compaction
    changes the context window
    
    ## Tests
    - `just test -p codex-core current_time_reminder`
  • Namespace multi-agent v2 tools under collaboration (#29067)
    ## Summary
    
    Multi-agent v2 tools now use the fixed `collaboration` namespace when
    namespace tools are available. This keeps the model-visible hint and the
    actual tool surface aligned around `functions.collaboration.*`, without
    exposing an unshipped namespace knob to users.
    
    The PR also removes the old `features.multi_agent_v2.tool_namespace`
    config/schema surface, updates the MAv2 test fixtures for namespaced
    calls, and fixes stale `TurnContext.features` references that were
    breaking `codex-core` builds.
    
    ## Changes
    
    - Expose MAv2 tools under `collaboration` instead of relying on a
    configurable namespace.
    - Remove `tool_namespace` from MAv2 TOML config, resolved config,
    validation, schema, and tests.
    - Update tool-planning and integration fixtures to assert or emit
    namespaced MAv2 tool calls.
    - Read feature state through `TurnContext.config.features` in the
    multi-agent mode context paths.
    
    ## Testing
    
    - `just write-config-schema`
    - `just test -p codex-features`
  • [codex] reject remote images at app-server ingress (#29419)
    ## Stack
    
    Stacked on #29417. Review and land that PR first.
    
    ## Summary
    
    - reject HTTP(S) image URLs in the handlers for `turn/start` and
    `turn/steer`
    - validate `thread/inject_items` after its existing
    JSON-to-`ResponseItem` conversion, so each item is deserialized once
    - turn invalid dynamic-tool image responses into the existing
    unsuccessful text fallback; the model receives the validation message as
    the function output
    - leave `thread/resume.history` compatible with legacy history; #29417
    replaces remote images before model input
    - continue accepting inline data URLs and `localImage` inputs
    - keep this policy in app-server; this PR does not add a shared protocol
    API or change core image preparation
    
    ## Test plan
    
    - `just test -p codex-app-server -E
    'test(/request_handlers_reject_remote_image_urls|dynamic_tool_remote_image_response_becomes_model_visible_error|dynamic_tool_call_round_trip_sends_content_items_to_model|turn_start_tracks_turn_event_analytics|standalone_image_edit_uses_recent_pathless_image/)'`
    (5 passed)
    - `just fix -p codex-app-server`
    - `just fmt`
  • feat(core): store turn_id on ResponseItem metadata (#28360)
    ## Description
    
    This PR is a followup to https://github.com/openai/codex/pull/28355 and
    starts assigning `internal_chat_message_metadata_passthrough.turn_id` to
    durable Responses API items created during a turn.
    
    The goal is that those items keep the `turn_id` that introduced them
    when Codex resends stateless HTTP context, reconstructs history for
    resume/fork paths, or reuses websocket response state.
    
    ## What changed
    
    - Set `internal_chat_message_metadata_passthrough.turn_id` when missing
    as response items enter durable history, initial/replacement history,
    inter-agent communication history, and local compaction summaries.
    - Preserve existing item turn IDs instead of overwriting them during
    persistence, resume reconstruction, compaction, forked history, and
    websocket incremental reuse.
    - Keep `compaction_trigger` fieldless because it is a request control,
    not a durable response item.
    - Update focused history/request assertions and fixtures for stateless
    requests, websocket incrementals, compaction, thread injection, prompt
    debug, and related CI coverage.
  • [codex] replace remote images with model-visible error text (#29417)
    ## What
    
    This PR will extend the existing centralized image-preparation path to
    replace HTTP(S) image inputs with a model visible error message. It
    won't "ruin" and break existing rollouts, but it will deprecate support
    for the pathway. App server clients should no longer use HTTP image urls
    if they'd like to upgrade.
    
    The HTTP image url pathway is currently resolved in the responsesapi. It
    is slow and not reccomended.
    
    ## Behavior
    
    - HTTP(S) image URL: replace with `input_text`
    - data URL: use the existing decode and resize path
    - other image URL schemes: leave unchanged
    
    This intentionally does not change app-server ingress. That validation
    remains a follow-up.
    
    ## Test plan
    
    - `just test -p codex-core -E
    'test(/image_preparation|prepares_image_failures_before_history_insertion|prepares_resumed_history_before_installing_it|responses_lite_prepares_images/)'`
    — 7 passed
    - `just fix -p codex-core`
    - `just fmt`
  • [plugins] Add dark-mode logo metadata (#29488)
    Adds additive dark-mode plugin logo metadata across manifests, remote
    catalogs, and the app-server protocol while keeping uninstalled Git
    listings free of synthetic local paths.
    
    Supersedes #28945. This replacement uses an upstream branch so trusted
    CI can use the repository-provided remote Bazel configuration.
    
    ## Current state
    
    Plugin interfaces expose only the default logo asset. Clients therefore
    cannot select a dedicated dark-mode logo even when a plugin provides
    one.
    
    ## What this PR changes
    
    - Adds nullable `logoDark` and `logoUrlDark` fields to
    `PluginInterface`.
    - Resolves local `interface.logoDark` assets and maps remote
    `logo_url_dark` values.
    - Removes path-backed interface assets, including `logoDark`, from
    uninstalled Git fallback listings until the plugin has a real local
    root.
    - Updates the bundled plugin validator and manifest reference.
    - Regenerates the app-server JSON schemas and TypeScript types.
    
    Local manifests expose `interface.logoDark` as a package-relative asset
    path. Remote catalog responses expose `logo_url_dark`. These values map
    into separate app-server fields so clients can preserve local-path and
    remote-URL handling.
    
    ## Risk
    
    The fields are additive and nullable, so existing clients retain their
    current logo behavior. The main risks are an incomplete mapping path or
    exposing a synthetic local path for an uninstalled Git plugin.
    Local-manifest, remote-catalog, fallback-listing, protocol
    serialization, and app-server integration tests cover those paths.
    
    Spiciness: 2/5
    
    ## Testing
    
    - `just write-app-server-schema`
    - `just fmt`
    - Regression test first failed with `logo_dark` resolved to
    `/assets/logo-dark.png`, then passed after the fallback-listing fix.
    - `just test -p codex-core-plugins` (267 tests passed)
    - `just test -p codex-app-server 'suite::v2::plugin'` (114 tests passed)
    - `just test -p codex-app-server-protocol -p codex-core-plugins -p
    codex-plugin -p codex-skills` (517 tests passed before the follow-up)
    - `just test -p codex-tui plugin` (47 tests passed)
    - Validated a local plugin manifest containing `interface.logoDark` with
    the bundled validator.
    
    ## Manual verification
    
    Create a local plugin with both `interface.logo` and
    `interface.logoDark`, then call `plugin/list` or `plugin/read`. Confirm
    the response contains separate `logo` and `logoDark` paths. For a remote
    catalog entry, confirm `logoUrlDark` is populated from `logo_url_dark`.
    For an uninstalled Git marketplace entry, confirm path-backed interface
    assets remain absent until installation.
    
    Issue: N/A - coordinated maintainer change.
  • [codex] fetch featured IDs for remote plugins (#29485)
    ## Summary
    
    - fetch featured plugin IDs when the loaded catalog includes
    `openai-curated-remote`
    - extend the existing remote marketplace regression test to cover the
    featured IDs response
    
    ## Why
    
    When the remote plugin catalog was enabled, app-server loaded
    `openai-curated-remote` but skipped `/plugins/featured` because the
    request processor only fetched featured IDs for the local
    `openai-curated` marketplace. As a result, the desktop app could not
    render the backend-curated remote featured set.
    
    This keeps the existing local behavior and also returns the curated
    ranking for remote plugins.
    
    ## Test plan
    
    - `just fmt`
    - `git diff --check`
    - `just test -p codex-app-server
    plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled`
  • permission profiles: expose availability to clients (#26678)
    ## Why
    
    `permissionProfile/list` currently advertises every built-in and
    configured profile even when effective enterprise requirements prevent
    selecting it. That forces each client to reconstruct policy from
    lower-level requirement fields, which is easy to miss and difficult to
    keep consistent.
    
    The catalog should remain complete so clients can explain that an option
    was disabled by an administrator, while also reporting whether each
    profile is selectable.
    
    ## What
    
    - Add an `allowed` field to each permission profile summary.
    - Build a shared catalog from the effective config and current
    requirements, including `allowed_sandbox_modes`, `allowed_permissions`,
    and filesystem restrictions.
    - Use the shared catalog in app-server and the TUI so disallowed
    profiles remain visible but cannot be selected.
    - Use the canonical `:danger-full-access` profile ID in the TUI.
    - Update the app-server schemas, API documentation, behavioral tests,
    and TUI snapshots.
    
    ## Scope
    
    This PR targets `main` directly and is independent of #24852. It
    preserves the current behavior where built-in profiles are constrained
    by sandbox-mode requirements and `allowed_permissions` applies to
    configured profiles.
    
    ## Testing
    
    - `just test -p codex-core
    permission_profile_catalog_marks_profiles_disallowed_by_requirements`
    - `just test -p codex-app-server permission_profile_list`
    - `just test -p codex-app-server-protocol`
    - `just test -p codex-tui profile_permissions`
    - `just fix -p codex-core`
    - `just fix -p codex-app-server-protocol`
    - `just fix -p codex-app-server`
    - `just fix -p codex-tui`
    - `just fmt`
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
    Co-authored-by: Joey Trasatti <joey.trasatti@openai.com>
  • Allow ChatGPT accounts without email (#28991)
    # Summary
    
    Codex required every ChatGPT account to have an email address. A
    service-account personal access token can return valid account metadata
    without one, so PAT login failed while decoding the metadata response.
    
    This change makes email optional in the account metadata type that owns
    it and preserves that absence through authentication, provider account
    state, the app-server API, generated clients, and TUI bootstrap.
    Existing accounts with email addresses keep the same behavior.
    
    ## Behavior-changing call sites
    
    | Call site | Behavior after this change |
    | --- | --- |
    | `login/src/auth/personal_access_token.rs` | PAT metadata accepts a
    missing or null email and retains `None`. |
    | `agent-identity/src/lib.rs` | Agent Identity JWT claims accept an
    omitted email. |
    | `login/src/auth/storage.rs` and `login/src/auth/agent_identity.rs` |
    Stored and managed Agent Identity records carry `Option<String>`.
    Deserialization maps the legacy empty-string sentinel to `None`. |
    | `login/src/auth/manager.rs` | `get_account_email` returns the stored
    option, and managed identity bootstrap no longer converts `None` to an
    empty string. |
    | `model-provider/src/provider.rs` and `protocol/src/account.rs` | A
    ChatGPT provider account requires a plan type but may carry no email. |
    | `app-server-protocol/src/protocol/v2/account.rs` | `account/read`
    keeps the `email` field on the wire and returns `null` when the account
    has no email. Generated TypeScript and JSON schemas describe a required,
    nullable field. |
    | `sdk/python/src/openai_codex/generated/v2_all.py` | The generated
    Python `ChatgptAccount` model accepts `None` for email. |
    | `tui/src/app_server_session.rs` | Email-less ChatGPT accounts
    bootstrap normally, keep external feedback routing, omit account-email
    telemetry, and display the plan in account status. |
    
    ## Design decisions
    
    - Missing email remains `None` at every layer. The code never uses an
    empty string as a substitute.
    - The app-server response includes `"email": null` instead of omitting
    the field. Clients retain a stable response shape.
    - Plan type remains required for provider account state. This change
    relaxes only the email assumption.
    
    ## Testing
    
    Tests: affected test targets compile, scoped Clippy and formatting pass,
    a focused TUI snapshot covers plan-only account status, real
    before/after PAT login smoke covers metadata without email, app-server
    smoke covers `account/read` with `email: null`, and a regression smoke
    covers an existing email-bearing PAT. Unit tests run in CI.
    
    ## Evidence
    
    Visual smoke evidence will be attached here.
  • core: rename metadata -> internal_chat_message_metadata_passthrough (#28968)
    ## Description
    This PR cuts Codex over from generic `ResponseItem.metadata` (introduced
    here: https://github.com/openai/codex/pull/28355) to
    `ResponseItem.internal_chat_message_metadata_passthrough`, which is the
    blessed path and has strongly-typed keys.
    
    For now we have to drop this MAv2 usage of `metadata`:
    https://github.com/openai/codex/pull/28561 until we figure out where
    that should live.
  • remove flag for image preparation (#29429)
    ## What
    
    - make Fjord's centralized response-item image preparation unconditional
    for new and resumed history
    - have local user images and `view_image` outputs always defer decoding
    and resizing to that path
    - retain `resize_all_images` as an ignored, removed compatibility key
    for released clients
    - delete the flag-off producer paths and obsolete policy-specific tests
    
    ## Why
    
    Centralized preparation is now the intended image path. Keeping the
    runtime feature checks also kept two image-processing implementations
    alive and allowed client config to select the legacy behavior.
    
    This is a clean replacement for #28975, rebuilt from the latest `main`.
    
    ## How
    
    `prepare_response_items` now runs whenever items enter history and
    whenever persisted history is reconstructed. Producers emit deferred
    image data, so malformed images become the existing model-visible
    placeholder instead of failing the session at the producer.
    
    ## Test plan
    
    - `just fmt`
    - `just fix -p codex-core -p codex-features`
    - `just test -p codex-features` — 52 passed
    - focused affected `codex-core` set — 20 passed
    - `just test -p codex-core handle_accepts_explicit_high_detail` — 1
    passed
    - full `just test -p codex-core` attempt — 2,723 passed; 88 unrelated
    environment failures from read-only `~/.codex` SQLite state and
    unavailable integration helper binaries
  • fix(core): restore thread_source in x-codex-turn-metadata (#29455)
    ## Description
    
    Restore `thread_source` in `x-codex-turn-metadata`.
    
    Inadvertently removed `thread_source` from `x-codex-turn-metadata` in
    https://github.com/openai/codex/pull/27122 - didn't realize it was a
    top-level thread app-server API field, not passed in
    `responsesapi_client_metadata`.
    
    This also reserves the key so `responsesapi_client_metadata` cannot
    override it.
  • Simplify multi-agent mode controls (#29324)
    ## Why
    
    Multi-agent delegation policy was split across `multiAgentMode`,
    `features.multi_agent_mode`, and `usage_hint_enabled`. These controls
    could disagree: a requested mode could be downgraded by the feature
    flag, and disabling usage hints also disabled mode instructions.
    
    Some clients also need multi-agent tools without adding
    delegation-policy text to model context. The previous two-mode API could
    not express that directly.
    
    ## What changed
    
    `multiAgentMode` is now the only live delegation-policy control:
    
    | Mode | Behavior |
    | --- | --- |
    | `none` | Keep multi-agent tools available without adding mode
    instructions. |
    | `explicitRequestOnly` | Only delegate after an explicit user request.
    |
    | `proactive` | Delegate when parallel work materially improves speed or
    quality. |
    
    - new threads default to `explicitRequestOnly`; omitting the mode on
    later turns keeps the current value
    - thread start, resume, fork, and settings responses always report the
    concrete current mode instead of `null`
    - mode selection remains sticky across turns and resume
    - usage-hint text no longer controls whether mode instructions apply
    - `features.multi_agent_mode` and `usage_hint_enabled` remain accepted
    as ignored compatibility settings so existing configs continue to load
    - app-server documentation and generated schemas describe the three-mode
    API
    
    ## Tests
    
    - `just test -p codex-core multi_agent_mode`
    - `just test -p codex-core multi_agent_v2_config_from_feature_table`
    - `just test -p codex-core spawn_agent_description`
    - `just test -p codex-features`
    - `just test -p codex-app-server-protocol`
    - `just test -p codex-app-server multi_agent_mode`
  • Persist session IDs across thread resume (#29327)
    ## Summary
    
    A cold-resumed subagent kept its durable thread ID but could receive a
    new session ID, splitting one agent tree across multiple sessions after
    a restart.
    
    Persist the root session ID in every rollout `SessionMeta`, carry it
    through thread creation, and restore it before initializing the resumed
    `Session` and `AgentControl`.
    
    ## Behavior
    
    For a nested agent tree:
    
    ```text
    root session R
      parent thread P
        child thread C
    ```
    
    The child rollout stores:
    
    ```text
    session_id:       R
    parent_thread_id: P
    id:               C
    ```
    
    After a cold resume, the child still belongs to root session `R` while
    its immediate parent remains `P`. The integration coverage uses distinct
    values for all three IDs so it catches restoring the session from
    `parent_thread_id`.
    
    ## Legacy rollouts
    
    Previous rollouts have `id` but no `session_id`. `SessionMetaLine`
    deserialization treats a missing `session_id` as `id`, keeping those
    files readable, listable, and resumable. When a legacy subagent is
    resumed through its root, that synthesized child ID no longer overrides
    the inherited root-scoped `AgentControl`. New rollouts always persist
    the explicit root session ID.
  • Add config toggles for orchestrator skills and MCP (#28942)
    ## Why
    
    Orchestrator-provided skills and Codex Apps MCP tools add model-visible
    instructions, resources, and tools beyond the local workspace. Hosts
    need config-level switches to disable those orchestrator-owned surfaces
    independently, without disabling regular skills or regular MCP servers.
    
    ## What changed
    
    - Adds `[orchestrator.skills].enabled` and `[orchestrator.mcp].enabled`
    config entries, both defaulting to `true`.
    - Includes the new settings in `config.schema.json` and in the config
    lock so resolved thread configuration preserves the same orchestrator
    exposure decisions.
    - Threads `orchestrator.skills.enabled` through the app-server skills
    extension so disabled orchestrator skills do not expose the `skills`
    namespace or inject orchestrator skill context.
    - Gates Codex Apps MCP exposure, app instructions, and app auth
    eligibility on `orchestrator.mcp.enabled` while leaving non-Codex-Apps
    MCP tools available.
    - Updates the thread-manager sample config to disable both
    orchestrator-owned surfaces.
    
    ## Verification
    
    - Added config parsing, loading, defaulting, and schema coverage for the
    new settings.
    - Added MCP exposure coverage that `orchestrator.mcp.enabled = false`
    removes Codex Apps tools while preserving regular MCP tools.
    - Added app-server coverage that `orchestrator.skills.enabled = false`
    prevents orchestrator skill tools, prompts, and resource reads from
    reaching the model turn.
  • Expose thread-level multi-agent mode (#28792)
    ## Why
    
    Once multi-agent mode can be selected per turn, clients also need to
    choose the initial selection when creating a thread and observe that
    selection through lifecycle and settings APIs.
    
    The selected value is intentionally distinct from the effective
    model-visible value: no client selection is represented as `null`, even
    though an eligible multi-agent v2 turn derives `explicitRequestOnly` as
    its effective default.
    
    ## What changed
    
    - Add the optional experimental `thread/start.multiAgentMode` parameter
    and pass it through thread creation.
    - Preserve an omitted initial value as an unset selection rather than
    eagerly storing `explicitRequestOnly`.
    - Apply an explicit `thread/start` selection to the first turn through
    the session configuration established at thread creation.
    - Restore the latest persisted effective mode as the selected baseline
    on cold resume when rollout history contains one.
    - Inherit the optional selected mode from a loaded parent when creating
    related runtime threads.
    - Return the current selected `multiAgentMode` from `thread/start`,
    `thread/resume`, `thread/fork`, and thread settings, using `null` when
    no mode is selected.
    - Keep lifecycle reporting independent from model capability and feature
    eligibility; core turn construction remains responsible for calculating
    and persisting the effective mode.
    
    ## Not covered
    
    - Clearing an existing loaded-session selection back to unset through
    `turn/start`; omitted or `null` currently retains the session's
    selection.
    - A TUI control, slash command, or `config.toml` preference.
    
    ## Verification
    
    - `CARGO_INCREMENTAL=0 just test -p codex-app-server-protocol`
    - `CARGO_INCREMENTAL=0 just test -p codex-app-server multi_agent_mode`
    
    The focused app-server coverage verifies explicit `thread/start`
    initialization, first-turn prompting, nullable reporting for an omitted
    selection, and retention of selections that are not currently
    runtime-eligible.
    
    ## Stack
    
    Stacked on #28685. This PR contains only the thread initialization and
    lifecycle/settings API layer.
  • Add per-turn multi-agent mode (#28685)
    ## Why
    
    Multi-agent v2 currently carries an explicit-request-only delegation
    rule in its static usage hint. That provides a safe default, but it
    prevents clients from selecting proactive delegation per turn without
    changing static guidance or rewriting prior model context.
    
    This change makes delegation mode a session selection that can be
    updated through `turn/start`, while deriving the effective model-visible
    mode separately for each turn. Eligible multi-agent v2 turns remain
    explicit-request-only unless proactive mode is both selected and
    enabled.
    
    ## What changed
    
    - Add the experimental `turn/start.multiAgentMode` parameter with
    `explicitRequestOnly` and `proactive` values. Omission retains the
    loaded session's current optional selection.
    - Add the default-off `features.multi_agent_mode` feature gate. Eligible
    multi-agent v2 turns use the selected mode when enabled; an unset
    selection or disabled gate resolves to `explicitRequestOnly`.
    - Treat mode prompting as inapplicable for multi-agent v1 and other
    unsupported session configurations, producing no multi-agent mode
    developer message rather than rejecting the turn.
    - Move the explicit-request-only rule out of the static v2 usage hint
    and into a bounded, tagged developer context fragment.
    - Emit the effective mode in initial context and only when that
    effective mode changes on later turns.
    - Persist the effective mode in `TurnContextItem` as the durable
    baseline for resume and context-update comparisons.
    
    Historical rollout items are not rewritten. Later mode developer
    messages establish the current rule incrementally.
    
    ## Not covered
    
    - Initial selection through `thread/start` and selected-mode reporting
    from thread lifecycle/settings APIs; those are isolated in the stacked
    #28792.
    - A TUI control or slash command for selecting the mode.
    - Persisting a preferred mode to `config.toml`; selection remains
    session/turn scoped.
    - Changes to multi-agent concurrency limits, tool availability, or model
    catalog capability declarations.
    - Rewriting historical rollout prompt items. Cold resume restores the
    latest persisted effective mode when available while leaving historical
    developer messages intact.
    
    ## Verification
    
    - `CARGO_INCREMENTAL=0 just test -p codex-core multi_agent_mode`
    - Focused app-server coverage verifies that `turn/start.multiAgentMode`
    produces proactive developer instructions for an eligible v2 turn.
    
    ## Stack
    
    Followed by #28792, which adds `thread/start` initialization and
    lifecycle/settings observability.
  • [3/3] app-server: configure environment connection timeout (#29025)
    ## Why
    
    Remote environments registered through `environment/add` currently use
    the fixed 10-second WebSocket connection timeout. Slow-starting
    executors need a caller-selected connection window, but this should not
    add retry policy or couple exec-server behavior to Core’s
    `deferred_executor` feature.
    
    Make the timeout an optional part of the existing experimental request.
    Existing clients continue using the current default, while callers that
    know an executor may take longer can request a larger window explicitly.
    
    Depends on #28683.
    
    ## What changed
    
    - Add optional `connectTimeoutMs` to `EnvironmentAddParams` and document
    it in the app-server README.
    - Pass the optional timeout through `EnvironmentRequestProcessor` into
    one `EnvironmentManager::upsert_environment()` path; the manager applies
    the existing default when it is omitted.
    - Preserve the existing single-attempt lifecycle. The configured value
    controls WebSocket connection and handshake time for both initial
    connection and later reconnects; initialization retains its separate
    timeout.
    - Add an app-server integration test that sends the real JSON-RPC
    request and verifies a stalled handshake observes the requested timeout.
    
    ## Test plan
    
    - `just test -p codex-app-server-protocol`
    - `just test -p codex-exec-server`
    - `just test -p codex-app-server
    environment_add_applies_connect_timeout`
    
    ## Rollout
    
    This is additive and does not enable `deferred_executor`. Callers should
    send a non-default timeout only after a compatible app-server is
    deployed; omitted or `null` values retain the existing 10-second
    default.
  • [codex] Support protected resource OAuth discovery (#29022)
    ## Why
    
    Plugin-install preflight and the actual OAuth login flow used different
    discovery implementations. Preflight had a Codex-specific implementation
    that only queried authorization-server metadata on the MCP host, while
    login already used the upstream `rmcp` Rust MCP SDK. As a result,
    servers that advertise a separate authorization server through RFC 9728
    Protected Resource Metadata were classified as OAuth-unsupported during
    plugin installation, so login was skipped.
    
    ## What changed
    
    - delegate plugin-install OAuth discovery to
    `rmcp::transport::AuthorizationManager`, the same implementation used by
    the login flow
    - let `rmcp` follow Protected Resource Metadata first and perform direct
    RFC 8414 authorization-server discovery when protected-resource
    discovery does not yield usable metadata
    - retain Codex's existing HTTP headers, timeout, `no_proxy` behavior,
    and scope normalization around that discovery
    - add unit coverage and a pure-MCP plugin-install integration test that
    proves the protected-resource path reaches OAuth client registration
    
    This only changes shared MCP OAuth discovery. App declarations and
    `appsNeedingAuth` behavior are unchanged.
    
    ## Verification
    
    - `just test -p codex-rmcp-client auth_status`
    - `just test -p codex-app-server plugin_install_starts_mcp_oauth`
    - real plugin-install smoke test with an isolated `CODEX_HOME`: both
    DigitalOcean MCP servers started OAuth callback listeners, while Linear
    continued to start its existing direct-discovery OAuth flow
  • Always use AVAS for realtime WebRTC calls (#28856)
    ## Summary
    
    - Remove the realtime `architecture` selector from core protocol,
    app-server protocol, config parsing, generated schemas, and callers.
    - Always create WebRTC realtime calls with the AVAS query params:
    `intent=quicksilver&architecture=avas`.
    - Keep direct websocket realtime behavior on the existing config/default
    path, while WebRTC starts without an explicit version now default to
    realtime v1 because AVAS requires v1.
    
    ## Notes
    
    - WebRTC realtime now means AVAS. If a caller explicitly asks to start
    WebRTC with realtime v2, Codex rejects that request because the AVAS
    WebRTC path only supports realtime v1. Websocket realtime is separate
    and can still use realtime v2.
    - The old `[realtime] architecture = "realtimeapi" | "avas"` config knob
    is removed. Local configs that still set it will need to delete that
    line.
    - Some app-server tests that were only trying to exercise realtime v2
    protocol behavior now use websocket transport, because WebRTC is
    intentionally locked to AVAS/v1. Separate WebRTC tests cover the AVAS
    query params, v1 startup, SDP flow, and sideband join.
    
    ## Validation
    
    - Merged fresh `origin/main` at `83e6a786a2`.
    - `just fmt`
    - `just write-config-schema`
    - `just write-app-server-schema`
    - `git diff --check`
    - `just test -p codex-api -p codex-core -p codex-app-server-protocol -p
    codex-app-server realtime` (176 passed)
    - `just test -p codex-protocol -p codex-config` (413 passed)
  • core: load AGENTS.md from foreign environments (#28958)
    ## Why
    
    Make it possible to load AGENTS.md from remote exec-servers whose OS is
    different than app-server.
    
    ## What
    
    - keep `AGENTS.md` discovery and provenance as `PathUri`, with
    root-aware parent and ancestor traversal
    - expose lifecycle instruction sources as legacy app-server path strings
    in events while retaining `PathUri` internally
    - preserve and test mixed POSIX and Windows paths in model context and
    TUI status output
    - cover remote Windows loading end to end by seeding the Wine prefix
    through host filesystem APIs
    - fix bug in `PathUri`'s parent() implementation that would erase
    Windows drive letters
  • [codex] Preserve remote plugin download status errors (#28863)
    ## Summary
    
    - preserve the original HTTP status when a remote plugin bundle download
    returns a non-success response
    - retain at most 8 KiB of the error response body and annotate
    truncation or body-read failures
    - add regression coverage for an oversized error response
    
    ## Root cause
    
    The non-success response path reused the normal size-limited body
    reader. When an error response exceeded 8 KiB, that reader returned
    `DownloadTooLarge` before the code constructed `DownloadStatus`, masking
    the upstream HTTP status and response context.
    
    ## Impact
    
    Remote plugin installation failures now retain the actionable upstream
    HTTP status without allowing unbounded error bodies into logs.
    
    ## Validation
    
    - `just test -p codex-app-server
    plugin_install_preserves_status_when_remote_bundle_error_body_is_too_large`
    - `just fmt`
    - `git diff --check`
  • Emit Trusted MCP App Identity on Tool-Call Items (#27132)
    ## Summary
    
    - Add optional `appContext` to app-server MCP tool-call items with
    trusted `connectorId`, `linkId`, and `mcpAppResourceUri` metadata.
    - Preserve that context across tool-call events, persisted history,
    reconnects, and thread resume.
    - Keep the deprecated top-level `mcpAppResourceUri` temporarily for
    client migration.
    
    The consumer contract is `{ appContext: { connectorId, linkId,
    mcpAppResourceUri }, tool }`.
    
    ## Validation
    
    - Full GitHub Actions suite passes, including CLA, Bazel tests, clippy,
    release builds, and argument-comment lint.
    
    ---------
    
    Co-authored-by: martinauyeung-oai <280153141+martinauyeung-oai@users.noreply.github.com>
  • [codex] Remove hardcoded app ID filters (#28947)
    ## Summary
    
    - remove the duplicated originator-specific connector ID denylists
    - stop filtering connector directory/accessibility results and
    live/cached Codex Apps MCP tools by hardcoded connector ID
    - remove the now-unused `codex-login` dependency from
    `codex-utils-plugins`
    - update regression coverage so formerly blocked connector IDs are
    preserved
    
    ## Why
    
    The client-side policy was duplicated across crates, used opaque IDs
    without ownership or expiry information, and could drift between app
    listing and MCP tool behavior. Server-provided visibility,
    authorization, plugin discoverability, accessibility, enabled-state
    handling, and consequential-tool approval templates remain unchanged.
    
    ## Validation
    
    - `just fmt`
    - `just bazel-lock-update`
    - `just bazel-lock-check`
    - `git diff --check`
    - confirmed the final diff contains no hardcoded denylist symbols
    
    A targeted `codex-mcp` test build spent an unusually long time in local
    compilation/linking. Its first attempt exposed a test-only `PartialEq`
    assertion issue, which was corrected. A follow-up non-linking `cargo
    check -p codex-mcp --tests` was still running when this draft was
    opened; CI should provide the complete Rust validation.
  • Add app-server current-time impl (varlatency 3/n) (#28835)
    ## What
    
    Server should request:
    
    ```
    {
      "id": 42,
      "method": "currentTime/read",
      "params": {
        "threadId": "11111111-1111-1111-1111-aaaaafdc2c11"
      }
    }
    ```
    
    Client should respond with something like:
    
    ```rust
    {
      "id": 42,
      "result": {
        "currentTimeAt": 1781717655
      }
    }
    ```
    
    ## Why
    
    Sessions configured with `clock_source = "external"` need a
    thread-specific external time source before inference. The system clock
    remains the default production provider.
    
    ## Validation
    
    - `cargo test -p codex-app-server-protocol`
    - `cargo test -p codex-app-server --test all
    current_time_read_round_trip_adds_reminder_to_model_input`
    - `cargo test -p codex-app-server
    first_attestation_capable_connection_for_thread_only_uses_thread_subscribers`
    - `cargo test -p codex-analytics`
    - `just fix -p codex-app-server-protocol`
    - `just fix -p codex-app-server`
    
    Stacked on #28824.
  • Support openai/form extended form elicitations (#27500)
    # Summary
    Allow App Server clients to opt into `openai/form` MCP elicitations.
  • Synchronize realtime notification test requests (#28946)
    ## What
    
    Deliver the scripted realtime notification batch after the assistant
    text append request instead of after the preceding developer text append
    request.
    
    ## Why
    
    The batch ends with an upstream error that closes the realtime
    conversation. When it is emitted after the developer append, it races
    the subsequent assistant append: the app-server RPC can acknowledge the
    append before its downstream WebSocket send completes, and the test
    intermittently observes three requests instead of four.
    
    Making the fake server wait for the assistant append before emitting the
    terminal batch establishes the ordering the test asserts without sleeps
    or production-code changes.
    
    ## Validation
    
    - `git diff --check`
    - CI (the failure is timing-dependent and most reproducible in the
    Windows Bazel shard)
  • Fix goal-first live threads missing from thread/list (#28808)
    Fixes #28263.
    
    ## Why
    
    When a thread starts with `/goal`, the goal extension can update SQLite
    goal state before the thread has any user-turn rollout items.
    `thread/list` and `thread/search` rely on persisted listing metadata, so
    a goal-first live thread could be absent from app-server listings after
    restart even though the goal itself existed.
    
    This regressed when goal handling moved out of core: the core path wrote
    the goal update through the live thread rollout path, while the
    extension-backed app-server path only updated goal state and emitted the
    live notification.
    
    ## What
    
    - Add `GoalSetOutcome::thread_goal_updated_item()` so the goal extension
    owns the canonical `ThreadGoalUpdated` rollout item shape.
    - Expose a narrow `CodexThread::append_rollout_items()` helper that
    appends through the live thread and keeps derived SQLite metadata in
    sync.
    - When app-server sets a goal on an active live thread, persist the goal
    update through that live-thread path.
    - Add an app-server regression test that starts a live thread with
    `thread/goal/set` and verifies it appears in state-DB-only
    `thread/list`.
    
    ## Verification
    
    - `env -u CODEX_SQLITE_HOME just test -p codex-app-server
    goal_first_live_thread_appears_in_state_db_thread_list`
  • unified-exec: retain PathUri in command events (#28780)
    ## Why
    
    App-server must report command events containing foreign-platform paths
    without changing existing client or rollout path-string formats.
    
    ## What changed
    
    - retain `PathUri` through exec command begin/end events
    - convert cwd values to `LegacyAppPathString` at the app-server
    compatibility boundary
    - drop command actions with foreign paths and log them
    - serialize rollout-trace cwd values using their inferred native path
    representation
    - restore Wine coverage for retained Windows cwd values and successful
    completion
  • [codex] Support assistant realtime append text (#28836)
    ## Why
    
    Frontend realtime voice continuity needs to replay a tiny
    previous-session overlap as actual conversation items, including
    assistant text. The app-server `thread/realtime/appendText` API already
    carries a role through to the Rust realtime websocket layer, but the
    shared role enum only accepted `user` and `developer`.
    
    ## What Changed
    
    - Added `assistant` to `ConversationTextRole` and regenerated the
    app-server schema/type fixtures.
    - Added `output_text` as a realtime conversation content type.
    - Updated realtime websocket item creation so assistant appendText emits
    `content: [{ type: "output_text", text }]`, while user and developer
    continue to emit `input_text`.
    - Updated app-server docs and tests to cover assistant appendText
    alongside the existing developer role behavior.
    
    ## Validation
    
    - `just write-app-server-schema`
    - `just fmt` (first sandboxed attempt failed because `uv` could not
    access `~/.cache/uv`; reran with filesystem access and passed)
    - `just test -p codex-api` passed: 126/126
    - `just test -p codex-app-server-protocol` passed: 239/239, including
    generated JSON/TypeScript fixture checks
    - `just test -p codex-app-server` was started locally but stopped per
    request after unrelated local sandbox/Seatbelt failures (`sandbox-exec:
    sandbox_apply: Operation not permitted`) and one missing local `codex`
    binary failure; CI should be faster and more authoritative for the full
    suite.
  • [codex] control automatic realtime handoff delivery (#27986)
    ## What
    
    Built on the realtime speech-control plumbing merged in #27917.
    
    - Add optional `codexResponseHandoffPrefix` to `thread/realtime/start`.
    - Apply that prefix only to automatic V1 commentary sent through
    `conversation.handoff.append`; final answers remain unprefixed.
    - Add opt-in `clientManagedHandoffs`. When true, core suppresses
    automatic response handoffs and completion output so delivery is
    controlled by explicit client append APIs.
    - Preserve existing automatic behavior by default.
    `codexResponsesAsItems: true` continues to select item routing when
    client-managed mode is disabled.
    
    ## Why
    
    Voice clients need two delivery policies: automatic background context
    with silent commentary instructions and fully client-owned handoffs.
    Phase-aware prefixing keeps routine commentary silent without
    suppressing the final answer, while client-managed mode lets an app
    decide exactly which updates to append.
    
    ## Validation
    
    - `just fmt`
    - `cargo test -p codex-app-server-protocol
    serialize_thread_realtime_start`
    - `RUST_MIN_STACK=16777216 cargo test -p codex-core --test all
    conversation_handoff_persists_across_item_done_until_turn_complete`
    - `RUST_MIN_STACK=16777216 cargo test -p codex-app-server --test all
    webrtc_v1_client_managed_handoffs_disable_automatic_output`
    - `RUST_MIN_STACK=16777216 cargo test -p codex-app-server --test all
    webrtc_v1_final_automatic_handoff_omits_silent_prefix`
    - `cargo build -p codex-cli --bin codex`
    - Local Codex Apps compatibility check: 43 focused webview tests passed,
    and a live voice session routed through the source-built app-server.
    
    The explicit `RUST_MIN_STACK` avoids a macOS Tokio test-worker stack
    overflow seen with the default test environment.
  • [codex] Add optional IDs to response items (#28812)
    ## Why
    
    `ResponseItem` variants do not have a consistent internal ID shape: some
    variants carry required IDs, some carry optional IDs, and some cannot
    represent an ID at all. The existing fields also use inconsistent serde,
    TypeScript, and JSON-schema annotations. A single enum-level access path
    is needed before history recording can assign and retain IDs.
    
    This PR establishes that internal model only. It intentionally does not
    generate or serialize IDs; allocation and wire persistence are isolated
    in the stacked follow-up.
    
    ## What changed
    
    - Give every concrete `ResponseItem` variant an `Option<String>` ID
    field.
    - Apply the same internal-only annotations to every ID field:
    `#[serde(default, skip_serializing)]`, `#[ts(skip)]`, and
    `#[schemars(skip)]`.
    - Add `ResponseItem::id()` and `ResponseItem::set_id()` as the shared
    accessors.
    - Preserve IDs when history items are rewritten for truncation.
    - Adapt consumers that previously assumed reasoning and image-generation
    IDs were required.
    - Regenerate app-server schemas so the hidden fields are represented
    consistently.
    
    The serde catch-all `ResponseItem::Other` remains ID-less because it
    must remain a unit variant.
    
    ## Test plan
    
    - `cargo check --tests -p codex-core -p codex-api -p codex-rollout-trace
    -p codex-image-generation-extension`
    - `just test -p codex-protocol`
    - `just test -p codex-app-server-protocol`
    - `just test -p codex-api -p codex-rollout-trace -p
    codex-image-generation-extension`
    - `just test -p codex-core event_mapping`
  • [codex] Track plugin install and import telemetry failures (#28731)
    ## Summary
    - Track plugin install failures through the unified
    `codex_plugin_install_failed` event for local installs, remote install
    preflight failures, bundle failures, and remote catalog/backend
    failures.
    - Send classified `error_type` values in plugin install failure
    analytics instead of raw error strings.
    - Stop sending raw external-agent import errors in analytics while
    preserving raw failure details in app-facing import
    notifications/history.
    - Keep raw plugin/migration diagnostics in `tracing::warn!` logs.
    - Keep remote failure plugin names as the existing local placeholder
    (`unknown`) and remove the extra telemetry plugin-name override.
    - Change `ExternalAgentConfigImportParams.source` from a generated enum
    to `string | null`, with legacy `claudeCode` / `claudeCowork` inputs
    normalized to existing analytics values.
    
    ## Testing
  • [codex] Restore thread recency with compatible migration history (#28671)
    ## Summary
    
    - Revert #28655, restoring the thread `recencyAt` behavior introduced by
    #27910.
    - Move `threads_recency_at` to migration 0039 so it no longer collides
    with `external_agent_config_imports` at version 0038.
    - Repair databases that already applied the recency migration as version
    38 by moving the matching migration-history row to version 39 before
    SQLx validation. The current version-38 migration can then apply
    normally.
    
    ## Validation
    
    - `just test -p codex-state
    migrations::tests::repairs_recency_migration_that_was_applied_as_version_38`
    - `just test -p codex-state -p codex-rollout -p codex-thread-store -p
    codex-app-server-protocol -p codex-tui`: 3,439 passed; six TUI tests
    could not open the machine's existing read-only incident database at
    `~/.codex/sqlite/state_5.sqlite`.
    - `just fix -p codex-state`
    - `just fmt`
    - Verified that state migration versions are unique.
  • Scope command approvals by execution environment (#28738)
    ## Why
    
    Command approval cache keys included the command and working directory,
    but not the execution environment. An approval for `/workspace` locally
    could therefore be reused for the same command and path on an executor.
    
    ## What changed
    
    - Include the selected environment ID in shell and unified-exec approval
    cache keys.
    - Carry that ID through the normal command approval request so clients
    can show which environment is being approved.
    - Expose the environment through app-server as a required nullable
    `environmentId` and show it in the inline TUI approval prompt.
    - Keep older recorded approval events compatible when the environment is
    absent.
    
    For example, `echo ok` in local `/workspace` and `echo ok` in executor
    `/workspace` now produce different approval keys and separate prompts.
    
    ## Scope
    
    This PR does not change network approvals, Guardian review actions, MCP
    elicitation, full-screen TUI rendering, or environment-ID validation.
    Remote `shell_command` execution itself remains in #28722; this PR only
    makes its approval key environment-aware.
  • [ez][codex-rs] Support apps._default.default_tools_approval_mode (#27965)
    [from codex]
    
    ## Summary
    
    - add `default_tools_approval_mode` to `[apps._default]` and expose it
    through app-server v2 `config/read`
    - apply it after managed, per-tool, and per-app approval settings,
    before the built-in `auto` fallback
    - document the precedence, regenerate config/app-server schemas, and add
    unit plus end-to-end approval coverage
    
    ## Configuration
    
    ```toml
    [apps._default]
    default_tools_approval_mode = "prompt"
    ```
    
    The effective precedence is managed requirements, tool-specific
    `approval_mode`, app-specific `default_tools_approval_mode`,
    `apps._default.default_tools_approval_mode`, then `auto`.
    
    ## Test plan
    
    - `just write-config-schema`
    - `just write-app-server-schema`
    - `just write-app-server-schema --experimental`
    - `just test -p codex-core app_tool_policy`
    - `just test -p codex-core mcp_turn_metadata`
    - `just test -p codex-config`
    - `just test -p codex-app-server-protocol`
    - `just test -p codex-app-server config_read_includes_apps`
    - `just fix -p codex-config -p codex-core -p codex-app-server-protocol
    -p codex-app-server`
    - `just fmt`
  • app-server: keep the model cache warm (#28699)
    ## Why
    
    The app server is long-lived, but its shared model cache otherwise
    refreshes only when a caller needs it. Once the five-minute cache
    expires, starting a thread or calling `model/list` can wait for
    `/models` on the request path.
    
    Refresh the cache in the background before it expires so foreground
    callers normally use fresh local state.
    
    ## What changed
    
    - Start an app-server worker that refreshes models immediately and then
    every three minutes using the existing models-manager API.
    - Hold only a weak reference to the models manager between refreshes, so
    the worker does not extend its lifetime.
    - Stop scheduling refreshes when the app-server lifecycle handle is shut
    down or dropped. A refresh already in progress is allowed to finish.
    - Adjust affected app-server test fixtures to distinguish the background
    `/models` probe from the connection they are testing.
    
    The existing models-manager cache, refresh strategies, auth handling,
    ETag behavior, and concurrency semantics are unchanged.
    
    ## Testing
    
    -
    `models_refresh_worker::tests::refreshes_immediately_periodically_and_stops_when_dropped`
    -
    `suite::v2::remote_control::listen_off_honors_persisted_remote_control_enable`
    -
    `suite::v2::attestation::attestation_generate_round_trip_adds_header_to_responses_websocket_handshake`