Compare commits

..

184 Commits

Author SHA1 Message Date
Cooper Gamble
baf9188c93 [codex-app-server] declare HTTP state dependency at first use [ci changed_files] 2026-06-03 10:12:34 +00:00
Cooper Gamble
81ccd4cbf2 [codex-app-server] expose generic per-surface HTTP state bridge [ci changed_files] 2026-06-03 09:58:07 +00:00
Cooper Gamble
bf1b28e1c1 [codex-app-server] test stable native integrity state bridge [ci changed_files] 2026-06-03 09:57:46 +00:00
Cooper Gamble
f6c9720d9f [codex-app-server] stabilize native integrity state bridge [ci changed_files] 2026-06-03 09:57:46 +00:00
Cooper Gamble
eacd9bc5c4 [codex-app-server] add native integrity state bridge [ci changed_files] 2026-06-03 09:57:36 +00:00
Cooper Gamble
bc27ae4419 [codex-http-state] add Bazel crate target [ci changed_files] 2026-06-03 09:56:58 +00:00
Cooper Gamble
34cb91c747 [codex-http-state] keep per-surface store protocol agnostic [ci changed_files] 2026-06-03 09:56:58 +00:00
Cooper Gamble
a9a1d9ddd3 [codex-http-state] extract generic per-surface HTTP state store [ci changed_files] 2026-06-03 09:56:58 +00:00
Cooper Gamble
0321ad1486 [codex-client] add native integrity state store [ci changed_files] 2026-06-03 09:56:58 +00:00
Shijie Rao
d36a3ead3c revert: publish release symbol artifacts (#25988)
revert https://github.com/openai/codex/pull/25916 and
https://github.com/openai/codex/pull/25649.
2026-06-02 17:34:04 -07:00
Anton Panasenko
98a62a62ce feat(app-server): add remote control client management RPCs (#25785)
## Why

Remote-control clients need to list and revoke controller-device grants
without enabling or enrolling the local relay. These are signed-in
account-management operations, so coupling them to websocket, pairing,
enrollment, or persisted relay state would prevent clients from managing
stale grants from the picker.

Related enhancement request: N/A. This adds the Codex app-server surface
for the planned upstream environment-scoped revoke endpoint.

## What Changed

- Added experimental app-server v2 RPCs:
  - `remoteControl/client/list`
  - `remoteControl/client/revoke`
- Added picker-oriented protocol types and standard generated schema
fixtures. The list response intentionally omits backend account id,
enrollment status, and location fields.
- Added `app-server-transport/src/transport/remote_control/clients.rs`
for environment-scoped GET and DELETE requests. It builds escaped URL
path segments, forwards optional pagination query fields, sends ChatGPT
auth plus `chatgpt-account-id`, converts RFC3339 `last_seen_at` values
to Unix seconds, accepts `204 No Content` revoke responses, and retries
once after a `401`.
- Extracted shared ChatGPT auth loading and recovery into
`app-server-transport/src/transport/remote_control/auth.rs` so
websocket, pairing, and client management use the same account-auth
boundary.
- Retained the configured remote-control base URL on
`RemoteControlHandle` and resolve management URLs lazily, preserving
deferred validation while relay startup is disabled.
- Registered list as `global_shared_read("remote-control-clients")` and
revoke as `global("remote-control-clients")`.

## Verification

- Added transport coverage proving list and revoke work while relay
state is disabled, IDs are escaped, picker-only fields are returned,
timestamps are converted, revoke accepts `204`, auth headers are
forwarded, `401` retries exactly once, `403` is not retried, and
malformed list payloads retain decode context.
- Added an app-server integration test proving both JSON-RPC methods
work before relay enablement and successful revoke returns `{}`.
- Regenerated and validated experimental and standard app-server schema
fixtures.
2026-06-02 17:01:02 -07:00
joeflorencio-openai
1fd2a6d328 Allow EDU accounts to fetch cloud config bundles (#25963)
## Summary

Allow EDU ChatGPT workspaces to fetch cloud config bundles. The existing
cloud config eligibility gate only allowed business-like and enterprise
plans, which meant EDU admins could configure managed policies in the UI
but the Codex client would skip fetching them.

This keeps individual/pro and team-like usage-based plans excluded, and
adds service-level coverage for both `edu` and `education` plan aliases.

## Validation

- `just fmt`
- `just test -p codex-cloud-config`
- Built the Codex app locally, created a new EDU ChatGPT workspace, and
verified config bundles can be fetched and are properly applied.
2026-06-02 16:41:48 -07:00
jif
271d5cecf2 feat: add extension turn-input contributors (#25959)
## Disclaimer
Do not use for now

## Why

Extensions can already contribute prompt fragments and request same-turn
item injection, but there was no host-owned hook for contributing
structured `ResponseItem`s while Codex is assembling a new turn's
initial model input. This change adds that seam so extensions can attach
turn-local input that depends on the submitted user input and resolved
turn environments without routing through prompt text or late injection.

## What changed

- add `TurnInputContributor` to `codex_extension_api` and export the new
`TurnInputContext` / `TurnInputEnvironment` types it receives
- teach `ExtensionRegistry` to register and expose turn-input
contributors alongside the existing extension hooks
- call registered turn-input contributors from
`core/src/session/turn.rs` while building the initial injected input for
a turn, then append their returned `ResponseItem`s after the skill and
plugin injections
2026-06-03 01:33:31 +02:00
Michael Bolin
a28b32a835 config: express implicit sandbox defaults as permission profiles (#25926)
## Why

`PermissionProfile` is becoming the default way to represent Codex
permissions, but the implicit default behavior should stay the same for
now:

- trusted projects use `:workspace`
- untrusted projects also use `:workspace`
- roots without a trust decision use `:read-only`
- unsandboxed Windows falls back to `:read-only`

This keeps the existing sandbox semantics while making silent config
defaults observable as built-in permission profiles instead of treating
the legacy `SandboxPolicy` projection as the primary shape.

## What Changed

- Refactored legacy sandbox derivation to resolve the configured sandbox
mode once, then apply the implicit project fallback only when no sandbox
mode was configured.
- Preserved the existing trust-decision fallback: trusted and untrusted
projects default to workspace-write where supported.
- Added empty-config coverage asserting that an untrusted project
resolves to the built-in active permission profile (`:workspace` outside
unsandboxed Windows).

## Verification

- `just fmt`
- `just test -p codex-core 'config::'`
- `just test -p codex-config`

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/25926).
* __->__ #25926
2026-06-02 16:26:36 -07:00
Adam Perry @ OpenAI
6471f8b31a [codex] Fix Windows BuildBuddy Bazel wrapper execution (#25915)
## Why

#25156 moved Bazel CI launches into a shared Python wrapper. On Windows,
launching Bazel with `os.execvp` can split the spaced
`--test_env=PATH=...` argument and fail to propagate the eventual Bazel
exit status, allowing jobs to pass without running tests. This reapplies
the wrapper after #25909 with a Windows-safe launch path.

## What changed

Use a waited `subprocess.run` launch on Windows while preserving
`os.execvp` on Unix. Add a process-level regression test for spaced
arguments and child exit status, and run it on Windows Bazel shard 1.

## Experiment

To confirm Bazel was actually invoking tests, patch `87b61d0be6`
temporarily added an intentionally failing `codex-core` unit test. Bazel
failed on that sentinel on all three major platforms:

- [Linux Bazel
test](https://github.com/openai/codex/actions/runs/26841132773/job/79151062486)
- [macOS Bazel
test](https://github.com/openai/codex/actions/runs/26841132773/job/79151062362)
- [Windows Bazel test shard
1/4](https://github.com/openai/codex/actions/runs/26841132773/job/79151062155)

The sentinel was removed after collecting this evidence. Windows Bazel
[clippy](https://github.com/openai/codex/actions/runs/26841132773/job/79151062914)
and [release
verification](https://github.com/openai/codex/actions/runs/26841132773/job/79151062739)
also passed.

## Validation

After removing the sentinel, `just test -p codex-core` no longer
reported it. The local run retained two unrelated environment-specific
failures.
2026-06-02 16:22:32 -07:00
jif
2d385e166c feat: add skills extension scaffold (#25953)
## Disclaimer
This is only here for iteration purpose! Do not make any code rely on
this

## Why

Skills still live behind `codex-core` discovery and injection paths, but
the extension system needs an authority-aware home before that logic can
move. This adds that boundary without changing current skills behavior,
and keeps host, executor, and remote skills distinct so future
list/read/search flows do not collapse back to ambient local paths.

## What changed

- Add the `codex-skills-extension` workspace/Bazel crate under
`ext/skills`.
- Define the initial catalog, authority, provider, and turn-state types
for authority-bound skill packages and resources.
- Register placeholder thread/config/prompt/turn lifecycle contributors
plus host, executor, and remote provider aggregation points.
- Capture the remaining extraction work as TODOs, including the missing
extension API hooks needed for per-turn catalog construction and typed
skill injection.
- Keep plugins outside the runtime skills model: plugin-installed skills
are treated as materialized host-owned skill sources once available.

## Verification

- Not run locally.
2026-06-03 01:10:26 +02:00
Ahmed Ibrahim
34dc08c214 [codex] Publish Python runtime wheels with Python SDK releases (#25906)
## Summary
- stop publishing Python runtime wheels as a side effect of Rust
releases
- publish runtime wheels from the Python SDK release workflow, either
explicitly before updating the SDK pin or immediately before a
`python-v*` SDK release
- resolve the runtime release from the requested version or the SDK
package's exact `openai-codex-cli-bin` pin
- build two musllinux-tagged wheels from the Rust-release Linux package
archives alongside the six existing runtime wheels
- validate SDK beta tags before any PyPI write

## Release configuration
- update the `openai-codex-cli-bin` PyPI trusted publisher to trust
`.github/workflows/python-sdk-release.yml` and the
`publish-python-runtime` job

## Pin update flow
- run the `python-sdk-release` workflow manually with the new runtime
version before opening or updating the SDK pin PR
- after the pin lands, a `python-v*` SDK tag republishes with
`skip-existing: true` before publishing the SDK package

## Validation
- ran `just fmt`
- validated the edited workflow YAML
- validated the embedded `publish-python-runtime` Bash with `bash -n`
- validated manual `0.136.0 -> rust-v0.136.0` mapping
- validated tag-driven `python-v0.1.0b3 -> 0.132.0 -> rust-v0.132.0`
mapping
- validated rejection of an invalid SDK tag before publication
- confirmed `rust-v0.136.0` contains the two required Linux package
archives
- CI will provide the full test signal
2026-06-02 15:41:53 -07:00
Won Park
bec21c7114 Expose standalone image generation in code mode (#25923)
## Why

Standalone image generation remained top-level-only in code-mode
sessions.

## What changed

- Change imagegen exposure from `DirectModelOnly` to `Direct`.
- Keep direct-mode access while enabling nested code-mode access.
- Add a focused regression test for the exposure contract.

## Validation

- `just test -p codex-image-generation-extension`
2026-06-02 22:27:52 +00:00
Shijie Rao
f752b25fc4 Revert "Use environment secrets for Azure signing" (#25948)
Reverts openai/codex#24859
2026-06-02 15:12:07 -07:00
Michael Bolin
c6d76750e8 config: remove dead profile sandbox fallback (#25943)
## Why

`profile_sandbox_mode` was left over from the old selected legacy
profile path. Production now always derives permissions without that
value, and legacy profile contents are ignored, so keeping a parameter
that is always `None` makes `derive_permission_profile` look like it
still supports a fallback that no longer exists.

## What Changed

- Removed the `profile_sandbox_mode` argument from
`ConfigToml::derive_permission_profile`.
- Updated the production caller and legacy sandbox-policy test helper to
match.
- Dropped the stale unselected legacy-profile sandbox test that only
protected the removed fallback shape.

## Verification

- `just test -p codex-config`
- `just test -p codex-core 'config::'`


---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/25943).
* #25926
* __->__ #25943
2026-06-02 22:05:04 +00:00
jif
d55e5a9bde Add remote request permissions integration coverage (#25867)
## Stack

1. #25850 - Key request-permission grants by environment: stores and
applies sticky permission grants per environment id.
2. #25858 - Add `environmentId` to `request_permissions`: lets the model
target a selected environment and resolves relative permission paths
against it.
3. #25862 - Propagate permission approval environment id: carries the
selected environment id through approval events, app-server requests,
TUI prompts, and delegate forwarding.
4. This PR (#25867) - Add remote request permissions integration
coverage: verifies the selected remote environment across request,
approval, grant reuse, and exec.

This PR is stacked on #25862 and should be reviewed after #25850,
#25858, and #25862.

## Why

The environment-scoped permission stack needs one end-to-end check that
exercises the CCA-shaped path, not only unit-level parsing. This
verifies that a model-sent `environmentId` on `request_permissions`
reaches the approval event, stores the grant under the selected
environment, and is reused by a later tool call in that same
environment.

## What Changed

- Adds a remote executor integration test for `request_permissions` with
`environmentId: remote` and a relative write root.
- Asserts the permission event reports the remote environment and cwd,
and that the normalized grant resolves under the remote cwd.
- Approves the grant, then runs a remote `exec_command` without explicit
per-call permissions and verifies it completes without another exec
approval and writes only in the remote filesystem.

## Verification

- Not run locally per instruction.
- `git diff --check`
2026-06-02 23:55:08 +02:00
Ahmed Ibrahim
68e2c8ed69 [codex] Keep hosted tools visible in code-only mode (#25890)
## Why

`code_mode_only` moved ordinary runtime tools behind `exec`, but it also
hid hosted Responses tools. Hosted `web_search` and `image_generation`
do not have a nested `exec` runtime path, so code-only sessions lost
those capabilities entirely even when their existing provider, auth,
model, and configuration gates passed.

## What changed

- Keep hosted Responses tools top-level in `code_mode_only` sessions
after their existing gates pass.
- Preserve the existing nested-tool behavior for ordinary runtimes and
the direct-only behavior for multi-agent v2 tools.
- Add planner coverage for `code_mode_only` with default multi-agent v2
settings, hosted live web search, and hosted image generation.

## Verification

- Added focused regression coverage in
`codex-rs/core/src/tools/spec_plan_tests.rs`.
- Left execution to CI per repository workflow.
2026-06-02 14:50:16 -07:00
joeflorencio-openai
e7039f9844 Split cloud config bundle service modules (#25668)
## Summary

- Splits the monolithic `codex-cloud-config` implementation into focused
modules.
- Keeps behavior unchanged from the preceding config bundle runtime
switch.

## Details

This is the reviewability follow-up after the lineage-preserving
migration PRs. The split separates backend transport, loader
construction, cache handling, metrics, validation, service
orchestration, and focused tests into named files.

Verification: `just fmt`; `just test -p codex-cloud-config`.
2026-06-02 14:30:12 -07:00
Michael Bolin
f6d64bd6ab core: stop passing legacy SandboxPolicy to guardian reviews (#25911)
## Why

Guardian review turns already submit a read-only `PermissionProfile`,
which is the permissions model the runtime should honor. Passing the
equivalent legacy `SandboxPolicy` through `ThreadSettingsOverrides`
keeps two representations of the same read-only constraint alive on this
path and makes the guardian flow depend on compatibility plumbing that
is being phased out.

## What Changed

- Set `sandbox_policy` to `None` when the guardian review session
submits its child `Op::UserInput`.
- Keep `permission_profile: Some(PermissionProfile::read_only())` and
`approval_policy: Some(AskForApproval::Never)`, so the guardian review
remains read-only and cannot request approvals.
- Remove the now-unused `SandboxPolicy` import and redundant comment
from `codex-rs/core/src/guardian/review_session.rs`.

## Verification

Not run locally; this is a narrow cleanup of redundant thread-settings
override state.
2026-06-02 14:16:12 -07:00
joeflorencio-openai
c74be11672 fix: update image generation test helper rename (#25938)
## Summary
- update the app-server image generation integration test to use
`TestAppServer`
- completes the test helper rename from #25701 for this newer test file

## Validation
- `cargo fmt -- --config imports_granularity=Item`
- `cargo check -p codex-app-server --test all`

Note: `just fmt` ran Rust formatting but failed on Python/SDK formatting
because the sandbox could not access the local `uv` cache.
2026-06-02 14:11:20 -07:00
joeflorencio-openai
d45cd26248 Switch runtime to cloud config bundle (#24622)
## Summary

- Adapts the moved `codex-cloud-config` crate from the legacy cloud
requirements endpoint to the new config bundle endpoint.
- Switches runtime consumers from `CloudRequirementsLoader` to
`CloudConfigBundleLoader` so one shared bundle supplies cloud-delivered
config and requirements.
- Removes the legacy cloud requirements domain loader path.

## Details

This intentionally keeps `codex-cloud-config` monolithic for review
lineage: the previous PR establishes the crate move, and this PR shows
the behavior change against that moved implementation. A follow-up PR
splits the module back into focused files.

The new bundle path preserves the important cloud requirements loader
semantics where intended: account-scoped signed cache, 30 minute TTL, 5
minute refresh cadence, retry/backoff, auth recovery, and fail-closed
startup loading. The cached payload changes from a single requirements
TOML string to the backend-delivered bundle, and validation rejects
malformed config or requirements fragments before cache write/use.
2026-06-02 13:18:59 -07:00
knittel-openai
b794182ea7 Populate workspace kind on Codex turn events (#25135)
## Summary
- carry `workspace_kind` from Responses API client metadata into the
turn resolved analytics fact
- serialize the optional value on `codex_turn_event`
- cover both the turn metadata source and turn event serialization

The `workspace_kind` tells us whether a thread had a project attached vs
projectless. this is an indicator for who is adopting Codex for
knowledge work outside of coding

## Testing
- `env UV_CACHE_DIR=/private/tmp/uv-cache
/private/tmp/cargo-tools/bin/just fmt`
- `env PATH=/private/tmp/cargo-tools/bin:$PATH
CARGO_HOME=/private/tmp/cargo-home UV_CACHE_DIR=/private/tmp/uv-cache
/private/tmp/cargo-tools/bin/just test -p codex-analytics`
- `env PATH=/private/tmp/cargo-tools/bin:$PATH
CARGO_HOME=/private/tmp/cargo-home UV_CACHE_DIR=/private/tmp/uv-cache
/private/tmp/cargo-tools/bin/just test -p codex-core turn_metadata`

Paired with openai/openai#970661, which keeps forwarding the same
metadata key through Responses API headers.
2026-06-02 12:46:14 -07:00
Eric Traut
ad355d4c96 Fix Windows running thread resume path normalization (#25509)
## Why

Fixes #24944.

On Windows, app-server resume could reject an active running thread when
the requested session path used normal `C:\...` form and the
already-running path used verbatim `\\?\C:\...` form. The paths point at
the same JSONL file, but the resume stale-path guard compared raw
`PathBuf`s, so desktop resume and heartbeat flows could fail with a
mismatched-path error.

## What Changed

- Compare requested and active rollout paths with
`path_utils::paths_match_after_normalization`.
- Extend the existing running-thread mismatched-path test with a
Windows-only same-file resume case before the stale-path rejection.

## Verification

- `just test -p codex-app-server
thread_resume_rejects_mismatched_path_for_running_thread_id`
2026-06-02 12:42:42 -07:00
Shijie Rao
af18e92140 Use environment secrets for Azure signing (#24859)
## Summary
- Move Azure Trusted Signing values out of reusable workflow-call
secrets and into the `azure-artifact-signing` environment scope
- Attach the Windows signing job to the `azure-artifact-signing`
environment so it can resolve the signing secrets directly
- Stop inheriting caller secrets for the Windows release reusable
workflow

## Validation
- `git diff --check -- .github/workflows/rust-release.yml
.github/workflows/rust-release-windows.yml`
- `ruby -e 'require "yaml"; ARGV.each { |path| YAML.load_file(path);
puts "ok #{path}" }' .github/workflows/rust-release.yml
.github/workflows/rust-release-windows.yml`
2026-06-02 12:41:13 -07:00
Ahmed Ibrahim
bc49677ec8 [codex] Pin Python SDK to glibc-compatible runtime (#25907)
## Summary
- pin the Python SDK runtime package to `openai-codex-cli-bin==0.136.0`
so Ubuntu/glibc installs resolve a compatible wheel
- refresh generated SDK artifacts and lock data for the runtime update
- keep newly generated client-message-id wire models internal to the
generated protocol layer

## Dependency
- merge #25906 first so the Python SDK release publishes both manylinux
and musllinux runtime wheels before publishing the package with this pin

## Validation
- ran `just fmt`
- regenerated the Python public API helpers
- validated the edited workflow YAML
- CI passed 29/29 checks
2026-06-02 12:27:01 -07:00
jif
9de568372d Propagate permission approval environment id (#25862)
## Stack

1. #25850 - Key request-permission grants by environment: stores and
applies sticky permission grants per environment id.
2. #25858 - Add `environmentId` to `request_permissions`: lets the model
target a selected environment and resolves relative permission paths
against it.
3. This PR (#25862) - Propagate permission approval environment id:
carries the selected environment id through approval events, app-server
requests, TUI prompts, and delegate forwarding.
4. #25867 - Add remote request permissions integration coverage:
verifies the selected remote environment across request, approval, grant
reuse, and exec.

This PR is stacked on #25858, and #25867 is stacked on this PR.

## Why

PR2 lets the model bind a `request_permissions` call to a selected
environment, but the approval event and client-facing request still
needed to carry that binding. For CCA, the user-facing prompt and
delegated approval path should know which environment the grant applies
to instead of relying on cwd alone.

## What Changed

- Added optional `environmentId` to `RequestPermissionsEvent`.
- Emit the selected environment id from core permission approval events.
- Preserve the environment id through delegate forwarding, including
cwd-based delegated requests.
- Added `environmentId` to app-server permission approval params,
generated schema/TypeScript artifacts, and README examples.
- Preserve and display the environment id in TUI permission approval
prompts.
- Updated focused core, app-server protocol, and TUI conversion
coverage.

## Testing

Not run locally per instruction. Performed read-only `git diff --check`.
2026-06-02 21:09:34 +02:00
Shijie Rao
de124c32be Fix Windows release PDB staging (#25916)
## Summary
- Teach the Windows release prebuild staging step to locate Rust/MSVC
PDBs emitted with crate-style underscore names.
- Stage PDBs under the shipped hyphenated binary names so the downstream
symbol archive step keeps the same artifact contract.
- Keep a fallback for already-hyphenated PDB names and fail with a clear
diagnostic if neither form exists.

## Root cause
The recent symbol publishing change in #25649 started copying
`${binary}.pdb` from `target/<triple>/release` during Windows prebuild
staging. Cargo still emits the `.exe` with the hyphenated binary name,
but MSVC PDBs for hyphenated Rust crates are emitted with underscores,
for example `codex_app_server.pdb` for `codex-app-server.exe`. The
release workflow was still building into the expected directory; the new
PDB copy step was looking for the wrong filename.

## Impact
This unblocks the `rust-release` Windows prebuilt-binary jobs for
hyphenated binaries while preserving the hyphenated PDB names consumed
by the final Windows release packaging and symbol archive steps.

## Validation
- `just fmt` from `codex-rs`
- `git diff --check -- .github/workflows/rust-release-windows.yml`
- Parsed `.github/workflows/rust-release-windows.yml` as YAML locally
- Local bash staging sanity test for both underscore-emitted and
hyphenated PDB filenames
2026-06-02 12:05:52 -07:00
Won Park
57f337a8e9 Route standalone image generation through host finalization md (#25176)
## Why

Standalone image-generation extensions emitted turn items through the
low-level event path, bypassing host-owned finalization such as image
persistence and contributor processing. At the same time, the
generated-image save-path hint must remain visible to the model through
the extension tool's `FunctionCallOutput`, rather than the legacy
built-in developer-message path.

## What changed

- Extended `ExtensionTurnItem` to support image-generation items while
keeping the extension-facing emitter API limited to `emit_started` and
`emit_completed`.
- Routed extension completion through core `finalize_turn_item`, so
standalone image-generation items receive host-owned processing and
persisted `saved_path` values before publication.
- Kept legacy built-in image generation on its existing
developer-message hint path, while standalone image generation returns
its deterministic saved-path hint in `FunctionCallOutput`.
- Shared the image artifact path and output-hint formatting used by core
and the image-generation extension.
- Passed thread identity through extension tool calls so standalone
image generation can construct the same intended artifact path as core.
- Added an app-server integration test covering real standalone image
generation, saved artifact publication, model-visible output hint
wiring, and absence of the legacy developer-message hint.

## Validation

- `just fmt`
- `just test -p codex-image-generation-extension`
- `just test -p codex-web-search-extension`
- `just test -p codex-goal-extension`
- `just test -p codex-memories-extension`
- Targeted `codex-core` tests for image save history, extension
completion finalization, and contributor execution
- `just test -p codex-app-server
standalone_image_generation_returns_saved_path_hint_to_model`
- `just fix -p codex-core`
- `just fix -p codex-image-generation-extension`
- `just bazel-lock-update`
- `just bazel-lock-check`
2026-06-02 12:00:04 -07:00
jif
e29071e4c9 Add environmentId to request_permissions (#25858)
## Stack

1. #25850 - Key request-permission grants by environment: stores and
applies sticky permission grants per environment id.
2. This PR (#25858) - Add `environmentId` to `request_permissions`: lets
the model target a selected environment and resolves relative permission
paths against it.
3. #25862 - Propagate permission approval environment id: carries the
selected environment id through approval events, app-server requests,
TUI prompts, and delegate forwarding.
4. #25867 - Add remote request permissions integration coverage:
verifies the selected remote environment across request, approval, grant
reuse, and exec.

This PR is stacked on #25850; #25862 and #25867 are stacked on this PR.

## Why

PR1 made request-permission grants internally environment-keyed, but the
model-facing `request_permissions` tool could still only target the
primary environment. For CCA and multi-environment turns, the tool needs
an explicit way to bind a permission request to a selected attached
environment before resolving relative paths.

## What Changed

- Added optional `environmentId` to `RequestPermissionsArgs`, with
`environment_id` accepted as an alias.
- Exposed `environmentId` in the `request_permissions` tool schema and
description.
- Resolve the selected environment before parsing filesystem permission
paths, so relative paths bind to the selected environment cwd.
- Route validated tool calls through
`request_permissions_for_environment` directly instead of duplicating
environment lookup in `Session::request_permissions`.
- Reject unknown environment ids with a model-facing error.
- Updated focused request-permissions and Guardian call sites for the
new optional field.

## Testing

Not run locally per instruction.
2026-06-02 20:51:25 +02:00
rhan-oai
8e4b92d294 [codex-analytics] Track CodexErr details in turn analytics (#25707)
## Summary
- add analytics-only `CodexErr` telemetry to `codex_turn_event` while
leaving existing `turn_error` unchanged
- record terminal `CodexErr` facts from core immediately before the
existing turn error event is sent
- emit source-truth `codex_error_*` fields for downstream analytics,
including the raw `CodexErr::InvalidRequest(String)` message as
`codex_error_subreason`

## Validation
- `just test -p codex-analytics`
- attempted `just test -p codex-core`, but the local run timed out
across unrelated integration suites in this environment and is not being
used as validation
2026-06-02 11:40:35 -07:00
jif
503ec190a8 Key request-permission grants by environment (#25850)
## Stack

1. This PR (#25850) - Key request-permission grants by environment:
stores and applies sticky permission grants per environment id.
2. #25858 - Add `environmentId` to `request_permissions`: lets the model
target a selected environment and resolves relative permission paths
against it.
3. #25862 - Propagate permission approval environment id: carries the
selected environment id through approval events, app-server requests,
TUI prompts, and delegate forwarding.
4. #25867 - Add remote request permissions integration coverage:
verifies the selected remote environment across request, approval, grant
reuse, and exec.

#25858, #25862, and #25867 are stacked on this PR and should be reviewed
after it.

## Why

Multi-environment CCA turns can attach both local and remote executors,
but request-permission grants were still effectively cwd-only. Pending
permission requests tracked a cwd, while stored turn/session grants had
no environment identity, so sticky grants could be reused through the
wrong executor context.

This makes the first permission-grant step environment-aware without
changing the external `request_permissions` payload shape: omitted
environment targeting remains bound to the primary turn environment.

## What Changed

- Store turn- and session-scoped request-permission grants by
`environment_id`.
- Keep the selected `TurnEnvironmentSelection` with pending
`request_permissions` calls so approval responses normalize and record
grants against the same environment.
- Resolve relative `request_permissions` file paths against the primary
turn environment cwd instead of deprecated `turn.cwd`.
- Apply sticky grants in `shell`, `exec_command`, and `apply_patch` by
selected environment id while still using the actual tool cwd for
cwd-relative permission materialization.
- Update Guardian and request-permissions coverage for the
environment-keyed grant behavior.

## Testing

Not run locally. Added or updated focused coverage for:

- `request_permission_grants_are_environment_keyed`
-
`request_permissions_tool_resolves_relative_paths_against_primary_environment`
- related Guardian/request-permissions sticky grant tests
2026-06-02 20:16:57 +02:00
Adam Perry @ OpenAI
9e3d5f29e2 [codex] Revert shared BuildBuddy Bazel wrapper (#25909)
## Why

PR #25905 intentionally adds a failing `codex-core` unit test, but its
[Bazel test on Windows
check](https://github.com/openai/codex/actions/runs/26837526950/job/79135369259)
passed. That shows the Bazel configuration introduced by #25156 is not
behaving as expected, so revert it while the configuration can be
investigated separately.

## What changed

Revert #25156 in full, restoring the previous Bazel remote
configuration, CI scripts, workflows, `rusty_v8` handling, and
documentation. This removes the shared BuildBuddy wrapper and its tests.

## Validation

Not run locally; this exact revert was prioritized for a fast rollback.
2026-06-02 11:06:01 -07:00
Michael Bolin
593df8773d core: derive built-in permission profiles from raw policies (#25739)
## Why

Permission profiles that extend a built-in profile should behave like
other TOML inheritance: parent entries provide defaults, and child keys
override matching fields before the profile is compiled.

That was not true for `:workspace`. Previously, a profile with `extends
= ":workspace"` seeded the compiled runtime
`PermissionProfile::workspace_write()` policy and then appended child
filesystem entries. A child override such as `":tmpdir" = "read"`
therefore left the inherited `":tmpdir" = "write"` entry in the final
policy. Since same-target `write` wins over `read` during runtime
resolution, the child override was ineffective.

This also needs a clear source of truth for the built-in profiles. The
protocol-level sandbox policy constructors now define the raw built-in
filesystem entries, and both `PermissionProfile` presets and
config-profile inheritance derive from those same values.

## What Changed

- Add a canonical `FileSystemSandboxPolicy::read_only()` constructor
while keeping the read-only and workspace-write raw filesystem entries
explicit and independent.
- Derive `PermissionProfile::read_only()` from
`FileSystemSandboxPolicy::read_only()`;
`PermissionProfile::workspace_write()` continues to derive from
`FileSystemSandboxPolicy::workspace_write()`.
- Build extensible `:read-only` and `:workspace` parent profiles by
projecting those canonical sandbox policies into
`PermissionProfileToml`, then merge user overrides at the TOML layer
before compilation.
- Add config parsing support for `:slash_tmp` so the built-in
`:workspace` parent can be expressed in the same TOML-shaped filesystem
table as user profiles.
- Document that `PermissionsToml::resolve_profile()` returns an
already-merged `PermissionProfileToml`, and return that profile directly
after removing the resolved-profile wrapper.
- Extend the config test for `extends = ":workspace"` to assert that
inherited `":slash_tmp" = "write"` is preserved and that a child
`":tmpdir" = "read"` entry replaces the inherited `write` entry.

## Verification

- `just test -p codex-config`
- `just test -p codex-protocol`
- `just test -p codex-core
permissions_profiles_resolve_extends_parent_first_with_child_overrides`
- `just test -p codex-core
default_permissions_profile_can_extend_builtin_workspace`
- `just test -p codex-core`
  - Result: 2596 passed, 4 failed, 1 timed out.
- The failures were existing sandbox/environment-sensitive tests
unrelated to this permissions change:

`suite::user_shell_cmd::user_shell_command_does_not_set_network_sandbox_env_var`,

`suite::user_shell_cmd::user_shell_command_history_is_persisted_and_shared_with_model`,

`suite::abort_tasks::interrupt_persists_turn_aborted_marker_in_next_request`,
    `suite::abort_tasks::interrupt_tool_records_history_entries`, and

`thread_manager::tests::start_thread_uses_all_default_environments_from_codex_home`.
2026-06-02 10:57:35 -07:00
Adam Perry @ OpenAI
ebb7980369 Route Bazel CI through shared BuildBuddy remote config wrapper (#25156)
## Why

Bazel remote configuration was selected in several CI scripts and
workflow steps. That made the BuildBuddy tenant policy easy to duplicate
and harder to audit, especially for fork pull requests that must not use
the OpenAI tenant.

This builds on
[sluongng/buildbuddy-ci-host-routing](https://github.com/openai/codex/compare/main...sluongng:codex:sluongng/buildbuddy-ci-host-routing)
and consolidates the policy in one place.

## What to do if this breaks you

See `codex-rs/docs/bazel.md` for details. TLDR:

1. make a BuildBuddy API key and put it in `~/.bazelrc`
2. if you're an OpenAI employee, add `common
--config=buildbuddy-openai-rbe` to `user.bazelrc` in the repo root

Run `just bazel-test` to ensure it works.

Note that `just bazel-remote-test` no longer exists, you need to select
a remote configuration as documented to use RBE.

## What changed

- Add `.github/scripts/run_bazel_with_buildbuddy.py` as the shared Bazel
wrapper and Python library. It selects the OpenAI host only for trusted
upstream GitHub Actions runs, routes keyed fork runs to the generic
host, and falls back to local Bazel execution when no key is available.
- Move endpoint selection into explicit `.bazelrc` configurations and
update Bazel CI, query helpers, and `rusty_v8` staging to use the shared
policy. Loading-phase target-discovery queries remain local.
- Add wrapper and `rusty_v8` unit coverage, plus `just test-scripts` for
the `.github/scripts` Python tests.
- Document local Bazel usage, `user.bazelrc` setup, BuildBuddy
configurations, and CI behavior in `codex-rs/docs/bazel.md`.

## Validation

- `just test-scripts`
- `bash -n .github/scripts/run-bazel-ci.sh
.github/scripts/run-bazel-query-ci.sh
.github/scripts/run-argument-comment-lint-bazel.sh
scripts/list-bazel-clippy-targets.sh`
- `python3 -m py_compile .github/scripts/run_bazel_with_buildbuddy.py
.github/scripts/test_run_bazel_with_buildbuddy.py
.github/scripts/test_rusty_v8_bazel.py
.github/scripts/rusty_v8_bazel.py`
- `ruff check .github/scripts/run_bazel_with_buildbuddy.py
.github/scripts/test_run_bazel_with_buildbuddy.py
.github/scripts/test_rusty_v8_bazel.py
.github/scripts/rusty_v8_bazel.py`
2026-06-02 09:56:20 -07:00
jif
859dbe2761 Skip startup prewarm when websockets are disabled (#25868)
## Summary
- skip startup websocket prewarm setup when the model client has
Responses-over-WebSocket disabled
- avoid making HTTP-only sessions build prewarm prompt/tool state that
cannot produce a reusable websocket session

## Why
Recent macOS timing flakes were timing out while waiting for first-turn
events in HTTP-only core tests. Startup prewarm is only useful for
websocket-capable providers, but it was scheduled for every session. For
HTTP-only test providers this added unnecessary async startup work
before the regular turn could reach the mocked response flow.

## Testing
- bazel test //codex-rs/core:core-all-test
--test_filter=suite::auto_review::remote_model_override_uses_catalog_model_for_strict_auto_review
--test_output=errors
- bazel test //codex-rs/core:core-all-test
--test_filter=suite::request_permissions_tool::approved_folder_write_request_permissions_unblocks_later_apply_patch
--test_output=errors
2026-06-02 17:27:30 +02:00
Alex Zamoshchin
4d80d808b4 [app-server][core] Add connector-level Guardian reviewer overrides (#25167)
Context: https://openai.slack.com/archives/C0B4JAF0Q2C/p1779912328647229

```
approvals_reviewer = "auto_review"

[apps.connector_5f3c8c41a1e54ad7a76272c89e2554fa]
enabled = true
approvals_reviewer = "user"
default_tools_approval_mode = "prompt"
```

<img width="230" height="84" alt="Screenshot 2026-05-31 at 11 56 34 AM"
src="https://github.com/user-attachments/assets/e319f8f7-0983-42a7-98cd-3302732fa406"
/>

<img width="841" height="233" alt="Screenshot 2026-05-31 at 11 52 42 AM"
src="https://github.com/user-attachments/assets/7ac76645-4e90-4d00-8242-f031146a22a5"
/>

-------

```
approvals_reviewer = "user"

[apps.connector_5f3c8c41a1e54ad7a76272c89e2554fa]
enabled = true
approvals_reviewer = "auto_review"
default_tools_approval_mode = "prompt"
```
<img width="195" height="83" alt="Screenshot 2026-05-31 at 12 02 27 PM"
src="https://github.com/user-attachments/assets/3d374dc8-8aa2-466f-a13f-e4ed8567aa2e"
/>
<img width="771" height="207" alt="Screenshot 2026-05-31 at 12 05 42 PM"
src="https://github.com/user-attachments/assets/105c2575-68d6-4ca6-8e69-dc8c82da36a2"
/>



## Summary
- add `apps.<connector_id>.approvals_reviewer` to override Guardian or
user review routing per connected app
- apply overrides across direct app MCP calls, delegated MCP prompts,
and app-server MCP elicitation review while preserving global behavior
for non-app MCP servers
- expose and document the config through app-server v2 and generated
schemas, while honoring global managed reviewer requirements

---------

Co-authored-by: jif-oai <jif@openai.com>
2026-06-02 17:04:11 +02:00
Adam Perry @ OpenAI
c097ad3e9e [codex] Use git CLI for Cargo fetches across Rust workflows (#25775)
## Why
Cargo's libgit2 transport has intermittently failed while fetching git
dependencies with nested submodules.
[#25644](https://github.com/openai/codex/pull/25644) applied
`CARGO_NET_GIT_FETCH_WITH_CLI=true` to the main Rust release build after
macOS SecureTransport/libgit2 failures while cloning `libwebrtc`'s
nested `libyuv` submodule. Similar flakes can affect other Cargo-bearing
Rust jobs.

## What changed
Configure `CARGO_NET_GIT_FETCH_WITH_CLI=true` at workflow scope for the
remaining Cargo-bearing Rust workflows:

- fast Rust CI and `cargo-deny`
- reusable Windows and argument-comment-lint release workflows
- `rusty-v8-release` and `v8-canary` Cargo builds and smoke tests

The full Rust CI, reusable nextest workflow, and primary Rust release
build already had the override. Bazel-only workflows are unchanged
because they use a different dependency fetch path.

## Validation
- Parsed all `.github/workflows/*.yml` files as YAML.
- Scanned Cargo-bearing workflows to confirm they configure
`CARGO_NET_GIT_FETCH_WITH_CLI`.
2026-06-02 07:39:41 -07:00
jif
3766941161 Run Codex async main on a sized stack (#25847)
## Why

`Runtime::block_on` executes the top-level future on the caller's OS
thread, not on one of Tokio's worker threads. That matters for the
interactive CLI because the Tokio runtime already configures larger
worker stacks, while the process main thread can still have a smaller
platform default stack.

This showed up as a `/clear` crash on macOS: starting a fresh TUI thread
reloads config, and the stack-heavy TOML deserialization path can
overflow before the new session is actually started.

## What Changed

- Run the regular `arg0_dispatch_or_else` async entrypoint on a named
`codex-main` thread.
- Give that thread the same `TOKIO_WORKER_STACK_SIZE_BYTES` stack budget
already used for Tokio worker threads.
- Keep `Arg0DispatchPaths` and the arg0 alias guard lifetime behavior
the same.
- Resume panics from the spawned main thread so panic behavior is
preserved.

## Verification

- `cargo check -p codex-cli` currently fails because the top-level
CLI/TUI future is not `Send` under the new thread boundary.
2026-06-02 16:34:48 +02:00
jif
b9af5d1234 flake: Keep plugin test homes alive (#25857)
## Summary

Keep the full `TestCodex` harness alive in plugin integration tests
instead of returning only the `CodexThread`.

## Why

The helper was moving a temporary `codex_home` into `TestCodex`, then
immediately dropping the harness and returning only the thread. For
plugin MCP tests, the MCP server cwd is inside that temporary home. If
the temp directory is removed while MCP startup is still racing, the
server launch can fail with `No such file or directory`.

Keeping the harness in scope keeps the temp home alive for the test
duration and removes the lifetime race behind the recent
`explicit_plugin_mentions_inject_plugin_guidance` flake.

## Validation

- `just fmt`
- `just test -p codex-core
explicit_plugin_mentions_inject_plugin_guidance`
2026-06-02 16:21:22 +02:00
jif-oai
1dd731305a Reduce stack pressure in session startup and config rebuilds (#25844)
## Why

`/clear` starts a fresh thread with `InitialHistory::Cleared`, which
re-enters the thread/session startup path. That path now builds large
async futures through `ThreadManagerState::spawn_thread_with_source`,
`Codex::spawn`, and `Session::new`. Separately, TUI config rebuilds for
cwd and permission-profile changes build a similarly heavy
`ConfigBuilder::build()` future inside the app task. In debug and Bazel
runs, those call chains can put enough state on the caller stack to
abort before startup or config refresh completes.

This change keeps the behavior the same while moving the heaviest future
frames off the caller stack.

## What changed

- Box `Codex::spawn(...)` in `codex-rs/core/src/thread_manager.rs`
before awaiting it from `spawn_thread_with_source`.
- Box `Session::new(...)` in `codex-rs/core/src/session/mod.rs` before
awaiting it from `Codex::spawn_internal`.
- Route `ConfigBuilder::build()` through a small `tokio::spawn` helper
in `codex-rs/tui/src/app/config_persistence.rs` so cwd and
permission-profile config rebuilds run on a runtime worker stack while
preserving error context.

## Verification

CI is running on the PR.

No new targeted tests were added. This is a mechanical stack-pressure
reduction that keeps the existing behavior and error propagation intact.
2026-06-02 15:42:47 +02:00
jif-oai
33273e4258 Test runtime selector before first turn (#25724)
Stack split from #25708. Original PR intentionally left open. This fifth
PR adds coverage that a remotely selected multi-agent runtime is applied
when the model is selected before the first turn.
2026-06-02 15:01:10 +02:00
jif-oai
66991c949f Test remote multi-agent runtime selector override (#25723)
Stack split from #25708. Original PR intentionally left open. This
fourth PR adds coverage that remote model multi-agent runtime selectors
override local feature flag defaults.
2026-06-02 14:48:13 +02:00
jif-oai
06e9a33d09 fix: main oops (#25840)
Fix main, comment is self-explainatory
2026-06-02 14:48:04 +02:00
jif-oai
3cf6f08da5 session: keep startup prewarm aligned with resolved multi-agent runtime (#25841)
## Why

Follow-up to #25722. Startup prewarm builds a preview `TurnContext`
before the first real turn so it can precompute the initial prompt and
tool surface. After the per-thread runtime work landed, that preview
path still recomputed multi-agent mode from `model_info` and feature
defaults instead of reusing the runtime the session had already resolved
from persisted metadata or inheritance.

That could leave the prewarmed session primed for a different
multi-agent mode than the first real turn, which is especially risky
because collaboration tool exposure depends on
`turn_context.multi_agent_version`.

## What changed

- In the `TurnMultiAgentRuntime::Preview` path, prefer
`Session::multi_agent_version()` when it is already known.
- Only fall back to `model_info.multi_agent_version` and feature
defaults when the session has not resolved a runtime yet.
- Keep preview mode read-only: this still avoids storing a runtime
during startup prewarm.

## Testing

- Not run (small runtime-selection follow-up)
2026-06-02 14:35:26 +02:00
jif-oai
bf9fd885b2 Resolve per-thread multi-agent runtime (#25722)
Stack split from #25708. Original PR intentionally left open. This third
PR resolves the effective per-thread multi-agent runtime from persisted
metadata, inherited runtime, and current model selection.
2026-06-02 14:31:00 +02:00
jif-oai
0c5ccd18ab Persist multi-agent runtime metadata (#25721)
Stack split from #25708. Original PR intentionally left open. This
second PR persists multi-agent runtime metadata through thread creation,
rollout recording, and thread storage.
2026-06-02 13:05:20 +02:00
jif-oai
3f1fb7ed8b Add multi-agent runtime metadata types (#25720)
Stack split from #25708. Original PR intentionally left open. This first
PR adds the multi-agent runtime metadata types and catalog plumbing used
by the rest of the stack.
2026-06-02 12:10:14 +02:00
jif-oai
45912a6dc6 feat: reuse compressed rollout search snippets (#25814)
## Summary
- teach rollout search to return precomputed snippets for compressed
rollouts
- reuse those snippets in local thread search instead of reopening
matching compressed files
- keep the no-`rg` fallback single-pass and add regression coverage for
the compressed path

## Why
`thread/search` currently decodes matching compressed rollouts twice:
once to discover the matching path and again to extract the snippet
shown in results. That defeats a meaningful part of the compressed-read
optimization work.

## Impact
Compressed rollout hits now pay one decode pass on the search path while
plain `.jsonl` hits keep the existing ripgrep-driven flow.

## Validation
- `just test -p codex-rollout`
- `just test -p codex-thread-store`
- `just fix -p codex-rollout`
- `just fix -p codex-thread-store`
- `just fmt`
2026-06-02 11:32:36 +02:00
xl-openai
67b805fc11 [codex] Validate plugin skill base names (#25782)
## Summary

- Validate skill base name length before plugin namespacing.
- Bound the composed `plugin:skill` qualified name to 128 characters.
- Keep plugin skill runtime names in the existing `plugin:skill` form.
- Add regression tests for the max qualified-name boundary and rejection
path.

## Root Cause

Plugin skills are represented as `plugin_name:skill_name`, but the
loader previously applied the 64-character skill name limit after adding
the plugin namespace. Moving that check to the base name fixes valid
plugin skills with longer namespaces, and the separate 128-character
qualified-name limit keeps model-visible skill names bounded.

## Validation

- `just fmt`
- `just test -p codex-core-skills plugin_skill_name_length_limit`
- `git diff --check`
2026-06-02 06:33:02 +00:00
xl-openai
07f04cc3c7 [codex] Move plugin discoverable logic into core-plugins (#25783)
## Summary
- Move plugin discoverable recommendation filtering from `codex-core`
into `codex-core-plugins` behind `ToolSuggestPluginDiscoveryInput`.
- Keep `codex-core` as a thin adapter from `Config` to the core-plugins
API and back to `DiscoverablePluginInfo`.
- Keep the existing discoverable allowlist private to the core-plugins
implementation.

## Validation
- `just fmt`
- `just test -p codex-core list_tool_suggest_discoverable_plugins`
- `git diff --check`
- Read-only subagent review: no findings
2026-06-01 23:25:37 -07:00
xl-openai
f2b725102b [codex] Cache remote plugin catalog for suggestions (#25457)
## Summary
- cache the global remote plugin catalog when remote plugin listing runs
and warm it during startup
- use the cached remote catalog in plugin install recommendations with
canonical `plugin@openai-curated-remote` ids
- reuse the session `PluginsManager` for plugin recommendations so
remote cache state is visible on the recommend path
- skip core installed-state verification for remote plugin install
suggestions while leaving local plugin and connector verification
unchanged

## Testing
- `just fmt`
- `git diff --check`
- `cargo test -p codex-core
list_tool_suggest_discoverable_plugins_includes_cached_remote_global_plugins`
- `cargo test -p codex-core
remote_plugin_install_suggestions_skip_core_installed_verification`
- `cargo test -p codex-app-server
plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled`

Earlier focused checks during the same branch: codex-tools TUI filter
test, request_plugin_install tests, and codex-app-server build.
2026-06-01 22:10:52 -07:00
xl-openai
cb63ee7f5d [codex] Add plugin list JSON output (#25330)
## Summary
- add `--json` output to `codex plugin list` with `installed` and
`available` arrays
- add `--available` for JSON output only; using it without `--json` is
rejected
- keep the existing non-JSON table output unchanged
- add CLI coverage for JSON installed/available output and the
`--available`/`--json` requirement

## Validation
- `just test -p codex-cli plugin_list`
- `just fix -p codex-cli`
- `git diff --check`

Note: `just fmt` ran Rust formatting first, then failed in the Python
ruff step because `openai-codex-cli-bin==0.132.0` has no wheel for this
Linux platform.
2026-06-01 21:27:06 -07:00
efrazer-oai
c8e5db16c9 feat: show enterprise monthly credit limits in status (#24812)
## Summary

Enterprise users can have an effective monthly credit limit, but Codex
`/status` currently drops that metadata from the account-usage response.

This change adds the optional `spend_control.individual_limit`
projection to the existing rate-limit snapshot flow. The backend client
reads the monthly limit, app-server exposes it as `individualLimit`, and
the TUI renders a `Monthly credit limit` row through the existing
progress-bar renderer.

When the backend does not return an effective monthly limit, existing
rate-limit behavior is unchanged.

## Existing backend state

The account-usage backend already returns the effective monthly limit
and current usage together:

```json
{
  "spend_control": {
    "reached": false,
    "individual_limit": {
      "limit": "25000",
      "used": "8000",
      "remaining": "17000",
      "used_percent": 32,
      "remaining_percent": 68,
      "reset_after_seconds": 86400,
      "reset_at": 1778137680
    }
  }
}
```

Before this change, Codex projected rolling `primary` and `secondary`
windows plus `credits`. It ignored `spend_control.individual_limit`, so
app-server clients and `/status` could not render the monthly cap.

The updated flow is:

```text
account usage backend
  -> backend-client reads spend_control.individual_limit
  -> existing rate-limit snapshot carries optional individual_limit
  -> app-server exposes optional individualLimit
  -> TUI renders Monthly credit limit
```

## App-server contract

`account/rateLimits/read` and sparse `account/rateLimits/updated`
notifications now include an additive nullable
`rateLimits.individualLimit` field:

```json
{
  "individualLimit": {
    "limit": "25000",
    "used": "8000",
    "remainingPercent": 68,
    "resetsAt": 1778137680
  }
}
```

In an `account/rateLimits/read` response, `null` means no monthly limit
is available. `account/rateLimits/updated` remains a sparse rolling
notification: clients merge available values into their most recent
`account/rateLimits/read` snapshot or refetch. Nullable account metadata
in a rolling notification does not clear a previously observed value.

## Design decisions

- Extend the existing rate-limit snapshot instead of introducing a
separate request or wire-level update protocol.
- Keep the Codex projection narrow: `/status` needs the effective limit,
current usage, remaining percentage, and reset timestamp.
- Render the monthly row through the existing progress-bar renderer,
with one optional detail line for `8,000 of 25,000 credits used`.
- Keep the backend response optional so existing accounts and older
usage states preserve their current behavior.
- Preserve cached monthly metadata when sparse rolling notifications
omit it. Live account-usage reads remain authoritative and can clear a
removed limit.

## Visual evidence

```text
 Monthly credit limit:   [██████████████░░░░░░] 68% left (resets 07:08 on 7 May)
                         8,000 of 25,000 credits used
```

Snapshot:
`codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_enterprise_monthly_credit_limit.snap`

## Testing

Tests: generated app-server schema verification, protocol tests,
backend-client tests, app-server integration coverage, TUI snapshot
coverage, formatting, and workspace lint cleanup.
2026-06-01 21:25:42 -07:00
pakrym-oai
c955f73078 Move code review rules into AGENTS (#25738)
## Why
Codex Review now supports repository-specific review rules in AGENTS.md.
Adding the review prompts there makes the guidance available as
repository review rules next to the code it governs while keeping the
existing local review skills intact.

## What changed
- Added a `## Code Review Rules` section to `AGENTS.md` with the
existing review prompts for model context, breaking changes, test
authoring, and change size.
- Preserved the existing `.codex/skills/code-review*` skill files.

## Verification
- `git diff --check origin/main...HEAD`
2026-06-02 01:41:04 +00:00
Adam Perry @ OpenAI
747f1003dd [codex] Add comprehensive root formatting check (#25683)
## Why

The root formatting entrypoints could drift: `just fmt` did not format
the Justfile itself, and the CI-facing check recipe only checked Python
scripts instead of matching everything formatted by `just fmt`.

## What changed

- Add a shared cross-platform Python formatter driver used by both `just
fmt` and `just fmt-check`.
- Run Justfile, Rust, Python SDK, and internal-script formatter groups
concurrently while buffering each formatter group's output until it
finishes.
- Log formatter starts immediately, then print each formatter group's
labeled output when it completes.
- Keep the SDK lint-fix and Ruff formatting passes ordered, with source
comments explaining their distinct roles and the check-mode equivalents.
- Run Ruff through shared `uv run --no-sync --with ruff` overlays so
formatting works on clean glibc Linux checkouts without installing the
platform-specific SDK runtime wheel.
- Show `fmt-check` help text in `just -l` and simplify CI to call the
shared driver through `just fmt-check`.
- Pin the general CI workflow to `just@1.51.0` so its formatter agrees
with the checked-in Justfile.
- Add regression coverage for the thin Just recipes and the driver's
formatter graph.

## Validation

- `just fmt`
- `just fmt-check`
- `python3 -m pytest
sdk/python/tests/test_artifact_workflow_and_binaries.py -k 'root_fmt or
root_format' -q`
- `pnpm run format`
- `git diff --check`
- `just -l | rg -n '^    fmt|fmt-check'`
- `uvx --from uv==0.7.22 uv run --frozen --project sdk/python --no-sync
--with ruff ruff check --diff sdk/python`
2026-06-02 01:20:25 +00:00
Anton Panasenko
0002316687 feat(remote-control): add pairing start (#25675)
## Why

Remote control enrollment authorizes a desktop server, but app-server v2
did not expose the follow-up pairing operation needed to mint a
short-lived controller pairing artifact from that enrolled server.
Clients need a narrow RPC that starts pairing without exposing the
backend `serverId` or conflating pairing with websocket connection
state.

Issue: N/A; internal remote-control pairing API change.

## What Changed

Added experimental app-server v2 `remoteControl/pairing/start` with
`manualCode` input and `pairingCode`, nullable `manualPairingCode`,
`environmentId`, and Unix-seconds `expiresAt` output. The method
serializes under its own `global("remote-control-pairing")` scope and is
documented in `app-server/README.md`.

Extended the remote-control transport with private `/server/pair`
request/response types and normalized `pair_url` handling. Pairing uses
the current enrolled server bearer, refreshes that bearer when needed,
keeps backend `server_id` private, validates returned `server_id` and
`environment_id` against the current enrollment, and preserves backend
status/header/body context for failures and malformed responses.

Wired the request through `RemoteControlRequestProcessor` and
`MessageProcessor`, mapping unavailable/disabled pairing to
`invalid_request` and backend failures to internal errors.

## Verification

- `just test -p codex-app-server-transport`
- `just test -p codex-app-server
remote_control_pairing_start_returns_pairing_artifacts`
2026-06-02 01:05:50 +00:00
xli-oai
1ad0d7aa4b Handle invalid plugin skills manifest field (#25717)
## Summary
- Treat invalid `plugin.json` `skills` shapes as a field-level warning
instead of rejecting the whole manifest
- Keep valid string path behavior unchanged and continue falling back to
the default `skills/` root
- Add regression coverage for array-shaped `skills`

## Tests
- `just fmt`
- `cargo test -p codex-core-plugins`
2026-06-01 17:19:34 -07:00
joeflorencio-openai
0b3a6f7185 Move cloud requirements crate to cloud config (#24621)
## Summary

- Moves the existing `codex-cloud-requirements` crate to
`codex-cloud-config`.
- Updates workspace dependencies and imports to the new crate name.
- Intentionally keeps runtime behavior unchanged: this still fetches the
legacy cloud requirements endpoint.

## Details

This PR exists to make the lineage obvious before the bundle migration.
GitHub should show the old `codex-rs/cloud-requirements/src/lib.rs`
implementation as moved to `codex-rs/cloud-config/src/lib.rs`, rather
than as unrelated new code.

The follow-up PR adapts this moved crate to the new config bundle API
and switches runtime consumers over.
2026-06-01 16:43:52 -07:00
Owen Lin
11e0f3d3ae app-server: remove experimental persist_extended_history bool flag (#25712)
## Summary

Remove the dead experimental `persistExtendedHistory` app-server flag
and collapse rollout persistence to the single policy app-server already
used.

## What Changed

- Removed `persistExtendedHistory` from v2 thread start/resume/fork
params and deleted its deprecation notice path.
- Removed the persistence-mode enums and plumbing through core, rollout,
and thread-store.
- Made rollout filtering mode-free, keeping the existing limited
persisted-history behavior.

## Test Plan

- `just write-app-server-schema`
- `cargo nextest run --no-fail-fast -p codex-app-server-protocol
schema_fixtures`
- `cargo nextest run --no-fail-fast -p codex-app-server
thread_shell_command_history_responses_exclude_persisted_command_executions`
- `cargo nextest run --no-fail-fast -p codex-rollout -p
codex-thread-store`
- final `rg` for removed flag/type names
2026-06-01 23:33:42 +00:00
Winston Howes
bca18cba40 Wire managed MITM CA trust into child env (#22668)
## Stack
1. Parent PR: #18240 uses named MITM permissions config.
2. This PR wires managed MITM CA trust into spawned child processes.

## Why
When Codex terminates HTTPS for limited mode or MITM hooks, child HTTPS
clients need to trust Codex's managed MITM CA. Exporting proxy URLs
alone is not enough, but blindly replacing user CA settings would be
wrong: it can break custom enterprise/test roots, leak unreadable CA
files into generated bundles, or make the child env disagree with its
sandbox policy.

## Summary
1. Build immutable managed CA bundles under `$CODEX_HOME/proxy` that
include native roots, the managed MITM CA, and only inherited or
command-scoped CA bundles the child is allowed to read.
2. Export curated CA env vars alongside managed proxy env vars while
preserving user CA override semantics, including nested Codex
`SSL_CERT_FILE` precedence.
3. Thread generated CA bundle paths into child sandbox readable roots,
including debug sandbox execution, so the exported env vars work inside
sandboxed commands.
4. Remove only Codex-generated MITM CA bundle env when a child
intentionally drops managed proxying for escalation or no-proxy retry.
5. Document the managed CA bundle behavior and cover env injection,
per-child bundle generation, sandbox readable roots, and no-proxy
cleanup in tests.

## Validation
1. Ran `just test -p codex-network-proxy`.
2. Ran `just test -p codex-protocol`.
3. Ran `just fix -p codex-network-proxy -p codex-protocol`.
4. Tried focused `codex-core` validation, but the crate currently fails
to compile in `core/tests/suite/guardian_review.rs` because an existing
`Op::UserInput` initializer is missing `additional_context`.

---------

Co-authored-by: Eva Wong <evawong@openai.com>
2026-06-01 23:23:59 +00:00
Michael Bolin
b89bf1ef47 Reject directory rollout paths for pathless side chats (#25661)
## Why

Fixes openai/codex#20944.

Desktop side chats are intentionally ephemeral and pathless. They can
still accept live turns while loaded, but after a reload there is no
persisted rollout to resume. In the reported failure mode, Desktop could
send `$CODEX_HOME` as the resume/fork path for one of these pathless
side chats.

`thread/resume` and `thread/fork` prefer an explicit `path` over
`threadId`, and rollout path lookup only checked that a candidate
existed. That let `$CODEX_HOME` pass as a rollout path, so the later
rollout reader tried to open a directory and surfaced the low-level `Is
a directory` error.

## What Changed

- Reject explicit rollout paths that resolve to a directory or other
non-file before attempting to read rollout history.
- Make `codex_rollout::existing_rollout_path` return only plain or
compressed rollout candidates that are actual files.
- Add an app-server regression test that creates an ephemeral fork, runs
a turn while the side thread is loaded, simulates reload, then verifies
both `thread/resume` and `thread/fork` reject `$CODEX_HOME` with `path
is a directory` instead of the OS-level directory-read error.
- Rebase over the `TestAppServer` rename and update the remaining stale
test harness call sites to use `TestAppServer` with `app_server` local
variables.

Relevant code:

- `thread-store/src/local/read_thread.rs` validates explicit rollout
paths before rollout reading:
25b47c8f42/codex-rs/thread-store/src/local/read_thread.rs (L146-L165)
- `rollout/src/compression.rs` now requires file metadata for plain and
compressed rollout candidates:
25b47c8f42/codex-rs/rollout/src/compression.rs (L940-L950)
- The repro test covers the pathless ephemeral side-chat reload case:
25b47c8f42/codex-rs/app-server/tests/suite/v2/thread_fork.rs (L774-L886)

## Verification

- `just test -p codex-app-server
pathless_ephemeral_thread_rejects_codex_home_path_after_reload`
2026-06-01 16:02:06 -07:00
Jeremy Rose
75a08def98 [codex] Publish release symbol artifacts (#25649)
## Why

Production Codex binaries are stripped for distribution, which leaves
crashes and samples from released builds without the symbols needed for
useful stack traces. Publish symbols as separate release assets so
production artifacts stay small while released builds remain
symbolicateable.

## What changed

- Add `.github/scripts/archive-release-symbols-and-strip-binaries.sh` to
package platform-native symbols into `codex-symbols-<artifact>.tar.gz`
assets while stripping the corresponding Unix binaries before signing.
- Build release binaries with full debug information before producing
distribution artifacts.
- Publish macOS `.dSYM` bundles, Linux `.debug` files with
`.gnu_debuglink`, and Windows `.pdb` files.
- Strip Linux `bwrap` before computing its packaged-resource digest, but
intentionally omit `bwrap` from symbol archives.
- Preserve symbols artifacts in the unsigned macOS promotion flow.

## Verification

- Ran `shellcheck` and `bash -n` on
`.github/scripts/archive-release-symbols-and-strip-binaries.sh`.
- Parsed the modified workflow YAML files and ran `git diff --check`.
- Built a macOS release smoke binary and verified that the archived
`.dSYM` contains DWARF application source information and has the same
UUID as the stripped production binary.
- Built Linux smoke binaries and verified that the symbol archive
contains `codex.debug`, excludes `bwrap.debug`, leaves the expected
`.gnu_debuglink` in `codex`, and does not mutate the separately stripped
`bwrap` digest.
- Staged a Windows smoke archive and verified that it contains the
expected `.pdb` file.
2026-06-01 15:49:54 -07:00
Felipe Coury
4e540b1076 fix(tui): clarify footer shortcut overlay hints (#25625)
## Why

The TUI shortcut overlay used static labels for `Tab` and `Ctrl+C`, even
though both keys change behavior while a task is running. That made the
visible help misleading: idle `Tab` submits rather than queues, and
active-turn `Ctrl+C` interrupts rather than exits.

Closes #25531.
Closes #25564.

## What Changed

- Pass task-running state into the shortcut overlay renderer.
- Render `Tab` as `submit message` while idle and `queue message` while
work is running.
- Render `Ctrl+C` as `exit` while idle and `interrupt` while work is
running.
- Add snapshot coverage for the active-work shortcut overlay and update
idle overlay snapshots.

## How to Test

1. Start Codex and open the shortcut overlay with `?` while no task is
running.
2. Confirm the overlay shows `tab to submit message` and `ctrl + c to
exit`.
3. Start a task, then open or keep the shortcut overlay visible while
work is running.
4. Confirm the overlay shows `tab to queue message` and `ctrl + c to
interrupt`.
5. Type a follow-up prompt during active work and press `Tab`; confirm
it queues rather than submitting immediately.

Targeted tests:

- `just test -p codex-tui footer_snapshots`
- `just test -p codex-tui footer_mode_snapshots`

## Validation Notes

`just test -p codex-tui` currently has two unrelated guardian
feature-flag test failures on this base:

-
`app::tests::update_feature_flags_disabling_guardian_clears_manual_review_policy_without_history`
-
`app::tests::update_feature_flags_disabling_guardian_clears_review_policy_and_restores_default`

`just argument-comment-lint codex-rs/tui/src/bottom_pane/footer.rs`
could not run locally because the prebuilt wrapper requires `dotslash`;
the touched Rust diff was manually inspected for opaque positional
literals.
2026-06-01 19:41:22 -03:00
jif-oai
8d720feb69 Move tool search metadata onto ToolExecutor (#25684)
Deferred tools need to be searchable even when they are not implemented
inside `codex-core`. Extension-provided tools can be registered for
later discovery, but the search metadata path was still owned by
core-specific runtime hooks, which meant the shared `ToolExecutor`
abstraction could not describe how a deferred extension tool should
appear in `tool_search`.

## Changes

- Move `ToolSearchEntry` and `ToolSearchInfo` into `codex-tools` and
re-export them from the shared tools crate.
- Add a default `ToolExecutor::search_info` implementation that derives
loadable tool-search metadata from function and namespace specs.
- Forward search metadata through extension adapters and exposure
overrides while keeping custom search text/source metadata for dynamic,
MCP, and multi-agent tools.
- Remove the old core-local `tool_search_entry` module now that search
metadata lives with the shared executor APIs.

## Testing

- Added `deferred_extension_tools_are_discoverable_with_tool_search`
coverage in `core/src/tools/spec_plan_tests.rs`.
2026-06-02 00:24:41 +02:00
Michael Bolin
8ee49a2f74 Fix stale TestAppServer rename in plugin_list test (#25705)
## Why

#25701 renamed the app-server test harness to `TestAppServer`, but it
raced with #25681, which added a new `plugin_list` test call site still
using the old `McpProcess` name. Once both changes met on `main`,
app-server test builds failed before running the suite because
`McpProcess` no longer exists in that scope.

This PR fixes that CI break by updating the remaining stale call site to
the renamed helper.

## What Changed

- Replaced the `McpProcess::new(...)` use in
`codex-rs/app-server/tests/suite/v2/plugin_list.rs` with
`TestAppServer::new(...)`.
- Renamed the local variable from `mcp` to `app_server` at the same call
site to match the helper rename.

Relevant code:
aadd9c999b/codex-rs/app-server/tests/suite/v2/plugin_list.rs (L234-L246)

## Verification

Not run locally; this is a compile fix for the app-server test harness
rename.
2026-06-01 15:14:03 -07:00
sayan-oai
b3c4157034 [codex] enable parallel standalone web search calls (#25702)
## Summary
- opt the extension-backed standalone `web.run` tool into parallel tool
execution
- update the existing extension registration test to assert that the
tool advertises parallel-call support

## Why
The standalone web-search API endpoint now supports parallel requests.
The extension executor still inherited the shared serial default,
causing multiple `web.run` calls to acquire the exclusive runtime lock.

## Impact
Models that emit multiple standalone web-search calls can now execute
them concurrently when model-level parallel tool calls are enabled.

## Validation
- `just fmt`
- `just test -p codex-web-search-extension`
- `git diff --check origin/main...HEAD`
2026-06-01 15:04:11 -07:00
Michael Bolin
6536841d89 fix: rename McpServer to TestAppServer (#25701)
This PR brought to you via VS Code rather than Codex...

- opened `codex-rs/app-server/tests/common/mcp_process.rs`
- put the cursor on `McpServer`
- hit `F2` and renamed the symbol to `TestAppServer`
- went to the file tree
- hit enter and renamed `mcp_process.rs` to `test_app_server.rs`
- ran **Save All Files** from the Command Palette
- ran `just fmt`

The End

(Admittedly, most of the local variables for `TestAppServer` are still
named `mcp`, though.)
2026-06-01 21:49:38 +00:00
xl-openai
6ae99fd35f fix: Deduplicate installed local and remote curated plugins (#25681)
## Summary
- Deduplicate installed `openai-curated` and `openai-curated-remote`
plugin conflicts by feature flag.
- Prefer remote when remote plugins are enabled; otherwise prefer local,
while preserving one-sided installs.

## Testing
- `just fmt`
- `git diff --check`
- Targeted `just test` was blocked locally because `cargo-nextest` is
not installed.
2026-06-01 14:27:18 -07:00
Adam Perry @ OpenAI
433ac84102 Add Python version compatibility guidance (#25690)
## Why

Python contributions in this repository should target the declared
Python 3 runtime instead of carrying Python 2 compatibility patterns
forward. When compatibility across Python 3 point releases matters,
contributors need a consistent source of truth for the minimum supported
version.

## What changed

- Added Python development guidance to `AGENTS.md` stating that the
repository uses Python 3+ and should not use the `__future__` module.
- Documented that contributors should check the nearest `pyproject.toml`
`requires-python` field when evaluating Python 3 point-release
compatibility.

## Testing

Not run (guidance-only change).
2026-06-01 14:05:54 -07:00
sayan-oai
f0e15b916f [codex] Generalize deferred nested tool guidance (#25689)
## Summary
- describe omitted code-mode tools as deferred nested tools instead of
MCP/app tools
- update the prompt-description assertion to match

## Why
Deferred dynamic tools are also callable through `tools` and
discoverable in `ALL_TOOLS`, so the previous MCP/app-specific wording
was too narrow.

## Validation
- `just fmt`
- `just test -p codex-code-mode`
- `git diff --check`
2026-06-01 21:01:30 +00:00
jif-oai
7c285f9e9c Add rollout compression histograms (#25680)
## Summary

Stacked on #25679. Add histogram telemetry for rollout compression
runtime, per-file compression time, byte sizes, and compression ratio.

## Changes

- Emit `codex.rollout_compression.run.duration_ms` tagged by final run
status.
- Emit `codex.rollout_compression.file.duration_ms` tagged by file
outcome.
- Emit source and compressed byte histograms for compression
candidates/results.
- Emit `codex.rollout_compression.file.compression_ratio` for successful
compressions, recorded as integer basis points.

## Validation

- `just fmt`
- `just test -p codex-rollout`
- `just fix -p codex-rollout`
2026-06-01 22:54:25 +02:00
Adam Perry @ OpenAI
a29a5b0861 [codex] document out-of-line test module convention (#25682)
## Why

New unit test modules should follow one consistent layout so
implementation files stay focused and test suites remain easy to locate,
without creating cleanup churn in existing inline test modules.

## What changed

- Added `AGENTS.md` guidance requiring new test modules to use separate
sibling `*_tests.rs` files with an explicit `#[path = "..._tests.rs"]`
attribute.
- Clarified that existing inline `#[cfg(test)] mod tests { ... }`
modules should not be moved solely to follow the new convention.

## Validation

- Ran `git diff --check`.
2026-06-01 13:36:16 -07:00
jif-oai
9f4fac8ec4 Add rollout compression counters (#25679)
## Summary

Add counter telemetry for the local rollout compression worker so we can
see when it runs, why it skips, and how individual file/materialization
paths resolve.

## Changes

- Emit `codex.rollout_compression.run` with statuses for start,
completion, failure, duplicate-run skip, and missing runtime skip.
- Emit `codex.rollout_compression.file` outcomes for scanned,
compressed, skipped, and failed compression candidates.
- Emit `codex.rollout_compression.temp_cleanup` and
`codex.rollout_compression.materialize` counters for cleanup and
decompression paths.

## Validation

- `just fmt`
- `just test -p codex-rollout`
- `just fix -p codex-rollout`
2026-06-01 22:26:32 +02:00
Michael Bolin
feb9eddc51 refactor: hide shell override for zsh fork unified exec (#24980)
## Why

When unified exec is configured to launch through the zsh fork, local
commands should not let the model override the shell binary with the
`shell` parameter. The configured zsh fork is the mechanism that makes
`execv(2)` interception reliable, so exposing `shell` for local zsh-fork
execution would create a confusing API surface and undermine the
composition.

Remote environments are different: zsh-fork interception is local-only,
so remote unified-exec calls must keep direct unified-exec behavior and
still expose `shell` when a remote environment can be selected.

## What Changed

- Taught the `exec_command` schema builder to omit the `shell` parameter
when requested.
- Hid `shell` from the unified-exec tool schema only when zsh-fork
unified exec applies to all selectable environments.
- Kept `shell` visible when any remote environment can be targeted,
because those calls run through direct unified exec.
- Made unified exec choose the effective shell mode per selected
environment: local environments keep zsh-fork mode, remote environments
use direct mode.
- Left direct unified-exec behavior unchanged, including support for
model-specified shells there.

## Verification

- Added schema coverage showing `exec_command` can hide `shell`.
- Added planner coverage showing zsh-fork unified exec hides `shell` for
local-only execution while direct unified exec still exposes it.
- Added planner coverage showing `shell` remains visible when a remote
environment is available.
- Added handler coverage showing remote environments use direct
unified-exec shell mode instead of zsh-fork mode.
- Ran the focused `codex-core` shell-parameter and zsh-fork tests.







---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/24980).
* #24982
* #24981
* __->__ #24980
2026-06-01 20:22:28 +00:00
Michael Bolin
d6748f741a feat: gate unified exec zsh fork composition (#24979)
## Why

`shell_zsh_fork` and unified exec need to remain independently
controllable for enterprise rollouts, but we also need a third mode that
composes them. That composed mode is intended to preserve unified exec
command lifecycle support while letting the zsh fork provide more
accurate `execv(2)` interception.

Enabling `unified_exec_zsh_fork` by itself is intentionally not
sufficient. It is a composition gate, not a dependency-enabling
shortcut:

- `unified_exec` selects the PTY-backed unified exec tool.
- `shell_zsh_fork` opts into the zsh fork backend.
- `unified_exec_zsh_fork` only allows those two already-enabled modes to
be composed so local zsh unified exec commands can launch through the
zsh fork.

This separation is deliberate. Enterprises and staged rollouts must be
able to enable or disable unified exec and zsh-fork independently. If
`unified_exec_zsh_fork` implied either dependency, then enabling one
under-development composition flag would silently activate a shell
backend that the configured feature set left disabled.

This PR introduces only the configuration and planning gate for that
composition. Existing `shell_zsh_fork` behavior continues to use the
standalone shell tool unless the new composition feature is explicitly
enabled alongside both dependencies.

## What Changed

- Added the under-development feature flag `unified_exec_zsh_fork`.
- Added `UnifiedExecFeatureMode` so the three input feature flags
collapse into `Disabled`, `Direct`, or `ZshFork` mode before tool
planning.
- Updated tool selection so zsh-fork composition requires
`unified_exec`, `shell_zsh_fork`, and `unified_exec_zsh_fork`.
- Kept the existing standalone zsh-fork shell tool behavior when only
`shell_zsh_fork` is enabled.
- Updated config schema output for the new feature flag.

## Verification

- Added feature and tool-config coverage for the new gate.
- Added planner coverage proving `shell_zsh_fork` remains standalone
until composition is explicitly enabled.
- Ran focused tests for `codex-features`, `codex-tools`, and the
affected `codex-core` planner case.





---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/24979).
* #24982
* #24981
* #24980
* __->__ #24979
2026-06-01 13:01:36 -07:00
jif-oai
009e6c4817 fix: deflake zsh-fork approval test (#25669)
Fixes this flake:
https://github.com/openai/codex/actions/runs/26773809591/job/78919970410?pr=25659

This test is about zsh-fork subcommand approval behavior, not workspace
sandboxing, so it now runs with `DangerFullAccess` to avoid macOS
sandbox setup failures before the second subcommand approval.
2026-06-01 21:55:44 +02:00
starr-openai
53ac02356e exec-server: canonicalize bound filesystem paths (#25149)
## Summary
- add executor filesystem canonicalization as a bound-path operation
- route remote canonicalization through the exec-server filesystem RPC
surface
- keep path normalization attached to the filesystem that owns the path

## Stack
- 2/5 in the skills path authority stack extracted from
https://github.com/openai/codex/pull/25098
- follows merged https://github.com/openai/codex/pull/25121

## Validation
- `cd
/Users/starr/code/codex-worktrees/pr-25098-restack-review-pr1b/codex-rs
&& just fmt`
- Not run: tests/checks (not requested)
- GitHub CI pending on rewritten head
2026-06-01 11:53:31 -07:00
Won Park
f1609d9fb6 [codex-rs] auto-review model override (#23767)
## Why

Guardian auto-review normally uses the provider-preferred review model
when one is available. Some parent models need model-catalog metadata to
select a different review model while keeping older `/models` payloads
compatible when that metadata is absent.

## What changed

- Added optional `ModelInfo::auto_review_model_override` metadata to the
public model payload as a review-model slug.
- Updated Guardian review model selection to prefer the catalog override
when present, while preserving the existing provider preferred-model
path and parent-model fallback when it is omitted.
- Added focused Guardian coverage for override and no-override model
selection.
- Added an `auto_review` core integration suite test that loads override
metadata from a remote model catalog path and asserts the strict
auto-review `/responses` request uses the catalog-selected review model.
- Updated existing `ModelInfo` fixtures and local catalog constructors
for the new optional field.

## Validation

- `cargo test -p codex-protocol
model_info_defaults_availability_nux_to_none_when_omitted`
- `cargo test -p codex-core guardian_review_uses_`
- `cargo test -p codex-core
remote_model_override_uses_catalog_model_for_strict_auto_review --test
all`
- `just fix -p codex-protocol`
- `just fix -p codex-core`
- `just fmt`
- `git diff --check`
2026-06-01 11:51:15 -07:00
Adam Perry @ OpenAI
281b416c44 Check root Python script formatting in CI (#25165)
## Why

Python files under `scripts/` were not covered by the repository
formatting recipe or the CI formatting job, so formatting drift could
merge unnoticed.

## What

- Add a dedicated `scripts/pyproject.toml` and `scripts/uv.lock` so
root-script formatting uses a locked Ruff version.
- Extend `just fmt` to format root Python scripts and add
`fmt-scripts-check` for CI.
- Run `just fmt-scripts-check` from `.github/workflows/ci.yml`,
installing `uv` through SHA-pinned `astral-sh/setup-uv` while retaining
the `uv` `0.11.3` pin.
- Apply Ruff formatting to the root Python scripts, including
`scripts/just-shell.py`, and extend
`sdk/python/tests/test_artifact_workflow_and_binaries.py` to cover the
root formatting recipe.
- Update `AGENTS.md` so agents run `just fmt` after code changes
anywhere in the repository.

## Validation

- Extended the existing Python SDK workflow test to assert that `just
fmt` includes root Python scripts.
2026-06-01 18:50:23 +00:00
jif-oai
c3cdf3c007 Throttle repeated rollout compression runs (#25659)
## Why

[#25089](https://github.com/openai/codex/pull/25089) introduced the
background worker that compresses cold archived rollouts, and
[#25654](https://github.com/openai/codex/pull/25654) made that pass
faster once it starts. But the worker still deleted
`rollout-compression.lock` on successful exit, so the existing six-hour
staleness window only helped with overlapping or crashed workers. Each
new local thread-store initialization could immediately rescan archived
rollouts even if a full pass had just finished.

This change keeps the existing marker around long enough to throttle
redundant reruns. The worker is still best-effort, but it no longer does
repeated startup scans when nothing new is eligible for compression.

## What Changed

- Replace the drop-scoped `CompressionLock` with a
`CompressionRunMarker` that claims the existing
`.tmp/rollout-compression.lock` path and leaves it in place after
success.
- Reuse the existing six-hour staleness window to block both overlapping
starts and immediate reruns, while still letting a stale marker be
reclaimed.
- Update the worker docs and debug logging to describe the new "already
running or recently ran" behavior.
- Extend the rollout compression tests to assert that a successful run
leaves the marker behind and that a fresh marker suppresses a new run.

## Validation

- `just test -p codex-rollout`
2026-06-01 20:46:54 +02:00
Adam Perry @ OpenAI
ba2b67f9cd [codex] Consolidate shared prompts in codex-prompts (#25151)
## Why

`codex_core` is consistently a bottleneck for incremental builds during
iteration. The simplest fix is to make the crate smaller.

## Summary

`codex-core` owns several reusable prompt renderers and static prompt
assets, which makes the crate harder to split apart.

Rename `codex-review-prompts` to `codex-prompts` and move shared review,
goal, permissions, compaction, realtime, hierarchical AGENTS.md, and
`apply_patch` prompts into it. Move prompt-only tests and update
consumers and `CODEOWNERS`.

## Validation

- `just test -p codex-prompts -p codex-apply-patch`
- `just test -p codex-core prompt_caching`
- Bazel builds for the affected crates
2026-06-01 18:45:07 +00:00
iceweasel-oai
88c7a4ff07 [codex] Make justfile recipes Windows-aware (#24983)
## Summary

Make the root `justfile` usable from Windows without maintaining a
separate Windows copy of most recipes.

The repo recipes previously assumed POSIX shell behavior for things like
variadic argument forwarding (`"$@"`) and stderr redirection
(`2>/dev/null`). That made common workflows such as `just fmt`, `just
test`, and `just log` unreliable from Windows. This PR introduces a
small cross-platform shell adapter so recipes can stay mostly unified
while still expanding the few shell-specific constructs correctly on
macOS/Linux and Windows.

## What Changed

- Add `scripts/just-shell.py` as the configured `just` shell adapter.
  - On Unix it invokes `sh -cu`.
- On Windows it invokes `pwsh -CommandWithArgs` so arguments containing
spaces are preserved.
- Add portable recipe placeholders:
- `{args}` expands to `"$@"` on Unix and the equivalent PowerShell
forwarded-args expression on Windows.
- `{stderr-null}` expands to the platform-specific stderr suppression
used by `fmt`.
- Convert most variadic one-line recipes to the unified `{args}` form,
including `codex`, `exec`, `file-search`, `app-server-test-client`,
`fix`, `clippy`, `bench`, `mcp-server-run`, `write-app-server-schema`,
and `argument-comment-lint-from-source`.
- Keep genuinely shell-specific recipes split or Unix-only for now,
including recipes backed by `.sh` scripts or recipes whose bodies are
more than simple command forwarding.
- Add a Windows `just install` path that installs PowerShell via
`winget` when `pwsh` is not available, then runs the same basic Rust
setup steps.
- Update the SDK test that validates the root `fmt` recipe so it
recognizes the new portable stderr placeholder.

## Validation

- `just --summary`
- `just --dry-run fmt`
- `just --dry-run bench-smoke`
- `just --dry-run codex foo "bar binky" baz`
- `just --dry-run write-hooks-schema`
- `just --dry-run bazel-lock-update`
- `just --dry-run argument-comment-lint-from-source -- "foo bar"`
- `git diff --check -- justfile scripts/just-shell.py
sdk/python/tests/test_artifact_workflow_and_binaries.py`
- Verified Windows argv preservation through `scripts/just-shell.py`
with arguments containing spaces.
- `uv run --frozen --project sdk/python --extra dev pytest
sdk/python/tests/test_artifact_workflow_and_binaries.py::test_root_fmt_recipe_formats_rust_and_python_sdk`
2026-06-01 11:26:36 -07:00
charlesgong-openai
9756316d89 Preserve plugin app manifest order (#25491)
## Summary
- Preserve app declaration order when loading plugin .app.json files.
- Keep plugin connector summaries in plugin app order after connector
metadata is merged and filtered.
- Add regression coverage for .app.json order and connector summary
order.

## Validation
- just fmt
- just test -p codex-chatgpt
connectors_for_plugin_apps_returns_only_requested_plugin_apps
- just test -p codex-core-plugins
effective_apps_preserves_app_config_order
- just fix -p codex-core-plugins (passes with existing clippy
large_enum_variant warning in core-plugins/src/manifest.rs)
- just fix -p codex-chatgpt
- just bazel-lock-update
- just bazel-lock-check
2026-06-01 11:04:21 -07:00
jif-oai
6ddb747e76 [codex] Rename multi-agent v2 assign_task to followup_task (#25636)
## Summary

Renames the MultiAgentV2 turn-triggering tool from `assign_task` to
`followup_task` so the exposed tool name better describes sending an
additional task to an existing agent.

This updates the tool spec, handler/module names, registry wiring,
default multi-agent v2 usage hints, and tests. Rollout trace
classification keeps accepting legacy `assign_task` events so older
traces still reduce correctly, while docs show the new tool name.

## Test plan

- `just test -p codex-core followup_task`
- `just test -p codex-core -E
'test(multi_agent_feature_selects_one_agent_tool_family) |
test(multi_agent_v2_can_use_configured_tool_namespace) |
test(code_mode_only_can_expose_namespaced_multi_agent_v2_as_normal_tools)'`
- `just test -p codex-rollout-trace`
- `just fix -p codex-core`
- `just fix -p codex-rollout-trace`

Notes: `just fmt` ran `cargo fmt` but failed in the Python ruff phase
because the local environment could not resolve `hatchling>=1.27.0` from
the configured internal registry. A full `just test -p codex-core` also
hit unrelated environment-sensitive integration failures involving
missing spawned test binaries/sandbox behavior; the changed multi-agent
spec/handler tests passed in the filtered runs above.
2026-06-01 19:57:11 +02:00
starr-openai
fb94703b21 exec-server: add environment path refs (#25121)
## Summary
- add public `codex_exec_server::EnvironmentPathRef`
- bind an absolute path to its owning executor filesystem
- keep path operations in the next review slice

## Stack
- 1/5 in the skills path authority stack extracted from
https://github.com/openai/codex/pull/25098

## Validation
- `cd /Users/starr/code/codex-worktrees/pr-25098-restack4/codex-rs &&
just fmt`
- GitHub CI pending on rewritten head
2026-06-01 10:55:52 -07:00
jif-oai
917a9a41a3 Parallelize cold rollout compression (#25654)
## Why

[#25089](https://github.com/openai/codex/pull/25089) added the
background worker for compressing cold archived rollouts, but the worker
still processed files effectively one at a time: each compression job
was sent to `spawn_blocking` and then awaited before the next file
started. On machines with a backlog of archived rollouts, that makes
catch-up slower than it needs to be even though the actual compression
work already runs off the async runtime.

## What Changed

- Queue rollout compression work in a `JoinSet` while directory
traversal continues.
- Cap the worker at two in-flight compression jobs so it can overlap
compression without turning the background task into unbounded blocking
work.
- Drain pending jobs before returning, including the
`read_dir.next_entry()` error path, so every launched job still
contributes to the final `compressed`, `skipped`, and `failed` stats.
- Treat task join failures the same way as compression failures in the
worker's warning and failure accounting.
2026-06-01 19:54:52 +02:00
jif-oai
e6eb462f07 nit: drop todo (#25655) 2026-06-01 19:48:29 +02:00
Shijie Rao
795031621d [codex] Use git CLI for release Cargo fetches (#25644)
## Summary
- Configure the rust-release build job with
`CARGO_NET_GIT_FETCH_WITH_CLI=true`
- Document the macOS SecureTransport/libgit2 failure mode that hit the
`libwebrtc`/`libyuv` git submodule fetch

## Root cause
The release run at
https://github.com/openai/codex/actions/runs/26717498860/job/78745156683
repeatedly failed before compilation because Cargo's libgit2 fetch path
could not clone the nested `yuv-sys/libyuv` submodule from
`chromium.googlesource.com`, ending with `SecureTransport error:
connection closed via error`.

## Validation
- `git diff --check`

This is a workflow-only change, so I did not run Rust package tests.
2026-06-01 10:34:12 -07:00
Vivian Fang
2bf1c986f9 [codex] Inherit raw events for spawned child listeners (#25603) 2026-06-01 10:13:56 -07:00
Eric Traut
8b759b9c18 Disable SQLite intrinsics for Windows x64 releases (#25490)
## Why

Codex 0.135.0 started shipping bundled SQLite 3.51.x via SQLx 0.9.0 to
avoid the older WAL corruption bug fixed by #24728. On Windows x64,
#25367 reports an immediate `STATUS_ILLEGAL_INSTRUCTION` crash on a
Haswell CPU when starting normal Codex paths.

Rather than downgrading SQLite, this keeps the newer bundled SQLite
source and removes SQLite compiler-intrinsic code paths from the Windows
x64 release build.

## What changed

For `x86_64-pc-windows-msvc` release builds, export
`LIBSQLITE3_FLAGS=SQLITE_DISABLE_INTRINSIC` before `cargo build` in:

- `.github/workflows/rust-release.yml`
- `.github/workflows/rust-release-windows.yml`

Other targets keep their current SQLite build flags.

## Verification

- `git diff --check`
2026-06-01 09:49:55 -07:00
jif-oai
01cb97851b Compress cold local rollouts (#25089)
## Rollout compression stack

This stack splits #24941 into reviewable steps for local rollout
compression. The design is intentionally staged:

1. Teach readers, listing, search, and lookup to understand compressed
rollouts.
2. Make append and resume paths materialize compressed rollouts back to
plain JSONL before writing.
3. Add a disabled-by-default worker that can compress cold archived
rollouts behind `local_thread_store_compression`.

The key invariant is that writers append to plain `.jsonl`. A
`.jsonl.zst` file is a cold/read representation; if a write is needed,
the compressed file is materialized back to plain JSONL first. Readers
prefer plain `.jsonl` when both forms exist and can fall back to the
compressed sibling during transitions.

The worker is deliberately the last PR and remains behind an
under-development feature flag. It currently scans only
`archived_sessions`, not active `sessions`, because active sessions have
the highest resume/append race risk. That means this stack does not yet
compress most unarchived local history.

## Known race / follow-up

The remaining unresolved design question is writer/compressor
coordination. Even for archived rollouts, a resume or metadata update
can append while the worker is replacing the plain file with
`.jsonl.zst`; the current double-stat checks narrow but do not fully
eliminate the window where a writer has opened the plain file before
unlink. Do not treat the worker PR as production-ready until we either:

- prevent append/resume paths from racing archived compression, or
- introduce a shared representation/append lock or equivalent
coordination.

The first two PRs are useful independently: they make compressed
rollouts readable and make append paths safely recover back to plain
JSONL. The third PR isolates the worker behavior so that coordination
issue is reviewable separately.

## Validation

Focused local validation for the stack includes:

- `just test -p codex-rollout`
- `just test -p codex-thread-store` where thread-store paths were
touched
- `just test -p codex-features` for the feature flag slice
- `just bazel-lock-check` after dependency graph changes
- scoped `just fix -p ...` passes for changed crates

CI is still the source of truth for the full platform matrix.

## This PR in the stack

This is PR 3/3, based on #25088. It adds the under-development feature
flag and starts the best-effort background worker when enabled. The
worker currently compresses only cold archived rollouts, skips active
sessions, verifies compressed output, preserves mtime and permissions,
keeps a store-level lock heartbeat, and cleans stale temp files.

Stack order:

1. #25087: read compressed local rollouts.
2. #25088: materialize compressed rollouts before append.
3. This PR: add the disabled local compression worker.
2026-06-01 18:35:58 +02:00
jif-oai
3cdce52865 Preserve renamed thread titles during reconciliation (#25624)
## Summary
- preserve existing explicit SQLite thread titles during rollout
reconciliation/backfill when the incoming rollout title is only
first-message-derived
- keep stale inferred-title repair behavior while avoiding session-index
scans during startup backfill
- add a regression test for renamed titles surviving reconcile

## Testing
- just fmt
- just test -p codex-rollout
- just test -p codex-state
2026-06-01 18:33:05 +02:00
Eric Traut
f1d029cf75 Add reasoning-only status surface item (#25504)
Closes #24886.

## Why
Users can configure the TUI status line and terminal title with
`model-with-reasoning`, but issue #24886 asks for a compact
reasoning-only item. That lets a setup show just `default`, `low`,
`medium`, `high`, or `xhigh` without repeating the model name.

## What changed
- Added a `reasoning` item for `/statusline` and `/title` setup flows.
- Rendered the item from the effective reasoning effort, including
collaboration-mode overrides.
- Registered `reasoning` with `codex doctor` so Codex-generated
terminal-title config is not reported as invalid.
- Updated TUI setup snapshots so the picker previews include the new
item.
2026-06-01 09:30:20 -07:00
Eric Traut
6681446477 Reset slash popup selection when filter changes (#25492)
## Summary

Fixes #25295.

The slash-command popup reused its previous `ScrollState` when the
composer filter token changed. After scrolling the full `/` command
list, typing a narrower filter such as `/st` could clamp the stale
selection into the filtered results and highlight the wrong command.

This resets the popup selection and viewport only when the parsed filter
token changes, so normal arrow navigation is preserved while new filters
start at the first match.
2026-06-01 09:17:19 -07:00
Eric Traut
f94c49cf46 Use deep links for macOS codex app paths (#25485)
## Why

`codex app [PATH]` is the documented CLI entry point for opening Codex
Desktop on a workspace. Recent desktop builds can focus the app while
failing to honor paths passed as macOS document-open arguments via `open
-a Codex.app <workspace>`, which broke `codex app .` for users. See
#25333; related report: #25166.

The desktop app still supports the explicit
`codex://threads/new?path=...` route, so the CLI should use that
app-owned launch surface instead of depending on folder-open event
delivery.

## What Changed

- Build a `codex://threads/new?path=<workspace>` URL in the macOS app
launcher.
- Pass that URL to `open -a <Codex.app>` instead of passing the
workspace path as a document argument.
- Add coverage that workspace paths needing escaping round-trip through
URL query encoding.

## Verification

- `just test -p codex-cli codex_new_thread_url_encodes_workspace_path`
2026-06-01 09:17:08 -07:00
Charlie Marsh
12c37a6b5c Allow paste in searchable selection menus (#25400)
## Summary

I frequently want to be able to paste into the searchable menu -- the
most common use-case here is when specifying an upstream for a
`/review`, where I copy the upstream from an open terminal.
2026-06-01 18:01:52 +02:00
Won Park
13edafb6ed Preserve auto-review approval policy in codex exec (#23763)
## Why

`codex exec` was forcing headless runs to `approval_policy = "never"`
even when the resolved reviewer was `auto_review`. That prevented
unattended exec workflows from reaching the reviewed MCP write path they
were configured to use.

## What changed

- Keep the existing headless `never` default for ordinary exec runs.
- Re-resolve exec config without that synthetic override when the final
reviewer resolves to `AutoReview`, so configured or requirements-driven
approval policy is preserved.
- Add regression coverage for:
  - `auto_review` plus `on-request` from user config
- requirements-driven `AutoReview`, asserting exec’s final approval
policy matches the no-override control config exactly

## Validation

- `just fmt`
- `cargo test -p codex-exec`
2026-06-01 08:53:25 -07:00
Felipe Coury
c0ea566bb5 feat(tui): restore output-free cancelled prompts (#25316)
## TL;DR

When you press Esc or Ctrl+C after sending a prompt but before any
output was rendering, it restores the last composer and the message.

## Summary

Cancelling a prompt immediately after submission should behave like
returning to edit that prompt, not like discarding the user's draft.
Today, pressing `Esc` or `Ctrl+C` before Codex responds leaves the
submitted prompt in the transcript and returns an empty composer,
forcing the user to recall or retype it.

When an interrupted turn has not produced substantive visible output,
restore its submitted prompt directly into the composer and roll back
that latest turn. This also covers the first prompt in a fresh thread,
before the TUI has retained a local user-history cell. The restored
draft keeps its text, image attachments, and active collaboration mode
so it can be edited and resubmitted in place.

Restoration is intentionally suppressed once the turn has produced
user-visible activity such as assistant output, tool work, hooks, or
patches. A transient thinking status does not make the prompt
ineligible. Rollback also rebuilds terminal scrollback from the retained
transcript cells so repeated cancellations and terminal resizes do not
duplicate history.

## How to Test

1. Start the TUI with `cargo run -p codex-cli --bin codex`.
2. In a fresh thread, submit the first prompt and press `Esc` before
Codex emits substantive output. Confirm that the prompt returns to the
composer for editing and its submitted transcript row is removed.
3. Repeat with `Ctrl+C`, then repeat after at least one completed turn.
Confirm the same behavior.
4. Submit a prompt, wait for assistant output or tool activity, then
cancel. Confirm that the transcript remains intact and the prompt is not
restored into the composer.
5. Cancel several output-free prompts and resize the terminal between
attempts. Confirm that the startup banner, tip, and transcript history
do not duplicate in scrollback.

Targeted tests:
- `just test -p codex-tui cancelled_turn_edit_restores_prompt`
- `just test -p codex-tui
output_free_interrupted_turn_requests_prompt_restore`
- `just test -p codex-tui
visible_output_prevents_cancelled_turn_prompt_restore`
- `just test -p codex-tui
thinking_status_keeps_cancelled_turn_prompt_restore_eligible`
- `just test -p codex-tui
patch_activity_prevents_cancelled_turn_prompt_restore`

The full `just test -p codex-tui` run completed with `2746` passing
tests and two unrelated existing guardian feature-flag failures. `just
argument-comment-lint` remains blocked locally by the existing Bazel
LLVM `compiler-rt` sanitizer-header glob failure; the touched Rust diff
was manually audited for positional literal comments.
2026-06-01 11:49:14 -03:00
Felipe Coury
4eded02f52 [codex] fix compressed rollout fixture SessionMeta initialization (#25628)
## Summary
- initialize `parent_thread_id` in the compressed rollout test fixture's
`SessionMeta`
- restore rollout test compilation across Bazel test, clippy,
release-build, and argument-comment-lint jobs

## Root cause
PR #25087 (`Read compressed rollouts and materialize before append`)
added `codex-rs/rollout/src/compression_tests.rs` in merge commit
`a8a6071279b6f3112fcc5fc3fee69c48473d7149`. Its `write_rollout` fixture
constructs `SessionMeta` without the required `parent_thread_id` field,
causing `error[E0063]` when Bazel compiles `rollout-unit-tests-bin` on
`main` and downstream PRs.

## Validation
- `UV_CACHE_DIR=/private/tmp/codex-uv-cache just fmt`
- `just test -p codex-rollout` (`59` tests passed; bench smoke passed)
- `git diff --check`
- manually audited the touched Rust diff for positional literal argument
comments; the change adds no positional callsite

## Local lint blocker
- `just argument-comment-lint` could not reach source inspection locally
because Bazel's LLVM dependency fails analysis:
`compiler-rt/BUILD.bazel` glob `include/sanitizer/*.h` matched no files.
2026-06-01 16:43:21 +02:00
jif-oai
a8a6071279 Read compressed rollouts and materialize before append (#25087)
## Why

Local rollout compression needs a cold `.jsonl.zst` representation
without letting compressed physical paths leak into append-mode writers.
The unsafe case is resume or metadata update code successfully reading a
compressed rollout and then appending raw JSONL bytes to the zstd file.

This PR folds the former #25088 materialization slice into the
read-support PR so the reader changes and append-safety invariant land
together.

## What Changed

- Teach rollout readers, discovery, listing, search, and ID lookup to
understand compressed `.jsonl.zst` rollouts.
- Keep `.jsonl` as the logical/stored rollout path while allowing read
paths to open either plain or compressed storage.
- Materialize compressed rollouts back to plain `.jsonl` before
append-mode writes, including resume and direct metadata append paths.
- Preserve compressed-file permissions when materializing back to plain
JSONL.
- Refresh thread-store resolved rollout paths after compatibility
metadata writes so reconciliation follows the materialized file.
- Avoid treating transient compression temp files as real rollout lookup
results.

## Remaining Stack

#25089 remains the separate worker PR. It is based directly on this PR
and stays behind the disabled `local_thread_store_compression` feature
flag.

The worker still has a broader coordination question: a resume or
metadata update can race with background compression while a plain file
is being replaced by `.jsonl.zst`. This PR handles the read and
materialize-before-append primitives; it does not make the worker
production-ready.

## Validation

- `just test -p codex-rollout`
- `just test -p codex-thread-store`
- `just fix -p codex-rollout`
- `just fix -p codex-thread-store`
- `just bazel-lock-check`
2026-06-01 15:14:19 +02:00
jif-oai
f27bbbd49c Add goal extension GoalApi (#25096)
## Summary

- add an extension-owned `GoalApi` for thread goal get/set/clear
operations
- register live goal runtimes with the API from the goal extension
backend
- cover the API and runtime-effect paths in goal extension tests

## Stack

Follow-up app-server wiring PR: #25108

## Validation

- `just fmt`
- `just fix -p codex-goal-extension`
- `just test -p codex-goal-extension`
2026-06-01 11:32:13 +02:00
jif-oai
48c16b8bcb Remove Plan-mode gate from idle turn injection (#25577)
## Why

`try_start_turn_if_idle` is the core helper for starting injected input
only when the session is actually idle. It should stay focused on
generic turn-lifecycle safety. The previous `ModeKind::Plan` guard mixed
caller policy into that helper: Plan mode may choose not to auto-start
some extension work, but that decision belongs at the extension or
caller boundary rather than in the session injection primitive.

## What changed

- Removed the `ModeKind::Plan` early return from
`Session::try_start_turn_if_idle`.
- Removed the now-unused `ModeKind` import from
`core/src/session/inject.rs`.

## Testing

Not run locally.
2026-06-01 11:06:38 +02:00
jif-oai
c875bc8a33 Use templates for goal steering prompts (#25576)
## Why

Goal steering prompts have grown into long inline Rust strings, which
makes the authored prompt text hard to review and easy to damage while
changing the surrounding plumbing. Moving those prompts into embedded
Markdown templates keeps the policy text in the shape reviewers actually
read, while preserving the existing runtime substitution and objective
escaping behavior.

## What changed

- Added `ext/goal/templates/goals/continuation.md`, `budget_limit.md`,
and `objective_updated.md` for the three goal steering prompts.
- Updated `ext/goal/src/steering.rs` to parse those embedded templates
once with `codex-utils-template` and render the existing goal values
into them.
- Kept user objectives XML-escaped before rendering and converted budget
counters into template variables.
- Added the template directory to `ext/goal/BUILD.bazel` `compile_data`
so Bazel has the same embedded prompt inputs as Cargo.

## Testing

- Not run locally.
2026-06-01 10:55:14 +02:00
jif-oai
f1b1b64005 Add goal extension idle continuation (#25060)
## Why

The goal extension needs a way to resume an active goal after the thread
becomes idle, but the old core goal runtime should not be refactored as
part of this step. The missing piece is a small core-owned turn-start
primitive: let an extension ask for a normal model turn only when the
thread is idle, and otherwise fail without injecting into whatever is
currently active.

## What Changed

- Adds `CodexThread::try_start_turn_if_idle(...)` as the narrow
extension-facing primitive for synthetic idle work.
- Implements the session side so it refuses to start when:
  - the provided input is empty,
  - the session is in plan mode,
  - a turn is already active, or
  - trigger-turn mailbox work is pending.
- Gives trigger-turn mailbox work priority if it appears while the idle
turn is being prepared.
- Wires `GoalExtension::on_thread_idle` to read the active persisted
goal and submit the continuation prompt through this idle-only
primitive.
- Keeps the legacy core goal continuation implementation in place
instead of folding it into this PR.

## Behavior

This is intentionally best-effort. If `try_start_turn_if_idle` observes
that the thread is not idle, or that higher-priority mailbox work should
run first, it returns the input to the caller. The goal extension drops
that continuation prompt and waits for a future idle opportunity instead
of injecting stale synthetic goal text into an active turn.

## Validation

- `just test -p codex-core
try_start_turn_if_idle_rejects_active_turn_without_injecting`
- `just test -p codex-goal-extension`
2026-06-01 10:42:01 +02:00
jif-oai
8d49394feb Set multi-agent v2 dogfood defaults (#25266)
## Summary
- default multi-agent v2 to direct-model-only tools so code mode does
not wrap subagent tools
- add default root/subagent team prompts aligned with dogfood training
assumptions
- tighten spawn-agent model override wording to prefer the inherited
model by default

## Tests
- just fmt
- just test -p codex-core
spawn_agent_description_lists_visible_models_and_reasoning_efforts
- just test -p codex-core
multi_agent_v2_default_session_thread_cap_counts_root
- just test -p codex-rollout-trace
- just fix -p codex-core
- just fix -p codex-rollout-trace

Note: a broad just test -p codex-core run was attempted locally, but
this sandbox produced unrelated environment failures around
sandbox-exec, missing test_stdio_server, and realtime timeouts.
2026-06-01 10:24:46 +02:00
Owen Lin
cf0911076f store and expose parent_thread_id on Threads (#25113)
## Why

This PR
https://github.com/openai/codex/pull/24161#discussion_r3325692763
revealed a subagent data modeling issue, where we overloaded
`forked_from_id` to also mean `parent_thread_id`. That's incorrect since
guardian and review subagents can be a subagent and NOT fork the main
thread's history.

The solution here is to explicitly store a new `parent_thread_id` on
`SessionMeta`, alongside `forked_from_id` which already exists. While
we're at it, also expose it in the app-server protocol on the `Thread`
object.

A thread->subagent relationship and a fork of thread history are
orthogonal concepts.

## What Changed

- Added top-level `parent_thread_id` persistence on `SessionMeta` and
runtime/session plumbing through `SessionConfiguredEvent`,
`CodexSpawnArgs`, `SessionConfiguration`, `ThreadConfigSnapshot`,
`TurnContext`, and `ModelClient`.
- Made turn metadata, request headers, analytics, and subagent-start
events read the separate runtime/top-level parent field instead of
deriving general parent lineage from `SessionSource` or
`forked_from_thread_id`.
- Passed parent lineage separately at delegated subagent, review,
guardian, agent-job, and multi-agent spawn construction sites;
copied-history fork lineage remains derived only from `InitialHistory`.
- Persisted and exposed parent lineage through rollout/thread-store
projections and app-server v2 `Thread.parentThreadId`.
- Updated app-server README text and regenerated app-server schema
fixtures for the additive `parentThreadId` response field.
2026-06-01 04:33:20 +00:00
Shijie Rao
3b7334d099 Revert "Add build_unsigned_archive release mode" (#25462)
Reverts openai/codex#25435
2026-05-31 16:05:33 -07:00
joeflorencio-openai
8a556296f0 Add cloud-managed config layer support (#24620)
## Summary

PR 3 of 5 in the cloud-managed config client stack.

Adds enterprise-managed cloud config as a first-class config layer
source. The layer metadata is preserved through config loading,
diagnostics, debug output, hook attribution, and app-server protocol
surfaces.

## Details

- Enterprise-managed config becomes a normal config layer source with
backend-supplied `id` and display `name` attached for provenance.
- These layers are designed to behave like non-file managed config: they
can surface syntax/type diagnostics by layer name even though there is
no physical config file.
- Relative path settings are resolved from a stored config base so
cloud-delivered config remains consistent with existing MDM-delivered
config semantics.
- Hook attribution distinguishes config-delivered hooks from
requirements-delivered hooks via `HookSource::CloudManagedConfig`.
- This remains pull-based and snapshot-oriented; the PR adds layer
identity/diagnostics, not dynamic reload behavior.

## Validation

Validated through the targeted stack checks after rebasing onto current
`main`:

- Rust crate tests for
config/hooks/cloud-config/backend-client/app-server-protocol
- Filtered `codex-core` and `codex-app-server` `cloud_config_bundle`
tests
- Python generated-file contract test
- `cargo shear --deny-warnings`
- Targeted `argument-comment-lint` for config/hooks
2026-05-31 15:54:31 -07:00
joeflorencio-openai
20debf746b Compose requirements layers (#24619)
## Summary

PR 2 of 5 in the cloud-managed config client stack.

Adds a shared requirements-layer composition engine. The composer
defines how ordered requirements layers combine, with focused tests for
the merge semantics and provenance behavior. The final PR in the stack
wires runtime requirements sources into this path.

## Details

- Mental model: requirements layers are ordered lowest priority first,
matching `ConfigLayerStack`; lower-priority layers provide defaults
while higher-priority layers win scalar/list conflicts.
- Regular fields use config-style TOML merging, including recursive
table merging, so requirements layering follows the same broad model as
`config.toml` layering.
- Domain-specific fields keep explicit semantics: `rules.prefix_rules`
and hooks preserve high-priority-first output, hooks fail closed on
active managed-dir conflicts, and `permissions.filesystem.deny_read`
dedupes as a stable high-priority-first union.
- `remote_sandbox_config` is evaluated within each layer before the
regular TOML merge, so host-specific sandbox constraints do not leak
across layers.
- Provenance points at the exact source when one layer owns a value and
uses composite provenance when a table field is assembled from multiple
layers.

## Validation

Local validation:

- `just fmt`
- `cargo check -p codex-config`
- `just test -p codex-config requirements_composition`
- `git diff --check`

CI will run the broader test matrix.
2026-05-31 15:14:06 -07:00
Shijie Rao
5f60b01352 Add build_unsigned_archive release mode (#25435)
## Why
We want a manual mode that produces the full packaged unsigned macOS
Codex archive, including bundled resources like `rg`, without mixing
those archives into the signing and publishing flow.

The existing `build_unsigned` mode is the handoff used by external
signing and `promote_signed`, so archive-only inspection and local
packaging should live in a separate mode and artifact namespace.

## What Changed
- added `build_unsigned_archive` as a new manual `release_mode`
- kept the existing `build` matrix running for that mode instead of
introducing a separate archive-only job
- wrote unsigned macOS package archives to
`codex-rs/unsigned-archive-dist/...` instead of the normal `dist/...`
tree
- uploaded those packaged macOS outputs as dedicated
`*-unsigned-archive` workflow artifacts
- kept `build_unsigned` and `promote_signed` on their existing raw
unsigned binary path

## Validation
- parsed `.github/workflows/rust-release.yml` with `ruby -e 'require
"yaml"; YAML.load_file(".github/workflows/rust-release.yml")'`
- ran `git diff --check -- .github/workflows/rust-release.yml`
- reviewed the workflow diff to confirm `build_unsigned_archive` now
reuses the existing `build` job while isolating the unsigned macOS
package archives under dedicated artifact names
- locally verified the package builder layout against unsigned macOS
binaries to confirm the packaged archive contains `bin/codex`,
`codex-path/rg`, and `codex-resources/zsh/bin/zsh`
2026-05-31 14:56:06 -07:00
joeflorencio-openai
e93dc98a48 Add config bundle transport types (#24617)
## Summary

PR 1 of 5 in the cloud-managed config client stack.

Adds the generated backend models and client transport surface for the
config bundle endpoint. This bundle endpoint is the replacement backend
surface for legacy cloud requirements; the final PR in the stack
switches runtime consumers over to it.

## Details

- This is transport-only plumbing: no runtime config behavior changes in
this PR.
- The bundle endpoint is the new shared backend surface for
cloud-delivered config and requirements data.
- Both supported path styles are wired here: `/api/codex/config/bundle`
and `/wham/config/bundle`.
- The response types come from generated backend models so later PRs
consume the backend contract directly instead of maintaining
hand-written mirror structs.

## Validation

Validated through the targeted stack checks after rebasing onto current
`main`:

- Rust crate tests for
config/hooks/cloud-config/backend-client/app-server-protocol
- Filtered `codex-core` and `codex-app-server` `cloud_config_bundle`
tests
- Python generated-file contract test
- `cargo shear --deny-warnings`
- Targeted `argument-comment-lint` for config/hooks
2026-05-31 11:52:18 -07:00
Felipe Coury
2f0726ad6d feat(tui): allow function keys through f24 in keymaps (#25329)
## Why

Closes #25006.

`tui.keymap` currently rejects `F13` even though Codex's terminal event
layer can report higher function keys. This prevents users from using
common remappings such as Caps Lock to `F13`.

## What Changed

- Define a shared portable upper bound of `F24` for stored TUI
keybindings.
- Accept `f13` through `f24` in config normalization and runtime
parsing.
- Allow `/keymap` capture to persist `F13` through `F24`.
- Update the unsupported-function-key error and add boundary tests for
`F13`, `F24`, and `F25`.

## How to Test

1. Add a binding such as:

   ```toml
   [tui.keymap.global]
   open_transcript = "f13"
   ```

2. Start Codex and press the remapped `F13` key.
3. Confirm Codex loads the config without the previous `F1 through F12`
error and the action runs.
4. Open `/keymap`, capture `F13` for an action, and confirm the saved
binding is `f13`.
5. As a regression check, try to capture `F25` and confirm Codex reports
that only `F1` through `F24` can be stored.

Targeted tests:

- `just test -p codex-config`
- `just test -p codex-tui function_keys`

Full `just test -p codex-tui` completed with 2,752 passing tests, 4
skipped tests, and two unrelated guardian feature-flag failures:

-
`app::tests::update_feature_flags_disabling_guardian_clears_review_policy_and_restores_default`
-
`app::tests::update_feature_flags_disabling_guardian_clears_manual_review_policy_without_history`
2026-05-31 15:42:39 -03:00
xl-openai
cdde711fac [codex] Avoid forced directory refresh during plugin install auth checks (#25381)
## Summary
- Use normal directory loading for plugin install app metadata so
install avoids forced directory refresh while still loading metadata on
cold cache.
- Continue force-refreshing codex_apps tools for auth state.
- Add regression coverage that pre-warms the directory cache and asserts
install returns cached app metadata without extra directory requests.

## Validation
- just fmt
- git diff --check
- just test -p codex-app-server plugin_install_returns_apps_needing_auth
plugin_install_filters_disallowed_apps_needing_auth (blocked locally:
cargo-nextest is not installed)
2026-05-31 02:14:15 -07:00
Owen Lin
966932124c fix: Limit Bedrock GPT models to default service tier (#25318)
## Description

Bedrock currently only supports the implicit `default` service tier for
GPT models. This PR strips non-default service tier metadata from
Bedrock model catalogs so Codex does not advertise or send unsupported
tiers.

## What changed

- Normalize both built-in and configured Bedrock catalogs to
default-only service tier behavior.
- Add regression coverage for built-in and configured Bedrock catalogs.

## Validation

- `just fmt`
- `just test -p codex-model-provider`
2026-05-30 11:54:58 -07:00
jif-oai
8acaec73b6 Rename multi-agent v2 assignment tool (#25267)
## Summary
- rename the multi-agent v2 follow-up task tool surface to assign_task
- update core tests and spec-plan expectations
- keep rollout-trace classification backward-compatible with legacy
followup_task

## Tests
- just fmt
- just test -p codex-core
multi_agents_spec::tests::assign_task_tool_requires_message_and_has_no_output_schema
- just test -p codex-rollout-trace
- just fix -p codex-core
- just fix -p codex-rollout-trace

Note: a broad just test -p codex-core run was attempted locally, but
this sandbox produced unrelated environment failures around
sandbox-exec, missing test_stdio_server, and realtime timeouts.
2026-05-30 14:13:05 +02:00
Eric Traut
3e7baa00e4 Add thread archive CLI commands (#25021)
## Problem

Saved threads can already be archived through app-server RPCs, but the
command line did not expose direct archive or unarchive commands.

## Solution

Add `codex archive <thread>` and `codex unarchive <thread>`, resolving
UUIDs or exact thread names before calling the existing `thread/archive`
and `thread/unarchive` RPCs. The commands support scoped remote flags so
callers can target remote app-server endpoints when archiving or
unarchiving threads.

This also fixes a long-standing bug in `codex resume <thread id>` and
`codex fork <thread id>` that I found when testing the new commands.
These operations shouldn't be allowed on archived sessions. They now
fail with an error that tells the user to run `codex unarchive <thread
id>` first.

## Verification

Added app-server coverage for rejecting archived thread resume by id and
checking that the error includes the matching `codex unarchive <thread
id>` command.
2026-05-29 23:37:26 -07:00
Dylan Hurd
e0435afb72 feat(config) experimental_request_user_input toggle (#24541)
## Summary
Experimental flag to allow toggling `request_user_input`:

```
tools.experimental_request_user_input = false
```

## Testing
- [x] Added unit tests
2026-05-29 21:35:53 -07:00
Celia Chen
00ca857d3f fix: Bedrock API key region fallback (#25171)
## Why

Users following the Amazon Bedrock API-key setup can export
`AWS_BEARER_TOKEN_BEDROCK` and `AWS_REGION`, but Codex's bearer-token
auth path only accepted `model_providers.amazon-bedrock.aws.region`.
That made the documented env-based setup fail with a missing-region
error even though the standard AWS region environment variable was
present.

## What Changed

- Updates Bedrock bearer-token region resolution to use
`model_providers.amazon-bedrock.aws.region` first, then fall back to
`AWS_REGION`, then `AWS_DEFAULT_REGION`.
- Updates the missing-region error to list all supported region sources.
- Adds focused coverage for config precedence, `AWS_REGION`,
`AWS_DEFAULT_REGION`, and the missing-region failure.
2026-05-30 01:17:38 +00:00
Eric Ning
e929bb5c88 [codex] Update remote connector suggestions (#25172)
## Summary

- Use the session-loaded plugin app IDs as the source of connector
suggestion candidates.
- Remove the redundant plugin reload from
`tool_suggest_connector_ids()`.
- Add regression coverage for connectors declared by a loaded remote
plugin, using the Databricks app case.

## Context

Loaded remote plugins can declare app connector IDs in `.app.json`. The
session-owned `PluginsManager` already loads those plugins and exposes
their effective app IDs.

The connector suggestion path was creating a separate `PluginsManager`
and recomputing plugin app IDs. That new manager does not share the
session manager’s remote installed plugin cache, so app IDs from loaded
remote plugins were missing from connector suggestions.

## Fix

Pass the already-loaded effective app IDs into connector suggestion
generation and use them directly as the plugin-derived connector
candidate set.

Connector candidates are now built from:

- App IDs declared by loaded plugins
- Explicitly configured connector discoverables
- Existing disabled-suggestion filtering

This avoids a second plugin-manager lookup and keeps connector
suggestions aligned with the plugins actually loaded for the turn.

## Behavior

For example, when a plugin is loaded and its `.app.json` declares data
apps, `list_available_plugins_to_install` can now return those data
connectors.

This does not create plugin suggestions from the plugin itself. Plugin
suggestions still come from eligible uninstalled entries in the
marketplace catalog and require existing matching/filtering rules.

## Validation

- `just fmt`
- Added regression coverage for a loaded-plugin connector ID appearing
in discoverable tools
- Attempted `just test -p codex-core`; the command exited unsuccessfully
in the local test environment without useful failure detail captured in
the run output
2026-05-29 17:57:34 -07:00
Abhinav
a5a94ee5a7 Constrain Windows sandbox requirements (#23766)
# Why

Managed requirements can already constrain sandbox policy choices, but
Windows sandbox implementation selection was still resolved
independently from those requirements. That left the TUI able to
continue through the unelevated fallback even when an organization wants
to require the elevated Windows sandbox implementation.

# What

- Add `[windows].allowed_sandbox_implementations` requirements support
for the Windows `elevated` and `unelevated` implementations.
- Apply that allowlist during core config resolution so disallowed
configured or feature-selected Windows sandbox implementations fall back
to an allowed implementation with the existing requirements warning
path.
- Reuse the existing TUI Windows setup prompts to block disallowed
unelevated continuation, keep required elevated setup in front of the
user, and refuse to persist a TUI-selected Windows sandbox mode that
requirements disallow.

# Semantics

| Allowed | Selected | Effective |
| --- | --- | --- |
| `["elevated"]` | `unelevated` / unset | `elevated` |
| `["unelevated"]` | `elevated` / unset | `unelevated` |
| `["elevated", "unelevated"]` | `elevated` | `elevated` |
| `["elevated", "unelevated"]` | `unelevated` | `unelevated` |
| `["elevated", "unelevated"]` | unset | `elevated` |

Availability is handled by interactive setup surfaces after allowlist
resolution. If the effective elevated implementation is not ready,
elevated-only requirements block on setup. When unelevated is also
allowed, the UI may offer the existing unelevated fallback.

## TUI Screens

If elevated setup is not already complete:
```
  Your organization requires the default Codex agent sandbox to continue. Set it up to protect your files and control
  network access.
  Learn more <https://developers.openai.com/codex/windows>

› 1. Set up default sandbox (requires Administrator permissions)
  2. Quit
```

If admin setup fails under `["elevated"]`:
```
  Couldn't set up your sandbox with Administrator permissions

  Your organization requires the default sandbox before Codex can continue.
  Learn more <https://developers.openai.com/codex/windows>

› 1. Try setting up admin sandbox again
  2. Quit
```

# Next Steps


- extend the requirements/readout surface, such as
`configRequirements/read`, so clients can inspect the loaded
`[windows].allowed_sandbox_implementations` requirement instead of
inferring it from Windows setup state
- consider extending `windowsSandbox/readiness` as well
- update the App startup guide, setup flow, and banner surfaces so an
elevated-only requirement omits any continue-unelevated escape hatch and
blocks startup until a permitted implementation is ready;
- preserve the existing unelevated fallback path when requirements allow
it, including the `["unelevated"]` case where elevated is disallowed
2026-05-29 16:31:33 -07:00
Noah MacCallum
8e5f561697 Filter plugin install suggestions by installed apps (#24996)
## Summary

- Keep the original `TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST` as a
fallback seed list, so users with no installed plugins still get initial
install suggestions.
- Allow additional install suggestions from trusted marketplaces:
`openai-curated` and `openai-bundled`.
- Require non-fallback, non-configured marketplace candidates to share
`.app.json` connector IDs with already installed plugins.
- Preserve explicit configured plugin discoverables as an override,
while still omitting installed, disabled, and `NOT_AVAILABLE` plugins.

## Context

`list_available_plugins_to_install` controls which plugins the model can
trigger via `request_plugin_install`. We want a small starter set for
empty/new users, but we also want installed workflow plugins to unlock
relevant source plugins without maintaining every source plugin ID by
hand.

This keeps the legacy plugin ID allowlist only as the starter fallback.
For everything else, the trusted marketplace is the candidate boundary,
and installed app connector overlap is the relevance filter. For
example, an installed Sales plugin can make HubSpot and Granola
suggestible when those source plugins are in `openai-curated` and share
Sales app connector IDs, while an unrelated test-source plugin with an
app connector not declared by Sales stays hidden.

## Test Coverage

- Empty/no-installed-plugin case: returns the fallback seed plugins from
the original allowlist.
- Installed-app expansion: returns non-fallback marketplace plugins only
when their app connector IDs overlap with an installed plugin.
- Sales workflow case: installed Sales declares HubSpot and Granola
apps, so `hubspot@openai-curated` and `granola@openai-curated` are
returned.
- Sales negative case: `test-source@openai-curated` has an app connector
not declared by Sales, so it is not returned.
- Existing guardrails: installed plugins, disabled suggestions, and
`NOT_AVAILABLE` plugins remain omitted; explicit configured
discoverables still work as an override.

## Validation

- `just fmt`
- `just test -p codex-core plugins::discoverable::tests`
- `just test -p codex-core` was attempted earlier, but current `main` /
local env failed with unrelated existing failures around missing
`test_stdio_server`, CLI/code-mode MCP tool setup, and
unified_exec/shell snapshot flakes/timeouts. The touched discoverable
tests pass.
2026-05-29 15:32:04 -07:00
Adam Perry @ OpenAI
a076b21730 Recommend Bazel VSCode extension. (#25161)
Provides starlark syntax highlighting and editor formatting.
2026-05-29 15:24:41 -07:00
Jinghan Xu
f2e7b462a9 [codex] Fix Vim normal mode editing (#25022)
## Summary
- add Vim normal-mode `s` support to substitute the character under the
cursor and enter insert mode
- fix Vim normal-mode `o` so opening below the final line moves the
cursor onto the new blank line
- update keymap config/schema and keymap picker snapshots for the new
action

## Validation
- `just fmt`
- `just write-config-schema`
- `just test -p codex-config`
- focused `just test -p codex-tui` coverage for the Vim `s` and `o`
behavior, keymap conflict handling, and keymap picker snapshots
- `cargo insta pending-snapshots --manifest-path tui/Cargo.toml`
- `git diff --check`

## Notes
A full `just test -p codex-tui` run still has two unrelated Guardian
feature-flag failures in this checkout:
-
`app::tests::update_feature_flags_disabling_guardian_clears_review_policy_and_restores_default`
-
`app::tests::update_feature_flags_disabling_guardian_clears_manual_review_policy_without_history`
2026-05-29 14:01:27 -07:00
starr-openai
a717e4ef31 exec-server: preserve fs helper CoreFoundation env (#25118)
## Summary
- preserve macOS `__CF_USER_TEXT_ENCODING` when launching the sandboxed
fs helper
- keep the fs-helper env narrow; this adds only the CoreFoundation
startup var instead of copying the broader MCP stdio baseline
- add focused coverage that the helper keeps that var without admitting
`HOME`

## Diagnosis
The sandboxed fs helper is not launched like a normal child process.
Exec-server rebuilds its environment from an allowlist, then calls
`env_clear()` before re-execing Codex with `--codex-run-as-fs-helper`.
That helper dispatches before the normal Codex startup path and only
needs to boot a small Tokio runtime, read one JSON request from stdin,
perform the direct filesystem operation, and write one JSON response.

The reported macOS hang sampled the helper before Rust main, in
CoreFoundation initialization while resolving the default text encoding:
`_CFStringGetUserDefaultEncoding -> getpwuid_r -> notify_register_check
-> bootstrap_look_up3 -> mach_msg2_trap`. The fs-helper allowlist kept
`PATH` and temp vars for runtime needs, but it dropped macOS
`__CF_USER_TEXT_ENCODING`. Other Codex subprocess launchers that
intentionally build a minimal Unix baseline, such as MCP stdio, already
preserve that variable.

My read is that stripping `__CF_USER_TEXT_ENCODING` forced this internal
helper down CoreFoundation's fallback user-lookup path, and that lookup
intermittently wedged on the affected machine before the helper could
read stdin or touch the target file. Preserving only this macOS startup
variable avoids that fallback without broadening the fs-helper
environment to shell-like vars such as `HOME`, `USER`, locale settings,
terminal settings, or proxy credentials.

Internal Slack thread omitted from the public PR body.

## Validation
- `cd codex-rs && just fmt`
- `git diff --check`
2026-05-29 12:20:17 -07:00
Eric Traut
20da4c37c5 ci: use issue triage environment for issue workflows (#25134)
## Summary

This adds `environment: issue-triage` to the Codex-calling issue
workflow jobs so they can read the GitHub Environment Secret while
staying on GitHub-hosted runners for public issue-triggered workflows.
2026-05-29 12:06:55 -07:00
sayan-oai
1f93706e99 [codex] Require model for standalone web search (#25131)
## Why

The standalone `/v1/alpha/search` request now requires a `model`, but
the `web.run` extension currently omits it.

Adds `model` to extension `ToolCall` invocation.

Follow-up to #23823.

## What changed

- Make `SearchRequest.model` required.
- Expose the effective per-turn model on extension tool calls and pass
it in standalone web-search requests.
- Assert the model is forwarded in the app-server round-trip test.

## Testing

- `just test -p codex-api -p codex-tools -p codex-web-search-extension
-p codex-memories-extension -p codex-goal-extension`
- `just test -p codex-core -E
'test(passes_turn_fields_and_scoped_turn_item_emitter_to_extension_call)'`
- `just test -p codex-app-server -E
'test(standalone_web_search_round_trips_encrypted_output)'`
2026-05-29 12:03:04 -07:00
Michael Bolin
a1ecf0cf1c thread-store: store permission profiles (#23165)
## Why

`SandboxPolicy` is the legacy compatibility shape, but
`codex-thread-store` still exposed it through `StoredThread`,
`ThreadMetadataPatch`, and live metadata sync. That kept thread-store
consumers tied to the legacy representation and meant richer permission
profile data could not round-trip through thread metadata or cold
rollout reconciliation.

## What Changed

- Replaced thread-store `sandbox_policy` API fields with canonical
`PermissionProfile` fields.
- Persist new permission-profile metadata as canonical JSON in the
existing SQLite metadata slot while continuing to read older legacy
sandbox policy values.
- Updated local, in-memory, live metadata sync, and rollout extraction
paths to propagate `TurnContextItem::permission_profile()`.
- Re-materialize legacy permission metadata against the final rollout
cwd when rollout-derived metadata replaces stale SQLite summaries.
- Updated affected app-server and core test constructors to build
`PermissionProfile` values directly.

## Test Plan

- `cargo test -p codex-state`
- `cargo test -p codex-thread-store`
- `cargo test -p codex-app-server
summary_from_stored_thread_preserves_millisecond_precision --lib`
- `cargo test -p codex-core realtime_context --lib`
2026-05-29 11:55:31 -07:00
Channing Conger
c9dc0f6338 code-mode: introduce durable session interface (#24180)
## Summary

Introduce a `CodeModeSession` interface for executing and managing
code-mode cells.

This moves cell lifecycle, callback delegation, termination, and
shutdown behind a session abstraction, while continuing to use the
existing in-process implementation, and the ability to implement an
external process one behind this interface.

A Codex session owns one `CodeModeSession`, which in turn owns its
running cells and stored code-mode state. Each cell is represented to
the caller as a `StartedCell`, exposing its cell ID and initial
response.

It also introduces a `CodeModeSessionDelegate` callback interface. A
session uses the delegate to invoke nested host tools and emit
notifications while a cell is running, allowing the runtime to
communicate with its owning Codex session without depending directly on
core turn handling.

<img width="2121" height="1001" alt="image"
src="https://github.com/user-attachments/assets/c349a819-2a59-485c-bda4-2caf68ac4c31"
/>
2026-05-29 11:42:52 -07:00
Eric Horacek
451b386442 [exec-server] Kill dropped filesystem helpers (#25116)
## Summary
- terminate sandbox filesystem helpers when the Tokio child handle is
dropped

## Why
A sandbox filesystem helper can stall during process startup before
reading stdin. If the owning async operation is cancelled or torn down,
the spawned helper should not remain running as an orphaned process.

Setting `kill_on_drop(true)` gives the filesystem helper the cleanup
behavior that Tokio child processes otherwise do not enable by default.

This intentionally does not add a timeout. It does not detect or recover
an active hung file edit while the owning future remains alive. A more
precise startup-health mechanism can be handled separately.

## Validation
- `just test -p codex-exec-server` (186 tests passed; benchmark smoke
passed)
- `just fmt`
- `just fix -p codex-exec-server`
- `git diff --check`
2026-05-29 11:40:44 -07:00
Owen Lin
fc9cf62efb Add subagent lineage metadata for responsesapi (#24161)
## Why

We recently added `forked_from_thread_id` which lets us trace where a
thread's _context_ comes from, but we also want to understand subagent
lineage (e.g. which parent thread spawned this subagent? what kind of
subagent is it?) which is orthogonal.

This PR adds `parent_thread_id` and `subagent_kind` to the
`x-codex-turn-metadata` header sent to ResponsesAPI.

## What changed

- Adds `parent_thread_id` and `subagent_kind` to core-owned
`x-codex-turn-metadata`.
- Restores persisted `SessionSource` and `ThreadSource` from resumed
session metadata so cold-resumed subagent threads keep their lineage on
later Responses API requests.
- Centralizes parent-thread extraction on `SessionSource` /
`SubAgentSource` and reuses it in the Responses client, analytics, agent
control, and state parsing paths.
- Extends reserved-key, git-enrichment, thread-spawn, and app-server v2
metadata coverage for the new lineage fields.

## Verification

- Not run locally per request.
- Added focused coverage in `core/src/turn_metadata_tests.rs` and
`app-server/tests/suite/v2/client_metadata.rs`.
2026-05-29 11:28:12 -07:00
Eric Traut
62039e8d35 Use session wording in /rename confirmation (#25035)
## Why

The TUI `/rename` confirmation should use the term "session" for
consistency.
2026-05-29 11:09:40 -07:00
Eric Traut
36cd36626d Add /archive slash command (#25027)
## Why

TUI users can archive saved sessions from other surfaces, but there is
no in-session command for archiving the active session. Since archiving
the active session also exits the TUI, the command should ask for
explicit confirmation instead of firing immediately.

I'm also working on [a companion
PR](https://github.com/openai/codex/pull/25021) that adds `codex
archive` and `codex unarchive` top-level CLI commands.

## What changed

- Adds a new `/archive` slash command described as `archive this session
and exit`.
- Shows a confirmation dialog with `No, don't archive` selected first
and `Yes, archive and exit` as the explicit action.
- On confirmation, calls the existing `thread/archive` app-server RPC
for the active main session and exits after success.
- Keeps `/archive` disabled while a task is running and unavailable in
side conversations.

## Verification

Added focused TUI coverage for the `/archive` confirmation flow,
disabled-while-task-running behavior, and the `/ar` slash-command popup
snapshot.
2026-05-29 11:07:19 -07:00
Eric Traut
1333f4a689 Align TUI permissions labels with app (#25017)
## Summary

The desktop app now presents the on-request permissions mode as `Ask for
approval` and the manual-review-backed mode as `Approve for me`. The TUI
still exposed older/internal labels like `Default` and `Auto-review`,
which made the same underlying settings look different across clients.

This updates the TUI UX copy to match the app without changing the
underlying default behavior. Fresh threads continue to use the existing
on-request approval mode, now displayed as `Ask for approval`.

The label changes cover `/permissions`, explicit profile permissions
menus, status surfaces, config persistence history/error text, and the
corresponding TUI snapshots.

### Before
<img width="1181" height="119" alt="Screenshot 2026-05-28 at 10 19
47 PM"
src="https://github.com/user-attachments/assets/0664846b-b6dd-4931-b4dd-d0af0d42058e"
/>
<img width="523" height="19" alt="Screenshot 2026-05-28 at 10 21 29 PM"
src="https://github.com/user-attachments/assets/7899c33e-b35d-4684-8389-97e357803423"
/>

### After
<img width="1216" height="117" alt="Screenshot 2026-05-28 at 10 19
32 PM"
src="https://github.com/user-attachments/assets/015aab43-ac97-411f-8031-75cdd887251b"
/>
<img width="567" height="18" alt="Screenshot 2026-05-28 at 10 20 24 PM"
src="https://github.com/user-attachments/assets/28b6422c-b823-4298-b221-c83d46d09d66"
/>
2026-05-29 11:06:40 -07:00
iceweasel-oai
cb9178e8b3 Add Windows sandbox provisioning setup command (#24831)
## Why

Some Windows users do not have local admin access, so they cannot
complete the elevated portion of the Windows sandbox setup when Codex
first needs it. This adds an alpha provisioning path that an admin or IT
deployment script can run ahead of time for the Codex user.

The intended managed-deployment shape is:

```powershell
codex sandbox setup --elevated --user "$env:COMPUTERNAME\Alice" --codex-home "C:\Users\Alice\.codex"
```

`--elevated` is treated as the requested sandbox setup level, not as
proof that the process is elevated. The Windows sandbox setup
orchestration still checks that the caller is actually elevated before
launching the helper without a UAC prompt.

## What changed

- Added `codex sandbox setup --elevated` with explicit user selection
via either `--current-user` or `--user ... --codex-home ...`.
- Moved the CLI implementation into `cli/src/sandbox_setup.rs` instead
of growing `cli/src/main.rs`.
- Added a Windows sandbox `ProvisionOnly` helper mode that runs the
elevation-required provisioning work without requiring a workspace cwd
or runtime sandbox policy.
- Reused the existing elevated helper path for creating/updating sandbox
users, configuring firewall/WFP rules, and applying sandbox directory
ACLs.
- Persisted `windows.sandbox = "elevated"` into the target `CODEX_HOME`
so the desktop app does not show the initial sandbox setup banner after
pre-provisioning succeeds.

## Validation

- `cargo fmt -p codex-windows-sandbox -p codex-core -p codex-cli`
- `cargo test -p codex-cli sandbox_setup --target-dir
target\sandbox-setup-check`
- `cargo test -p codex-windows-sandbox
payload_accepts_provision_only_mode --target-dir
target\sandbox-setup-check`
- `git diff --check`
- Manual Windows alpha flow with a standard local user (`Mandi Lavida`):
ran the new setup command from an admin shell, verified the target
`.codex` contents, sandbox marker/secrets, ACLs, firewall rules, and
desktop startup without the sandbox setup banner once experimental
network proxy requirements were disabled.

## Notes

This intentionally does not solve later elevated update coordination for
IT-managed deployments. The setup command can still apply provisioning
updates when run again, but a broader coordination/process story is out
of scope for this alpha.
2026-05-29 11:01:44 -07:00
Won Park
10b0399034 Route extension image generation through the native image completion pipeline (#24972)
## Why

The standalone `image_gen.imagegen` extension should behave like native
image generation for artifact persistence and UI completion, while
returning its save-location guidance as part of the tool result instead
of injecting a developer message.

## What Changed

- Added an image-generation completion hook for extension tools so core
can persist generated images and emit the existing `ImageGeneration`
lifecycle events.
- Reused core image artifact persistence for extension output and
removed extension-local save-path/file-writing logic.
- Split shared image persistence from built-in finalization so native
image generation keeps its existing developer-message instruction
behavior.
- Returned the generated image save-location instruction through the
extension `FunctionCallOutput`, alongside the generated image input for
model follow-up.
- Preserved the existing image-generation event shape for current UI and
replay compatibility.
- Avoided cloning the full generated-image base64 payload when emitting
the in-progress image item.
- Removed dependencies no longer needed after moving persistence out of
the extension crate.

## Fast Follow
- Adjust the existing Extension API and add a general `TurnItem`
finalization path for re-usability of code

## Validation

- Ran `just fmt`.
- Ran `just bazel-lock-update`.
- Ran `just bazel-lock-check`.
- Ran `just test -p codex-tools -p codex-extension-api -p
codex-image-generation-extension`.
- Ran `just test -p codex-core
image_generation_publication_is_finalized_by_core`.
- Ran `just test -p codex-core
handle_output_item_done_records_image_save_history_message`.
- Ran `just fix -p codex-tools -p codex-extension-api -p codex-core -p
codex-image-generation-extension`.
2026-05-29 17:33:13 +00:00
Adam Perry @ OpenAI
3e666dd32a [codex] Wait for MCP readiness in core integration tests (#24964)
Ensures MCP-backed `codex-core` integration tests exercise initialized
servers instead of racing server startup.

I've been idly investigating a few flakes and the failure modes are much
more confusing when a tool call fails because of a failed server start
than when the failed server start causes the test to fail directly.
2026-05-29 10:22:27 -07:00
xl-openai
e29bbb5368 feat: Add focused diagnostics for MCP HTTP send failures (#25013)
Adds failure-only logging for MCP streamable HTTP post_message calls and
the underlying reqwest send path, capturing the MCP method/request id,
endpoint shape, auth-header presence, timeout/connect classification,
and sanitized error source chain without logging headers, bodies,
tokens, or full URLs.
2026-05-29 10:09:33 -07:00
jif-oai
f4e9d2caac Move config document helpers into their own module (#25110)
## Why

`core/src/config/edit.rs` owns the config edit state machine, but it
also carried the TOML document helper code inline as a nested module.
Moving those helpers into their own file keeps the edit orchestration
easier to scan without changing the config persistence behavior.

## What changed

- Moved the existing `document_helpers` module from
`core/src/config/edit.rs` into
`core/src/config/edit/document_helpers.rs`.
- Added `mod document_helpers;` so the existing `pub(super)` helper API
remains available to the rest of `config::edit`.

## Testing

Not run; this is a refactor-only module extraction with no intended
behavior change.
2026-05-29 18:49:21 +02:00
sayan-oai
96f1347fa3 Show activity for standalone web search calls (#24693)
## Why

Standalone `web.run` calls run in the extension, so they need normal
web-search progress activity while a request is in flight and durable
completed activity after a thread is reloaded.

Follow-up to #23823; uses the extension turn-item emission path added in
#24813.

## What changed

- Emit standalone `web.run` start/completion items through the host
turn-item emitter, preserving standard client delivery and rollout
persistence.
- Include useful completion detail for queries, image queries, and
literal-URL `open`/`find` commands.
- Render completed searches as `Searched the web` or `Searched the web
for <detail>`, with snapshot coverage for the detail-free case.
- Extend the app-server round-trip test to verify completed search
activity is reconstructed by `thread/read` after a fresh-process reload.

## Testing

- `just test -p codex-web-search-extension`
- `just test -p codex-app-server -E
"test(standalone_web_search_round_trips_encrypted_output)"`
2026-05-29 16:12:58 +00:00
Ahmed Ibrahim
5577a9e148 [codex] Add model tool mode selector (#25031)
## Why
Some models need to select their code-execution behavior through model
catalog metadata. Models without that metadata must continue to follow
the existing `CodeMode` and `CodeModeOnly` feature flags, including when
a newer server sends an enum value this client does not recognize.

## What changed
- add optional `ModelInfo.tool_mode` metadata with `direct`,
`code_mode`, and `code_mode_only`
- treat omitted and unknown wire values as `None`
- resolve `None` from the existing feature flags
- carry the resolved `ToolMode` directly on `TurnContext`, outside
`Config`
- use the resolved value for turn creation, model switches, review
turns, tool planning, and code execution

## Coverage
- add protocol coverage for omitted, known, and unknown enum values
- add focused coverage for flag fallback and explicit metadata
overriding feature flags
- add core integration coverage that fetches remote model metadata
through `/v1/models` and verifies the outbound `/responses` tools for
explicit `direct` and `code_mode_only` selectors

## Stack
- followed by #25032
2026-05-29 09:05:05 -07:00
Abhinav
251b2412b2 Render multiline hook output in TUI (#24965)
# Why

Fixes #24529. Completed hook output in the TUI rendered each
`HookOutputEntry` as one ratatui line, so explicit newlines inside hook
output were not shown as separate transcript rows. That made multiline
`SessionStart.additionalContext` hard to inspect even though the
model-facing context path preserved the original text.

# What

- Split completed hook output entries on explicit newlines before
rendering them in `codex-rs/tui/src/history_cell/hook_cell.rs`.
- Keep the hook output prefix, such as `hook context:` or `warning:`, on
the first physical line only.
- Preserve explicit blank lines and render continuation lines with the
hook body indent.
- Add unit coverage for multiline context and warning output, plus a
chatwidget snapshot regression for `SessionStart` history output.

# Testing

- `cargo nextest run -p codex-tui completed_hook_multiline
hook_completed_before_reveal_renders_completed_without_running_flash`
- `just argument-comment-lint -p codex-tui -- --ignore-rust-version
--lib --tests`
2026-05-29 15:12:40 +00:00
jif-oai
b40ad0d84d Remove stale rollout TODO tests (#25106)
## Summary

Remove a stale `TODO(jif)` block of commented-out rollout listing tests
that still referenced an older listing API.

The current rollout listing behavior is covered by the active state DB
and filesystem fallback tests, so keeping the dead commented tests just
adds noise.

## Validation

- `just fmt`
- `just test -p codex-rollout`
2026-05-29 17:09:00 +02:00
jif-oai
27e256bc40 Handle goal usage limits from turn errors (#25095)
## Summary
- handle goal usage-limit turn errors in the goal extension
- exercise the extension path in the goal backend test

## Tests
- just fmt
- just test -p codex-goal-extension
- just fix -p codex-goal-extension
2026-05-29 15:39:05 +02:00
jif-oai
1c55bb2702 [codex] Improve built-in tool schema docs (#24794)
## Summary
- Clarify default, omission, and bounded behavior across built-in tool
schemas, including unified exec, classic shell, Code Mode exec/wait,
multi-agent, agent job, MCP resource, image, goal, plan, tool_search,
and test-sync fields.
- Convert update_plan status to an enum and add short field descriptions
where the schema previously relied on surrounding context.
- Remove the dedicated permission-approval schema test and keep only
updates to existing expected-spec tests.

## Validation
- Ran `just fmt`.
- Ran `git diff --check`.
- Did not run clippy or tests, per request.

Regression has been eval
[here](https://openai.slack.com/archives/C09GDSP1J9X/p1779905065496949)
and we proved there are no regressions
2026-05-29 13:32:19 +02:00
jif-oai
3deda3116c fix: main (#25075) 2026-05-29 12:53:31 +02:00
jif-oai
191c39aa75 Drop debug-client prompt state tracking (#25070)
Deletes `codex-rs/debug-client/src/state.rs` as one step in removing the
stale app-server debug client.

This intentionally leaves Cargo workspace and lockfile cleanup for a
later follow-up PR.
2026-05-29 12:51:23 +02:00
jif-oai
43fa4e5d25 Remove debug-client server event reader (#25069)
Deletes `codex-rs/debug-client/src/reader.rs` as one step in removing
the stale app-server debug client.

This intentionally leaves Cargo workspace and lockfile cleanup for a
later follow-up PR.
2026-05-29 12:51:19 +02:00
jif-oai
5c1387846d Delete debug-client JSONL output helper (#25068)
Deletes `codex-rs/debug-client/src/output.rs` as one step in removing
the stale app-server debug client.

This intentionally leaves Cargo workspace and lockfile cleanup for a
later follow-up PR.
2026-05-29 12:51:16 +02:00
jif-oai
e2b8ec616a Remove the debug-client CLI entrypoint (#25067)
Deletes `codex-rs/debug-client/src/main.rs` as one step in removing the
stale app-server debug client.

This intentionally leaves Cargo workspace and lockfile cleanup for a
later follow-up PR.
2026-05-29 12:51:12 +02:00
jif-oai
3d3cc5a953 Retire debug-client interactive command parsing (#25066)
Deletes `codex-rs/debug-client/src/commands.rs` as one step in removing
the stale app-server debug client.

This intentionally leaves Cargo workspace and lockfile cleanup for a
later follow-up PR.
2026-05-29 12:51:09 +02:00
jif-oai
1197c7d654 Delete debug-client app-server process plumbing (#25065)
Deletes `codex-rs/debug-client/src/client.rs` as one step in removing
the stale app-server debug client.

This intentionally leaves Cargo workspace and lockfile cleanup for a
later follow-up PR.
2026-05-29 12:51:05 +02:00
jif-oai
a9a92cbb0a Remove the generated debug-client README (#25064)
Deletes `codex-rs/debug-client/README.md` as one step in removing the
stale app-server debug client.

This intentionally leaves Cargo workspace and lockfile cleanup for a
later follow-up PR.
2026-05-29 12:51:01 +02:00
jif-oai
fc8c723553 Drop the stale debug-client manifest (#25063)
Deletes `codex-rs/debug-client/Cargo.toml` as one step in removing the
stale app-server debug client.

This intentionally leaves Cargo workspace and lockfile cleanup for a
later follow-up PR.
2026-05-29 12:50:58 +02:00
jif-oai
8f6a945ec9 Use inject_if_running for active goal steering (#24924)
## Why

This PR is stacked on #24918, which moves goal steering onto
source-labeled internal model context fragments. Active-turn goal
steering should use the same running-turn injection path as other
runtime steering, so those fragments enter the pending input queue as
`ResponseItem`s through the existing
[`Session::inject_if_running`](8d6f6cdf69/codex-rs/core/src/session/inject.rs (L12-L27))
behavior instead of through a goal-specific conversion wrapper.

## What Changed

- Exposes a narrow `CodexThread::inject_if_running` bridge for callers
that only hold a thread handle.
- Changes `ext/goal` active-turn steering to pass `ResponseItem`s
directly.
- Builds goal steering prompts as contextual internal model context
`ResponseItem`s before injecting them into the running turn.

## Testing

Not run locally; PR metadata update only.
2026-05-29 11:24:39 +02:00
jif-oai
740d942f90 Use internal model context fragments for goal steering (#24918)
## Why

Goal steering is one form of runtime-owned model context, but the old
`<goal_context>` wrapper made the contextual-fragment hiding path
goal-specific. Using a source-labeled internal context fragment gives
core and extensions a shared shape for hidden model steering while
keeping those prompts out of visible turn history.

The change also keeps legacy `<goal_context>` messages recognized as
hidden contextual input so existing stored history does not start
rendering old goal-steering prompts as user-visible turn items.

## What Changed

- Replaces `GoalContext` with `InternalModelContextFragment` plus a
validated `InternalContextSource`.
- Renders goal steering as `<codex_internal_context
source="goal">...</codex_internal_context>`.
- Updates core goal steering and `ext/goal` steering to inject the new
internal-context fragment.
- Updates contextual-fragment, event-mapping, goal, and session tests
for the new wrapper.

## Test Coverage

- Adds coverage for detecting the new internal model context fragment.
- Preserves coverage for hiding legacy `<goal_context>` fragments.
- Verifies invalid internal context sources are rejected and arbitrary
context tags are not hidden.
- Updates goal steering/session assertions to expect the new
`source="goal"` wrapper.
2026-05-29 10:28:25 +02:00
Eric Traut
522f549922 Fix fs/watch debounce batching (#24716)
## Summary

`fs/watch` was using a local debounce wrapper whose deadline was
initialized once and then reused after the first batch. Once that stale
deadline was in the past, later file changes could bypass the intended
200ms debounce and send noisier `fs/changed` notifications.

This moves the debounce wrapper into `codex-file-watcher` as
`DebouncedWatchReceiver`, resets the debounce deadline for each event
batch, preserves pending paths across cancelled receives, and updates
app-server `fs/watch` to use the shared wrapper.

Fixes #24692.
2026-05-28 23:09:55 -07:00
Michael Bolin
6e10142199 fix: preserve deny-read sandboxing for safe commands (#23943)
## Why

Permission profiles can mark filesystem entries as unreadable with
`deny` rules, including glob patterns. Several shell execution paths
treated known-safe commands or execpolicy `allow` rules as sufficient to
run outside the filesystem sandbox. That is not valid for read-capable
commands: for example, `cat` or `ls` may be reasonable to allow
generally, but dropping the sandbox would also drop deny-read
constraints such as `**/*.env`.

## What changed

- Added a shared check that treats active deny-read restrictions as
incompatible with unsandboxed execution.
- Kept first-attempt execution sandboxed for explicit escalation and
execpolicy allow bypasses when deny-read entries are present.
- Prevented no-sandbox retry after a sandbox denial when the active
filesystem policy contains deny-read entries.
- Updated the zsh-fork execve path so prefix-rule `allow` decisions
continue inside the current sandbox when deny-read restrictions are
active.

## Verification

- `cargo test -p codex-core tools::sandboxing::tests`
- `cargo test -p codex-core
tools::runtimes::shell::unix_escalation::tests`
- `cargo test -p codex-core
shell_command_enforces_glob_deny_read_policy`
2026-05-28 22:49:37 -07:00
Eric Traut
56958f2512 Seed prompt history from resumed messages (#24298)
## Why

When the TUI resumes a thread, transcript replay renders prior user
messages but did not seed the composer history. That leaves the resumed
session with empty in-memory prompt history, so pressing Up can fall
through to persisted global history and surface a prompt from another
thread.

The expected behavior is that prompts from the resumed thread are
recalled first, with global history only as a fallback.

## What changed

- Record replayed user messages into the composer history during resume
replay.
- Preserve the existing persisted history format and avoid any startup
history scan.
- Add focused TUI coverage showing replayed prompts are recalled before
persisted global history.

## Validation

- Added `replayed_user_messages_seed_composer_history` in
`codex-rs/tui/src/chatwidget/tests/history_replay.rs`.
- `just test -p codex-tui replayed_user_messages_seed_composer_history`
passed.
2026-05-28 22:08:05 -07:00
xl-openai
f0a839ea0c Add runtime extra skill roots API (#24977)
## Summary
- Add v2 `skills/extraRoots/set` to replace app-server process-local
standalone skill roots. The setting is not persisted, accepts missing
roots, and `extraRoots: []` clears the runtime set.
- Wire runtime roots into core skill discovery for `skills/list` and
turn loads, clear skill caches on set, and register the roots with the
skills watcher so later filesystem changes emit `skills/changed`.
- Update app-server docs, generated JSON/TypeScript schemas, and
coverage for serialization, missing roots, empty clears, and restart
behavior.

## Testing
- `cargo test -p codex-app-server-protocol`
- `cargo test -p codex-core-skills`
- `cargo test -p codex-app-server
skills_extra_roots_set_updates_process_runtime_roots`
- `just fix -p codex-app-server-protocol`
- `just fix -p codex-core-skills`
- `just fix -p codex-app-server`
2026-05-28 21:14:34 -07:00
Adrian
42c80385cd [codex] Avoid PowerShell safety parsing off Windows (#24946)
## Summary

This fixes BUGB-17567 by preventing non-Windows command safety
classification from invoking the Windows PowerShell safelist/parser
path.

Previously, `is_known_safe_command` called the Windows PowerShell
classifier on every platform. That classifier recognizes
`pwsh`/`powershell` by basename and delegates script parsing to the
PowerShell AST parser. The parser starts the supplied executable, so on
macOS/Linux a repository-controlled `pwsh` path could execute during
safety parsing before the normal sandboxed command execution path.

The change gates the Windows PowerShell classifier and module behind
`#[cfg(windows)]`. On macOS/Linux, PowerShell-looking commands are no
longer auto-approved by the Windows classifier and instead fall through
to the normal non-Windows safe-command logic.

## Validation

- `/private/tmp/codex-tools/bin/just fmt`
- `PATH=/private/tmp/codex-tools/bin:$PATH
/private/tmp/codex-tools/bin/just test -p codex-shell-command`

The focused test run passed 135 tests with 0 skipped and completed the
crate bench-smoke step.

## Notes

This PR is scoped to the BUGB-17567 macOS/Linux path. Windows still uses
the PowerShell classifier; a separate hardening follow-up should ensure
Windows safety parsing only executes a trusted PowerShell parser binary
and does not spawn the command's `argv[0]` when that path may be
repository-controlled.
2026-05-29 03:00:35 +00:00
viyatb-oai
bf72be5927 fix(config): use deny for Unix socket permissions (#24970)
## Why

Unix socket permissions still accepted and displayed `"none"` while file
permissions use the clearer `"deny"` spelling. This keeps network Unix
socket policy vocabulary consistent with filesystem policy vocabulary.

## What changed

- Replace the Unix socket permission variant and serialized spelling
from `none` to `deny` across config, feature configuration, and network
proxy types.
- Update app-server v2 serialization, TUI debug output, focused tests,
and generated schemas to expose `"deny"`.
- Add coverage for denied Unix socket entries in managed requirements
and profile overlay behavior.

## Security

This is a vocabulary change for explicit Unix socket rejection, not a
network access expansion. Denied entries continue to be omitted from the
effective allowlist.

## Validation

- `just fmt`
- `just write-config-schema`
- `just write-app-server-schema`
- `just test -p codex-config -p codex-core -p codex-app-server-protocol
-p codex-tui -E
'test(network_requirements_are_preserved_as_constraints_with_source) |
test(network_permission_containers_project_allowed_and_denied_entries) |
test(network_toml_overlays_unix_socket_permissions_by_path) |
test(permissions_profiles_resolve_extends_parent_first_with_child_overrides)
| test(network_requirements_serializes_canonical_and_legacy_fields) |
test(debug_config_output_formats_unix_socket_permissions)'`\n- Automatic
`bench-smoke` follow-up from `just test`\n- `cargo clippy -p
codex-config -p codex-core -p codex-features -p codex-network-proxy -p
codex-app-server-protocol -p codex-app-server -p codex-tui --all-targets
-- -D warnings`
2026-05-28 23:53:26 +00:00
Anton Panasenko
912d7d4f75 feat(app-server): migrate remote control to server tokens (#24141)
## Why

`codex-backend` now authenticates remote-control server websocket
connections with short-lived server tokens instead of the user's ChatGPT
access token. `app-server` needs to mint and refresh those server tokens
without persisting them, so a restart can reconnect from durable
enrollment identity while keeping the bearer token memory-only.

## What Changed

Updated the remote-control transport to consume `remote_control_token`
and `expires_at` from server enroll responses and added
`/server/refresh` support for persisted enrollments or expiring cached
tokens.

Websocket handshakes now send `Authorization: Bearer
<remote_control_token>` with the existing server identity headers, and
no longer send the ChatGPT bearer token or `chatgpt-account-id` on that
websocket path.

The in-memory enrollment state now owns the ephemeral server token
cache, while SQLite still persists only `server_id`, `environment_id`,
and `server_name`. Websocket `401`/`403` clears only the cached token
for refresh on reconnect; websocket or refresh `404` clears stale
persisted enrollment and re-enrolls. Response body previews redact
`remote_control_token` before surfacing parse errors.

## Verification

- `just test -p codex-app-server-transport`
- Manual prod smoke with an isolated `CODEX_HOME`: `codex remote-control
--json -c 'chatgpt_base_url="https://chatgpt.com/backend-api"'` reached
`status:"connected"` with
`environmentId:"env_i_6a17d9f1d764832986da2e80f4554f1b"`.
2026-05-28 15:57:08 -07:00
Abhinav
a576be2b73 Tighten hook output event schemas (#24962)
# Why

Fixes #23993.

Hook command output schemas are published as the contract for hook
authors and schema-driven tooling. The event-specific output schemas
previously described `hookSpecificOutput.hookEventName` as the global
`HookEventNameWire` enum, so a `pre-tool-use.command.output` schema
would validate mismatched values like `PostToolUse`. That made the
schemas less precise than the intended event-specific contract.

# What

Constrain each hook-specific output schema to the matching literal
`hookEventName` value, mirroring the existing input-schema shape.

Also split `SubagentStartHookSpecificOutputWire` from the session-start
output wire so `subagent-start.command.output.schema.json` can emit
`const: "SubagentStart"` instead of sharing the session-start
definition.

# Verification

- `cargo nextest run -p codex-hooks`
- `just fix -p codex-hooks`
- `just argument-comment-lint -p codex-hooks -- --all-targets`
2026-05-28 15:55:40 -07:00
Michael Bolin
bcf2b55957 windows-sandbox: fix capture cancellation test roots (#24974)
## Why

The Windows Bazel job on `main` started failing after #24108 because one
Windows-only capture test still passed `cwd.as_path()` to
`run_windows_sandbox_capture`. That helper now expects the explicit
`workspace_roots` slice introduced by #24108, so the Windows test target
no longer compiled.

## What Changed

- Updates `legacy_capture_cancellation_is_not_reported_as_timeout` to
pass `workspace_roots_for(cwd.as_path()).as_slice()`, matching the
adjacent capture test and the new runner signature.

## Verification

- GitHub Actions CI is the important validation for this Windows-only
compile path.
- Created quickly to get Windows CI running while the separate Ubuntu
`compact_resume_fork` timeout is still under investigation.
2026-05-28 15:51:27 -07:00
Michael Bolin
986c60467b windows-sandbox: pass workspace roots to runner (#24108)
## Why

#23813 switches the Windows sandbox runner path to `PermissionProfile`,
but it still left one runtime anchor for resolving symbolic
`:workspace_roots` entries. That is not enough once a turn has multiple
effective workspace roots: exact entries and deny globs under
`:workspace_roots` need to be materialized for every runtime root before
the command runner chooses token mode or builds ACL plans.

## What Changed

- Replaces the Windows runner/setup `permission_profile_cwd` plumbing
with `workspace_roots: Vec<AbsolutePathBuf>`.
- Resolves Windows-local `PermissionProfile` data with
`materialize_project_roots_with_workspace_roots(...)` instead of the
single-cwd helper.
- Threads `Config::effective_workspace_roots()` through core execution,
unified exec, TUI setup/read-grant flows, app-server setup, app-server
`command/exec`, and `debug sandbox` on Windows.
- Preserves those workspace roots through the zsh-fork escalation
executor instead of rebuilding them from `sandbox_policy_cwd`.
- Makes `ExecRequest::new(...)` and the remaining
`build_exec_request(...)` helper path take
`windows_sandbox_workspace_roots` explicitly so new call sites cannot
silently fall back to `vec![cwd]`.
- Clarifies the `debug sandbox` non-Windows comment: remaining
cwd-dependent resolution still uses `sandbox_policy_cwd`, while
`:workspace_roots` entries are already materialized from config roots.
- Updates elevated runner IPC `SpawnRequest` to send `workspace_roots`
and bumps the framed IPC protocol version to `3` for the payload shape
change.
- Adds Windows-local resolver coverage for expanding exact and glob
`:workspace_roots` entries across multiple roots, plus core helper
coverage proving explicit roots are preserved.

## Verification

- `cargo check -p codex-windows-sandbox -p codex-core -p codex-tui -p
codex-cli -p codex-app-server`
- `cargo test -p codex-windows-sandbox`
- `cargo test -p codex-core windows_sandbox`
- `cargo test -p codex-core unix_escalation`
- `cargo test -p codex-app-server windows_sandbox`
- `cargo test -p codex-tui windows_sandbox`
- `cargo test -p codex-cli debug_sandbox`
- `just test -p codex-core unified_exec`
- `just test -p codex-core
build_exec_request_preserves_windows_workspace_roots`
- `env -u CODEX_NETWORK_PROXY_ACTIVE -u
CODEX_NETWORK_ALLOW_LOCAL_BINDING just test -p codex-app-server --lib
command_exec`
- `just test -p codex-windows-sandbox`
- `just test -p codex-exec sandbox`
- `just fix -p codex-core -p codex-app-server -p codex-windows-sandbox`

A local macOS cross-check with `cargo check --target
x86_64-pc-windows-msvc ...` did not reach crate Rust code because native
dependencies require Windows SDK headers (`windows.h` / `assert.h`) in
this environment; Windows CI remains the real target validation.

Two local targeted filters compile but do not run assertions on macOS:
`env -u CODEX_NETWORK_PROXY_ACTIVE -u CODEX_NETWORK_ALLOW_LOCAL_BINDING
just test -p codex-app-server --lib command_exec_processor` matched zero
tests, and `just test -p codex-linux-sandbox landlock` matched zero
tests because the landlock suite is Linux-only.
2026-05-28 15:26:55 -07:00
Michael Bolin
e7dda8070e Surface filesystem permission profiles in prompt context (#23924)
## Summary
Some permission profiles can encode filesystem reads that should remain
unavailable to the agent. Before this change, the model-visible context
and automatic approval review prompt summarized the effective
permissions as a legacy sandbox mode, which can omit permission-profile
filesystem entries from escalation decisions.

For example, a profile can grant workspace access while denying a
private subtree across every workspace root:

```toml
default_permissions = "restricted-workspace"

[permissions.restricted-workspace.workspace_roots]
"/Users/alice/project" = true
"/Users/alice/other-project" = true

[permissions.restricted-workspace.filesystem]
":minimal" = "read"

[permissions.restricted-workspace.filesystem.":workspace_roots"]
"." = "write"
"private" = "deny"
"private/**" = "deny"
```

The context window now describes the workspace roots and effective
filesystem side of the `PermissionProfile` directly, with deny entries
marked as non-escalatable:

```xml
<environment_context>
  <cwd>/Users/alice/project</cwd>
  <shell>zsh</shell>
  <filesystem><workspace_roots><root>/Users/alice/project</root><root>/Users/alice/other-project</root></workspace_roots><permission_profile type="managed"><file_system type="restricted"><entry access="read"><special>:minimal</special></entry><entry access="write"><path>/Users/alice/project</path></entry><entry access="write"><path>/Users/alice/other-project</path></entry><entry access="deny" escalatable="false"><path>/Users/alice/project/private</path></entry><entry access="deny" escalatable="false"><path>/Users/alice/other-project/private</path></entry><entry access="deny" escalatable="false"><glob>/Users/alice/project/private/**</glob></entry><entry access="deny" escalatable="false"><glob>/Users/alice/other-project/private/**</glob></entry></file_system></permission_profile></filesystem>
</environment_context>
```

Managed requirements can impose the same kind of deny-read restriction:

```toml
[permissions.filesystem]
deny_read = [
  "/Users/alice/project/private",
  "/Users/alice/project/private/**",
]
```

The automatic approval review prompt also receives the parent turn's
denied-read context, so review decisions can account for the active
permission profile.

## What Changed
- Render the effective filesystem profile in `<environment_context>`,
including profile type, filesystem entries, workspace roots, and
non-escalatable deny entries.
- Persist effective `workspace_roots` in `TurnContextItem` so
resumed/replayed context does not have to bind `:workspace_roots`
through legacy `cwd` fallback.
- Add explicit permission instructions that denied reads are policy
restrictions, not escalation targets.
- Pass the parent turn's denied-read context into automatic approval
reviews.
- Add targeted coverage for prompt rendering, workspace-root
materialization, replay context, and review prompt context.
- Keep the prompt-context test expectations platform-aware so the same
filesystem rendering assertions pass on Unix and Windows paths.

## Testing
- `just test -p codex-core
context::environment_context::tests::serialize_environment_context_with_full_filesystem_profile`
- `just test -p codex-core
context::environment_context::tests::turn_context_item_filesystem_uses_workspace_roots_instead_of_cwd`
- `just test -p codex-core
context::permissions_instructions::permissions_instructions_tests::builds_permissions_from_profile_with_denied_reads`
- `just fix -p codex-core`

I also attempted `just test -p codex-core`; the changed prompt-context
tests passed, but the full local run did not complete cleanly in this
sandboxed macOS environment due unrelated user-shell `CODEX_SANDBOX*`
expectations and integration-test timeouts.
2026-05-28 14:56:53 -07:00
Alexi Christakis
e92c952b2e [codex] Add user input client ids (#24653)
## Summary

Adds an optional `clientId` field to app-server v2 `UserInput` and
carries it through the core `UserInput` model so clients can correlate
echoed user input items without relying on payload equality.

## Details

- Adds `client_id: Option<String>` to core `UserInput` variants.
- Exposes the v2 app-server field as `clientId` on the wire and in
generated TypeScript.
- Preserves the id when converting between app-server v2 and core
protocol types.
- Regenerates app-server schema fixtures.

## Validation

- `just fmt`
- `just write-app-server-schema`
- `cargo test -p codex-app-server-protocol`
- `cargo test -p codex-protocol`
- `just fix -p codex-app-server-protocol`
- `just fix -p codex-protocol`
- `git diff --check`
2026-05-28 14:54:39 -07:00
viyatb-oai
a027135bc6 fix(exec-server): reject websocket requests with Origin headers (#24947)
## Why

`codex exec-server` has a local WebSocket listener, but it did not apply
the same browser-origin request handling as the `app-server` WebSocket
transport. Requests that carry an `Origin` header should not be upgraded
by this local transport, keeping both local WebSocket servers consistent
and avoiding unexpected browser-initiated connections.

## What changed

- Added an Axum middleware guard in
`codex-rs/exec-server/src/server/transport.rs` that returns `403
Forbidden` for requests carrying an `Origin` header.
- Added an integration test in `codex-rs/exec-server/tests/websocket.rs`
that covers rejection of an `Origin`-bearing WebSocket handshake.
- Kept ordinary WebSocket clients unchanged: existing no-`Origin`
initialization and process behavior remains covered by the crate tests.

## Validation

- `just test -p codex-exec-server` test phase (`186 passed`; run outside
the parent macOS sandbox so nested sandbox tests can execute)
- `just clippy -p codex-exec-server`
2026-05-28 14:44:14 -07:00
viyatb-oai
3cf737e4e3 fix: cancel Windows sandbox on network denial (#19880)
## Why

When Guardian or the sandbox network proxy detects and denies a network
attempt, core cancels the associated execution through `ExecExpiration`.
The Windows sandbox capture path was only forwarding the timeout
component of that expiration state. As a result, a sandboxed Windows
command whose network attempt had already been denied could keep running
until its timeout elapsed rather than terminating promptly in response
to the denial.

This change closes that cancellation-propagation gap for Windows sandbox
execution.

## What changed

- Added `WindowsSandboxCancellationToken` as the cancellation hook
exposed to Windows capture backends.
- Extracted the cancellation token from `ExecExpiration` in core and
passed it to both the direct and elevated Windows sandbox capture paths
alongside the existing timeout.
- Updated direct capture to poll for either process exit, timeout, or
cancellation and to terminate cancelled processes without reporting them
as timed out.
- Updated elevated capture to watch for cancellation and send the
existing `Terminate` IPC frame to the elevated runner. The watcher parks
for 50 ms between checks to bound response latency without a tight busy
wait.
- Added Windows regression coverage for a long-running PowerShell
command: cancellation ends capture before its timeout and does not set
`timed_out`.
- Added a visible skip diagnostic when that PowerShell-dependent
regression test cannot execute, and consolidated the duplicated
expiration-policy branch identified in review.

## Security

This improves enforcement after a denied network attempt has been
attributed to a Windows sandboxed execution: the command no longer
remains alive simply because Windows capture lost the cancellation
signal.

This PR does not claim to make Windows offline mode an airtight
no-network or no-exfiltration boundary. It does not introduce
AppContainer or change how network denial is detected; it makes an
already-detected denial promptly stop the affected sandboxed command.

## Validation

### Commands run

- `just fmt`
- `cargo test -p codex-windows-sandbox`
- `cargo test -p codex-core network_denial`
- `cargo clippy -p codex-core -p codex-windows-sandbox --tests --no-deps
-- -D warnings`
- `just argument-comment-lint -p codex-windows-sandbox -p codex-core`

The new capture regression is `cfg(target_os = "windows")`, so Windows
CI is the execution coverage for that test path. The local macOS test
runs validate the host-runnable crate and core network-denial behavior.

---------

Co-authored-by: Codex <noreply@openai.com>
2026-05-28 21:28:06 +00:00
Michael Bolin
bc10e5b390 runtime: prepend zsh fork bin dir to PATH (#23768)
## Why

#23756 makes packaged Codex builds include and default to the bundled
zsh fork. The important reason to put that fork's directory at the front
of `PATH` is to keep executable-level escalation working after a command
leaves the original shell and later re-enters zsh through `env`.

The expected chain is:

1. The zsh fork runs the top-level shell command.
2. That command launches another program, such as `python3`, while
inheriting the `EXEC_WRAPPER` environment and the escalation socket fd.
3. That program spawns a shell script whose shebang is `#!/usr/bin/env
zsh` rather than `#!/bin/zsh`, and it does not close the escalation fd.
4. `/usr/bin/env` resolves `zsh` through `PATH`, so it must find the
packaged zsh fork before the system zsh.
5. Commands inside that nested script are intercepted by the zsh fork
and can still request escalation from Codex.

If `PATH` resolves `zsh` to the system shell instead, the nested script
loses zsh-fork exec interception. Commands that should request
escalation can then run only in the original sandbox, or fail there,
without Codex ever receiving the approval request.

Shell snapshots make this slightly more subtle: a snapshot can restore
an older `PATH` after the child shell starts. This PR treats the zsh
fork `PATH` prepend as an explicit environment override so snapshot
wrapping preserves it.

## What Changed

- Added shared zsh-fork runtime helpers that prepend the configured zsh
executable parent directory to `PATH` without duplicate entries.
- Applied the zsh fork `PATH` prepend to both zsh-fork `shell_command`
launches and unified-exec zsh-fork launches before sandbox command
construction.
- Kept the shell-command zsh-fork backend API narrow: it derives the
configured zsh path from session services and rebuilds its sandbox
environment from `req.env`, rather than accepting a second, competing
environment map or a separately threaded bin dir.
- Kept Unix-only zsh-fork `PATH` mutation out of Windows clippy-visible
mutability.
- Added coverage for duplicate `PATH` entries, for preserving the zsh
fork prepend through shell snapshot wrapping, and for the nested
`python3` -> `#!/usr/bin/env zsh` escalation flow.

## Testing

- `just fmt`
- `just fix -p codex-core`

I left final test validation to CI after the latest review-comment
cleanup. Before that cleanup, `just test -p codex-core zsh_fork` passed
locally for the zsh-fork-focused tests.
2026-05-28 14:10:40 -07:00
Celia Chen
0a8c835845 [codex] Remove Bedrock OSS models from catalog (#24960)
Remove the GPT OSS 120B and 20B entries from the Amazon Bedrock static
model catalog, as they are no longer supported.
2026-05-28 14:10:26 -07:00
iceweasel-oai
d9f53128b7 [codex] Handle PowerShell UTF-8 setup failures (#24949)
Fixes #12496.

## Why

Windows sandboxed PowerShell commands can run under
`ConstrainedLanguage` on some machines, especially enterprise-managed
Windows environments. In that mode, our PowerShell command prelude could
fail before every command because it directly assigned
`[Console]::OutputEncoding` to UTF-8. The actual user command still ran,
but Codex surfaced noisy `Cannot set property. Property setting is
supported only on core types in this language mode.` output for every
shell call.

## What Changed

- Makes the PowerShell UTF-8 output encoding prelude best-effort by
wrapping the assignment in `try { ... } catch {}`.
- Keeps the existing UTF-8 behavior when PowerShell allows the
assignment.
- Adds focused tests for adding the prelude and avoiding duplicate
prelude insertion.

## Validation

- `cargo fmt -p codex-shell-command`
- `cargo check -p codex-shell-command`
- `git diff --check`
- Verified a local `ConstrainedLanguage` PowerShell probe prints only
the command output with no property-setting error.
- Verified `codex exec` from a temporary `chcp 437` context reports
`utf-8` / `65001` and preserves non-ASCII output (`café`, `漢字`).
2026-05-28 13:58:20 -07:00
Felipe Coury
2e0c4f4977 fix(tui): prevent repository-configured code execution in /diff (#24954)
## Why

`/diff` is intended to display working-tree changes, but its Git
invocations honored repository-selected executable helpers. A repository
could configure diff/text conversion helpers, clean/process filters,
`core.fsmonitor`, or `post-index-change` hooks that execute when a user
runs `/diff`.

Fixes
[PSEC-4395](https://linear.app/openai/issue/PSEC-4395/codex-cli-diff-executes-repository-selected-diff-helpers).

## What Changed

- Pass `--no-textconv` and `--no-ext-diff` for tracked and untracked
diff generation.
- Discover configured `filter.<driver>.clean` and `.process` entries,
then neutralize the selected drivers through structured
`GIT_CONFIG_KEY_*` / `GIT_CONFIG_VALUE_*` overrides, including driver
names containing `=`.
- Run all `/diff` Git probes with `core.fsmonitor=false` and a null
`core.hooksPath`.
- Use short submodule reporting while ignoring dirty submodule
worktrees, since inspecting a checked-out submodule for dirtiness can
execute filters from that child repository. This intentionally omits
dirty-only submodule markers in order to preserve the non-executing
security boundary.
- Add real-Git marker tests covering filters, fsmonitor, hooks, and
configured helpers inside checked-out submodules.

## How to Test

1. In a repository with ordinary tracked and untracked edits, run
`/diff`.
2. Confirm the normal working-tree diff is shown for top-level files.
3. Run the targeted tests below; they configure executable marker
helpers for repository filters, fsmonitor, hooks, and a checked-out
submodule, then verify `/diff` does not invoke them.
4. Confirm a dirty-only submodule does not cause Codex to enter the
submodule and execute its configured helper.

Targeted tests:
- `just test -p codex-tui get_git_diff_`

Validation note: `just test -p codex-tui` runs the new coverage, but
this worktree currently also has two unrelated failing guardian tests:
`app::tests::update_feature_flags_disabling_guardian_clears_review_policy_and_restores_default`
and
`app::tests::update_feature_flags_disabling_guardian_clears_manual_review_policy_without_history`.
2026-05-28 16:53:59 -03:00
Adam Perry @ OpenAI
b90ec46387 Add codex app-server --stdio alias (#24940)
## Summary
- Add `--stdio` as a direct alias for `codex app-server --listen
stdio://`.
- Keep `--stdio` and `--listen` mutually exclusive.
- Update the app-server README to document both forms.
2026-05-28 12:43:30 -07:00
Adam Perry @ OpenAI
9dd39f334e Move Bazel Windows jobs onto codex-runners (#24952)
The codex-windows runner group should be much faster than the default
GHA runners. Since bazel jobs on windows are frequently the long pole
for PRs checks, this will hopefully get people landing a bit faster.
2026-05-28 12:43:04 -07:00
Won Park
ecb41fcb64 Add feature-gated standalone image generation extension (#24723)
## Why

Add a standalone image generation path that can be exercised
independently of hosted Responses image generation, while retaining the
hosted tool as fallback unless the extension is actually available to
the model.

## What changed

- Added the `codex-image-generation-extension` crate with standalone
generate/edit execution, prior-image selection for edits, model-visible
image output, and local generated-image persistence.
- Installed the extension in app-server behind the disabled-by-default
`imagegenext` feature and backend eligibility checks.
- Updated core tool planning so eligible `image_gen.imagegen` exposure
replaces hosted `image_generation`, while unavailable configurations
retain hosted fallback.
- Added coverage for extension behavior, edit history reuse, feature
gating, auth eligibility, and hosted-tool replacement.
- The extension is installed through app-server only in this PR; other
execution paths retain hosted image generation because hosted
replacement occurs only when the standalone executor is actually
registered and model-visible.
- The initial extension contract intentionally fixes the image model to
`gpt-image-2` and uses automatic image parameters.
- Native generated-image history/card parity and rollout persistence
cleanup are intentionally deferred follow-up work.

## Validation

- `just test -p codex-image-generation-extension`
- `just test -p codex-features`
- `just test -p codex-core
hosted_tools_follow_provider_auth_model_and_config_gates`
- `just test -p codex-app-server`
- `just fix -p codex-image-generation-extension -p codex-features -p
codex-core -p codex-app-server`
- `just fmt`
- `just bazel-lock-update`
- `just bazel-lock-check`

---------

Co-authored-by: jif-oai <jif@openai.com>
2026-05-28 11:44:55 -07:00
jif-oai
462deb0426 Wire task completion into thread-idle lifecycle (#24928)
## Why

#24744 introduced the thread idle lifecycle hook so idle continuation
can be owned by lifecycle contributors instead of hard-coded goal
runtime plumbing. Task completion still called
`goal_runtime_apply(GoalRuntimeEvent::MaybeContinueIfIdle)` directly, so
the post-turn idle transition remained goal-specific and did not notify
generic thread lifecycle contributors.

## What Changed

- Add `Session::emit_thread_idle_lifecycle_if_idle()` to gate idle
emission on both no active turn and no queued trigger-turn mailbox work.
- Call that helper when a task clears the active turn, replacing the
direct `GoalRuntimeEvent::MaybeContinueIfIdle` path.
- Cover the behavior with `codex-core` session tests for emitting after
task completion and suppressing idle emission while trigger-turn mailbox
work is pending.

## Verification

- New tests in `core/src/session/tests.rs` exercise the idle lifecycle
emission and trigger-turn mailbox guard.
2026-05-28 20:05:41 +02:00
Adam Perry @ OpenAI
c2508db60d Revert "Add app-server startup benchmark crate" (#24937)
Reverts openai/codex#24651, broke musl job
https://github.com/openai/codex/actions/runs/26585495205/job/78330166927
2026-05-28 17:49:41 +00:00
canvrno-oai
6c1215dac6 TUI: Unified mentions tweaks + polish mentions rendering (#23363)
This change keeps unified @mentions behind the mentions_v2 gate, moves
the flag to under-development, and polishes mention rendering/history
behavior.

It also adds a few small improvements to the mentions feature around
mention rendering and history round-tripping for plugin/tool mentions in
message edit scenarios. Plugin selections now insert `@` mentions with
better casing, and saved history preserves the visible sigil so recalled
messages look the same as what the user typed.

- Preserves `@` sigils when encoding/decoding mention history for
tool/plugin paths.
- Improves plugin mention insertion so display names/casing are
reflected more cleanly in the composer.
- Update composer to render user-entered plugin mentions in the same
color as the mentions menu. ALso applies to recalled/edited messages.
- Left/right arrows no longer switch unified-mention search modes after
an @mention has already been accepted (Ex: arrowing left through a
composed message that contains @mentions).
- Keeps bound mentions stable around punctuation, so accepted `@`
mentions do not reopen the popup and punctuated `$` mentions still
persist to cross-session history.

**Steps to test**
- Ensure mentions_v2 is enabled through configuration or `--enable
mentions_v2`
- Type `@` in the TUI composer and verify filesystem/plugin/skill
results are displayed in the unified mentions menu.
- Select a plugin mention from the `@` popup and confirm the inserted
text is an `@...` mention with casing, then recall/edit the message and
confirm it still renders as `@...`.
- Mention a skill and verify that skills still insert as `$skill`
mentions rather than `@` mentions.
- Verify punctuated mentions such as `@plugin.` and `($skill)` keep
their bound mention behavior across editing and history recall.
2026-05-28 10:30:15 -07:00
897 changed files with 41970 additions and 14360 deletions

View File

@@ -38,24 +38,50 @@ common:windows --test_env=WINDIR
common --test_env=RUST_MIN_STACK=8388608 # 8 MiB
common --test_output=errors
common --bes_results_url=https://app.buildbuddy.io/invocation/
common --bes_backend=grpcs://remote.buildbuddy.io
common --remote_cache=grpcs://remote.buildbuddy.io
common --remote_download_toplevel
common --nobuild_runfile_links
# These settings tune BuildBuddy/RBE behavior but do not contact a remote
# service unless a `buildbuddy-*` configuration below supplies an endpoint.
common --remote_download_toplevel
common --remote_timeout=3600
common --noexperimental_throttle_remote_action_building
common --experimental_remote_execution_keepalive
common --grpc_keepalive_time=30s
common --experimental_remote_downloader=grpcs://remote.buildbuddy.io
# Opt-in remote configurations selected by
# `.github/scripts/run_bazel_with_buildbuddy.py`. Plain Bazel commands do not
# contact BuildBuddy unless a user selects one of these configurations.
# Use the generic host for cache, BES, and downloads without remote execution.
common:buildbuddy-generic --bes_backend=grpcs://remote.buildbuddy.io
common:buildbuddy-generic --bes_results_url=https://app.buildbuddy.io/invocation/
common:buildbuddy-generic --remote_cache=grpcs://remote.buildbuddy.io
common:buildbuddy-generic --experimental_remote_downloader=grpcs://remote.buildbuddy.io
# Add remote execution on the generic host.
common:buildbuddy-generic-rbe --config=buildbuddy-generic
common:buildbuddy-generic-rbe --config=remote
common:buildbuddy-generic-rbe --remote_executor=grpcs://remote.buildbuddy.io
# Use the OpenAI tenant for cache, BES, and downloads without remote execution.
common:buildbuddy-openai --bes_backend=grpcs://openai.buildbuddy.io
common:buildbuddy-openai --bes_results_url=https://openai.buildbuddy.io/invocation/
common:buildbuddy-openai --remote_cache=grpcs://openai.buildbuddy.io
common:buildbuddy-openai --experimental_remote_downloader=grpcs://openai.buildbuddy.io
# Add remote execution on the OpenAI tenant.
common:buildbuddy-openai-rbe --config=buildbuddy-openai
common:buildbuddy-openai-rbe --config=remote
common:buildbuddy-openai-rbe --remote_executor=grpcs://openai.buildbuddy.io
# This limits both in-flight executions and concurrent downloads. Even with high number
# of jobs execution will still be limited by CPU cores, so this just pays a bit of
# memory in exchange for higher download concurrency.
common --jobs=30
# Shared remote execution policy. The endpoint-bearing `buildbuddy-*-rbe`
# configurations include this group; CI configs override TestRunner below
# when tests must remain local on their runner.
common:remote --strategy=remote
common:remote --extra_execution_platforms=//:rbe
common:remote --remote_executor=grpcs://remote.buildbuddy.io
common:remote --jobs=800
# TODO(team): Evaluate if this actually helps, zbarsky is not sure, everything seems bottlenecked on `core` either way.
# Enable pipelined compilation since we are not bound by local CPU count.
@@ -146,15 +172,11 @@ common:ci-windows --repo_contents_cache=D:/a/.cache/bazel-repo-contents-cache
# Linux crossbuilds don't work until we untangle the libc constraint mess.
common:ci-linux --config=ci-bazel
common:ci-linux --build_metadata=TAG_os=linux
common:ci-linux --config=remote
common:ci-linux --strategy=remote
common:ci-linux --platforms=//:rbe
# On mac, we can run all the build actions remotely but test actions locally.
common:ci-macos --config=ci-bazel
common:ci-macos --build_metadata=TAG_os=macos
common:ci-macos --config=remote
common:ci-macos --strategy=remote
common:ci-macos --strategy=TestRunner=darwin-sandbox,local
# On Windows, use Linux remote execution for build actions but keep test actions
@@ -162,9 +184,7 @@ common:ci-macos --strategy=TestRunner=darwin-sandbox,local
# still run against Windows binaries.
common:ci-windows-cross --config=ci-windows
common:ci-windows-cross --build_metadata=TAG_windows_cross_compile=true
common:ci-windows-cross --config=remote
common:ci-windows-cross --host_platform=//:rbe
common:ci-windows-cross --strategy=remote
common:ci-windows-cross --strategy=TestRunner=local
common:ci-windows-cross --local_test_jobs=4
common:ci-windows-cross --test_env=RUST_TEST_THREADS=1
@@ -180,8 +200,6 @@ common:ci-windows-cross --extra_toolchains=//:windows_gnullvm_tests_on_msvc_host
common:ci-v8 --config=ci
common:ci-v8 --build_metadata=TAG_workflow=v8
common:ci-v8 --build_metadata=TAG_os=linux
common:ci-v8 --config=remote
common:ci-v8 --strategy=remote
# Source-built Bazel V8 artifacts use the in-process sandbox by default. This
# does not affect Cargo's default prebuilt rusty_v8 path.

1
.github/CODEOWNERS vendored
View File

@@ -1,6 +1,7 @@
# Core crate ownership.
/codex-rs/core/ @openai/codex-core-agent-team
/codex-rs/ext/extension-api/ @openai/codex-core-agent-team
/codex-rs/prompts/ @openai/codex-core-agent-team
# Keep ownership changes reviewed by the same team.
/.github/CODEOWNERS @openai/codex-core-agent-team

View File

@@ -53,11 +53,20 @@ fi
run_bazel() {
if [[ "${RUNNER_OS:-}" == "Windows" ]]; then
MSYS2_ARG_CONV_EXCL='*' bazel "$@"
MSYS2_ARG_CONV_EXCL='*' "$(dirname "${BASH_SOURCE[0]}")/run_bazel_with_buildbuddy.py" "$@"
return
fi
bazel "$@"
"$(dirname "${BASH_SOURCE[0]}")/run_bazel_with_buildbuddy.py" "$@"
}
run_bazel_with_startup_args() {
if (( ${#bazel_startup_args[@]} > 0 )); then
run_bazel "${bazel_startup_args[@]}" "$@"
return
fi
run_bazel "$@"
}
ci_config=ci-linux
@@ -77,23 +86,16 @@ esac
print_bazel_test_log_tails() {
local console_log="$1"
local testlogs_dir
local -a bazel_info_cmd=(bazel)
local -a bazel_info_args=(info)
if (( ${#bazel_startup_args[@]} > 0 )); then
bazel_info_cmd+=("${bazel_startup_args[@]}")
fi
# `bazel info` needs the same CI config as the failed test invocation so
# platform-specific output roots match. On Windows, omitting `ci-windows`
# would point at `local_windows-fastbuild` even when the test ran with the
# MSVC host platform under `local_windows_msvc-fastbuild`.
if [[ -n "${BUILDBUDDY_API_KEY:-}" ]]; then
bazel_info_args+=(
"--config=${ci_config}"
"--remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}"
)
# `bazel info` needs the same CI config as the failed test invocation so
# platform-specific output roots match. On Windows, omitting `ci-windows`
# would point at `local_windows-fastbuild` even when the test ran with the
# MSVC host platform under `local_windows_msvc-fastbuild`.
bazel_info_args+=("--config=${ci_config}")
fi
# Only pass flags that affect Bazel's output-root selection or repository
# lookup. Test/build-only flags such as execution logs or remote download
# mode can make `bazel info` fail, which would hide the real test log path.
@@ -105,7 +107,7 @@ print_bazel_test_log_tails() {
esac
done
testlogs_dir="$(run_bazel "${bazel_info_cmd[@]:1}" \
testlogs_dir="$(run_bazel_with_startup_args \
--noexperimental_remote_repo_contents_cache \
"${bazel_info_args[@]}" \
bazel-testlogs 2>/dev/null || echo bazel-testlogs)"
@@ -254,8 +256,9 @@ if [[ ${#bazel_args[@]} -eq 0 || ${#bazel_targets[@]} -eq 0 ]]; then
fi
if [[ "${RUNNER_OS:-}" == "Windows" && $windows_cross_compile -eq 1 && -z "${BUILDBUDDY_API_KEY:-}" ]]; then
# Fork PRs do not receive the BuildBuddy secret needed for the remote
# cross-compile config. Preserve the previous local Windows build shape.
# Windows cross-compilation depends on authenticated RBE. Preserve the local
# Windows build shape when credentials are unavailable.
ci_config=ci-windows
windows_msvc_host_platform=1
fi
@@ -297,9 +300,9 @@ if [[ "${RUNNER_OS:-}" == "Windows" && $windows_cross_compile -eq 1 && -n "${BUI
fi
if [[ "${RUNNER_OS:-}" == "Windows" && $windows_cross_compile -eq 1 && -z "${BUILDBUDDY_API_KEY:-}" ]]; then
# The Windows cross-compile config depends on remote execution. Fork PRs do
# not receive the BuildBuddy secret, so fall back to the existing local build
# shape and keep its lower concurrency cap.
# The Windows cross-compile config depends on authenticated remote
# execution. When credentials are unavailable, keep the local build shape
# and its lower concurrency cap.
post_config_bazel_args+=(--jobs=8)
fi
@@ -377,70 +380,31 @@ fi
bazel_console_log="$(mktemp)"
trap 'rm -f "$bazel_console_log"' EXIT
bazel_cmd=(bazel)
if (( ${#bazel_startup_args[@]} > 0 )); then
bazel_cmd+=("${bazel_startup_args[@]}")
fi
bazel_run_args=(
"${bazel_args[@]}"
)
if [[ -n "${BUILDBUDDY_API_KEY:-}" ]]; then
echo "BuildBuddy API key is available; using remote Bazel configuration."
# Work around Bazel 9 remote repo contents cache / overlay materialization failures
# seen in CI (for example "is not a symlink" or permission errors while
# materializing external repos such as rules_perl). We still use BuildBuddy for
# remote execution/cache; this only disables the startup-level repo contents cache.
bazel_run_args=(
"${bazel_args[@]}"
"--config=${ci_config}"
"--remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}"
)
if (( ${#post_config_bazel_args[@]} > 0 )); then
bazel_run_args+=("${post_config_bazel_args[@]}")
fi
set +e
run_bazel "${bazel_cmd[@]:1}" \
--noexperimental_remote_repo_contents_cache \
"${bazel_run_args[@]}" \
-- \
"${bazel_targets[@]}" \
2>&1 | tee "$bazel_console_log"
bazel_status=${PIPESTATUS[0]}
set -e
bazel_run_args+=("--config=${ci_config}")
else
echo "BuildBuddy API key is not available; using local Bazel configuration."
# Keep fork/community PRs on Bazel but disable remote services that are
# configured in .bazelrc and require auth.
#
# Flag docs:
# - Command-line reference: https://bazel.build/reference/command-line-reference
# - Remote caching overview: https://bazel.build/remote/caching
# - Remote execution overview: https://bazel.build/remote/rbe
# - Build Event Protocol overview: https://bazel.build/remote/bep
#
# --noexperimental_remote_repo_contents_cache:
# disable remote repo contents cache enabled in .bazelrc startup options.
# https://bazel.build/reference/command-line-reference#startup_options-flag--experimental_remote_repo_contents_cache
# --remote_cache= and --remote_executor=:
# clear remote cache/execution endpoints configured in .bazelrc.
# https://bazel.build/reference/command-line-reference#common_options-flag--remote_cache
# https://bazel.build/reference/command-line-reference#common_options-flag--remote_executor
bazel_run_args=(
"${bazel_args[@]}"
--remote_cache=
--remote_executor=
)
if (( ${#post_config_bazel_args[@]} > 0 )); then
bazel_run_args+=("${post_config_bazel_args[@]}")
fi
set +e
run_bazel "${bazel_cmd[@]:1}" \
--noexperimental_remote_repo_contents_cache \
"${bazel_run_args[@]}" \
-- \
"${bazel_targets[@]}" \
2>&1 | tee "$bazel_console_log"
bazel_status=${PIPESTATUS[0]}
set -e
fi
if (( ${#post_config_bazel_args[@]} > 0 )); then
bazel_run_args+=("${post_config_bazel_args[@]}")
fi
set +e
# Work around Bazel 9 remote repo contents cache / overlay materialization
# failures seen in CI (for example "is not a symlink" or permission errors
# while materializing external repos such as rules_perl). This only disables
# the startup-level repo contents cache; keyed runs still use BuildBuddy.
run_bazel_with_startup_args \
--noexperimental_remote_repo_contents_cache \
"${bazel_run_args[@]}" \
-- \
"${bazel_targets[@]}" \
2>&1 | tee "$bazel_console_log"
bazel_status=${PIPESTATUS[0]}
set -e
if [[ ${bazel_status:-0} -ne 0 ]]; then
if [[ $print_failed_bazel_action_summary -eq 1 ]]; then

View File

@@ -2,48 +2,17 @@
set -euo pipefail
# Run Bazel queries with the same CI startup settings as the main build/test
# invocation so target-discovery queries can reuse the same Bazel server.
# Run target-discovery queries with the same startup settings as the main
# build/test invocation so they can reuse the same Bazel server. Queries only
# enumerate labels, so they intentionally do not select CI or remote configs.
query_args=()
windows_cross_compile=0
while [[ $# -gt 0 ]]; do
case "$1" in
--windows-cross-compile)
windows_cross_compile=1
shift
;;
--)
shift
break
;;
*)
query_args+=("$1")
shift
;;
esac
done
if [[ $# -ne 1 ]]; then
echo "Usage: $0 [--windows-cross-compile] [<bazel query args>...] -- <query expression>" >&2
if [[ $# -lt 2 || "${@: -2:1}" != "--" ]]; then
echo "Usage: $0 [<bazel query args>...] -- <query expression>" >&2
exit 1
fi
query_expression="$1"
ci_config=ci-linux
case "${RUNNER_OS:-}" in
macOS)
ci_config=ci-macos
;;
Windows)
if [[ $windows_cross_compile -eq 1 ]]; then
ci_config=ci-windows-cross
else
ci_config=ci-windows
fi
;;
esac
query_args=("${@:1:$#-2}")
query_expression="${@: -1}"
bazel_startup_args=()
if [[ -n "${BAZEL_OUTPUT_USER_ROOT:-}" ]]; then
@@ -60,12 +29,6 @@ run_bazel() {
}
bazel_query_args=(--noexperimental_remote_repo_contents_cache query)
if [[ -n "${BUILDBUDDY_API_KEY:-}" ]]; then
bazel_query_args+=(
"--config=${ci_config}"
"--remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}"
)
fi
if [[ -n "${BAZEL_REPO_CONTENTS_CACHE:-}" ]]; then
bazel_query_args+=("--repo_contents_cache=${BAZEL_REPO_CONTENTS_CACHE}")
@@ -75,7 +38,10 @@ if [[ -n "${BAZEL_REPOSITORY_CACHE:-}" ]]; then
bazel_query_args+=("--repository_cache=${BAZEL_REPOSITORY_CACHE}")
fi
bazel_query_args+=("${query_args[@]}" "$query_expression")
if (( ${#query_args[@]} > 0 )); then
bazel_query_args+=("${query_args[@]}")
fi
bazel_query_args+=("$query_expression")
if (( ${#bazel_startup_args[@]} > 0 )); then
run_bazel "${bazel_startup_args[@]}" "${bazel_query_args[@]}"

147
.github/scripts/run_bazel_with_buildbuddy.py vendored Executable file
View File

@@ -0,0 +1,147 @@
#!/usr/bin/env python3
import json
import os
import subprocess
import sys
from collections.abc import Mapping
from collections.abc import Sequence
from pathlib import Path
OPENAI_REPOSITORY = "openai/codex"
# Remote configurations select cache/BES/download endpoints. Their -rbe forms
# also select the matching remote executor endpoint.
GENERIC_REMOTE_CONFIG = "buildbuddy-generic"
OPENAI_REMOTE_CONFIG = "buildbuddy-openai"
# These CI configurations require remote build execution. The wrapper supplies
# an RBE configuration, which also includes the common `remote` settings.
REMOTE_EXECUTION_CONFIGS = {
"--config=ci-linux",
"--config=ci-macos",
"--config=ci-v8",
"--config=ci-windows-cross",
}
# Only authenticated workflow runs executing trusted upstream code may use the
# OpenAI BuildBuddy host. A pull request event without proof that its head is
# in the upstream repository fails closed to the generic host.
def is_trusted_upstream_run(env: Mapping[str, str]) -> bool:
# `GITHUB_REPOSITORY` is easy to set locally. Requiring GitHub's workflow
# marker prevents a local command from opting itself into the OpenAI host.
if (
env.get("GITHUB_ACTIONS") != "true"
or env.get("GITHUB_REPOSITORY") != OPENAI_REPOSITORY
):
return False
# Non-PR workflow runs in `openai/codex` execute upstream refs, so they are
# trusted. Fork code reaches these workflows only through pull requests.
if env.get("GITHUB_EVENT_NAME") != "pull_request":
return True
event_path = env.get("GITHUB_EVENT_PATH")
if not event_path:
return False
try:
event = json.loads(Path(event_path).read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return False
try:
return event["pull_request"]["head"]["repo"]["fork"] is False
except (KeyError, TypeError):
return False
def uses_openai_host(env: Mapping[str, str]) -> bool:
return bool(env.get("BUILDBUDDY_API_KEY")) and is_trusted_upstream_run(env)
def uses_remote_execution(args: Sequence[str]) -> bool:
try:
separator_idx = args.index("--")
except ValueError:
separator_idx = len(args)
return any(arg in REMOTE_EXECUTION_CONFIGS for arg in args[:separator_idx])
def remote_config(args: Sequence[str], env: Mapping[str, str]) -> str | None:
if not env.get("BUILDBUDDY_API_KEY"):
return None
config = OPENAI_REMOTE_CONFIG if uses_openai_host(env) else GENERIC_REMOTE_CONFIG
if uses_remote_execution(args):
config += "-rbe"
return config
def bazel_args_without_remote_execution(args: Sequence[str]) -> list[str]:
# Remote CI configs require BuildBuddy credentials. Removing them preserves
# the local fallback used for fork pull requests.
try:
separator_idx = args.index("--")
except ValueError:
separator_idx = len(args)
return [
*(arg for arg in args[:separator_idx] if arg not in REMOTE_EXECUTION_CONFIGS),
*args[separator_idx:],
]
def bazel_args_with_remote_config(
args: Sequence[str], env: Mapping[str, str]
) -> list[str]:
config = remote_config(args, env)
if config is None:
return bazel_args_without_remote_execution(args)
# `remote_config()` returns a configuration only when this key is present.
api_key = env["BUILDBUDDY_API_KEY"]
remote_args = [
f"--config={config}",
f"--remote_header=x-buildbuddy-api-key={api_key}",
]
# Insert immediately after the Bazel command. This keeps wrapper-added
# options out of positional payloads and lets later CI configs override
# shared RBE defaults such as the Windows cross-compilation exec platforms.
insertion_idx = next(
(idx + 1 for idx, arg in enumerate(args) if not arg.startswith("-")),
len(args),
)
return [*args[:insertion_idx], *remote_args, *args[insertion_idx:]]
def bazel_command(*args: str, env: Mapping[str, str] | None = None) -> list[str]:
env = os.environ if env is None else env
bazel = env.get("CODEX_BAZEL_BIN", "bazel")
return [bazel, *bazel_args_with_remote_config(args, env)]
def main() -> None:
config = remote_config(sys.argv[1:], os.environ)
if config is None:
print(
"BuildBuddy key unavailable; using local Bazel configuration.",
file=sys.stderr,
)
else:
host_description = (
"OpenAI tenant" if uses_openai_host(os.environ) else "generic"
)
print(
f"Using {host_description} BuildBuddy configuration: {config}.",
file=sys.stderr,
)
command = bazel_command(*sys.argv[1:])
if os.name == "nt":
# Windows CRT exec can split arguments containing spaces and lose the
# eventual child exit status. Wait for Bazel and propagate its status.
result = subprocess.run(command, check=False)
raise SystemExit(result.returncode)
os.execvp(command[0], command)
if __name__ == "__main__":
main()

View File

@@ -5,7 +5,6 @@ from __future__ import annotations
import argparse
import gzip
import hashlib
import os
import re
import shutil
import subprocess
@@ -13,6 +12,7 @@ import sys
import tomllib
from pathlib import Path
from run_bazel_with_buildbuddy import bazel_command
from rusty_v8_module_bazel import (
RustyV8ChecksumError,
check_module_bazel,
@@ -29,33 +29,22 @@ SANDBOX_ARTIFACT_PROFILE = "ptrcomp_sandbox_release"
ARTIFACT_BAZEL_CONFIGS = ["rusty-v8-upstream-libcxx"]
def bazel_remote_args() -> list[str]:
buildbuddy_api_key = os.environ.get("BUILDBUDDY_API_KEY")
if not buildbuddy_api_key:
return []
return [f"--remote_header=x-buildbuddy-api-key={buildbuddy_api_key}"]
def bazel_execroot() -> Path:
result = subprocess.run(
["bazel", "info", "execution_root"],
output = subprocess.check_output(
bazel_command("info", "execution_root"),
cwd=ROOT,
check=True,
capture_output=True,
text=True,
)
return Path(result.stdout.strip())
return Path(output.strip())
def bazel_output_base() -> Path:
result = subprocess.run(
["bazel", "info", "output_base"],
output = subprocess.check_output(
bazel_command("info", "output_base"),
cwd=ROOT,
check=True,
capture_output=True,
text=True,
)
return Path(result.stdout.strip())
return Path(output.strip())
def bazel_output_path(path: str) -> Path:
@@ -72,24 +61,22 @@ def bazel_output_files(
) -> list[Path]:
expression = "set(" + " ".join(labels) + ")"
bazel_configs = bazel_configs or []
result = subprocess.run(
[
"bazel",
output = subprocess.check_output(
bazel_command(
"cquery",
"-c",
compilation_mode,
f"--platforms=@llvm//platforms:{platform}",
*[f"--config={config}" for config in bazel_configs],
*bazel_remote_args(),
"--output=files",
expression,
],
),
cwd=ROOT,
check=True,
capture_output=True,
text=True,
)
return [bazel_output_path(line.strip()) for line in result.stdout.splitlines() if line.strip()]
return [
bazel_output_path(line.strip()) for line in output.splitlines() if line.strip()
]
def bazel_build(
@@ -102,17 +89,15 @@ def bazel_build(
bazel_configs = bazel_configs or []
download_args = ["--remote_download_toplevel"] if download_toplevel else []
subprocess.run(
[
"bazel",
bazel_command(
"build",
"-c",
compilation_mode,
f"--platforms=@llvm//platforms:{platform}",
*[f"--config={config}" for config in bazel_configs],
*bazel_remote_args(),
*download_args,
*labels,
],
),
cwd=ROOT,
check=True,
)
@@ -172,7 +157,7 @@ def resolved_v8_crate_version() -> str:
matches = sorted(
set(
re.findall(
r'https://static\.crates\.io/crates/v8/v8-([0-9]+\.[0-9]+\.[0-9]+)\.crate',
r"https://static\.crates\.io/crates/v8/v8-([0-9]+\.[0-9]+\.[0-9]+)\.crate",
module_bazel,
)
)
@@ -234,13 +219,17 @@ def stage_artifacts(
output_dir: Path,
sandbox: bool,
) -> None:
missing_paths = [str(path) for path in [lib_path, binding_path] if not path.exists()]
missing_paths = [
str(path) for path in [lib_path, binding_path] if not path.exists()
]
if missing_paths:
raise SystemExit(f"missing release outputs for {target}: {missing_paths}")
output_dir.mkdir(parents=True, exist_ok=True)
artifact_profile = SANDBOX_ARTIFACT_PROFILE if sandbox else RELEASE_ARTIFACT_PROFILE
staged_library = output_dir / staged_archive_name(target, lib_path, artifact_profile)
staged_library = output_dir / staged_archive_name(
target, lib_path, artifact_profile
)
staged_binding = output_dir / staged_binding_name(target, artifact_profile)
with lib_path.open("rb") as src, staged_library.open("wb") as dst:
@@ -270,7 +259,9 @@ def stage_artifacts(
def upstream_release_pair_paths(source_root: Path, target: str) -> tuple[Path, Path]:
lib_name = "rusty_v8.lib" if target.endswith("-pc-windows-msvc") else "librusty_v8.a"
lib_name = (
"rusty_v8.lib" if target.endswith("-pc-windows-msvc") else "librusty_v8.a"
)
gn_out = source_root / "target" / target / "release" / "gn_out"
return gn_out / "obj" / lib_name, gn_out / "src_binding.rs"
@@ -338,7 +329,9 @@ def parse_args() -> argparse.Namespace:
stage_upstream_release_pair_parser = subparsers.add_parser(
"stage-upstream-release-pair"
)
stage_upstream_release_pair_parser.add_argument("--source-root", type=Path, required=True)
stage_upstream_release_pair_parser.add_argument(
"--source-root", type=Path, required=True
)
stage_upstream_release_pair_parser.add_argument("--target", required=True)
stage_upstream_release_pair_parser.add_argument("--output-dir", required=True)
stage_upstream_release_pair_parser.add_argument("--sandbox", action="store_true")

View File

@@ -0,0 +1,214 @@
#!/usr/bin/env python3
import json
import os
import subprocess
import sys
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
import run_bazel_with_buildbuddy
class RunBazelWithBuildBuddyTest(unittest.TestCase):
def github_env(
self,
temp_dir: str,
*,
repository: str = "openai/codex",
fork: bool = False,
event_name: str = "pull_request",
) -> dict[str, str]:
event_path = Path(temp_dir) / "event.json"
event_path.write_text(
json.dumps({"pull_request": {"head": {"repo": {"fork": fork}}}}),
encoding="utf-8",
)
return {
"BUILDBUDDY_API_KEY": "token",
"GITHUB_ACTIONS": "true",
"GITHUB_EVENT_NAME": event_name,
"GITHUB_EVENT_PATH": str(event_path),
"GITHUB_REPOSITORY": repository,
}
def test_keyless_invocation_drops_remote_ci_configuration(self) -> None:
self.assertIsNone(
run_bazel_with_buildbuddy.remote_config(
["build", "--config=ci-linux", "//codex-rs/cli:codex"],
{},
)
)
self.assertEqual(
run_bazel_with_buildbuddy.bazel_args_with_remote_config(
["build", "--config=ci-linux", "--", "//codex-rs/cli:codex"],
{},
),
["build", "--", "//codex-rs/cli:codex"],
)
def test_program_arguments_after_separator_do_not_select_or_lose_rbe(self) -> None:
args = ["run", "//codex-rs/cli:codex", "--", "--config=remote"]
self.assertEqual(
run_bazel_with_buildbuddy.bazel_args_with_remote_config(args, {}),
args,
)
self.assertEqual(
run_bazel_with_buildbuddy.remote_config(
args, {"BUILDBUDDY_API_KEY": "fork-token"}
),
"buildbuddy-generic",
)
def test_upstream_push_selects_openai_rbe_before_target_separator(self) -> None:
with TemporaryDirectory() as temp_dir:
env = self.github_env(temp_dir, event_name="push")
self.assertEqual(
run_bazel_with_buildbuddy.bazel_args_with_remote_config(
["build", "--config=ci-linux", "--", "//codex-rs/cli:codex"],
env,
),
[
"build",
"--config=buildbuddy-openai-rbe",
"--remote_header=x-buildbuddy-api-key=token",
"--config=ci-linux",
"--",
"//codex-rs/cli:codex",
],
)
def test_windows_cross_ci_configuration_follows_remote_configuration(self) -> None:
env = {"BUILDBUDDY_API_KEY": "fork-token"}
self.assertEqual(
run_bazel_with_buildbuddy.bazel_args_with_remote_config(
["build", "--config=ci-windows-cross", "//codex-rs/cli:codex"],
env,
),
[
"build",
"--config=buildbuddy-generic-rbe",
"--remote_header=x-buildbuddy-api-key=fork-token",
"--config=ci-windows-cross",
"//codex-rs/cli:codex",
],
)
def test_query_remote_configuration_is_inserted_before_expression(self) -> None:
expression = 'kind("rust_library rule", //codex-rs/...)'
env = {"BUILDBUDDY_API_KEY": "fork-token"}
for command in ("query", "cquery", "aquery"):
with self.subTest(command=command):
self.assertEqual(
run_bazel_with_buildbuddy.bazel_args_with_remote_config(
[
command,
"--config=ci-windows-cross",
"--output=label",
expression,
],
env,
),
[
command,
"--config=buildbuddy-generic-rbe",
"--remote_header=x-buildbuddy-api-key=fork-token",
"--config=ci-windows-cross",
"--output=label",
expression,
],
)
def test_same_repository_pull_request_selects_openai_host(self) -> None:
with TemporaryDirectory() as temp_dir:
self.assertEqual(
run_bazel_with_buildbuddy.remote_config(
["build", "--config=ci-v8"], self.github_env(temp_dir)
),
"buildbuddy-openai-rbe",
)
def test_fork_pull_request_cannot_select_openai_host(self) -> None:
with TemporaryDirectory() as temp_dir:
env = self.github_env(temp_dir, fork=True)
self.assertEqual(
run_bazel_with_buildbuddy.remote_config(
["build", "--config=ci-v8"], env
),
"buildbuddy-generic-rbe",
)
def test_run_in_fork_repository_cannot_select_openai_host(self) -> None:
with TemporaryDirectory() as temp_dir:
env = self.github_env(temp_dir, repository="contributor/codex")
self.assertEqual(
run_bazel_with_buildbuddy.remote_config(
["build", "--config=ci-v8"], env
),
"buildbuddy-generic-rbe",
)
def test_pull_request_without_readable_event_payload_fails_closed(self) -> None:
for event_path in (None, "missing-event.json"):
env = {
"BUILDBUDDY_API_KEY": "token",
"GITHUB_ACTIONS": "true",
"GITHUB_EVENT_NAME": "pull_request",
"GITHUB_REPOSITORY": "openai/codex",
}
if event_path is not None:
env["GITHUB_EVENT_PATH"] = event_path
with self.subTest(event_path=event_path):
self.assertEqual(
run_bazel_with_buildbuddy.remote_config(["build"], env),
"buildbuddy-generic",
)
def test_bazel_command_uses_configured_binary_locally(self) -> None:
self.assertEqual(
run_bazel_with_buildbuddy.bazel_command(
"info",
"execution_root",
env={"CODEX_BAZEL_BIN": "fake-bazel"},
),
["fake-bazel", "info", "execution_root"],
)
def test_main_preserves_spaced_argument_and_child_exit_status(self) -> None:
spaced_arg = (
r"--test_env=PATH=C:\Program Files\PowerShell\7;C:\Program Files\Git\bin"
)
child_code = (
f"import sys; sys.exit(37 if sys.argv[1] == {spaced_arg!r} else 91)"
)
env = os.environ.copy()
env["CODEX_BAZEL_BIN"] = sys.executable
env.pop("BUILDBUDDY_API_KEY", None)
result = subprocess.run(
[
sys.executable,
str(Path(run_bazel_with_buildbuddy.__file__)),
"-c",
child_code,
spaced_arg,
],
env=env,
check=False,
capture_output=True,
text=True,
)
self.assertEqual(result.returncode, 37, result.stderr)
if __name__ == "__main__":
unittest.main()

View File

@@ -88,24 +88,49 @@ class RustyV8BazelTest(unittest.TestCase):
),
)
def test_bazel_remote_args_include_buildbuddy_header_when_present(self) -> None:
with patch.dict(environ, {"BUILDBUDDY_API_KEY": "token"}, clear=False):
def test_bazel_commands_use_shared_buildbuddy_remote_config_library(self) -> None:
with patch.dict(environ, {}, clear=True):
self.assertEqual(
["--remote_header=x-buildbuddy-api-key=token"],
rusty_v8_bazel.bazel_remote_args(),
[
"bazel",
"build",
"//third_party/v8:release",
],
rusty_v8_bazel.bazel_command(
"build",
"--config=ci-v8",
"//third_party/v8:release",
),
)
with patch.dict(environ, {"BUILDBUDDY_API_KEY": "token"}, clear=True):
self.assertEqual(
[
"bazel",
"build",
"--config=buildbuddy-generic-rbe",
"--remote_header=x-buildbuddy-api-key=token",
"--config=ci-v8",
"//third_party/v8:release",
],
rusty_v8_bazel.bazel_command(
"build",
"--config=ci-v8",
"//third_party/v8:release",
),
)
with patch.dict(environ, {}, clear=True):
self.assertEqual([], rusty_v8_bazel.bazel_remote_args())
def test_release_pair_labels_and_staged_names_distinguish_sandbox_artifacts(self) -> None:
def test_release_pair_labels_and_staged_names_distinguish_sandbox_artifacts(
self,
) -> None:
self.assertEqual(
"//third_party/v8:rusty_v8_release_pair_x86_64_unknown_linux_musl",
rusty_v8_bazel.release_pair_label("x86_64-unknown-linux-musl"),
)
self.assertEqual(
"//third_party/v8:rusty_v8_sandbox_release_pair_x86_64_unknown_linux_musl",
rusty_v8_bazel.release_pair_label("x86_64-unknown-linux-musl", sandbox=True),
rusty_v8_bazel.release_pair_label(
"x86_64-unknown-linux-musl", sandbox=True
),
)
self.assertEqual(
"//third_party/v8:rusty_v8_sandbox_release_pair_x86_64_apple_darwin",
@@ -205,11 +230,7 @@ class RustyV8BazelTest(unittest.TestCase):
with TemporaryDirectory() as source_dir, TemporaryDirectory() as output_dir:
source_root = Path(source_dir)
gn_out = (
source_root
/ "target"
/ "x86_64-pc-windows-msvc"
/ "release"
/ "gn_out"
source_root / "target" / "x86_64-pc-windows-msvc" / "release" / "gn_out"
)
(gn_out / "obj").mkdir(parents=True)
(gn_out / "obj" / "rusty_v8.lib").write_bytes(b"archive")

View File

@@ -15,6 +15,7 @@ concurrency:
# See https://docs.github.com/en/actions/using-jobs/using-concurrency and https://docs.github.com/en/actions/learn-github-actions/contexts for more info.
group: concurrency-group::${{ github.workflow }}::${{ github.event.pull_request.number > 0 && format('pr-{0}', github.event.pull_request.number) || github.ref_name }}${{ github.ref_name == 'main' && format('::{0}', github.run_id) || ''}}
cancel-in-progress: ${{ github.ref_name != 'main' }}
jobs:
test:
# PRs use the sharded Windows cross-compiled test jobs below. Post-merge
@@ -55,12 +56,17 @@ jobs:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
if: matrix.os == 'ubuntu-24.04' && matrix.target == 'x86_64-unknown-linux-gnu'
with:
tool: just
- name: Check rusty_v8 MODULE.bazel checksums
if: matrix.os == 'ubuntu-24.04' && matrix.target == 'x86_64-unknown-linux-gnu'
shell: bash
run: |
python3 .github/scripts/rusty_v8_bazel.py check-module-bazel
python3 -m unittest discover -s .github/scripts -p test_rusty_v8_bazel.py
just test-github-scripts
- name: Prepare Bazel CI
id: prepare_bazel
@@ -141,7 +147,9 @@ jobs:
- 2
- 3
- 4
runs-on: windows-latest
runs-on:
group: codex-runners
labels: codex-windows-x64
name: Bazel test on windows-latest for x86_64-pc-windows-gnullvm shard ${{ matrix.shard }}/4
steps:
@@ -150,6 +158,11 @@ jobs:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- name: Test BuildBuddy Bazel wrapper
if: matrix.shard == 1
shell: pwsh
run: python .github/scripts/test_run_bazel_with_buildbuddy.py
- name: Prepare Bazel CI
id: prepare_bazel
uses: ./.github/actions/prepare-bazel-ci
@@ -246,7 +259,9 @@ jobs:
# it a larger timeout.
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
timeout-minutes: 40
runs-on: windows-latest
runs-on:
group: codex-runners
labels: codex-windows-x64
name: Bazel test on windows-latest for x86_64-pc-windows-gnullvm (native main)
steps:
@@ -332,7 +347,10 @@ jobs:
target: aarch64-apple-darwin
- os: windows-latest
target: x86_64-pc-windows-gnullvm
runs-on: ${{ matrix.os }}
runs_on:
group: codex-runners
labels: codex-windows-x64
runs-on: ${{ matrix.runs_on || matrix.os }}
name: Bazel clippy on ${{ matrix.os }} for ${{ matrix.target }}
steps:
@@ -422,7 +440,10 @@ jobs:
target: aarch64-apple-darwin
- os: windows-latest
target: x86_64-pc-windows-gnullvm
runs-on: ${{ matrix.os }}
runs_on:
group: codex-runners
labels: codex-windows-x64
runs-on: ${{ matrix.runs_on || matrix.os }}
name: Verify release build on ${{ matrix.os }} for ${{ matrix.target }}
steps:

View File

@@ -6,6 +6,11 @@ on:
branches:
- main
# Cargo's libgit2 transport has been flaky when fetching git dependencies with
# nested submodules. Prefer the system git CLI across every Cargo invocation.
env:
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
jobs:
cargo-deny:
runs-on: ubuntu-latest

View File

@@ -74,5 +74,15 @@ jobs:
- name: Check root README ToC
run: python3 scripts/readme_toc.py README.md
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: just@1.51.0
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: "0.11.3"
- name: Check formatting (run `just fmt` to fix)
run: just fmt-check
- name: Prettier (run `pnpm run format:fix` to fix)
run: pnpm run format

View File

@@ -12,6 +12,7 @@ jobs:
# Prevent runs on forks (requires OpenAI API key, wastes Actions minutes)
if: github.repository == 'openai/codex' && (github.event.action == 'opened' || (github.event.action == 'labeled' && github.event.label.name == 'codex-deduplicate'))
runs-on: ubuntu-latest
environment: issue-triage
permissions:
contents: read
outputs:
@@ -157,6 +158,7 @@ jobs:
needs: normalize-duplicates-all
if: ${{ needs.normalize-duplicates-all.result == 'success' && needs.normalize-duplicates-all.outputs.has_matches != 'true' }}
runs-on: ubuntu-latest
environment: issue-triage
permissions:
contents: read
outputs:

View File

@@ -12,6 +12,7 @@ jobs:
# Prevent runs on forks (requires OpenAI API key, wastes Actions minutes)
if: github.repository == 'openai/codex' && (github.event.action == 'opened' || (github.event.action == 'labeled' && github.event.label.name == 'codex-label'))
runs-on: ubuntu-latest
environment: issue-triage
permissions:
contents: read
outputs:

View File

@@ -4,15 +4,161 @@ on:
push:
tags:
- "python-v*"
workflow_dispatch:
inputs:
runtime_version:
description: "Runtime version to publish before updating the SDK pin, for example 0.136.0 or 0.136.0a2."
required: true
type: string
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
jobs:
build-python-sdk:
# Publish the platform-specific Python runtime wheels before building the SDK
# package that pins them, or explicitly before updating the SDK runtime pin.
# PyPI project configuration must trust this workflow and job for publishing.
publish-python-runtime:
if: github.repository == 'openai/codex'
name: publish-python-runtime
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: read
id-token: write # Required for PyPI trusted publishing.
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Validate SDK tag and resolve Python runtime release
id: python_runtime
shell: bash
env:
REQUESTED_RUNTIME_VERSION: ${{ inputs.runtime_version }}
run: |
set -euo pipefail
python3 - <<'PY'
import os
import re
import tomllib
from pathlib import Path
event_name = os.environ["GITHUB_EVENT_NAME"]
if event_name == "workflow_dispatch":
python_version = os.environ["REQUESTED_RUNTIME_VERSION"]
elif event_name == "push":
sdk_version = os.environ["GITHUB_REF_NAME"].removeprefix("python-v")
if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+b[0-9]+", sdk_version):
raise SystemExit(
"Python SDK release tags must identify a beta release, "
"for example python-v0.1.0b1."
)
pyproject = tomllib.loads(Path("sdk/python/pyproject.toml").read_text())
prefix = "openai-codex-cli-bin=="
versions = [
dependency.removeprefix(prefix)
for dependency in pyproject["project"]["dependencies"]
if dependency.startswith(prefix)
]
if len(versions) != 1:
raise SystemExit(f"Expected exactly one pinned {prefix} dependency, found {versions}")
python_version = versions[0]
else:
raise SystemExit(f"Unsupported workflow event: {event_name}")
if match := re.fullmatch(r"([0-9]+\.[0-9]+\.[0-9]+)a([0-9]+)", python_version):
release_version = f"{match.group(1)}-alpha.{match.group(2)}"
elif re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", python_version):
release_version = python_version
else:
raise SystemExit(
"Python runtime version must be stable or a numbered alpha, "
f"for example 0.136.0 or 0.136.0a2; found {python_version}"
)
with Path(os.environ["GITHUB_OUTPUT"]).open("a") as output:
print(f"python_version={python_version}", file=output)
print(f"release_tag=rust-v{release_version}", file=output)
PY
- name: Download Python runtime release artifacts
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PYTHON_RUNTIME_VERSION: ${{ steps.python_runtime.outputs.python_version }}
RELEASE_TAG: ${{ steps.python_runtime.outputs.release_tag }}
run: |
set -euo pipefail
mkdir -p dist/python-runtime dist/python-runtime-packages
gh release download "$RELEASE_TAG" \
--repo "${GITHUB_REPOSITORY}" \
--pattern "openai_codex_cli_bin-${PYTHON_RUNTIME_VERSION}-*.whl" \
--dir dist/python-runtime
gh release download "$RELEASE_TAG" \
--repo "${GITHUB_REPOSITORY}" \
--pattern "codex-package-*-unknown-linux-musl.tar.gz" \
--dir dist/python-runtime-packages
shopt -s nullglob
wheels=(dist/python-runtime/*.whl)
if [[ "${#wheels[@]}" -ne 6 ]]; then
echo "Expected 6 Python runtime wheels for ${PYTHON_RUNTIME_VERSION}, found ${#wheels[@]}."
exit 1
fi
packages=(dist/python-runtime-packages/*.tar.gz)
if [[ "${#packages[@]}" -ne 2 ]]; then
echo "Expected 2 Linux package archives for ${PYTHON_RUNTIME_VERSION}, found ${#packages[@]}."
exit 1
fi
- name: Build musllinux Python runtime wheels
env:
RELEASE_TAG: ${{ steps.python_runtime.outputs.release_tag }}
run: |
set -euo pipefail
python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv"
"${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build
while read -r target platform_tag; do
stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${target}-${platform_tag}"
python3 sdk/python/scripts/update_sdk_artifacts.py \
stage-runtime \
"$stage_dir" \
"dist/python-runtime-packages/codex-package-${target}.tar.gz" \
--codex-version "$RELEASE_TAG" \
--platform-tag "$platform_tag"
"${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build \
--wheel \
--outdir dist/python-runtime \
"$stage_dir"
done <<'EOF'
aarch64-unknown-linux-musl musllinux_1_1_aarch64
x86_64-unknown-linux-musl musllinux_1_1_x86_64
EOF
shopt -s nullglob
wheels=(dist/python-runtime/*.whl)
if [[ "${#wheels[@]}" -ne 8 ]]; then
echo "Expected 8 Python runtime wheels, found ${#wheels[@]}."
exit 1
fi
ls -lh dist/python-runtime
- name: Publish Python runtime wheels to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
packages-dir: dist/python-runtime
skip-existing: true
build-python-sdk:
if: github.event_name == 'push' && github.repository == 'openai/codex'
name: build-python-sdk
needs: publish-python-runtime
runs-on: ubuntu-latest
permissions:
contents: read
@@ -29,13 +175,8 @@ jobs:
set -euo pipefail
sdk_version="${GITHUB_REF_NAME#python-v}"
if [[ ! "${sdk_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+b[0-9]+$ ]]; then
echo "Python SDK release tags must identify a beta release, for example python-v0.1.0b1."
exit 1
fi
# The pinned runtime currently publishes a musllinux Linux wheel.
# Build in Alpine so release type generation installs that wheel.
# Build in a glibc Linux image so release type generation installs
# the pinned manylinux runtime wheel.
docker run --rm \
--user "$(id -u):$(id -g)" \
-e HOME=/tmp/codex-python-sdk-home \
@@ -46,7 +187,7 @@ jobs:
-v "${GITHUB_WORKSPACE}:${GITHUB_WORKSPACE}" \
-v "${RUNNER_TEMP}:${RUNNER_TEMP}" \
-w "${GITHUB_WORKSPACE}/sdk/python" \
python:3.12-alpine \
python:3.12-slim \
sh -euxc '
python -m venv /tmp/release-tools
/tmp/release-tools/bin/python -m pip install build twine uv==0.11.3

View File

@@ -402,13 +402,6 @@ jobs:
- name: cargo clippy
run: cargo clippy --target ${{ matrix.target }} --tests --profile ${{ matrix.profile }} --timings -- -D warnings
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: just
- name: End-to-end benchmark smoke test
run: just bench-e2e-smoke
- name: Upload Cargo timings (clippy)
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0

View File

@@ -3,6 +3,11 @@ on:
pull_request: {}
workflow_dispatch:
# Cargo's libgit2 transport has been flaky when fetching git dependencies with
# nested submodules. Prefer the system git CLI across every Cargo invocation.
env:
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
jobs:
# --- Detect what changed so the fast PR workflow only runs relevant jobs ----
changed:

View File

@@ -7,6 +7,11 @@ on:
required: true
type: boolean
# Cargo's libgit2 transport has been flaky when fetching git dependencies with
# nested submodules. Prefer the system git CLI across every Cargo invocation.
env:
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
jobs:
skip:
if: ${{ !inputs.publish }}

View File

@@ -20,6 +20,11 @@ on:
AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME:
required: true
# Cargo's libgit2 transport has been flaky when fetching git dependencies with
# nested submodules. Prefer the system git CLI across every Cargo invocation.
env:
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
jobs:
build-windows-binaries:
name: Build Windows binaries - ${{ matrix.runner }} - ${{ matrix.target }} - ${{ matrix.bundle }}
@@ -107,11 +112,15 @@ jobs:
- name: Cargo build (Windows binaries)
shell: bash
run: |
target="${{ matrix.target }}"
if [[ "$target" == "x86_64-pc-windows-msvc" ]]; then
export LIBSQLITE3_FLAGS=SQLITE_DISABLE_INTRINSIC
fi
build_args=()
for binary in ${{ matrix.binaries }}; do
build_args+=(--bin "$binary")
done
cargo build --target ${{ matrix.target }} --release --timings "${build_args[@]}"
cargo build --target "$target" --release --timings "${build_args[@]}"
- name: Upload Cargo timings
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0

View File

@@ -149,6 +149,11 @@ jobs:
# 2026-03-04: temporarily change releases to use thin LTO because
# Ubuntu ARM is timing out at 60 minutes.
CARGO_PROFILE_RELEASE_LTO: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'thin' }}
# Use the git CLI instead of Cargo's libgit2 path for git dependencies.
# macOS release runners have intermittently failed to fetch nested
# submodules through SecureTransport/libgit2, especially libwebrtc's
# libyuv submodule from chromium.googlesource.com.
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' }}
strategy:
@@ -310,12 +315,16 @@ jobs:
- name: Cargo build
shell: bash
run: |
target="${{ matrix.target }}"
if [[ "$target" == "x86_64-pc-windows-msvc" ]]; then
export LIBSQLITE3_FLAGS=SQLITE_DISABLE_INTRINSIC
fi
build_args=()
for binary in ${{ matrix.binaries }}; do
build_args+=(--bin "$binary")
done
echo "CARGO_PROFILE_RELEASE_LTO: ${CARGO_PROFILE_RELEASE_LTO}"
cargo build --target ${{ matrix.target }} --release --timings "${build_args[@]}"
cargo build --target "$target" --release --timings "${build_args[@]}"
- name: Upload Cargo timings
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
@@ -882,7 +891,6 @@ jobs:
sign_macos: ${{ steps.release_mode.outputs.sign_macos }}
should_publish_npm: ${{ steps.npm_publish_settings.outputs.should_publish }}
npm_tag: ${{ steps.npm_publish_settings.outputs.npm_tag }}
should_publish_python_runtime: ${{ steps.python_runtime_publish_settings.outputs.should_publish }}
steps:
- name: Checkout repository
@@ -1106,27 +1114,6 @@ jobs:
echo "npm_tag=" >> "$GITHUB_OUTPUT"
fi
- name: Determine Python runtime publish settings
id: python_runtime_publish_settings
env:
VERSION: ${{ steps.release_name.outputs.name }}
run: |
set -euo pipefail
version="${VERSION}"
if [[ "${SIGN_MACOS}" != "true" ]]; then
echo "should_publish=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "should_publish=true" >> "$GITHUB_OUTPUT"
elif [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then
echo "should_publish=true" >> "$GITHUB_OUTPUT"
else
echo "should_publish=false" >> "$GITHUB_OUTPUT"
fi
- name: Setup pnpm
if: ${{ env.SIGN_MACOS == 'true' }}
uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
@@ -1391,53 +1378,6 @@ jobs:
exit "${publish_status}"
done
# Publish the platform-specific Python runtime wheels using PyPI trusted publishing.
# PyPI project configuration must trust this workflow and job. Keep this
# non-blocking while the Python runtime publishing path is new; failures still
# need release follow-up, but should not invalidate the Rust release itself.
publish-python-runtime:
# Publish to PyPI for stable releases and alpha pre-releases with numeric suffixes.
if: >-
${{
!cancelled() &&
needs.release.result == 'success' &&
needs.release.outputs.should_publish_python_runtime == 'true'
}}
name: publish-python-runtime
needs: release
runs-on: ubuntu-latest
continue-on-error: true
environment: pypi
permissions:
id-token: write # Required for PyPI trusted publishing.
contents: read
steps:
- name: Download Python runtime wheels from release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ needs.release.outputs.tag }}
RELEASE_VERSION: ${{ needs.release.outputs.version }}
run: |
set -euo pipefail
python_version="$RELEASE_VERSION"
python_version="${python_version/-alpha./a}"
python_version="${python_version/-beta./b}"
python_version="${python_version/-rc./rc}"
mkdir -p dist/python-runtime
gh release download "$RELEASE_TAG" \
--repo "${GITHUB_REPOSITORY}" \
--pattern "openai_codex_cli_bin-${python_version}-*.whl" \
--dir dist/python-runtime
ls -lh dist/python-runtime
- name: Publish Python runtime wheels to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
packages-dir: dist/python-runtime
skip-existing: true
deploy-dev-website:
name: Trigger developers.openai.com deploy
needs: release

View File

@@ -5,6 +5,11 @@ on:
tags:
- "rusty-v8-v*.*.*"
# Cargo's libgit2 transport has been flaky when fetching git dependencies with
# nested submodules. Prefer the system git CLI for Cargo smoke tests.
env:
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
concurrency:
group: ${{ github.workflow }}::${{ github.ref_name }}
cancel-in-progress: false
@@ -186,11 +191,10 @@ jobs:
bazel_args+=(--config=v8-release-compat)
fi
bazel \
./.github/scripts/run_bazel_with_buildbuddy.py \
--noexperimental_remote_repo_contents_cache \
"${bazel_args[@]}" \
"--config=${{ matrix.bazel_config }}" \
"--remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}"
"--config=${{ matrix.bazel_config }}"
- name: Stage release pair
env:

View File

@@ -23,15 +23,15 @@ jobs:
run: |
set -euo pipefail
# Run inside Alpine so dependency resolution exercises the pinned
# runtime wheel on the same Linux wheel family that CI installs.
# Run inside a glibc Linux image so dependency resolution exercises
# the pinned manylinux runtime wheel that users install.
docker run --rm \
--user "$(id -u):$(id -g)" \
-e HOME=/tmp/codex-python-sdk-home \
-e UV_LINK_MODE=copy \
-v "${GITHUB_WORKSPACE}:${GITHUB_WORKSPACE}" \
-w "${GITHUB_WORKSPACE}/sdk/python" \
python:3.12-alpine \
python:3.12-slim \
sh -euxc '
python -m venv /tmp/uv
/tmp/uv/bin/python -m pip install uv==0.11.3

View File

@@ -5,6 +5,7 @@ on:
paths:
- ".bazelrc"
- ".github/actions/setup-bazel-ci/**"
- ".github/scripts/run_bazel_with_buildbuddy.py"
- ".github/scripts/rusty_v8_bazel.py"
- ".github/scripts/rusty_v8_module_bazel.py"
- ".github/workflows/rusty-v8-release.yml"
@@ -23,6 +24,7 @@ on:
paths:
- ".bazelrc"
- ".github/actions/setup-bazel-ci/**"
- ".github/scripts/run_bazel_with_buildbuddy.py"
- ".github/scripts/rusty_v8_bazel.py"
- ".github/scripts/rusty_v8_module_bazel.py"
- ".github/workflows/rusty-v8-release.yml"
@@ -37,6 +39,11 @@ on:
- "third_party/v8/**"
workflow_dispatch:
# Cargo's libgit2 transport has been flaky when fetching git dependencies with
# nested submodules. Prefer the system git CLI for Cargo builds and smoke tests.
env:
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
concurrency:
group: ${{ github.workflow }}::${{ github.event.pull_request.number > 0 && format('pr-{0}', github.event.pull_request.number) || github.ref_name }}
cancel-in-progress: ${{ github.ref_name != 'main' }}
@@ -198,11 +205,10 @@ jobs:
bazel_args+=(--config=v8-release-compat)
fi
bazel \
./.github/scripts/run_bazel_with_buildbuddy.py \
--noexperimental_remote_repo_contents_cache \
"${bazel_args[@]}" \
"--config=${{ matrix.bazel_config }}" \
"--remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}"
"--config=${{ matrix.bazel_config }}"
- name: Stage release pair
env:

View File

@@ -1,5 +1,6 @@
{
"recommendations": [
"BazelBuild.vscode-bazel",
"rust-lang.rust-analyzer",
"charliermarsh.ruff",
"tamasfe.even-better-toml",

View File

@@ -55,7 +55,7 @@ In the codex-rs folder where the rust code lives:
trivial; prefer new modules/files and keep `chatwidget.rs` focused on orchestration.
- When running Rust commands (e.g. `just fix` or `just test`) be patient with the command and never try to kill them using the PID. Rust lock can make the execution slow, this is expected.
Run `just fmt` (in `codex-rs` directory) automatically after you have finished making Rust code changes; do not ask for approval to run it. Additionally, run the tests:
Run `just fmt` (in the `codex-rs` directory) automatically after you have finished making code changes anywhere in this repository; do not ask for approval to run it. Additionally, run the tests:
1. Do not run `cargo test` directly. Use `just test` so test execution follows the repo defaults.
2. Run the test for the specific project that was changed. For example, if changes were made in `codex-rs/tui`, run `just test -p codex-tui`.
@@ -76,6 +76,49 @@ Particularly when introducing a new concept/feature/API, before adding to `codex
Likewise, when reviewing code, do not hesitate to push back on PRs that would unnecessarily add code to `codex-core`.
## Code Review Rules
### Model visible context
Codex maintains a context (history of messages) that is sent to the model in inference requests.
1. No history rewrite - the context must be built up incrementally.
2. Avoid frequent changes to context that cause cache misses.
3. No unbounded items - everything injected in the model context must have a bounded size and a hard cap.
4. No items larger than 10K tokens.
5. Highlight new individual items that can cross >1k tokens as P0. These need an additional manual review.
6. All injected fragments must be defined as structs in `core/context` and implement ContextualUserFragment trait
### Breaking changes
Search for breaking changes in external integration surfaces:
- app-server APIs
- CLI parameters
- configuration loading
- resuming sessions from existing rollouts
### Test authoring guidance
For agent changes prefer integration tests over unit tests. Integration tests are under `core/suite` and use `test_codex` to set up a test instance of codex.
Features that change the agent logic MUST add an integration test:
- Provide a list of major logic changes and user-facing behaviors that need to be tested.
If unit tests are needed, put them in a dedicated test file (\*\_tests.rs).
Avoid test-only functions in the main implementation.
Check whether there are existing helpers to make tests more streamlined and readable.
### Change size guidance (800 lines)
Unless the change is mechanical the total number of changed lines should not exceed 800 lines.
For complex logic changes the size should be under 500 lines.
If the change is larger, explore whether it can be split into reviewable stages and identify the smallest coherent stage to land first.
Base the staging suggestion on the actual diff, dependencies, and affected call sites.
## TUI style conventions
See `codex-rs/tui/styles.md`.
@@ -110,6 +153,19 @@ See `codex-rs/tui/styles.md`.
## Tests
### Test module organization
- When adding a new test module, define its contents in a separate sibling file rather than inline in the implementation file.
- Use an explicit `#[path = "..._tests.rs"]` attribute so the test filename is descriptive and easy to locate:
```rust
#[cfg(test)]
#[path = "parser_tests.rs"]
mod tests;
```
- This applies only when introducing a new test module. Do not move or rewrite existing inline `#[cfg(test)] mod tests { ... }` modules solely to follow this convention.
### Snapshot tests
This repo uses snapshot tests (via `insta`), especially in `codex-rs/tui`, to validate rendered output.
@@ -219,3 +275,12 @@ These guidelines apply to app-server protocol work in `codex-rs`, especially:
- Validate with `just test -p codex-app-server-protocol`.
- Avoid boilerplate tests that only assert experimental field markers for individual
request fields in `common.rs`; rely on schema generation/tests and behavioral coverage instead.
## Python Development Best Practices
### Ignore Python 2 compatibility
This project uses Python 3+. You should not use the `__future__` module.
If you need to worry about feature compatibility between different 3.xx point releases, check the
closest `pyproject.toml`'s `requires-python` field to see what minimum runtime version is supported.

102
codex-rs/Cargo.lock generated
View File

@@ -1913,7 +1913,7 @@ dependencies = [
"codex-arg0",
"codex-backend-client",
"codex-chatgpt",
"codex-cloud-requirements",
"codex-cloud-config",
"codex-config",
"codex-core",
"codex-core-plugins",
@@ -1928,6 +1928,8 @@ dependencies = [
"codex-git-utils",
"codex-guardian",
"codex-hooks",
"codex-http-state",
"codex-image-generation-extension",
"codex-login",
"codex-mcp",
"codex-memories-extension",
@@ -2058,17 +2060,6 @@ dependencies = [
"uuid",
]
[[package]]
name = "codex-app-server-start-bench"
version = "0.0.0"
dependencies = [
"anyhow",
"codex-app-server-protocol",
"divan",
"serde_json",
"tempfile",
]
[[package]]
name = "codex-app-server-test-client"
version = "0.0.0"
@@ -2354,10 +2345,9 @@ dependencies = [
]
[[package]]
name = "codex-cloud-requirements"
name = "codex-cloud-config"
version = "0.0.0"
dependencies = [
"async-trait",
"base64 0.22.1",
"chrono",
"codex-backend-client",
@@ -2366,7 +2356,6 @@ dependencies = [
"codex-login",
"codex-otel",
"codex-protocol",
"codex-utils-absolute-path",
"hmac 0.12.1",
"pretty_assertions",
"serde",
@@ -2375,7 +2364,6 @@ dependencies = [
"tempfile",
"thiserror 2.0.18",
"tokio",
"toml 0.9.11+spec-1.1.0",
"tracing",
]
@@ -2440,8 +2428,6 @@ dependencies = [
name = "codex-code-mode"
version = "0.0.0"
dependencies = [
"async-channel",
"async-trait",
"codex-protocol",
"deno_core_icudata",
"pretty_assertions",
@@ -2562,6 +2548,7 @@ dependencies = [
"codex-network-proxy",
"codex-otel",
"codex-plugin",
"codex-prompts",
"codex-protocol",
"codex-response-debug-context",
"codex-rmcp-client",
@@ -2586,7 +2573,6 @@ dependencies = [
"codex-utils-pty",
"codex-utils-stream-parser",
"codex-utils-string",
"codex-utils-template",
"codex-windows-sandbox",
"core_test_support",
"csv",
@@ -2681,6 +2667,7 @@ dependencies = [
"codex-utils-plugins",
"dirs",
"flate2",
"indexmap 2.13.0",
"libc",
"pretty_assertions",
"reqwest 0.12.28",
@@ -2729,18 +2716,6 @@ dependencies = [
"zip 2.4.2",
]
[[package]]
name = "codex-debug-client"
version = "0.0.0"
dependencies = [
"anyhow",
"clap",
"codex-app-server-protocol",
"pretty_assertions",
"serde",
"serde_json",
]
[[package]]
name = "codex-exec"
version = "0.0.0"
@@ -2752,7 +2727,7 @@ dependencies = [
"codex-app-server-protocol",
"codex-apply-patch",
"codex-arg0",
"codex-cloud-requirements",
"codex-cloud-config",
"codex-config",
"codex-core",
"codex-feedback",
@@ -3009,6 +2984,7 @@ dependencies = [
"codex-protocol",
"codex-state",
"codex-tools",
"codex-utils-template",
"pretty_assertions",
"serde",
"serde_json",
@@ -3050,6 +3026,38 @@ dependencies = [
"uuid",
]
[[package]]
name = "codex-http-state"
version = "0.0.0"
dependencies = [
"codex-utils-path",
"pretty_assertions",
"serde",
"serde_json",
"tempfile",
]
[[package]]
name = "codex-image-generation-extension"
version = "0.0.0"
dependencies = [
"async-trait",
"codex-api",
"codex-core",
"codex-extension-api",
"codex-features",
"codex-login",
"codex-model-provider",
"codex-model-provider-info",
"codex-protocol",
"codex-tools",
"http 1.4.0",
"pretty_assertions",
"schemars 0.8.22",
"serde",
"serde_json",
]
[[package]]
name = "codex-install-context"
version = "0.0.0"
@@ -3360,6 +3368,7 @@ version = "0.0.0"
dependencies = [
"anyhow",
"async-trait",
"base64 0.22.1",
"chrono",
"clap",
"codex-utils-absolute-path",
@@ -3375,8 +3384,10 @@ dependencies = [
"rama-tcp",
"rama-tls-rustls",
"rama-unix",
"rustls-native-certs",
"serde",
"serde_json",
"sha2 0.10.9",
"tempfile",
"thiserror 2.0.18",
"time",
@@ -3454,6 +3465,19 @@ dependencies = [
"pretty_assertions",
]
[[package]]
name = "codex-prompts"
version = "0.0.0"
dependencies = [
"anyhow",
"codex-execpolicy",
"codex-git-utils",
"codex-protocol",
"codex-utils-absolute-path",
"codex-utils-template",
"pretty_assertions",
]
[[package]]
name = "codex-protocol"
version = "0.0.0"
@@ -3583,7 +3607,6 @@ dependencies = [
"codex-protocol",
"codex-state",
"codex-utils-path",
"codex-utils-string",
"pretty_assertions",
"regex",
"serde",
@@ -3593,6 +3616,7 @@ dependencies = [
"tokio",
"tracing",
"uuid",
"zstd 0.13.3",
]
[[package]]
@@ -3703,6 +3727,15 @@ dependencies = [
"thiserror 2.0.18",
]
[[package]]
name = "codex-skills-extension"
version = "0.0.0"
dependencies = [
"async-trait",
"codex-core",
"codex-extension-api",
]
[[package]]
name = "codex-state"
version = "0.0.0"
@@ -3826,7 +3859,7 @@ dependencies = [
"codex-app-server-protocol",
"codex-arg0",
"codex-cli",
"codex-cloud-requirements",
"codex-cloud-config",
"codex-config",
"codex-connectors",
"codex-core-plugins",
@@ -4171,6 +4204,7 @@ dependencies = [
"pretty_assertions",
"schemars 0.8.22",
"serde_json",
"url",
]
[[package]]

View File

@@ -14,8 +14,6 @@ members = [
"app-server-client",
"app-server-protocol",
"app-server-test-client",
"benchmarks/app-server-start",
"debug-client",
"apply-patch",
"arg0",
"feedback",
@@ -23,7 +21,7 @@ members = [
"install-context",
"codex-backend-openapi-models",
"code-mode",
"cloud-requirements",
"cloud-config",
"cloud-tasks",
"cloud-tasks-client",
"cloud-tasks-mock-client",
@@ -39,6 +37,7 @@ members = [
"core-plugins",
"core-skills",
"hooks",
"http-state",
"secrets",
"exec",
"file-system",
@@ -48,7 +47,9 @@ members = [
"ext/extension-api",
"ext/goal",
"ext/guardian",
"ext/image-generation",
"ext/memories",
"ext/skills",
"ext/web-search",
"external-agent-migration",
"external-agent-sessions",
@@ -69,6 +70,7 @@ members = [
"process-hardening",
"protocol",
"realtime-webrtc",
"prompts",
"rollout",
"rollout-trace",
"rmcp-client",
@@ -149,7 +151,7 @@ codex-chatgpt = { path = "chatgpt" }
codex-cli = { path = "cli" }
codex-client = { path = "codex-client" }
codex-collaboration-mode-templates = { path = "collaboration-mode-templates" }
codex-cloud-requirements = { path = "cloud-requirements" }
codex-cloud-config = { path = "cloud-config" }
codex-cloud-tasks-client = { path = "cloud-tasks-client" }
codex-cloud-tasks-mock-client = { path = "cloud-tasks-mock-client" }
codex-code-mode = { path = "code-mode" }
@@ -166,6 +168,7 @@ codex-execpolicy = { path = "execpolicy" }
codex-extension-api = { path = "ext/extension-api" }
codex-goal-extension = { path = "ext/goal" }
codex-guardian = { path = "ext/guardian" }
codex-image-generation-extension = { path = "ext/image-generation" }
codex-external-agent-migration = { path = "external-agent-migration" }
codex-external-agent-sessions = { path = "external-agent-sessions" }
codex-experimental-api-macros = { path = "codex-experimental-api-macros" }
@@ -176,6 +179,7 @@ codex-file-search = { path = "file-search" }
codex-file-watcher = { path = "file-watcher" }
codex-git-utils = { path = "git-utils" }
codex-hooks = { path = "hooks" }
codex-http-state = { path = "http-state" }
codex-keyring-store = { path = "keyring-store" }
codex-linux-sandbox = { path = "linux-sandbox" }
codex-lmstudio = { path = "lmstudio" }
@@ -197,6 +201,7 @@ codex-model-provider = { path = "model-provider" }
codex-process-hardening = { path = "process-hardening" }
codex-protocol = { path = "protocol" }
codex-realtime-webrtc = { path = "realtime-webrtc" }
codex-prompts = { path = "prompts" }
codex-responses-api-proxy = { path = "responses-api-proxy" }
codex-response-debug-context = { path = "response-debug-context" }
codex-rmcp-client = { path = "rmcp-client" }

View File

@@ -62,6 +62,7 @@ use crate::facts::SkillInvokedInput;
use crate::facts::SubAgentThreadStartedInput;
use crate::facts::ThreadInitializationMode;
use crate::facts::TrackEventsContext;
use crate::facts::TurnCodexErrorFact;
use crate::facts::TurnResolvedConfigFact;
use crate::facts::TurnStatus;
use crate::facts::TurnSteerRequestError;
@@ -132,6 +133,7 @@ use codex_plugin::PluginTelemetryMetadata;
use codex_protocol::approvals::NetworkApprovalProtocol;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::ModeKind;
use codex_protocol::error::CodexErr;
use codex_protocol::models::NetworkPermissions as CoreNetworkPermissions;
use codex_protocol::models::PermissionProfile as CorePermissionProfile;
use codex_protocol::protocol::AskForApproval;
@@ -160,11 +162,13 @@ fn sample_thread_with_metadata(
ephemeral: bool,
source: AppServerSessionSource,
thread_source: Option<AppServerThreadSource>,
parent_thread_id: Option<String>,
) -> Thread {
Thread {
id: thread_id.to_string(),
session_id: format!("session-{thread_id}"),
forked_from_id: None,
parent_thread_id,
preview: "first prompt".to_string(),
ephemeral,
model_provider: "openai".to_string(),
@@ -195,6 +199,7 @@ fn sample_thread_start_response(
ephemeral,
AppServerSessionSource::Exec,
Some(AppServerThreadSource::User),
/*parent_thread_id*/ None,
),
model: model.to_string(),
model_provider: "openai".to_string(),
@@ -240,6 +245,7 @@ fn sample_thread_resume_response(
model,
AppServerSessionSource::Exec,
Some(AppServerThreadSource::User),
/*parent_thread_id*/ None,
)
}
@@ -249,9 +255,16 @@ fn sample_thread_resume_response_with_source(
model: &str,
source: AppServerSessionSource,
thread_source: Option<AppServerThreadSource>,
parent_thread_id: Option<String>,
) -> ClientResponsePayload {
ClientResponsePayload::ThreadResume(ThreadResumeResponse {
thread: sample_thread_with_metadata(thread_id, ephemeral, source, thread_source),
thread: sample_thread_with_metadata(
thread_id,
ephemeral,
source,
thread_source,
parent_thread_id,
),
model: model.to_string(),
model_provider: "openai".to_string(),
service_tier: None,
@@ -272,6 +285,7 @@ fn sample_turn_start_request(thread_id: &str, request_id: i64) -> ClientRequest
request_id: RequestId::Integer(request_id),
params: TurnStartParams {
thread_id: thread_id.to_string(),
client_user_message_id: None,
input: vec![
UserInput::Text {
text: "hello".to_string(),
@@ -377,6 +391,7 @@ fn sample_turn_resolved_config(thread_id: &str, turn_id: &str) -> TurnResolvedCo
sandbox_network_access: true,
collaboration_mode: ModeKind::Plan,
personality: None,
workspace_kind: None,
is_first_turn: true,
}
}
@@ -391,6 +406,7 @@ fn sample_turn_steer_request(
params: TurnSteerParams {
thread_id: thread_id.to_string(),
expected_turn_id: expected_turn_id.to_string(),
client_user_message_id: None,
input: vec![
UserInput::Text {
text: "more".to_string(),
@@ -820,6 +836,7 @@ fn sample_permissions_approval_request(request_id: i64) -> ServerRequest {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
item_id: "permissions-1".to_string(),
environment_id: None,
started_at_ms: 1_000,
cwd: test_path_buf("/tmp").abs(),
reason: Some("need network".to_string()),
@@ -1753,6 +1770,7 @@ async fn compaction_event_ingests_custom_fact() {
agent_role: None,
}),
Some(AppServerThreadSource::Subagent),
Some(parent_thread_id.to_string()),
)),
},
&mut events,
@@ -2454,7 +2472,7 @@ fn subagent_thread_started_thread_spawn_serializes_parent_thread_id() {
SubAgentThreadStartedInput {
session_id: "session-root".to_string(),
thread_id: "thread-spawn".to_string(),
parent_thread_id: None,
parent_thread_id: Some(parent_thread_id.to_string()),
product_client_id: "codex-tui".to_string(),
client_name: "codex-tui".to_string(),
client_version: "1.0.0".to_string(),
@@ -2532,11 +2550,14 @@ fn subagent_thread_started_other_serializes_expected_shape() {
#[test]
fn subagent_thread_started_other_serializes_explicit_parent_thread_id() {
let parent_thread_id =
codex_protocol::ThreadId::from_string("33333333-3333-4333-8333-333333333333")
.expect("valid thread id");
let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request(
SubAgentThreadStartedInput {
session_id: "session-root".to_string(),
thread_id: "thread-guardian".to_string(),
parent_thread_id: Some("parent-thread-guardian".to_string()),
parent_thread_id: Some(parent_thread_id.to_string()),
product_client_id: "codex-tui".to_string(),
client_name: "codex-tui".to_string(),
client_version: "1.0.0".to_string(),
@@ -2551,7 +2572,7 @@ fn subagent_thread_started_other_serializes_explicit_parent_thread_id() {
assert_eq!(payload["event_params"]["subagent_source"], "guardian");
assert_eq!(
payload["event_params"]["parent_thread_id"],
"parent-thread-guardian"
"33333333-3333-4333-8333-333333333333"
);
}
@@ -2640,7 +2661,7 @@ async fn subagent_thread_started_inherits_parent_connection_for_new_thread() {
SubAgentThreadStartedInput {
session_id: "session-root".to_string(),
thread_id: "thread-review".to_string(),
parent_thread_id: None,
parent_thread_id: Some(parent_thread_id.to_string()),
product_client_id: "parent-client".to_string(),
client_name: "parent-client".to_string(),
client_version: "1.0.0".to_string(),
@@ -3235,10 +3256,14 @@ fn turn_event_serializes_expected_shape() {
sandbox_network_access: true,
collaboration_mode: Some("plan"),
personality: Some("pragmatic".to_string()),
workspace_kind: Some("projectless".to_string()),
num_input_images: 2,
is_first_turn: true,
status: Some(TurnStatus::Completed),
turn_error: None,
codex_error_kind: None,
codex_error_subreason: None,
codex_error_http_status_code: None,
steer_count: Some(0),
total_tool_call_count: None,
shell_command_count: None,
@@ -3297,10 +3322,14 @@ fn turn_event_serializes_expected_shape() {
"sandbox_network_access": true,
"collaboration_mode": "plan",
"personality": "pragmatic",
"workspace_kind": "projectless",
"num_input_images": 2,
"is_first_turn": true,
"status": "completed",
"turn_error": null,
"codex_error_kind": null,
"codex_error_subreason": null,
"codex_error_http_status_code": null,
"steer_count": 0,
"total_tool_call_count": null,
"shell_command_count": null,
@@ -3614,6 +3643,7 @@ async fn turn_lifecycle_emits_turn_event() {
);
assert!(payload["event_params"].get("product_client_id").is_none());
assert_eq!(payload["event_params"]["ephemeral"], json!(false));
assert_eq!(payload["event_params"]["workspace_kind"], json!(null));
assert_eq!(payload["event_params"]["num_input_images"], json!(1));
assert_eq!(payload["event_params"]["status"], json!("completed"));
assert_eq!(payload["event_params"]["steer_count"], json!(0));
@@ -3966,6 +3996,18 @@ async fn turn_lifecycle_emits_failed_turn_event() {
/*include_token_usage*/ false,
)
.await;
reducer
.ingest(
AnalyticsFact::Custom(CustomAnalyticsFact::TurnCodexError(Box::new(
TurnCodexErrorFact::from_codex_err(
"thread-2".to_string(),
"turn-2".to_string(),
&CodexErr::InvalidRequest("unknown turn environment id `env-2`".to_string()),
),
))),
&mut out,
)
.await;
reducer
.ingest(
AnalyticsFact::Notification(Box::new(sample_turn_completed_notification(
@@ -3982,6 +4024,18 @@ async fn turn_lifecycle_emits_failed_turn_event() {
let payload = serde_json::to_value(&out[0]).expect("serialize turn event");
assert_eq!(payload["event_params"]["status"], json!("failed"));
assert_eq!(payload["event_params"]["turn_error"], json!("badRequest"));
assert_eq!(
payload["event_params"]["codex_error_kind"],
json!("invalid_request")
);
assert_eq!(
payload["event_params"]["codex_error_subreason"],
json!("unknown turn environment id `env-2`")
);
assert_eq!(
payload["event_params"]["codex_error_http_status_code"],
json!(null)
);
}
#[tokio::test]
@@ -4014,6 +4068,7 @@ async fn turn_lifecycle_emits_interrupted_turn_event_without_error() {
let payload = serde_json::to_value(&out[0]).expect("serialize turn event");
assert_eq!(payload["event_params"]["status"], json!("interrupted"));
assert_eq!(payload["event_params"]["turn_error"], json!(null));
assert_eq!(payload["event_params"]["codex_error_kind"], json!(null));
}
#[tokio::test]

View File

@@ -18,6 +18,7 @@ use crate::facts::SkillInvocation;
use crate::facts::SkillInvokedInput;
use crate::facts::SubAgentThreadStartedInput;
use crate::facts::TrackEventsContext;
use crate::facts::TurnCodexErrorFact;
use crate::facts::TurnResolvedConfigFact;
use crate::facts::TurnTokenUsageFact;
use crate::reducer::AnalyticsReducer;
@@ -256,6 +257,12 @@ impl AnalyticsEventsClient {
)));
}
pub fn track_turn_codex_error(&self, fact: TurnCodexErrorFact) {
self.record_fact(AnalyticsFact::Custom(CustomAnalyticsFact::TurnCodexError(
Box::new(fact),
)));
}
pub fn track_plugin_installed(&self, plugin: PluginTelemetryMetadata) {
self.record_fact(AnalyticsFact::Custom(
CustomAnalyticsFact::PluginStateChanged(PluginStateChangedInput {

View File

@@ -89,6 +89,7 @@ fn sample_turn_start_request() -> ClientRequest {
request_id: RequestId::Integer(1),
params: TurnStartParams {
thread_id: "thread-1".to_string(),
client_user_message_id: None,
input: Vec::new(),
..Default::default()
},
@@ -101,6 +102,7 @@ fn sample_turn_steer_request() -> ClientRequest {
params: TurnSteerParams {
thread_id: "thread-1".to_string(),
expected_turn_id: "turn-1".to_string(),
client_user_message_id: None,
input: Vec::new(),
responsesapi_client_metadata: None,
additional_context: None,
@@ -122,6 +124,7 @@ fn sample_thread(thread_id: &str) -> Thread {
id: thread_id.to_string(),
session_id: format!("session-{thread_id}"),
forked_from_id: None,
parent_thread_id: None,
preview: "first prompt".to_string(),
ephemeral: false,
model_provider: "openai".to_string(),

View File

@@ -3,6 +3,7 @@ use std::time::Instant;
use crate::facts::AcceptedLineFingerprint;
use crate::facts::AppInvocation;
use crate::facts::CodexCompactionEvent;
use crate::facts::CodexErrKind;
use crate::facts::CompactionImplementation;
use crate::facts::CompactionPhase;
use crate::facts::CompactionReason;
@@ -793,10 +794,14 @@ pub(crate) struct CodexTurnEventParams {
pub(crate) sandbox_network_access: bool,
pub(crate) collaboration_mode: Option<&'static str>,
pub(crate) personality: Option<String>,
pub(crate) workspace_kind: Option<String>,
pub(crate) num_input_images: usize,
pub(crate) is_first_turn: bool,
pub(crate) status: Option<TurnStatus>,
pub(crate) turn_error: Option<CodexErrorInfo>,
pub(crate) codex_error_kind: Option<CodexErrKind>,
pub(crate) codex_error_subreason: Option<String>,
pub(crate) codex_error_http_status_code: Option<u16>,
pub(crate) steer_count: Option<usize>,
pub(crate) total_tool_call_count: Option<usize>,
pub(crate) shell_command_count: Option<usize>,
@@ -1012,6 +1017,7 @@ fn analytics_hook_source(source: HookSource) -> &'static str {
HookSource::SessionFlags => "session_flags",
HookSource::Plugin => "plugin",
HookSource::CloudRequirements => "cloud_requirements",
HookSource::CloudManagedConfig => "cloud_managed_config",
HookSource::LegacyManagedConfigFile => "legacy_managed_config_file",
HookSource::LegacyManagedConfigMdm => "legacy_managed_config_mdm",
HookSource::Unknown => "unknown",
@@ -1047,9 +1053,7 @@ pub(crate) fn subagent_thread_started_event_request(
thread_source: Some(ThreadSource::Subagent),
initialization_mode: ThreadInitializationMode::New,
subagent_source: Some(subagent_source_name(&input.subagent_source)),
parent_thread_id: input
.parent_thread_id
.or_else(|| subagent_parent_thread_id(&input.subagent_source)),
parent_thread_id: input.parent_thread_id,
created_at: input.created_at,
};
ThreadInitializedEvent {
@@ -1059,22 +1063,7 @@ pub(crate) fn subagent_thread_started_event_request(
}
pub(crate) fn subagent_source_name(subagent_source: &SubAgentSource) -> String {
match subagent_source {
SubAgentSource::Review => "review".to_string(),
SubAgentSource::Compact => "compact".to_string(),
SubAgentSource::ThreadSpawn { .. } => "thread_spawn".to_string(),
SubAgentSource::MemoryConsolidation => "memory_consolidation".to_string(),
SubAgentSource::Other(other) => other.clone(),
}
}
pub(crate) fn subagent_parent_thread_id(subagent_source: &SubAgentSource) -> Option<String> {
match subagent_source {
SubAgentSource::ThreadSpawn {
parent_thread_id, ..
} => Some(parent_thread_id.to_string()),
_ => None,
}
subagent_source.kind().to_string()
}
fn analytics_hook_status(status: HookRunStatus) -> HookRunStatus {

View File

@@ -15,6 +15,7 @@ use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::error::CodexErr;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AskForApproval;
@@ -29,6 +30,9 @@ use codex_protocol::request_permissions::RequestPermissionsResponse;
use serde::Serialize;
use std::path::PathBuf;
const INVALID_REQUEST_SUBREASON_MAX_BYTES: usize = 512;
const INVALID_REQUEST_SUBREASON_TRUNCATION_SUFFIX: &str = "...";
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct AcceptedLineFingerprint {
pub path_hash: String,
@@ -81,6 +85,7 @@ pub struct TurnResolvedConfigFact {
pub sandbox_network_access: bool,
pub collaboration_mode: ModeKind,
pub personality: Option<Personality>,
pub workspace_kind: Option<String>,
pub is_first_turn: bool,
}
@@ -99,6 +104,147 @@ pub struct TurnTokenUsageFact {
pub token_usage: TokenUsage,
}
#[derive(Clone)]
pub struct TurnCodexErrorFact {
pub(crate) turn_id: String,
pub(crate) thread_id: String,
pub(crate) error: TurnCodexError,
}
impl TurnCodexErrorFact {
pub fn from_codex_err(thread_id: String, turn_id: String, error: &CodexErr) -> Self {
Self {
turn_id,
thread_id,
error: TurnCodexError::from_codex_err(error),
}
}
}
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum CodexErrKind {
TurnAborted,
Stream,
ContextWindowExceeded,
ThreadNotFound,
AgentLimitReached,
SessionConfiguredNotFirstEvent,
Timeout,
RequestTimeout,
Spawn,
Interrupted,
UnexpectedStatus,
InvalidRequest,
InvalidImageRequest,
UsageLimitReached,
ServerOverloaded,
CyberPolicy,
ResponseStreamFailed,
ConnectionFailed,
QuotaExceeded,
UsageNotIncluded,
InternalServerError,
RetryLimit,
InternalAgentDied,
Sandbox,
LandlockSandboxExecutableNotProvided,
UnsupportedOperation,
RefreshTokenFailed,
Fatal,
Io,
Json,
#[cfg(target_os = "linux")]
LandlockRuleset,
#[cfg(target_os = "linux")]
LandlockPathFd,
TokioJoin,
EnvVar,
}
#[derive(Clone)]
pub(crate) struct TurnCodexError {
pub(crate) kind: CodexErrKind,
pub(crate) subreason: Option<String>,
pub(crate) http_status_code: Option<u16>,
}
impl TurnCodexError {
fn from_codex_err(error: &CodexErr) -> Self {
Self {
kind: error.into(),
subreason: match error {
CodexErr::InvalidRequest(message) => {
// InvalidRequest can contain raw provider response bodies, so bound the
// analytics copy without changing the source CodexErr.
let subreason = if message.len() <= INVALID_REQUEST_SUBREASON_MAX_BYTES {
message.clone()
} else {
let truncated_len = message.floor_char_boundary(
INVALID_REQUEST_SUBREASON_MAX_BYTES
.saturating_sub(INVALID_REQUEST_SUBREASON_TRUNCATION_SUFFIX.len()),
);
format!(
"{}{INVALID_REQUEST_SUBREASON_TRUNCATION_SUFFIX}",
&message[..truncated_len]
)
};
Some(subreason)
}
_ => None,
},
http_status_code: error.http_status_code_value(),
}
}
}
impl From<&CodexErr> for CodexErrKind {
fn from(error: &CodexErr) -> Self {
match error {
CodexErr::TurnAborted => CodexErrKind::TurnAborted,
CodexErr::Stream(..) => CodexErrKind::Stream,
CodexErr::ContextWindowExceeded => CodexErrKind::ContextWindowExceeded,
CodexErr::ThreadNotFound(_) => CodexErrKind::ThreadNotFound,
CodexErr::AgentLimitReached { .. } => CodexErrKind::AgentLimitReached,
CodexErr::SessionConfiguredNotFirstEvent => {
CodexErrKind::SessionConfiguredNotFirstEvent
}
CodexErr::Timeout => CodexErrKind::Timeout,
CodexErr::RequestTimeout => CodexErrKind::RequestTimeout,
CodexErr::Spawn => CodexErrKind::Spawn,
CodexErr::Interrupted => CodexErrKind::Interrupted,
CodexErr::UnexpectedStatus(_) => CodexErrKind::UnexpectedStatus,
CodexErr::InvalidRequest(_) => CodexErrKind::InvalidRequest,
CodexErr::InvalidImageRequest() => CodexErrKind::InvalidImageRequest,
CodexErr::UsageLimitReached(_) => CodexErrKind::UsageLimitReached,
CodexErr::ServerOverloaded => CodexErrKind::ServerOverloaded,
CodexErr::CyberPolicy { .. } => CodexErrKind::CyberPolicy,
CodexErr::ResponseStreamFailed(_) => CodexErrKind::ResponseStreamFailed,
CodexErr::ConnectionFailed(_) => CodexErrKind::ConnectionFailed,
CodexErr::QuotaExceeded => CodexErrKind::QuotaExceeded,
CodexErr::UsageNotIncluded => CodexErrKind::UsageNotIncluded,
CodexErr::InternalServerError => CodexErrKind::InternalServerError,
CodexErr::RetryLimit(_) => CodexErrKind::RetryLimit,
CodexErr::InternalAgentDied => CodexErrKind::InternalAgentDied,
CodexErr::Sandbox(_) => CodexErrKind::Sandbox,
CodexErr::LandlockSandboxExecutableNotProvided => {
CodexErrKind::LandlockSandboxExecutableNotProvided
}
CodexErr::UnsupportedOperation(_) => CodexErrKind::UnsupportedOperation,
CodexErr::RefreshTokenFailed(_) => CodexErrKind::RefreshTokenFailed,
CodexErr::Fatal(_) => CodexErrKind::Fatal,
CodexErr::Io(_) => CodexErrKind::Io,
CodexErr::Json(_) => CodexErrKind::Json,
#[cfg(target_os = "linux")]
CodexErr::LandlockRuleset(_) => CodexErrKind::LandlockRuleset,
#[cfg(target_os = "linux")]
CodexErr::LandlockPathFd(_) => CodexErrKind::LandlockPathFd,
CodexErr::TokioJoin(_) => CodexErrKind::TokioJoin,
CodexErr::EnvVar(_) => CodexErrKind::EnvVar,
}
}
}
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TurnStatus {
@@ -329,6 +475,7 @@ pub(crate) enum CustomAnalyticsFact {
GuardianReview(Box<GuardianReviewEventParams>),
TurnResolvedConfig(Box<TurnResolvedConfigFact>),
TurnTokenUsage(Box<TurnTokenUsageFact>),
TurnCodexError(Box<TurnCodexErrorFact>),
SkillInvoked(SkillInvokedInput),
AppMentioned(AppMentionedInput),
AppUsed(AppUsedInput),

View File

@@ -38,6 +38,7 @@ pub use facts::SkillInvocation;
pub use facts::SubAgentThreadStartedInput;
pub use facts::ThreadInitializationMode;
pub use facts::TrackEventsContext;
pub use facts::TurnCodexErrorFact;
pub use facts::TurnResolvedConfigFact;
pub use facts::TurnStatus;
pub use facts::TurnSteerRejectionReason;

View File

@@ -55,7 +55,6 @@ use crate::events::codex_hook_run_metadata;
use crate::events::codex_plugin_metadata;
use crate::events::codex_plugin_used_metadata;
use crate::events::plugin_state_event_type;
use crate::events::subagent_parent_thread_id;
use crate::events::subagent_source_name;
use crate::events::subagent_thread_started_event_request;
use crate::facts::AnalyticsFact;
@@ -71,6 +70,8 @@ use crate::facts::PluginUsedInput;
use crate::facts::SkillInvokedInput;
use crate::facts::SubAgentThreadStartedInput;
use crate::facts::ThreadInitializationMode;
use crate::facts::TurnCodexError;
use crate::facts::TurnCodexErrorFact;
use crate::facts::TurnResolvedConfigFact;
use crate::facts::TurnStatus;
use crate::facts::TurnSteerRejectionReason;
@@ -267,20 +268,18 @@ impl ThreadMetadataState {
session_id: String,
session_source: &SessionSource,
thread_source: Option<ThreadSource>,
parent_thread_id: Option<String>,
initialization_mode: ThreadInitializationMode,
) -> Self {
let (subagent_source, parent_thread_id) = match session_source {
SessionSource::SubAgent(subagent_source) => (
Some(subagent_source_name(subagent_source)),
subagent_parent_thread_id(subagent_source),
),
let subagent_source = match session_source {
SessionSource::SubAgent(subagent_source) => Some(subagent_source_name(subagent_source)),
SessionSource::Cli
| SessionSource::VSCode
| SessionSource::Exec
| SessionSource::Mcp
| SessionSource::Custom(_)
| SessionSource::Internal(_)
| SessionSource::Unknown => (None, None),
| SessionSource::Unknown => None,
};
Self {
session_id,
@@ -325,6 +324,7 @@ struct TurnState {
started_at: Option<u64>,
token_usage: Option<TokenUsage>,
completed: Option<CompletedTurnState>,
codex_error: Option<TurnCodexError>,
latest_diff: Option<String>,
steer_count: usize,
tool_counts: TurnToolCounts,
@@ -464,6 +464,9 @@ impl AnalyticsReducer {
CustomAnalyticsFact::TurnTokenUsage(input) => {
self.ingest_turn_token_usage(*input, out).await;
}
CustomAnalyticsFact::TurnCodexError(input) => {
self.ingest_turn_codex_error(*input);
}
CustomAnalyticsFact::SkillInvoked(input) => {
self.ingest_skill_invoked(input, out).await;
}
@@ -516,10 +519,7 @@ impl AnalyticsReducer {
input: SubAgentThreadStartedInput,
out: &mut Vec<TrackEventRequest>,
) {
let parent_thread_id = input
.parent_thread_id
.clone()
.or_else(|| subagent_parent_thread_id(&input.subagent_source));
let parent_thread_id = input.parent_thread_id.clone();
let parent_connection_id = parent_thread_id
.as_ref()
.and_then(|parent_thread_id| self.threads.get(parent_thread_id))
@@ -612,6 +612,7 @@ impl AnalyticsReducer {
started_at: None,
token_usage: None,
completed: None,
codex_error: None,
latest_diff: None,
steer_count: 0,
tool_counts: TurnToolCounts::default(),
@@ -636,6 +637,7 @@ impl AnalyticsReducer {
started_at: None,
token_usage: None,
completed: None,
codex_error: None,
latest_diff: None,
steer_count: 0,
tool_counts: TurnToolCounts::default(),
@@ -645,6 +647,29 @@ impl AnalyticsReducer {
self.maybe_emit_turn_event(&turn_id, out).await;
}
fn ingest_turn_codex_error(&mut self, input: TurnCodexErrorFact) {
let TurnCodexErrorFact {
turn_id,
thread_id,
error,
} = input;
let turn_state = self.turns.entry(turn_id).or_insert(TurnState {
connection_id: None,
thread_id: None,
num_input_images: None,
resolved_config: None,
started_at: None,
token_usage: None,
completed: None,
codex_error: None,
latest_diff: None,
steer_count: 0,
tool_counts: TurnToolCounts::default(),
});
turn_state.thread_id.get_or_insert(thread_id);
turn_state.codex_error = Some(error);
}
async fn ingest_skill_invoked(
&mut self,
input: SkillInvokedInput,
@@ -801,6 +826,7 @@ impl AnalyticsReducer {
started_at: None,
token_usage: None,
completed: None,
codex_error: None,
latest_diff: None,
steer_count: 0,
tool_counts: TurnToolCounts::default(),
@@ -1160,6 +1186,7 @@ impl AnalyticsReducer {
started_at: None,
token_usage: None,
completed: None,
codex_error: None,
latest_diff: None,
steer_count: 0,
tool_counts: TurnToolCounts::default(),
@@ -1181,6 +1208,7 @@ impl AnalyticsReducer {
started_at: None,
token_usage: None,
completed: None,
codex_error: None,
latest_diff: None,
steer_count: 0,
tool_counts: TurnToolCounts::default(),
@@ -1200,6 +1228,7 @@ impl AnalyticsReducer {
started_at: None,
token_usage: None,
completed: None,
codex_error: None,
latest_diff: None,
steer_count: 0,
tool_counts: TurnToolCounts::default(),
@@ -1238,6 +1267,7 @@ impl AnalyticsReducer {
let session_source: SessionSource = thread.source.into();
let session_id = thread.session_id;
let thread_id = thread.id;
let parent_thread_id = thread.parent_thread_id;
let Some(connection_state) = self.connections.get(&connection_id) else {
return;
};
@@ -1245,6 +1275,7 @@ impl AnalyticsReducer {
session_id.clone(),
&session_source,
thread.thread_source.map(Into::into),
parent_thread_id,
initialization_mode,
);
self.threads.insert(
@@ -2452,9 +2483,11 @@ fn codex_turn_event_params(
sandbox_network_access,
collaboration_mode,
personality,
workspace_kind,
is_first_turn,
} = resolved_config;
let token_usage = turn_state.token_usage.clone();
let codex_error = turn_state.codex_error.as_ref();
CodexTurnEventParams {
thread_id,
session_id: thread_metadata.session_id.clone(),
@@ -2483,10 +2516,14 @@ fn codex_turn_event_params(
sandbox_network_access,
collaboration_mode: Some(collaboration_mode_mode(collaboration_mode)),
personality: personality_mode(personality),
workspace_kind,
num_input_images,
is_first_turn,
status: completed.status,
turn_error: completed.turn_error,
codex_error_kind: codex_error.map(|error| error.kind),
codex_error_subreason: codex_error.and_then(|error| error.subreason.clone()),
codex_error_http_status_code: codex_error.and_then(|error| error.http_status_code),
steer_count: Some(turn_state.steer_count),
total_tool_call_count: Some(turn_state.tool_counts.total),
shell_command_count: Some(turn_state.tool_counts.shell_command),

View File

@@ -43,7 +43,7 @@ use codex_app_server_protocol::Result as JsonRpcResult;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerRequest;
use codex_arg0::Arg0DispatchPaths;
use codex_config::CloudRequirementsLoader;
use codex_config::CloudConfigBundleLoader;
use codex_config::LoaderOverrides;
use codex_config::NoopThreadConfigLoader;
use codex_config::RemoteThreadConfigLoader;
@@ -339,8 +339,8 @@ pub struct InProcessClientStartArgs {
pub loader_overrides: LoaderOverrides,
/// Whether config API paths should reject unknown config fields.
pub strict_config: bool,
/// Preloaded cloud requirements provider.
pub cloud_requirements: CloudRequirementsLoader,
/// Preloaded cloud config bundle provider.
pub cloud_config_bundle: CloudConfigBundleLoader,
/// Feedback sink used by app-server/core telemetry and logs.
pub feedback: CodexFeedback,
/// SQLite tracing layer used to flush recently emitted logs before feedback upload.
@@ -406,7 +406,7 @@ impl InProcessClientStartArgs {
cli_overrides: self.cli_overrides,
loader_overrides: self.loader_overrides,
strict_config: self.strict_config,
cloud_requirements: self.cloud_requirements,
cloud_config_bundle: self.cloud_config_bundle,
thread_config_loader,
feedback: self.feedback,
log_db: self.log_db,
@@ -1035,7 +1035,7 @@ mod tests {
cli_overrides: Vec::new(),
loader_overrides: LoaderOverrides::default(),
strict_config: false,
cloud_requirements: CloudRequirementsLoader::default(),
cloud_config_bundle: CloudConfigBundleLoader::default(),
feedback: CodexFeedback::new(),
log_db: None,
state_db: Some(state_db),
@@ -2199,7 +2199,7 @@ mod tests {
cli_overrides: Vec::new(),
loader_overrides: LoaderOverrides::default(),
strict_config: false,
cloud_requirements: CloudRequirementsLoader::default(),
cloud_config_bundle: CloudConfigBundleLoader::default(),
feedback: CodexFeedback::new(),
log_db: None,
state_db: None,
@@ -2240,7 +2240,7 @@ mod tests {
cli_overrides: Vec::new(),
loader_overrides: LoaderOverrides::default(),
strict_config: false,
cloud_requirements: CloudRequirementsLoader::default(),
cloud_config_bundle: CloudConfigBundleLoader::default(),
feedback: CodexFeedback::new(),
log_db: None,
state_db: None,

View File

@@ -1083,6 +1083,24 @@
},
"type": "object"
},
"HttpStateSetParams": {
"properties": {
"expectedState": {
"description": "When present, write only if the calling surface still stores this state.",
"type": [
"string",
"null"
]
},
"state": {
"type": "string"
}
},
"required": [
"state"
],
"type": "object"
},
"ImageDetail": {
"enum": [
"auto",
@@ -2988,6 +3006,20 @@
],
"type": "object"
},
"SkillsExtraRootsSetParams": {
"properties": {
"extraRoots": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
}
},
"required": [
"extraRoots"
],
"type": "object"
},
"SkillsListParams": {
"properties": {
"cwds": {
@@ -3985,6 +4017,12 @@
],
"description": "Override where approval requests are routed for review on this turn and subsequent turns."
},
"clientUserMessageId": {
"type": [
"string",
"null"
]
},
"cwd": {
"description": "Override the working directory for this turn and subsequent turns.",
"type": [
@@ -4071,6 +4109,12 @@
},
"TurnSteerParams": {
"properties": {
"clientUserMessageId": {
"type": [
"string",
"null"
]
},
"expectedTurnId": {
"description": "Required active turn id precondition. The request fails when it does not match the currently active turn.",
"type": "string"
@@ -4771,6 +4815,30 @@
"title": "Skills/listRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"skills/extraRoots/set"
],
"title": "Skills/extraRoots/setRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/SkillsExtraRootsSetParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Skills/extraRoots/setRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -5611,6 +5679,76 @@
"title": "ExperimentalFeature/enablement/setRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"httpState/get"
],
"title": "HttpState/getRequestMethod",
"type": "string"
},
"params": {
"type": "null"
}
},
"required": [
"id",
"method"
],
"title": "HttpState/getRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"httpState/set"
],
"title": "HttpState/setRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/HttpStateSetParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "HttpState/setRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"httpState/clear"
],
"title": "HttpState/clearRequestMethod",
"type": "string"
},
"params": {
"type": "null"
}
},
"required": [
"id",
"method"
],
"title": "HttpState/clearRequest",
"type": "object"
},
{
"properties": {
"id": {

View File

@@ -285,6 +285,13 @@
"cwd": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"environmentId": {
"default": null,
"type": [
"null",
"string"
]
},
"itemId": {
"type": "string"
},
@@ -319,4 +326,4 @@
],
"title": "PermissionsRequestApprovalParams",
"type": "object"
}
}

View File

@@ -29,6 +29,7 @@
"type": "object"
},
"AccountRateLimitsUpdatedNotification": {
"description": "Sparse rolling rate-limit update.\n\nClients should merge available values into the most recent `account/rateLimits/read` response or refetch that snapshot. Nullable account metadata may be unavailable in a rolling update and does not clear a previously observed value.",
"properties": {
"rateLimits": {
"$ref": "#/definitions/RateLimitSnapshot"
@@ -2002,6 +2003,7 @@
"sessionFlags",
"plugin",
"cloudRequirements",
"cloudManagedConfig",
"legacyManagedConfigFile",
"legacyManagedConfigMdm",
"unknown"
@@ -2676,6 +2678,16 @@
}
]
},
"individualLimit": {
"anyOf": [
{
"$ref": "#/definitions/SpendControlLimitSnapshot"
},
{
"type": "null"
}
]
},
"limitId": {
"type": [
"string",
@@ -3134,6 +3146,31 @@
"description": "Notification emitted when watched local skill files change.\n\nTreat this as an invalidation signal and re-run `skills/list` with the client's current parameters when refreshed skill metadata is needed.",
"type": "object"
},
"SpendControlLimitSnapshot": {
"properties": {
"limit": {
"type": "string"
},
"remainingPercent": {
"format": "int32",
"type": "integer"
},
"resetsAt": {
"format": "int64",
"type": "integer"
},
"used": {
"type": "string"
}
},
"required": [
"limit",
"remainingPercent",
"resetsAt",
"used"
],
"type": "object"
},
"SubAgentSource": {
"oneOf": [
{
@@ -3365,6 +3402,13 @@
"null"
]
},
"parentThreadId": {
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
"type": [
"string",
"null"
]
},
"path": {
"description": "[UNSTABLE] Path to the thread on disk.",
"type": [
@@ -3561,6 +3605,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"

View File

@@ -1590,6 +1590,13 @@
"cwd": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"environmentId": {
"default": null,
"type": [
"null",
"string"
]
},
"itemId": {
"type": "string"
},
@@ -1998,4 +2005,4 @@
}
],
"title": "ServerRequest"
}
}

View File

@@ -709,6 +709,30 @@
"title": "Skills/listRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/v2/RequestId"
},
"method": {
"enum": [
"skills/extraRoots/set"
],
"title": "Skills/extraRoots/setRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/SkillsExtraRootsSetParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Skills/extraRoots/setRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -1549,6 +1573,76 @@
"title": "ExperimentalFeature/enablement/setRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/v2/RequestId"
},
"method": {
"enum": [
"httpState/get"
],
"title": "HttpState/getRequestMethod",
"type": "string"
},
"params": {
"type": "null"
}
},
"required": [
"id",
"method"
],
"title": "HttpState/getRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/v2/RequestId"
},
"method": {
"enum": [
"httpState/set"
],
"title": "HttpState/setRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/HttpStateSetParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "HttpState/setRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/v2/RequestId"
},
"method": {
"enum": [
"httpState/clear"
],
"title": "HttpState/clearRequestMethod",
"type": "string"
},
"params": {
"type": "null"
}
},
"required": [
"id",
"method"
],
"title": "HttpState/clearRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -3759,6 +3853,13 @@
"cwd": {
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
"environmentId": {
"default": null,
"type": [
"null",
"string"
]
},
"itemId": {
"type": "string"
},
@@ -5696,6 +5797,7 @@
},
"AccountRateLimitsUpdatedNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Sparse rolling rate-limit update.\n\nClients should merge available values into the most recent `account/rateLimits/read` response or refetch that snapshot. Nullable account metadata may be unavailable in a rolling update and does not clear a previously observed value.",
"properties": {
"rateLimits": {
"$ref": "#/definitions/v2/RateLimitSnapshot"
@@ -5927,6 +6029,16 @@
},
"AppConfig": {
"properties": {
"approvals_reviewer": {
"anyOf": [
{
"$ref": "#/definitions/v2/ApprovalsReviewer"
},
{
"type": "null"
}
]
},
"default_tools_approval_mode": {
"anyOf": [
{
@@ -7579,6 +7691,33 @@
"title": "SystemConfigLayerSource",
"type": "object"
},
{
"description": "Enterprise-managed config layer delivered by the cloud config bundle.",
"properties": {
"id": {
"description": "Stable identifier for the delivered layer.",
"type": "string"
},
"name": {
"description": "Admin-facing name for the delivered layer. This is surfaced in diagnostics so users know which cloud layer needs administrator attention.",
"type": "string"
},
"type": {
"enum": [
"enterpriseManaged"
],
"title": "EnterpriseManagedConfigLayerSourceType",
"type": "string"
}
},
"required": [
"id",
"name",
"type"
],
"title": "EnterpriseManagedConfigLayerSource",
"type": "object"
},
{
"description": "User config layer from $CODEX_HOME/config.toml. This layer is special in that it is expected to be: - writable by the user - generally outside the workspace directory",
"properties": {
@@ -7785,6 +7924,15 @@
"null"
]
},
"allowedWindowsSandboxImplementations": {
"items": {
"$ref": "#/definitions/v2/WindowsSandboxSetupMode"
},
"type": [
"array",
"null"
]
},
"computerUse": {
"anyOf": [
{
@@ -10058,6 +10206,7 @@
"sessionFlags",
"plugin",
"cloudRequirements",
"cloudManagedConfig",
"legacyManagedConfigFile",
"legacyManagedConfigMdm",
"unknown"
@@ -10158,6 +10307,57 @@
"title": "HooksListResponse",
"type": "object"
},
"HttpStateClearResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "HttpStateClearResponse",
"type": "object"
},
"HttpStateGetResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"state": {
"type": [
"string",
"null"
]
}
},
"title": "HttpStateGetResponse",
"type": "object"
},
"HttpStateSetParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"expectedState": {
"description": "When present, write only if the calling surface still stores this state.",
"type": [
"string",
"null"
]
},
"state": {
"type": "string"
}
},
"required": [
"state"
],
"title": "HttpStateSetParams",
"type": "object"
},
"HttpStateSetResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"written": {
"type": "boolean"
}
},
"required": [
"written"
],
"title": "HttpStateSetResponse",
"type": "object"
},
"ImageDetail": {
"enum": [
"auto",
@@ -11845,7 +12045,7 @@
"NetworkUnixSocketPermission": {
"enum": [
"allow",
"none"
"deny"
],
"type": "string"
},
@@ -13266,6 +13466,16 @@
}
]
},
"individualLimit": {
"anyOf": [
{
"$ref": "#/definitions/v2/SpendControlLimitSnapshot"
},
{
"type": "null"
}
]
},
"limitId": {
"type": [
"string",
@@ -15121,6 +15331,27 @@
"title": "SkillsConfigWriteResponse",
"type": "object"
},
"SkillsExtraRootsSetParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"extraRoots": {
"items": {
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
"type": "array"
}
},
"required": [
"extraRoots"
],
"title": "SkillsExtraRootsSetParams",
"type": "object"
},
"SkillsExtraRootsSetResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "SkillsExtraRootsSetResponse",
"type": "object"
},
"SkillsListEntry": {
"properties": {
"cwd": {
@@ -15187,6 +15418,31 @@
],
"type": "string"
},
"SpendControlLimitSnapshot": {
"properties": {
"limit": {
"type": "string"
},
"remainingPercent": {
"format": "int32",
"type": "integer"
},
"resetsAt": {
"format": "int64",
"type": "integer"
},
"used": {
"type": "string"
}
},
"required": [
"limit",
"remainingPercent",
"resetsAt",
"used"
],
"type": "object"
},
"SubAgentSource": {
"oneOf": [
{
@@ -15431,6 +15687,13 @@
"null"
]
},
"parentThreadId": {
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
"type": [
"string",
"null"
]
},
"path": {
"description": "[UNSTABLE] Path to the thread on disk.",
"type": [
@@ -15996,6 +16259,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/v2/UserInput"
@@ -18413,6 +18682,12 @@
],
"description": "Override where approval requests are routed for review on this turn and subsequent turns."
},
"clientUserMessageId": {
"type": [
"string",
"null"
]
},
"cwd": {
"description": "Override the working directory for this turn and subsequent turns.",
"type": [
@@ -18540,6 +18815,12 @@
"TurnSteerParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"clientUserMessageId": {
"type": [
"string",
"null"
]
},
"expectedTurnId": {
"description": "Required active turn id precondition. The request fails when it does not match the currently active turn.",
"type": "string"
@@ -19079,4 +19360,4 @@
},
"title": "CodexAppServerProtocol",
"type": "object"
}
}

View File

@@ -92,6 +92,7 @@
},
"AccountRateLimitsUpdatedNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Sparse rolling rate-limit update.\n\nClients should merge available values into the most recent `account/rateLimits/read` response or refetch that snapshot. Nullable account metadata may be unavailable in a rolling update and does not clear a previously observed value.",
"properties": {
"rateLimits": {
"$ref": "#/definitions/RateLimitSnapshot"
@@ -323,6 +324,16 @@
},
"AppConfig": {
"properties": {
"approvals_reviewer": {
"anyOf": [
{
"$ref": "#/definitions/ApprovalsReviewer"
},
{
"type": "null"
}
]
},
"default_tools_approval_mode": {
"anyOf": [
{
@@ -1457,6 +1468,30 @@
"title": "Skills/listRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"skills/extraRoots/set"
],
"title": "Skills/extraRoots/setRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/SkillsExtraRootsSetParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Skills/extraRoots/setRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -2297,6 +2332,76 @@
"title": "ExperimentalFeature/enablement/setRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"httpState/get"
],
"title": "HttpState/getRequestMethod",
"type": "string"
},
"params": {
"type": "null"
}
},
"required": [
"id",
"method"
],
"title": "HttpState/getRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"httpState/set"
],
"title": "HttpState/setRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/HttpStateSetParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "HttpState/setRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"httpState/clear"
],
"title": "HttpState/clearRequestMethod",
"type": "string"
},
"params": {
"type": "null"
}
},
"required": [
"id",
"method"
],
"title": "HttpState/clearRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -3948,6 +4053,33 @@
"title": "SystemConfigLayerSource",
"type": "object"
},
{
"description": "Enterprise-managed config layer delivered by the cloud config bundle.",
"properties": {
"id": {
"description": "Stable identifier for the delivered layer.",
"type": "string"
},
"name": {
"description": "Admin-facing name for the delivered layer. This is surfaced in diagnostics so users know which cloud layer needs administrator attention.",
"type": "string"
},
"type": {
"enum": [
"enterpriseManaged"
],
"title": "EnterpriseManagedConfigLayerSourceType",
"type": "string"
}
},
"required": [
"id",
"name",
"type"
],
"title": "EnterpriseManagedConfigLayerSource",
"type": "object"
},
{
"description": "User config layer from $CODEX_HOME/config.toml. This layer is special in that it is expected to be: - writable by the user - generally outside the workspace directory",
"properties": {
@@ -4154,6 +4286,15 @@
"null"
]
},
"allowedWindowsSandboxImplementations": {
"items": {
"$ref": "#/definitions/WindowsSandboxSetupMode"
},
"type": [
"array",
"null"
]
},
"computerUse": {
"anyOf": [
{
@@ -6538,6 +6679,7 @@
"sessionFlags",
"plugin",
"cloudRequirements",
"cloudManagedConfig",
"legacyManagedConfigFile",
"legacyManagedConfigMdm",
"unknown"
@@ -6638,6 +6780,57 @@
"title": "HooksListResponse",
"type": "object"
},
"HttpStateClearResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "HttpStateClearResponse",
"type": "object"
},
"HttpStateGetResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"state": {
"type": [
"string",
"null"
]
}
},
"title": "HttpStateGetResponse",
"type": "object"
},
"HttpStateSetParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"expectedState": {
"description": "When present, write only if the calling surface still stores this state.",
"type": [
"string",
"null"
]
},
"state": {
"type": "string"
}
},
"required": [
"state"
],
"title": "HttpStateSetParams",
"type": "object"
},
"HttpStateSetResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"written": {
"type": "boolean"
}
},
"required": [
"written"
],
"title": "HttpStateSetResponse",
"type": "object"
},
"ImageDetail": {
"enum": [
"auto",
@@ -8374,7 +8567,7 @@
"NetworkUnixSocketPermission": {
"enum": [
"allow",
"none"
"deny"
],
"type": "string"
},
@@ -9795,6 +9988,16 @@
}
]
},
"individualLimit": {
"anyOf": [
{
"$ref": "#/definitions/SpendControlLimitSnapshot"
},
{
"type": "null"
}
]
},
"limitId": {
"type": [
"string",
@@ -12945,6 +13148,27 @@
"title": "SkillsConfigWriteResponse",
"type": "object"
},
"SkillsExtraRootsSetParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"extraRoots": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
}
},
"required": [
"extraRoots"
],
"title": "SkillsExtraRootsSetParams",
"type": "object"
},
"SkillsExtraRootsSetResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "SkillsExtraRootsSetResponse",
"type": "object"
},
"SkillsListEntry": {
"properties": {
"cwd": {
@@ -13011,6 +13235,31 @@
],
"type": "string"
},
"SpendControlLimitSnapshot": {
"properties": {
"limit": {
"type": "string"
},
"remainingPercent": {
"format": "int32",
"type": "integer"
},
"resetsAt": {
"format": "int64",
"type": "integer"
},
"used": {
"type": "string"
}
},
"required": [
"limit",
"remainingPercent",
"resetsAt",
"used"
],
"type": "object"
},
"SubAgentSource": {
"oneOf": [
{
@@ -13255,6 +13504,13 @@
"null"
]
},
"parentThreadId": {
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
"type": [
"string",
"null"
]
},
"path": {
"description": "[UNSTABLE] Path to the thread on disk.",
"type": [
@@ -13820,6 +14076,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"
@@ -16237,6 +16499,12 @@
],
"description": "Override where approval requests are routed for review on this turn and subsequent turns."
},
"clientUserMessageId": {
"type": [
"string",
"null"
]
},
"cwd": {
"description": "Override the working directory for this turn and subsequent turns.",
"type": [
@@ -16364,6 +16632,12 @@
"TurnSteerParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"clientUserMessageId": {
"type": [
"string",
"null"
]
},
"expectedTurnId": {
"description": "Required active turn id precondition. The request fails when it does not match the currently active turn.",
"type": "string"

View File

@@ -61,6 +61,16 @@
}
]
},
"individualLimit": {
"anyOf": [
{
"$ref": "#/definitions/SpendControlLimitSnapshot"
},
{
"type": "null"
}
]
},
"limitId": {
"type": [
"string",
@@ -141,8 +151,34 @@
"usedPercent"
],
"type": "object"
},
"SpendControlLimitSnapshot": {
"properties": {
"limit": {
"type": "string"
},
"remainingPercent": {
"format": "int32",
"type": "integer"
},
"resetsAt": {
"format": "int64",
"type": "integer"
},
"used": {
"type": "string"
}
},
"required": [
"limit",
"remainingPercent",
"resetsAt",
"used"
],
"type": "object"
}
},
"description": "Sparse rolling rate-limit update.\n\nClients should merge available values into the most recent `account/rateLimits/read` response or refetch that snapshot. Nullable account metadata may be unavailable in a rolling update and does not clear a previously observed value.",
"properties": {
"rateLimits": {
"$ref": "#/definitions/RateLimitSnapshot"

View File

@@ -19,6 +19,16 @@
},
"AppConfig": {
"properties": {
"approvals_reviewer": {
"anyOf": [
{
"$ref": "#/definitions/ApprovalsReviewer"
},
{
"type": "null"
}
]
},
"default_tools_approval_mode": {
"anyOf": [
{
@@ -498,6 +508,33 @@
"title": "SystemConfigLayerSource",
"type": "object"
},
{
"description": "Enterprise-managed config layer delivered by the cloud config bundle.",
"properties": {
"id": {
"description": "Stable identifier for the delivered layer.",
"type": "string"
},
"name": {
"description": "Admin-facing name for the delivered layer. This is surfaced in diagnostics so users know which cloud layer needs administrator attention.",
"type": "string"
},
"type": {
"enum": [
"enterpriseManaged"
],
"title": "EnterpriseManagedConfigLayerSourceType",
"type": "string"
}
},
"required": [
"id",
"name",
"type"
],
"title": "EnterpriseManagedConfigLayerSource",
"type": "object"
},
{
"description": "User config layer from $CODEX_HOME/config.toml. This layer is special in that it is expected to be: - writable by the user - generally outside the workspace directory",
"properties": {

View File

@@ -121,6 +121,15 @@
"null"
]
},
"allowedWindowsSandboxImplementations": {
"items": {
"$ref": "#/definitions/WindowsSandboxSetupMode"
},
"type": [
"array",
"null"
]
},
"computerUse": {
"anyOf": [
{
@@ -460,7 +469,7 @@
"NetworkUnixSocketPermission": {
"enum": [
"allow",
"none"
"deny"
],
"type": "string"
},
@@ -485,6 +494,13 @@
"live"
],
"type": "string"
},
"WindowsSandboxSetupMode": {
"enum": [
"elevated",
"unelevated"
],
"type": "string"
}
},
"properties": {

View File

@@ -73,6 +73,33 @@
"title": "SystemConfigLayerSource",
"type": "object"
},
{
"description": "Enterprise-managed config layer delivered by the cloud config bundle.",
"properties": {
"id": {
"description": "Stable identifier for the delivered layer.",
"type": "string"
},
"name": {
"description": "Admin-facing name for the delivered layer. This is surfaced in diagnostics so users know which cloud layer needs administrator attention.",
"type": "string"
},
"type": {
"enum": [
"enterpriseManaged"
],
"title": "EnterpriseManagedConfigLayerSourceType",
"type": "string"
}
},
"required": [
"id",
"name",
"type"
],
"title": "EnterpriseManagedConfigLayerSource",
"type": "object"
},
{
"description": "User config layer from $CODEX_HOME/config.toml. This layer is special in that it is expected to be: - writable by the user - generally outside the workspace directory",
"properties": {

View File

@@ -61,6 +61,16 @@
}
]
},
"individualLimit": {
"anyOf": [
{
"$ref": "#/definitions/SpendControlLimitSnapshot"
},
{
"type": "null"
}
]
},
"limitId": {
"type": [
"string",
@@ -141,6 +151,31 @@
"usedPercent"
],
"type": "object"
},
"SpendControlLimitSnapshot": {
"properties": {
"limit": {
"type": "string"
},
"remainingPercent": {
"format": "int32",
"type": "integer"
},
"resetsAt": {
"format": "int64",
"type": "integer"
},
"used": {
"type": "string"
}
},
"required": [
"limit",
"remainingPercent",
"resetsAt",
"used"
],
"type": "object"
}
},
"properties": {

View File

@@ -166,6 +166,7 @@
"sessionFlags",
"plugin",
"cloudRequirements",
"cloudManagedConfig",
"legacyManagedConfigFile",
"legacyManagedConfigMdm",
"unknown"

View File

@@ -166,6 +166,7 @@
"sessionFlags",
"plugin",
"cloudRequirements",
"cloudManagedConfig",
"legacyManagedConfigFile",
"legacyManagedConfigMdm",
"unknown"

View File

@@ -130,6 +130,7 @@
"sessionFlags",
"plugin",
"cloudRequirements",
"cloudManagedConfig",
"legacyManagedConfigFile",
"legacyManagedConfigMdm",
"unknown"

View File

@@ -0,0 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "HttpStateClearResponse",
"type": "object"
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"state": {
"type": [
"string",
"null"
]
}
},
"title": "HttpStateGetResponse",
"type": "object"
}

View File

@@ -0,0 +1,20 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"expectedState": {
"description": "When present, write only if the calling surface still stores this state.",
"type": [
"string",
"null"
]
},
"state": {
"type": "string"
}
},
"required": [
"state"
],
"title": "HttpStateSetParams",
"type": "object"
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"written": {
"type": "boolean"
}
},
"required": [
"written"
],
"title": "HttpStateSetResponse",
"type": "object"
}

View File

@@ -500,6 +500,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"

View File

@@ -500,6 +500,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"

View File

@@ -644,6 +644,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"

View File

@@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"AbsolutePathBuf": {
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
}
},
"properties": {
"extraRoots": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
}
},
"required": [
"extraRoots"
],
"title": "SkillsExtraRootsSetParams",
"type": "object"
}

View File

@@ -0,0 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "SkillsExtraRootsSetResponse",
"type": "object"
}

View File

@@ -1036,6 +1036,13 @@
"null"
]
},
"parentThreadId": {
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
"type": [
"string",
"null"
]
},
"path": {
"description": "[UNSTABLE] Path to the thread on disk.",
"type": [
@@ -1121,6 +1128,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"

View File

@@ -851,6 +851,13 @@
"null"
]
},
"parentThreadId": {
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
"type": [
"string",
"null"
]
},
"path": {
"description": "[UNSTABLE] Path to the thread on disk.",
"type": [
@@ -936,6 +943,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"

View File

@@ -851,6 +851,13 @@
"null"
]
},
"parentThreadId": {
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
"type": [
"string",
"null"
]
},
"path": {
"description": "[UNSTABLE] Path to the thread on disk.",
"type": [
@@ -936,6 +943,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"

View File

@@ -851,6 +851,13 @@
"null"
]
},
"parentThreadId": {
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
"type": [
"string",
"null"
]
},
"path": {
"description": "[UNSTABLE] Path to the thread on disk.",
"type": [
@@ -936,6 +943,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"

View File

@@ -1036,6 +1036,13 @@
"null"
]
},
"parentThreadId": {
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
"type": [
"string",
"null"
]
},
"path": {
"description": "[UNSTABLE] Path to the thread on disk.",
"type": [
@@ -1121,6 +1128,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"

View File

@@ -851,6 +851,13 @@
"null"
]
},
"parentThreadId": {
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
"type": [
"string",
"null"
]
},
"path": {
"description": "[UNSTABLE] Path to the thread on disk.",
"type": [
@@ -936,6 +943,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"

View File

@@ -1036,6 +1036,13 @@
"null"
]
},
"parentThreadId": {
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
"type": [
"string",
"null"
]
},
"path": {
"description": "[UNSTABLE] Path to the thread on disk.",
"type": [
@@ -1121,6 +1128,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"

View File

@@ -851,6 +851,13 @@
"null"
]
},
"parentThreadId": {
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
"type": [
"string",
"null"
]
},
"path": {
"description": "[UNSTABLE] Path to the thread on disk.",
"type": [
@@ -936,6 +943,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"

View File

@@ -851,6 +851,13 @@
"null"
]
},
"parentThreadId": {
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
"type": [
"string",
"null"
]
},
"path": {
"description": "[UNSTABLE] Path to the thread on disk.",
"type": [
@@ -936,6 +943,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"

View File

@@ -644,6 +644,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"

View File

@@ -516,6 +516,12 @@
],
"description": "Override where approval requests are routed for review on this turn and subsequent turns."
},
"clientUserMessageId": {
"type": [
"string",
"null"
]
},
"cwd": {
"description": "Override the working directory for this turn and subsequent turns.",
"type": [

View File

@@ -644,6 +644,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"

View File

@@ -644,6 +644,12 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"

View File

@@ -218,6 +218,12 @@
}
},
"properties": {
"clientUserMessageId": {
"type": [
"string",
"null"
]
},
"expectedTurnId": {
"description": "Required active turn id precondition. The request fails when it does not match the currently active turn.",
"type": "string"

File diff suppressed because one or more lines are too long

View File

@@ -3,4 +3,11 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { RateLimitSnapshot } from "./RateLimitSnapshot";
/**
* Sparse rolling rate-limit update.
*
* Clients should merge available values into the most recent `account/rateLimits/read` response
* or refetch that snapshot. Nullable account metadata may be unavailable in a rolling update and
* does not clear a previously observed value.
*/
export type AccountRateLimitsUpdatedNotification = { rateLimits: RateLimitSnapshot, };

View File

@@ -3,6 +3,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AppToolApproval } from "./AppToolApproval";
import type { AppToolsConfig } from "./AppToolsConfig";
import type { ApprovalsReviewer } from "./ApprovalsReviewer";
import type { AppsDefaultConfig } from "./AppsDefaultConfig";
export type AppsConfig = { _default: AppsDefaultConfig | null, } & ({ [key in string]?: { enabled: boolean, destructive_enabled: boolean | null, open_world_enabled: boolean | null, default_tools_approval_mode: AppToolApproval | null, default_tools_enabled: boolean | null, tools: AppToolsConfig | null, } });
export type AppsConfig = { _default: AppsDefaultConfig | null, } & ({ [key in string]?: { enabled: boolean, approvals_reviewer: ApprovalsReviewer | null, destructive_enabled: boolean | null, open_world_enabled: boolean | null, default_tools_approval_mode: AppToolApproval | null, default_tools_enabled: boolean | null, tools: AppToolsConfig | null, } });

View File

@@ -8,7 +8,17 @@ export type ConfigLayerSource = { "type": "mdm", domain: string, key: string, }
* This is the path to the system config.toml file, though it is not
* guaranteed to exist.
*/
file: AbsolutePathBuf, } | { "type": "user",
file: AbsolutePathBuf, } | { "type": "enterpriseManaged",
/**
* Stable identifier for the delivered layer.
*/
id: string,
/**
* Admin-facing name for the delivered layer. This is surfaced in
* diagnostics so users know which cloud layer needs administrator
* attention.
*/
name: string, } | { "type": "user",
/**
* This is the path to the user's config.toml file, though it is not
* guaranteed to exist.

View File

@@ -6,5 +6,6 @@ import type { AskForApproval } from "./AskForApproval";
import type { ComputerUseRequirements } from "./ComputerUseRequirements";
import type { ResidencyRequirement } from "./ResidencyRequirement";
import type { SandboxMode } from "./SandboxMode";
import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode";
export type ConfigRequirements = {allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedPermissions: Array<string> | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null};
export type ConfigRequirements = {allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWindowsSandboxImplementations: Array<WindowsSandboxSetupMode> | null, allowedPermissions: Array<string> | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null};

View File

@@ -2,4 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type HookSource = "system" | "user" | "project" | "mdm" | "sessionFlags" | "plugin" | "cloudRequirements" | "legacyManagedConfigFile" | "legacyManagedConfigMdm" | "unknown";
export type HookSource = "system" | "user" | "project" | "mdm" | "sessionFlags" | "plugin" | "cloudRequirements" | "cloudManagedConfig" | "legacyManagedConfigFile" | "legacyManagedConfigMdm" | "unknown";

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type HttpStateClearResponse = Record<string, never>;

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type HttpStateGetResponse = { state: string | null, };

View File

@@ -0,0 +1,9 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type HttpStateSetParams = { state: string,
/**
* When present, write only if the calling surface still stores this state.
*/
expectedState?: string | null, };

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type HttpStateSetResponse = { written: boolean, };

View File

@@ -2,4 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type NetworkUnixSocketPermission = "allow" | "none";
export type NetworkUnixSocketPermission = "allow" | "deny";

View File

@@ -4,7 +4,7 @@
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
import type { RequestPermissionProfile } from "./RequestPermissionProfile";
export type PermissionsRequestApprovalParams = { threadId: string, turnId: string, itemId: string,
export type PermissionsRequestApprovalParams = { threadId: string, turnId: string, itemId: string, environmentId: string | null,
/**
* Unix timestamp (in milliseconds) when this approval request started.
*/

View File

@@ -5,5 +5,6 @@ import type { PlanType } from "../PlanType";
import type { CreditsSnapshot } from "./CreditsSnapshot";
import type { RateLimitReachedType } from "./RateLimitReachedType";
import type { RateLimitWindow } from "./RateLimitWindow";
import type { SpendControlLimitSnapshot } from "./SpendControlLimitSnapshot";
export type RateLimitSnapshot = { limitId: string | null, limitName: string | null, primary: RateLimitWindow | null, secondary: RateLimitWindow | null, credits: CreditsSnapshot | null, planType: PlanType | null, rateLimitReachedType: RateLimitReachedType | null, };
export type RateLimitSnapshot = { limitId: string | null, limitName: string | null, primary: RateLimitWindow | null, secondary: RateLimitWindow | null, credits: CreditsSnapshot | null, individualLimit: SpendControlLimitSnapshot | null, planType: PlanType | null, rateLimitReachedType: RateLimitReachedType | null, };

View File

@@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
export type SkillsExtraRootsSetParams = { extraRoots: Array<AbsolutePathBuf>, };

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type SkillsExtraRootsSetResponse = Record<string, never>;

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type SpendControlLimitSnapshot = { limit: string, used: string, remainingPercent: number, resetsAt: number, };

View File

@@ -17,6 +17,10 @@ sessionId: string,
* Source thread id when this thread was created by forking another thread.
*/
forkedFromId: string | null,
/**
* The ID of the parent thread. This will only be set if this thread is a subagent.
*/
parentThreadId: string | null,
/**
* Usually the first user message in the thread, if available.
*/

View File

@@ -23,7 +23,7 @@ import type { PatchApplyStatus } from "./PatchApplyStatus";
import type { UserInput } from "./UserInput";
import type { WebSearchAction } from "./WebSearchAction";
export type ThreadItem = { "type": "userMessage", id: string, content: Array<UserInput>, } | { "type": "hookPrompt", id: string, fragments: Array<HookPromptFragment>, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, memoryCitation: MemoryCitation | null, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array<string>, content: Array<string>, } | { "type": "commandExecution", id: string,
export type ThreadItem = { "type": "userMessage", id: string, clientId: string | null, content: Array<UserInput>, } | { "type": "hookPrompt", id: string, fragments: Array<HookPromptFragment>, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, memoryCitation: MemoryCitation | null, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array<string>, content: Array<string>, } | { "type": "commandExecution", id: string,
/**
* The command to be executed.
*/

View File

@@ -10,7 +10,7 @@ import type { AskForApproval } from "./AskForApproval";
import type { SandboxPolicy } from "./SandboxPolicy";
import type { UserInput } from "./UserInput";
export type TurnStartParams = {threadId: string, input: Array<UserInput>, /**
export type TurnStartParams = {threadId: string, clientUserMessageId?: string | null, input: Array<UserInput>, /**
* Override the working directory for this turn and subsequent turns.
*/
cwd?: string | null, /**

View File

@@ -3,7 +3,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { UserInput } from "./UserInput";
export type TurnSteerParams = {threadId: string, input: Array<UserInput>, /**
export type TurnSteerParams = {threadId: string, clientUserMessageId?: string | null, input: Array<UserInput>, /**
* Required active turn id precondition. The request fails when it does not
* match the currently active turn.
*/

View File

@@ -167,6 +167,10 @@ export type { HookTrustStatus } from "./HookTrustStatus";
export type { HooksListEntry } from "./HooksListEntry";
export type { HooksListParams } from "./HooksListParams";
export type { HooksListResponse } from "./HooksListResponse";
export type { HttpStateClearResponse } from "./HttpStateClearResponse";
export type { HttpStateGetResponse } from "./HttpStateGetResponse";
export type { HttpStateSetParams } from "./HttpStateSetParams";
export type { HttpStateSetResponse } from "./HttpStateSetResponse";
export type { ItemCompletedNotification } from "./ItemCompletedNotification";
export type { ItemGuardianApprovalReviewCompletedNotification } from "./ItemGuardianApprovalReviewCompletedNotification";
export type { ItemGuardianApprovalReviewStartedNotification } from "./ItemGuardianApprovalReviewStartedNotification";
@@ -344,10 +348,13 @@ export type { SkillToolDependency } from "./SkillToolDependency";
export type { SkillsChangedNotification } from "./SkillsChangedNotification";
export type { SkillsConfigWriteParams } from "./SkillsConfigWriteParams";
export type { SkillsConfigWriteResponse } from "./SkillsConfigWriteResponse";
export type { SkillsExtraRootsSetParams } from "./SkillsExtraRootsSetParams";
export type { SkillsExtraRootsSetResponse } from "./SkillsExtraRootsSetResponse";
export type { SkillsListEntry } from "./SkillsListEntry";
export type { SkillsListParams } from "./SkillsListParams";
export type { SkillsListResponse } from "./SkillsListResponse";
export type { SortDirection } from "./SortDirection";
export type { SpendControlLimitSnapshot } from "./SpendControlLimitSnapshot";
export type { SubagentMigration } from "./SubagentMigration";
export type { TerminalInteractionNotification } from "./TerminalInteractionNotification";
export type { TextElement } from "./TextElement";

View File

@@ -39,6 +39,8 @@ use ts_rs::TS;
pub(crate) const GENERATED_TS_HEADER: &str = "// GENERATED CODE! DO NOT MODIFY BY HAND!\n\n";
const IGNORED_DEFINITIONS: &[&str] = &["Option<()>"];
const JSON_V1_ALLOWLIST: &[&str] = &["InitializeParams", "InitializeResponse"];
const EXPERIMENTAL_CLIENT_METHOD_DEPENDENCY_TYPES: &[&str] =
&["RemoteControlClient", "RemoteControlClientsListOrder"];
const SPECIAL_DEFINITIONS: &[&str] = &[
"ClientNotification",
"ClientRequest",
@@ -554,6 +556,7 @@ fn experimental_method_types() -> HashSet<String> {
let mut type_names = HashSet::new();
collect_experimental_type_names(EXPERIMENTAL_CLIENT_METHOD_PARAM_TYPES, &mut type_names);
collect_experimental_type_names(EXPERIMENTAL_CLIENT_METHOD_RESPONSE_TYPES, &mut type_names);
collect_experimental_type_names(EXPERIMENTAL_CLIENT_METHOD_DEPENDENCY_TYPES, &mut type_names);
type_names
}
@@ -2132,6 +2135,14 @@ mod tests {
fixture_tree.contains_key(Path::new("v2/MockExperimentalMethodResponse.ts")),
false
);
assert_eq!(
fixture_tree.contains_key(Path::new("v2/RemoteControlClient.ts")),
false
);
assert_eq!(
fixture_tree.contains_key(Path::new("v2/RemoteControlClientsListOrder.ts")),
false
);
let mut undefined_offenders = Vec::new();
let mut optional_nullable_offenders = BTreeSet::new();
@@ -2847,6 +2858,11 @@ permissionProfile?: string | null};
flat_v2_bundle_json.contains("MockExperimentalMethodResponse"),
false
);
assert_eq!(flat_v2_bundle_json.contains("RemoteControlClient"), false);
assert_eq!(
flat_v2_bundle_json.contains("RemoteControlClientsListOrder"),
false
);
assert_eq!(flat_v2_bundle_json.contains("#/definitions/v2/"), false);
assert_eq!(
flat_v2_bundle_json.contains("\"title\": \"CodexAppServerProtocolV2\""),
@@ -2920,6 +2936,45 @@ permissionProfile?: string | null};
.exists(),
false
);
assert_eq!(
output_dir
.join("v2")
.join("RemoteControlClient.json")
.exists(),
false
);
assert_eq!(
output_dir
.join("v2")
.join("RemoteControlClientsListOrder.json")
.exists(),
false
);
let _cleanup = fs::remove_dir_all(&output_dir);
Ok(())
}
#[test]
fn generate_json_includes_remote_control_methods_with_experimental_api() -> Result<()> {
let output_dir = std::env::temp_dir().join(format!("codex_schema_{}", Uuid::now_v7()));
fs::create_dir(&output_dir)?;
generate_json_with_experimental(&output_dir, /*experimental_api*/ true)?;
let client_request_json = fs::read_to_string(output_dir.join("ClientRequest.json"))?;
assert!(client_request_json.contains("remoteControl/pairing/start"));
assert!(client_request_json.contains("remoteControl/client/list"));
assert!(client_request_json.contains("remoteControl/client/revoke"));
for schema in [
"RemoteControlPairingStartParams.json",
"RemoteControlPairingStartResponse.json",
"RemoteControlClientsListParams.json",
"RemoteControlClientsListResponse.json",
"RemoteControlClientsRevokeParams.json",
"RemoteControlClientsRevokeResponse.json",
] {
assert!(output_dir.join("v2").join(schema).exists());
}
let _cleanup = fs::remove_dir_all(&output_dir);
Ok(())

View File

@@ -610,6 +610,11 @@ client_request_definitions! {
serialization: global_shared_read("config"),
response: v2::SkillsListResponse,
},
SkillsExtraRootsSet => "skills/extraRoots/set" {
params: v2::SkillsExtraRootsSetParams,
serialization: global("config"),
response: v2::SkillsExtraRootsSetResponse,
},
HooksList => "hooks/list" {
params: v2::HooksListParams,
serialization: global("config"),
@@ -838,6 +843,39 @@ client_request_definitions! {
serialization: global_shared_read("remote-control"),
response: v2::RemoteControlStatusReadResponse,
},
#[experimental("remoteControl/pairing/start")]
RemoteControlPairingStart => "remoteControl/pairing/start" {
params: v2::RemoteControlPairingStartParams,
serialization: global("remote-control-pairing"),
response: v2::RemoteControlPairingStartResponse,
},
#[experimental("remoteControl/client/list")]
RemoteControlClientsList => "remoteControl/client/list" {
params: v2::RemoteControlClientsListParams,
serialization: global_shared_read("remote-control-clients"),
response: v2::RemoteControlClientsListResponse,
},
#[experimental("remoteControl/client/revoke")]
RemoteControlClientsRevoke => "remoteControl/client/revoke" {
params: v2::RemoteControlClientsRevokeParams,
serialization: global("remote-control-clients"),
response: v2::RemoteControlClientsRevokeResponse,
},
HttpStateGet => "httpState/get" {
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
serialization: global_shared_read("http-state"),
response: v2::HttpStateGetResponse,
},
HttpStateSet => "httpState/set" {
params: v2::HttpStateSetParams,
serialization: global("http-state"),
response: v2::HttpStateSetResponse,
},
HttpStateClear => "httpState/clear" {
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
serialization: global("http-state"),
response: v2::HttpStateClearResponse,
},
#[experimental("collaborationMode/list")]
/// Lists collaboration mode presets.
CollaborationModeList => "collaborationMode/list" {
@@ -1721,6 +1759,17 @@ mod tests {
Some(ClientRequestSerializationScope::GlobalSharedRead("config"))
);
let skills_extra_roots_set = ClientRequest::SkillsExtraRootsSet {
request_id: request_id(),
params: v2::SkillsExtraRootsSetParams {
extra_roots: vec![absolute_path("/tmp/skills")],
},
};
assert_eq!(
skills_extra_roots_set.serialization_scope(),
Some(ClientRequestSerializationScope::Global("config"))
);
let plugin_list = ClientRequest::PluginList {
request_id: request_id(),
params: v2::PluginListParams {
@@ -1961,6 +2010,40 @@ mod tests {
},
};
assert_eq!(mcp_resource_read.serialization_scope(), None);
let remote_control_pairing_start = ClientRequest::RemoteControlPairingStart {
request_id: request_id(),
params: v2::RemoteControlPairingStartParams::default(),
};
assert_eq!(
remote_control_pairing_start.serialization_scope(),
Some(ClientRequestSerializationScope::Global(
"remote-control-pairing"
))
);
let remote_control_clients_list = ClientRequest::RemoteControlClientsList {
request_id: request_id(),
params: v2::RemoteControlClientsListParams::default(),
};
assert_eq!(
remote_control_clients_list.serialization_scope(),
Some(ClientRequestSerializationScope::GlobalSharedRead(
"remote-control-clients"
))
);
let remote_control_clients_revoke = ClientRequest::RemoteControlClientsRevoke {
request_id: request_id(),
params: v2::RemoteControlClientsRevokeParams {
environment_id: "environment-id".to_string(),
client_id: "client-id".to_string(),
},
};
assert_eq!(
remote_control_clients_revoke.serialization_scope(),
Some(ClientRequestSerializationScope::Global(
"remote-control-clients"
))
);
}
#[test]
@@ -2312,6 +2395,7 @@ mod tests {
id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(),
session_id: "67e55044-10b1-426f-9247-bb680e5fe0c7".to_string(),
forked_from_id: None,
parent_thread_id: None,
preview: "first prompt".to_string(),
ephemeral: true,
model_provider: "openai".to_string(),
@@ -2354,6 +2438,7 @@ mod tests {
"id": "67e55044-10b1-426f-9247-bb680e5fe0c8",
"sessionId": "67e55044-10b1-426f-9247-bb680e5fe0c7",
"forkedFromId": null,
"parentThreadId": null,
"preview": "first prompt",
"ephemeral": true,
"modelProvider": "openai",

View File

@@ -278,7 +278,11 @@ impl ThreadHistoryBuilder {
.unwrap_or_else(|| self.new_turn(/*id*/ None));
let id = self.next_item_id();
let content = self.build_user_inputs(payload);
turn.items.push(ThreadItem::UserMessage { id, content });
turn.items.push(ThreadItem::UserMessage {
id,
client_id: payload.client_id.clone(),
content,
});
self.current_turn = Some(turn);
}
@@ -1246,6 +1250,7 @@ mod tests {
fn builds_multiple_turns_with_reasoning_items() {
let events = vec![
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "First turn".into(),
images: Some(vec!["https://example.com/one.png".into()]),
text_elements: Vec::new(),
@@ -1264,6 +1269,7 @@ mod tests {
text: "full reasoning".into(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "Second turn".into(),
images: None,
text_elements: Vec::new(),
@@ -1292,6 +1298,7 @@ mod tests {
first.items[0],
ThreadItem::UserMessage {
id: "item-1".into(),
client_id: None,
content: vec![
UserInput::Text {
text: "First turn".into(),
@@ -1330,6 +1337,7 @@ mod tests {
second.items[0],
ThreadItem::UserMessage {
id: "item-4".into(),
client_id: None,
content: vec![UserInput::Text {
text: "Second turn".into(),
text_elements: Vec::new(),
@@ -1352,6 +1360,7 @@ mod tests {
let local_path = PathBuf::from("/tmp/local.png");
let events = vec![RolloutItem::EventMsg(EventMsg::UserMessage(
UserMessageEvent {
client_id: None,
message: "inspect these".into(),
images: Some(vec!["https://example.com/image.png".into()]),
image_details: vec![Some(ImageDetail::Original)],
@@ -1368,6 +1377,7 @@ mod tests {
turns[0].items[0],
ThreadItem::UserMessage {
id: "item-1".into(),
client_id: None,
content: vec![
UserInput::Text {
text: "inspect these".into(),
@@ -1399,6 +1409,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "hello".into(),
images: None,
text_elements: Vec::new(),
@@ -1410,6 +1421,7 @@ mod tests {
turn_id: turn_id.to_string(),
item: CoreTurnItem::UserMessage(CoreUserMessageItem {
id: "user-item-id".to_string(),
client_id: None,
content: Vec::new(),
}),
started_at_ms: 0,
@@ -1434,6 +1446,7 @@ mod tests {
turns[0].items[0],
ThreadItem::UserMessage {
id: "item-1".into(),
client_id: None,
content: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
@@ -1442,6 +1455,67 @@ mod tests {
);
}
#[test]
fn preserves_user_message_client_id_from_legacy_event() {
let turn_id = "turn-1";
let thread_id = ThreadId::new();
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: turn_id.to_string(),
trace_id: None,
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
EventMsg::ItemStarted(ItemStartedEvent {
thread_id,
turn_id: turn_id.to_string(),
item: CoreTurnItem::UserMessage(CoreUserMessageItem {
id: "user-item-id".to_string(),
client_id: Some("client-message-1".to_string()),
content: vec![codex_protocol::user_input::UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
}),
started_at_ms: 0,
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: Some("client-message-1".to_string()),
message: "hello".into(),
images: None,
text_elements: Vec::new(),
local_images: Vec::new(),
..Default::default()
}),
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: turn_id.to_string(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
}),
];
let items = events
.into_iter()
.map(RolloutItem::EventMsg)
.collect::<Vec<_>>();
let turns = build_turns_from_rollout_items(&items);
assert_eq!(turns.len(), 1);
assert_eq!(
turns[0].items,
vec![ThreadItem::UserMessage {
id: "item-1".into(),
client_id: Some("client-message-1".to_string()),
content: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
}]
);
}
#[test]
fn preserves_agent_message_phase_in_history() {
let events = vec![EventMsg::AgentMessage(AgentMessageEvent {
@@ -1478,6 +1552,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
})),
RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "generate an image".into(),
images: None,
text_elements: Vec::new(),
@@ -1515,6 +1590,7 @@ mod tests {
items: vec![
ThreadItem::UserMessage {
id: "item-1".into(),
client_id: None,
content: vec![UserInput::Text {
text: "generate an image".into(),
text_elements: Vec::new(),
@@ -1536,6 +1612,7 @@ mod tests {
fn splits_reasoning_when_interleaved() {
let events = vec![
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "Turn start".into(),
images: None,
text_elements: Vec::new(),
@@ -1589,6 +1666,7 @@ mod tests {
fn marks_turn_as_interrupted_when_aborted() {
let events = vec![
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "Please do the thing".into(),
images: None,
text_elements: Vec::new(),
@@ -1607,6 +1685,7 @@ mod tests {
duration_ms: None,
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "Let's try again".into(),
images: None,
text_elements: Vec::new(),
@@ -1634,6 +1713,7 @@ mod tests {
first_turn.items[0],
ThreadItem::UserMessage {
id: "item-1".into(),
client_id: None,
content: vec![UserInput::Text {
text: "Please do the thing".into(),
text_elements: Vec::new(),
@@ -1657,6 +1737,7 @@ mod tests {
second_turn.items[0],
ThreadItem::UserMessage {
id: "item-3".into(),
client_id: None,
content: vec![UserInput::Text {
text: "Let's try again".into(),
text_elements: Vec::new(),
@@ -1678,6 +1759,7 @@ mod tests {
fn drops_last_turns_on_thread_rollback() {
let events = vec![
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "First".into(),
images: None,
text_elements: Vec::new(),
@@ -1690,6 +1772,7 @@ mod tests {
memory_citation: None,
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "Second".into(),
images: None,
text_elements: Vec::new(),
@@ -1703,6 +1786,7 @@ mod tests {
}),
EventMsg::ThreadRolledBack(ThreadRolledBackEvent { num_turns: 1 }),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "Third".into(),
images: None,
text_elements: Vec::new(),
@@ -1732,6 +1816,7 @@ mod tests {
vec![
ThreadItem::UserMessage {
id: "item-1".into(),
client_id: None,
content: vec![UserInput::Text {
text: "First".into(),
text_elements: Vec::new(),
@@ -1750,6 +1835,7 @@ mod tests {
vec![
ThreadItem::UserMessage {
id: "item-3".into(),
client_id: None,
content: vec![UserInput::Text {
text: "Third".into(),
text_elements: Vec::new(),
@@ -1769,6 +1855,7 @@ mod tests {
fn thread_rollback_clears_all_turns_when_num_turns_exceeds_history() {
let events = vec![
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "One".into(),
images: None,
text_elements: Vec::new(),
@@ -1781,6 +1868,7 @@ mod tests {
memory_citation: None,
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "Two".into(),
images: None,
text_elements: Vec::new(),
@@ -1814,6 +1902,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "Start".into(),
images: None,
text_elements: Vec::new(),
@@ -1821,6 +1910,7 @@ mod tests {
..Default::default()
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "Steer".into(),
images: None,
text_elements: Vec::new(),
@@ -1848,6 +1938,7 @@ mod tests {
vec![
ThreadItem::UserMessage {
id: "item-1".into(),
client_id: None,
content: vec![UserInput::Text {
text: "Start".into(),
text_elements: Vec::new(),
@@ -1855,6 +1946,7 @@ mod tests {
},
ThreadItem::UserMessage {
id: "item-2".into(),
client_id: None,
content: vec![UserInput::Text {
text: "Steer".into(),
text_elements: Vec::new(),
@@ -1875,6 +1967,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "run tools".into(),
images: None,
text_elements: Vec::new(),
@@ -2054,6 +2147,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "run dynamic tool".into(),
images: None,
text_elements: Vec::new(),
@@ -2121,6 +2215,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "run tools".into(),
images: None,
text_elements: Vec::new(),
@@ -2212,6 +2307,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "review this command".into(),
images: None,
text_elements: Vec::new(),
@@ -2297,6 +2393,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "run a subcommand".into(),
images: None,
text_elements: Vec::new(),
@@ -2362,6 +2459,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "first".into(),
images: None,
text_elements: Vec::new(),
@@ -2383,6 +2481,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "second".into(),
images: None,
text_elements: Vec::new(),
@@ -2458,6 +2557,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "first".into(),
images: None,
text_elements: Vec::new(),
@@ -2479,6 +2579,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "second".into(),
images: None,
text_elements: Vec::new(),
@@ -2528,6 +2629,7 @@ mod tests {
turns[1].items[0],
ThreadItem::UserMessage {
id: "item-2".into(),
client_id: None,
content: vec![UserInput::Text {
text: "second".into(),
text_elements: Vec::new(),
@@ -2549,6 +2651,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "apply patch".into(),
images: None,
text_elements: Vec::new(),
@@ -2584,6 +2687,7 @@ mod tests {
vec![
ThreadItem::UserMessage {
id: "item-1".into(),
client_id: None,
content: vec![UserInput::Text {
text: "apply patch".into(),
text_elements: Vec::new(),
@@ -2615,6 +2719,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "apply patch".into(),
images: None,
text_elements: Vec::new(),
@@ -2652,6 +2757,7 @@ mod tests {
vec![
ThreadItem::UserMessage {
id: "item-1".into(),
client_id: None,
content: vec![UserInput::Text {
text: "apply patch".into(),
text_elements: Vec::new(),
@@ -2681,6 +2787,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "first".into(),
images: None,
text_elements: Vec::new(),
@@ -2702,6 +2809,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "second".into(),
images: None,
text_elements: Vec::new(),
@@ -2751,6 +2859,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "first".into(),
images: None,
text_elements: Vec::new(),
@@ -2772,6 +2881,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "second".into(),
images: None,
text_elements: Vec::new(),
@@ -2846,6 +2956,7 @@ mod tests {
fn reconstructs_collab_resume_end_item() {
let events = vec![
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "resume agent".into(),
images: None,
text_elements: Vec::new(),
@@ -2904,6 +3015,7 @@ mod tests {
.expect("valid receiver thread id");
let events = vec![
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "spawn agent".into(),
images: None,
text_elements: Vec::new(),
@@ -2966,6 +3078,7 @@ mod tests {
.expect("valid receiver thread id");
let events = vec![
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "redirect".into(),
images: None,
text_elements: Vec::new(),
@@ -3030,6 +3143,7 @@ mod tests {
fn rollback_failed_error_does_not_mark_turn_failed() {
let events = vec![
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "hello".into(),
images: None,
text_elements: Vec::new(),
@@ -3068,6 +3182,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "hello".into(),
images: None,
text_elements: Vec::new(),
@@ -3105,6 +3220,7 @@ mod tests {
items_view: TurnItemsView::Full,
items: vec![ThreadItem::UserMessage {
id: "item-1".into(),
client_id: None,
content: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
@@ -3125,6 +3241,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "hello".into(),
images: None,
text_elements: Vec::new(),
@@ -3184,6 +3301,7 @@ mod tests {
collaboration_mode_kind: Default::default(),
})),
RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
client_id: None,
message: "hello".into(),
images: None,
text_elements: Vec::new(),

View File

@@ -6,6 +6,7 @@ use codex_protocol::protocol::CreditsSnapshot as CoreCreditsSnapshot;
use codex_protocol::protocol::RateLimitReachedType as CoreRateLimitReachedType;
use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot;
use codex_protocol::protocol::RateLimitWindow as CoreRateLimitWindow;
use codex_protocol::protocol::SpendControlLimitSnapshot as CoreSpendControlLimitSnapshot;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
@@ -247,6 +248,11 @@ pub struct AccountUpdatedNotification {
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
/// Sparse rolling rate-limit update.
///
/// Clients should merge available values into the most recent `account/rateLimits/read` response
/// or refetch that snapshot. Nullable account metadata may be unavailable in a rolling update and
/// does not clear a previously observed value.
pub struct AccountRateLimitsUpdatedNotification {
pub rate_limits: RateLimitSnapshot,
}
@@ -260,6 +266,7 @@ pub struct RateLimitSnapshot {
pub primary: Option<RateLimitWindow>,
pub secondary: Option<RateLimitWindow>,
pub credits: Option<CreditsSnapshot>,
pub individual_limit: Option<SpendControlLimitSnapshot>,
pub plan_type: Option<PlanType>,
pub rate_limit_reached_type: Option<RateLimitReachedType>,
}
@@ -272,6 +279,7 @@ impl From<CoreRateLimitSnapshot> for RateLimitSnapshot {
primary: value.primary.map(RateLimitWindow::from),
secondary: value.secondary.map(RateLimitWindow::from),
credits: value.credits.map(CreditsSnapshot::from),
individual_limit: value.individual_limit.map(SpendControlLimitSnapshot::from),
plan_type: value.plan_type,
rate_limit_reached_type: value
.rate_limit_reached_type
@@ -371,6 +379,28 @@ impl From<CoreCreditsSnapshot> for CreditsSnapshot {
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct SpendControlLimitSnapshot {
pub limit: String,
pub used: String,
pub remaining_percent: i32,
#[ts(type = "number")]
pub resets_at: i64,
}
impl From<CoreSpendControlLimitSnapshot> for SpendControlLimitSnapshot {
fn from(value: CoreSpendControlLimitSnapshot) -> Self {
Self {
limit: value.limit,
used: value.used,
remaining_percent: value.remaining_percent,
resets_at: value.resets_at,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]

View File

@@ -1,6 +1,7 @@
use super::ApprovalsReviewer;
use super::AskForApproval;
use super::SandboxMode;
use super::WindowsSandboxSetupMode;
use super::shared::default_enabled;
use codex_experimental_api_macros::ExperimentalApi;
use codex_protocol::config_types::AutoCompactTokenLimitScope;
@@ -42,6 +43,19 @@ pub enum ConfigLayerSource {
file: AbsolutePathBuf,
},
/// Enterprise-managed config layer delivered by the cloud config bundle.
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
EnterpriseManaged {
/// Stable identifier for the delivered layer.
id: String,
/// Admin-facing name for the delivered layer. This is surfaced in
/// diagnostics so users know which cloud layer needs administrator
/// attention.
name: String,
},
/// User config layer from $CODEX_HOME/config.toml. This layer is special
/// in that it is expected to be:
/// - writable by the user
@@ -89,6 +103,7 @@ impl ConfigLayerSource {
match self {
ConfigLayerSource::Mdm { .. } => 0,
ConfigLayerSource::System { .. } => 10,
ConfigLayerSource::EnterpriseManaged { .. } => 15,
ConfigLayerSource::User { profile, .. } => {
if profile.is_some() {
21
@@ -185,6 +200,7 @@ pub struct AppToolsConfig {
pub struct AppConfig {
#[serde(default = "default_enabled")]
pub enabled: bool,
pub approvals_reviewer: Option<ApprovalsReviewer>,
pub destructive_enabled: Option<bool>,
pub open_world_enabled: Option<bool>,
pub default_tools_approval_mode: Option<AppToolApproval>,
@@ -358,6 +374,7 @@ pub struct ConfigRequirements {
#[experimental("configRequirements/read.allowedApprovalsReviewers")]
pub allowed_approvals_reviewers: Option<Vec<ApprovalsReviewer>>,
pub allowed_sandbox_modes: Option<Vec<SandboxMode>>,
pub allowed_windows_sandbox_implementations: Option<Vec<WindowsSandboxSetupMode>>,
pub allowed_permissions: Option<Vec<String>>,
pub allowed_web_search_modes: Option<Vec<WebSearchMode>>,
pub allow_managed_hooks_only: Option<bool>,
@@ -490,7 +507,7 @@ pub enum NetworkDomainPermission {
#[ts(export_to = "v2/")]
pub enum NetworkUnixSocketPermission {
Allow,
None,
Deny,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]

View File

@@ -48,6 +48,7 @@ v2_enum_from_core!(
SessionFlags,
Plugin,
CloudRequirements,
CloudManagedConfig,
LegacyManagedConfigFile,
LegacyManagedConfigMdm,
Unknown,

View File

@@ -0,0 +1,33 @@
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use ts_rs::TS;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct HttpStateGetResponse {
pub state: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct HttpStateSetParams {
pub state: String,
/// When present, write only if the calling surface still stores this state.
#[ts(optional = nullable)]
pub expected_state: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct HttpStateSetResponse {
pub written: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct HttpStateClearResponse {}

View File

@@ -212,7 +212,11 @@ impl CommandAction {
pub enum ThreadItem {
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
UserMessage { id: String, content: Vec<UserInput> },
UserMessage {
id: String,
client_id: Option<String>,
content: Vec<UserInput>,
},
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
HookPrompt {
@@ -776,6 +780,7 @@ impl From<CoreTurnItem> for ThreadItem {
match value {
CoreTurnItem::UserMessage(user) => ThreadItem::UserMessage {
id: user.id,
client_id: user.client_id,
content: user.content.into_iter().map(UserInput::from).collect(),
},
CoreTurnItem::HookPrompt(hook_prompt) => ThreadItem::HookPrompt {

Some files were not shown because too many files have changed in this diff Show More