Compare commits

...

396 Commits

Author SHA1 Message Date
Ahmed Ibrahim
8ff066947c window 2025-12-01 18:27:30 -08:00
Manoel Calixto
32e4a3a4d7 fix(tui): handle WSL clipboard image paths (#3990)
Fixes #3939 
Fixes #2803

## Summary
- convert Windows clipboard file paths into their `/mnt/<drive>`
equivalents when running inside WSL so pasted images resolve correctly
- add WSL detection helpers and share them with unit tests to cover both
native Windows and WSL clipboard normalization cases
- improve the test suite by exercising Windows path handling plus a
dedicated WSL conversion scenario and keeping the code path guarded by
targeted cfgs

## Testing
- just fmt
- cargo test -p codex-tui
- cargo clippy -p codex-tui --tests
- just fix -p codex-tui

## Screenshots
_Codex TUI screenshot:_
<img width="1880" height="848" alt="describe this copied image"
src="https://github.com/user-attachments/assets/c620d43c-f45c-451e-8893-e56ae85a5eea"
/>

_GitHub docs directory screenshot:_
<img width="1064" height="478" alt="image-copied"
src="https://github.com/user-attachments/assets/eb5eef6c-eb43-45a0-8bfe-25c35bcae753"
/>

Co-authored-by: Eric Traut <etraut@openai.com>
2025-12-01 16:54:20 -08:00
Steve Mostovoy
f443555728 fix(core): enable history lookup on windows (#7457)
- Add portable history log id helper to support inode-like tracking on
Unix and creation time on Windows
- Refactor history metadata and lookup to share code paths and allow
nonzero log ids across platforms
- Add coverage for lookup stability after appends
2025-12-01 16:29:01 -08:00
Celia Chen
ff4ca9959c [app-server] Add ImageView item (#7468)
Add view_image tool call as image_view item.

Before:
```
< {
<   "method": "codex/event/view_image_tool_call",
<   "params": {
<     "conversationId": "019adc2f-2922-7e43-ace9-64f394019616",
<     "id": "0",
<     "msg": {
<       "call_id": "call_nBQDxnTfZQtgjGpVoGuDnRjz",
<       "path": "/Users/celia/code/codex/codex-rs/app-server-protocol/codex-cli-login.png",
<       "type": "view_image_tool_call"
<     }
<   }
< }
```

After:
```
< {
<   "method": "item/started",
<   "params": {
<     "item": {
<       "id": "call_nBQDxnTfZQtgjGpVoGuDnRjz",
<       "path": "/Users/celia/code/codex/codex-rs/app-server-protocol/codex-cli-login.png",
<       "type": "imageView"
<     },
<     "threadId": "019adc2f-2922-7e43-ace9-64f394019616",
<     "turnId": "0"
<   }
< }

< {
<   "method": "item/completed",
<   "params": {
<     "item": {
<       "id": "call_nBQDxnTfZQtgjGpVoGuDnRjz",
<       "path": "/Users/celia/code/codex/codex-rs/app-server-protocol/codex-cli-login.png",
<       "type": "imageView"
<     },
<     "threadId": "019adc2f-2922-7e43-ace9-64f394019616",
<     "turnId": "0"
<   }
< }
```
2025-12-01 23:56:05 +00:00
Dylan Hurd
5b25915d7e fix(apply_patch) tests for shell_command (#7307)
## Summary
Adds test coverage for invocations of apply_patch via shell_command with
heredoc, to validate behavior.

## Testing
- [x] These are tests
2025-12-01 15:09:22 -08:00
Michael Bolin
c0564edebe chore: update to rmcp@0.10.0 to pick up support for custom client notifications (#7462)
In https://github.com/openai/codex/pull/7112, I updated our `rmcp`
dependency to point to a personal fork while I tried to upstream my
proposed change. Now that
https://github.com/modelcontextprotocol/rust-sdk/pull/556 has been
upstreamed and included in the `0.10.0` release of the crate, we can go
back to using the mainline release.
2025-12-01 14:01:50 -08:00
linuxmetel
c936c68c84 fix: prevent MCP startup failure on missing 'type' field (#7417)
Fix the issue #7416 that the codex-cli produce an error "MCP startup
failure on missing 'type' field" in the startup.

- Cause: serde in `convert_to_rmcp`
(`codex-rs/rmcp-client/src/utils.rs`) failed because no `r#type` value
was provided
- Fix: set a default `r#type` value in the corresponding structs
2025-12-01 13:58:20 -05:00
Kaden Gruizenga
41760f8a09 docs: clarify codex max defaults and xhigh availability (#7449)
## Summary
Adds the missing `xhigh` reasoning level everywhere it should have been
documented, and makes clear it only works with `gpt-5.1-codex-max`.

## Changes

* `docs/config.md`

* Add `xhigh` to the official list of reasoning levels with a note that
`xhigh` is exclusive to Codex Max.

* `docs/example-config.md`

* Update the example comment adding `xhigh` as a valid option but only
for Codex Max.

* `docs/faq.md`

  * Update the model recommendation to `GPT-5.1 Codex Max`.
* Mention that users can choose `high` or the newly documented `xhigh`
level when using Codex Max.
2025-12-01 10:46:53 -08:00
Albert O'Shea
440c7acd8f fix: nix build missing rmcp output hash (#7436)
Output hash for `rmcp-0.9.0` was missing from the nix package, (i.e.
`error: No hash was found while vendoring the git dependency
rmcp-0.9.0.`) blocking the build.
2025-12-01 10:45:31 -08:00
Ali Towaiji
0cc3b50228 Fix recent_commits(limit=0) returning 1 commit instead of 0 (#7334)
Fixes #7333

This is a small bug fix.

This PR fixes an inconsistency in `recent_commits` where `limit == 0`
still returns 1 commit due to the use of `limit.max(1)` when
constructing the `git log -n` argument.

Expected behavior: requesting 0 commits should return an empty list.

This PR:
- returns an empty `Vec` when `limit == 0`
- adds a test for `recent_commits(limit == 0)` that fails before the
change and passes afterwards
- maintains existing behavior for `limit > 0`

This aligns behavior with API expectations and avoids downstream
consumers misinterpreting the repository as having commit history when
`limit == 0` is used to explicitly request none.

Happy to adjust if the current behavior is intentional.
2025-12-01 10:14:36 -08:00
Owen Lin
8532876ad8 [app-server] fix: emit item/fileChange/outputDelta for file change items (#7399) 2025-12-01 17:52:34 +00:00
Owen Lin
44d92675eb [app-server] fix: ensure thread_id and turn_id are on all events (#7408)
This is an improvement for client-side developer ergonomics by
simplifying the state the client needs to keep track of.
2025-12-01 08:50:47 -08:00
jif-oai
a421eba31f fix: disable review rollout filtering (#7371) 2025-12-01 09:04:13 +00:00
Celia Chen
40006808a3 [app-server] add turn/plan/updated event (#7329)
transform `EventMsg::PlanDate` to v2 `turn/plan/updated` event. similar
to `turn/diff/updated`.
2025-11-30 21:09:59 -08:00
dependabot[bot]
ba58184349 chore(deps): bump image from 0.25.8 to 0.25.9 in /codex-rs (#7421)
Bumps [image](https://github.com/image-rs/image) from 0.25.8 to 0.25.9.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/image-rs/image/blob/main/CHANGES.md">image's
changelog</a>.</em></p>
<blockquote>
<h3>Version 0.25.9</h3>
<p>Features:</p>
<ul>
<li>Support extracting XMP metadata from PNG, JPEG, GIF, WebP and TIFF
files (<a
href="https://redirect.github.com/image-rs/image/issues/2567">#2567</a>,
<a
href="https://redirect.github.com/image-rs/image/issues/2634">#2634</a>,
<a
href="https://redirect.github.com/image-rs/image/issues/2644">#2644</a>)</li>
<li>Support reading IPTC metadata from PNG and JPG files (<a
href="https://redirect.github.com/image-rs/image/issues/2611">#2611</a>)</li>
<li>Support reading ICC profile from GIF files (<a
href="https://redirect.github.com/image-rs/image/issues/2644">#2644</a>)</li>
<li>Allow setting a specific DEFLATE compression level when writing PNG
(<a
href="https://redirect.github.com/image-rs/image/issues/2583">#2583</a>)</li>
<li>Initial support for 16-bit CMYK TIFF files (<a
href="https://redirect.github.com/image-rs/image/issues/2588">#2588</a>)</li>
<li>Allow extracting the alpha channel of a <code>Pixel</code> in a
generic way (<a
href="https://redirect.github.com/image-rs/image/issues/2638">#2638</a>)</li>
</ul>
<p>Structural changes:</p>
<ul>
<li>EXR format decoding now only uses multi-threading via Rayon when the
<code>rayon</code> feature is enabled (<a
href="https://redirect.github.com/image-rs/image/issues/2643">#2643</a>)</li>
<li>Upgraded zune-jpeg to 0.5.x, ravif to 0.12.x, gif to 0.14.x</li>
<li>pnm: parse integers in PBM/PGM/PPM headers without allocations (<a
href="https://redirect.github.com/image-rs/image/issues/2620">#2620</a>)</li>
<li>Replace <code>doc_auto_cfg</code> with <code>doc_cfg</code> (<a
href="https://redirect.github.com/image-rs/image/issues/2637">#2637</a>)</li>
</ul>
<p>Bug fixes:</p>
<ul>
<li>Do not encode empty JPEG images (<a
href="https://redirect.github.com/image-rs/image/issues/2624">#2624</a>)</li>
<li>tga: reject empty images (<a
href="https://redirect.github.com/image-rs/image/issues/2614">#2614</a>)</li>
<li>tga: fix orientation flip for color mapped images (<a
href="https://redirect.github.com/image-rs/image/issues/2607">#2607</a>)</li>
<li>tga: adjust colormap lookup to match tga 2.0 spec (<a
href="https://redirect.github.com/image-rs/image/issues/2608">#2608</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="5ceb6af6c2"><code>5ceb6af</code></a>
Merge pull request <a
href="https://redirect.github.com/image-rs/image/issues/2640">#2640</a>
from Shnatsel/release-v0.25.9</li>
<li><a
href="282d7b345c"><code>282d7b3</code></a>
Merge pull request <a
href="https://redirect.github.com/image-rs/image/issues/2646">#2646</a>
from oligamiq/main</li>
<li><a
href="5412aeee5a"><code>5412aee</code></a>
Amend the note in accordance with the advice of 197g.</li>
<li><a
href="4e8a4ed2e8"><code>4e8a4ed</code></a>
Clarify default features in README and add usage note</li>
<li><a
href="ca8fa528ff"><code>ca8fa52</code></a>
Merge pull request <a
href="https://redirect.github.com/image-rs/image/issues/2644">#2644</a>
from image-rs/gif-0.14</li>
<li><a
href="d9bc8fe790"><code>d9bc8fe</code></a>
mention GIF 0.14 changes</li>
<li><a
href="053220a0b1"><code>053220a</code></a>
Provide gif's XMP and ICC metadata</li>
<li><a
href="2ec20b3b3b"><code>2ec20b3</code></a>
Prepare codec with gif@0.14</li>
<li><a
href="31939facce"><code>31939fa</code></a>
Mention EXR rayon change</li>
<li><a
href="c7f68be265"><code>c7f68be</code></a>
Merge pull request <a
href="https://redirect.github.com/image-rs/image/issues/2643">#2643</a>
from Shnatsel/really-optional-rayon</li>
<li>Additional commits viewable in <a
href="https://github.com/image-rs/image/compare/v0.25.8...v0.25.9">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=image&package-manager=cargo&previous-version=0.25.8&new-version=0.25.9)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-30 20:50:51 -08:00
Eric Traut
14df5c9492 Fixed CLA action to properly exempt dependabot (#7429) 2025-11-30 20:45:17 -08:00
dependabot[bot]
cb85a7b96e chore(deps): bump tracing from 0.1.41 to 0.1.43 in /codex-rs (#7428)
Bumps [tracing](https://github.com/tokio-rs/tracing) from 0.1.41 to
0.1.43.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/tokio-rs/tracing/releases">tracing's
releases</a>.</em></p>
<blockquote>
<h2>tracing 0.1.43</h2>
<h4>Important</h4>
<p>The previous release [0.1.42] was yanked because <a
href="https://redirect.github.com/tokio-rs/tracing/issues/3382">#3382</a>
was a breaking change.
See further details in <a
href="https://redirect.github.com/tokio-rs/tracing/issues/3424">#3424</a>.
This release contains all the changes from that
version, plus a revert for the problematic part of the breaking PR.</p>
<h3>Fixed</h3>
<ul>
<li>Revert &quot;make <code>valueset</code> macro sanitary&quot; (<a
href="https://redirect.github.com/tokio-rs/tracing/issues/3425">#3425</a>)</li>
</ul>
<p><a
href="https://redirect.github.com/tokio-rs/tracing/issues/3382">#3382</a>:
<a
href="https://redirect.github.com/tokio-rs/tracing/pull/3382">tokio-rs/tracing#3382</a>
<a
href="https://redirect.github.com/tokio-rs/tracing/issues/3424">#3424</a>:
<a
href="https://redirect.github.com/tokio-rs/tracing/pull/3424">tokio-rs/tracing#3424</a>
<a
href="https://redirect.github.com/tokio-rs/tracing/issues/3425">#3425</a>:
<a
href="https://redirect.github.com/tokio-rs/tracing/pull/3425">tokio-rs/tracing#3425</a>
[0.1.42]: <a
href="https://github.com/tokio-rs/tracing/releases/tag/tracing-0.1.42">https://github.com/tokio-rs/tracing/releases/tag/tracing-0.1.42</a></p>
<h2>tracing 0.1.42</h2>
<h3>Important</h3>
<p>The [<code>Span::record_all</code>] method has been removed from the
documented API. It
was always unsuable via the documented API as it requried a
<code>ValueSet</code> which
has no publically documented constructors. The method remains, but
should not
be used outside of <code>tracing</code> macros.</p>
<h3>Added</h3>
<ul>
<li><strong>attributes</strong>: Support constant expressions as
instrument field names (<a
href="https://redirect.github.com/tokio-rs/tracing/issues/3158">#3158</a>)</li>
<li>Add <code>record_all!</code> macro for recording multiple values in
one call (<a
href="https://redirect.github.com/tokio-rs/tracing/issues/3227">#3227</a>)</li>
<li><strong>core</strong>: Improve code generation at trace points
significantly (<a
href="https://redirect.github.com/tokio-rs/tracing/issues/3398">#3398</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li><code>tracing-core</code>: updated to 0.1.35 (<a
href="https://redirect.github.com/tokio-rs/tracing/issues/3414">#3414</a>)</li>
<li><code>tracing-attributes</code>: updated to 0.1.31 (<a
href="https://redirect.github.com/tokio-rs/tracing/issues/3417">#3417</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Fix &quot;name / parent&quot; variant of <code>event!</code> (<a
href="https://redirect.github.com/tokio-rs/tracing/issues/2983">#2983</a>)</li>
<li>Remove 'r#' prefix from raw identifiers in field names (<a
href="https://redirect.github.com/tokio-rs/tracing/issues/3130">#3130</a>)</li>
<li>Fix perf regression when <code>release_max_level_*</code> not set
(<a
href="https://redirect.github.com/tokio-rs/tracing/issues/3373">#3373</a>)</li>
<li>Use imported instead of fully qualified path (<a
href="https://redirect.github.com/tokio-rs/tracing/issues/3374">#3374</a>)</li>
<li>Make <code>valueset</code> macro sanitary (<a
href="https://redirect.github.com/tokio-rs/tracing/issues/3382">#3382</a>)</li>
</ul>
<h3>Documented</h3>
<ul>
<li><strong>core</strong>: Add missing <code>dyn</code> keyword in
<code>Visit</code> documentation code sample (<a
href="https://redirect.github.com/tokio-rs/tracing/issues/3387">#3387</a>)</li>
</ul>
<p><a
href="https://redirect.github.com/tokio-rs/tracing/issues/2983">#2983</a>:
<a
href="https://redirect.github.com/tokio-rs/tracing/pull/%5B#2983%5D(https://redirect.github.com/tokio-rs/tracing/issues/2983)">tokio-rs/tracing#2983</a>
<a
href="https://redirect.github.com/tokio-rs/tracing/issues/3130">#3130</a>:
<a
href="https://redirect.github.com/tokio-rs/tracing/pull/%5B#3130%5D(https://redirect.github.com/tokio-rs/tracing/issues/3130)">tokio-rs/tracing#3130</a>
<a
href="https://redirect.github.com/tokio-rs/tracing/issues/3158">#3158</a>:
<a
href="https://redirect.github.com/tokio-rs/tracing/pull/%5B#3158%5D(https://redirect.github.com/tokio-rs/tracing/issues/3158)">tokio-rs/tracing#3158</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="64e1c8d3ae"><code>64e1c8d</code></a>
chore: prepare tracing 0.1.43 (<a
href="https://redirect.github.com/tokio-rs/tracing/issues/3427">#3427</a>)</li>
<li><a
href="7c44f7bb21"><code>7c44f7b</code></a>
tracing: revert &quot;make <code>valueset</code> macro sanitary&quot;
(<a
href="https://redirect.github.com/tokio-rs/tracing/issues/3425">#3425</a>)</li>
<li><a
href="cdaf661c13"><code>cdaf661</code></a>
chore: prepare tracing-mock 0.1.0-beta.2 (<a
href="https://redirect.github.com/tokio-rs/tracing/issues/3422">#3422</a>)</li>
<li><a
href="a164fd3021"><code>a164fd3</code></a>
chore: prepare tracing-journald 0.3.2 (<a
href="https://redirect.github.com/tokio-rs/tracing/issues/3421">#3421</a>)</li>
<li><a
href="405397b8cc"><code>405397b</code></a>
chore: prepare tracing-appender 0.2.4 (<a
href="https://redirect.github.com/tokio-rs/tracing/issues/3420">#3420</a>)</li>
<li><a
href="a9eeed7394"><code>a9eeed7</code></a>
chore: prepare tracing-subscriber 0.3.21 (<a
href="https://redirect.github.com/tokio-rs/tracing/issues/3419">#3419</a>)</li>
<li><a
href="5bd5505478"><code>5bd5505</code></a>
chore: prepare tracing 0.1.42 (<a
href="https://redirect.github.com/tokio-rs/tracing/issues/3418">#3418</a>)</li>
<li><a
href="55086231ec"><code>5508623</code></a>
chore: prepare tracing-attributes 0.1.31 (<a
href="https://redirect.github.com/tokio-rs/tracing/issues/3417">#3417</a>)</li>
<li><a
href="d92b4c0feb"><code>d92b4c0</code></a>
chore: prepare tracing-core 0.1.35 (<a
href="https://redirect.github.com/tokio-rs/tracing/issues/3414">#3414</a>)</li>
<li><a
href="9751b6e776"><code>9751b6e</code></a>
chore: run <code>tracing-subscriber</code> tests with all features (<a
href="https://redirect.github.com/tokio-rs/tracing/issues/3412">#3412</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/tokio-rs/tracing/compare/tracing-0.1.41...tracing-0.1.43">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tracing&package-manager=cargo&previous-version=0.1.41&new-version=0.1.43)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Eric Traut <etraut@openai.com>
2025-11-30 20:36:03 -08:00
dependabot[bot]
3f12f1140f chore(deps): bump reqwest from 0.12.23 to 0.12.24 in /codex-rs (#7424)
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.12.23 to
0.12.24.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/seanmonstar/reqwest/releases">reqwest's
releases</a>.</em></p>
<blockquote>
<h2>v0.12.24</h2>
<h2>Highlights</h2>
<ul>
<li>Refactor cookie handling to an internal middleware.</li>
<li>Refactor internal random generator.</li>
<li>Refactor base64 encoding to reduce a copy.</li>
<li>Documentation updates.</li>
</ul>
<h2>What's Changed</h2>
<ul>
<li>build(deps): silence unused deps in WASM build by <a
href="https://github.com/0x676e67"><code>@​0x676e67</code></a> in <a
href="https://redirect.github.com/seanmonstar/reqwest/pull/2799">seanmonstar/reqwest#2799</a></li>
<li>perf(util): avoid extra copy when base64 encoding by <a
href="https://github.com/0x676e67"><code>@​0x676e67</code></a> in <a
href="https://redirect.github.com/seanmonstar/reqwest/pull/2805">seanmonstar/reqwest#2805</a></li>
<li>docs: fix method name in changelog entry by <a
href="https://github.com/johannespfrang"><code>@​johannespfrang</code></a>
in <a
href="https://redirect.github.com/seanmonstar/reqwest/pull/2807">seanmonstar/reqwest#2807</a></li>
<li>chore: Align the name usage of TotalTimeout by <a
href="https://github.com/Xuanwo"><code>@​Xuanwo</code></a> in <a
href="https://redirect.github.com/seanmonstar/reqwest/pull/2657">seanmonstar/reqwest#2657</a></li>
<li>refactor(cookie): add <code>CookieService</code> by <a
href="https://github.com/linyihai"><code>@​linyihai</code></a> in <a
href="https://redirect.github.com/seanmonstar/reqwest/pull/2787">seanmonstar/reqwest#2787</a></li>
<li>Fixes typo in retry max_retries_per_request doc comment re 2813 by
<a href="https://github.com/dmackinn"><code>@​dmackinn</code></a> in <a
href="https://redirect.github.com/seanmonstar/reqwest/pull/2824">seanmonstar/reqwest#2824</a></li>
<li>test(multipart): fix build failure with
<code>no-default-features</code> by <a
href="https://github.com/0x676e67"><code>@​0x676e67</code></a> in <a
href="https://redirect.github.com/seanmonstar/reqwest/pull/2801">seanmonstar/reqwest#2801</a></li>
<li>refactor(cookie): avoid duplicate cookie insertion by <a
href="https://github.com/0x676e67"><code>@​0x676e67</code></a> in <a
href="https://redirect.github.com/seanmonstar/reqwest/pull/2834">seanmonstar/reqwest#2834</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/johannespfrang"><code>@​johannespfrang</code></a>
made their first contribution in <a
href="https://redirect.github.com/seanmonstar/reqwest/pull/2807">seanmonstar/reqwest#2807</a></li>
<li><a href="https://github.com/dmackinn"><code>@​dmackinn</code></a>
made their first contribution in <a
href="https://redirect.github.com/seanmonstar/reqwest/pull/2824">seanmonstar/reqwest#2824</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/seanmonstar/reqwest/compare/v0.12.23...v0.12.24">https://github.com/seanmonstar/reqwest/compare/v0.12.23...v0.12.24</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md">reqwest's
changelog</a>.</em></p>
<blockquote>
<h2>v0.12.24</h2>
<ul>
<li>Refactor cookie handling to an internal middleware.</li>
<li>Refactor internal random generator.</li>
<li>Refactor base64 encoding to reduce a copy.</li>
<li>Documentation updates.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="b126ca49da"><code>b126ca4</code></a>
v0.12.24</li>
<li><a
href="4023493096"><code>4023493</code></a>
refactor: change fast_random from xorshift to siphash a counter</li>
<li><a
href="fd61bc93e6"><code>fd61bc9</code></a>
refactor(cookie): avoid duplicate cookie insertion (<a
href="https://redirect.github.com/seanmonstar/reqwest/issues/2834">#2834</a>)</li>
<li><a
href="0bfa526776"><code>0bfa526</code></a>
test(multipart): fix build failure with <code>no-default-features</code>
(<a
href="https://redirect.github.com/seanmonstar/reqwest/issues/2801">#2801</a>)</li>
<li><a
href="994b8a0b7a"><code>994b8a0</code></a>
docs: typo in retry max_retries_per_request (<a
href="https://redirect.github.com/seanmonstar/reqwest/issues/2824">#2824</a>)</li>
<li><a
href="da0702b762"><code>da0702b</code></a>
refactor(cookie): de-duplicate cookie support as
<code>CookieService</code> middleware (...</li>
<li><a
href="7ebddeaa87"><code>7ebddea</code></a>
chore: align internal name usage of TotalTimeout (<a
href="https://redirect.github.com/seanmonstar/reqwest/issues/2657">#2657</a>)</li>
<li><a
href="b540a4e746"><code>b540a4e</code></a>
chore(readme): use correct CI status badge</li>
<li><a
href="e4550c4cc5"><code>e4550c4</code></a>
docs: fix method name in changelog entry (<a
href="https://redirect.github.com/seanmonstar/reqwest/issues/2807">#2807</a>)</li>
<li><a
href="f4694a2922"><code>f4694a2</code></a>
perf(util): avoid extra copy when base64 encoding (<a
href="https://redirect.github.com/seanmonstar/reqwest/issues/2805">#2805</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/seanmonstar/reqwest/compare/v0.12.23...v0.12.24">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=reqwest&package-manager=cargo&previous-version=0.12.23&new-version=0.12.24)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Eric Traut <etraut@openai.com>
2025-11-30 20:35:49 -08:00
dependabot[bot]
c22cd2e953 chore(deps): bump serde_with from 3.14.0 to 3.16.1 in /codex-rs (#7422)
Bumps [serde_with](https://github.com/jonasbb/serde_with) from 3.14.0 to
3.16.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/jonasbb/serde_with/releases">serde_with's
releases</a>.</em></p>
<blockquote>
<h2>serde_with v3.16.1</h2>
<h3>Fixed</h3>
<ul>
<li>Fix <code>JsonSchemaAs</code> of <code>SetPreventDuplicates</code>
and <code>SetLastValueWins</code>. (<a
href="https://redirect.github.com/jonasbb/serde_with/issues/906">#906</a>,
<a
href="https://redirect.github.com/jonasbb/serde_with/issues/907">#907</a>)</li>
</ul>
<h2>serde_with v3.16.0</h2>
<h3>Added</h3>
<ul>
<li>Added support for <code>smallvec</code> v1 under the
<code>smallvec_1</code> feature flag by <a
href="https://github.com/isharma228"><code>@​isharma228</code></a> (<a
href="https://redirect.github.com/jonasbb/serde_with/issues/895">#895</a>)</li>
<li>Add <code>JsonSchemaAs</code> implementation for
<code>json::JsonString</code> by <a
href="https://github.com/yogevm15"><code>@​yogevm15</code></a> (<a
href="https://redirect.github.com/jonasbb/serde_with/issues/901">#901</a>)</li>
</ul>
<h2>serde_with v3.15.1</h2>
<h3>Fixed</h3>
<ul>
<li>Fix building of the documentation by updating references to use
<code>serde_core</code>.</li>
</ul>
<h2>serde_with v3.15.0</h2>
<h3>Added</h3>
<ul>
<li>
<p>Added error inspection to <code>VecSkipError</code> and
<code>MapSkipError</code> by <a
href="https://github.com/michelhe"><code>@​michelhe</code></a> (<a
href="https://redirect.github.com/jonasbb/serde_with/issues/878">#878</a>)
This allows interacting with the previously hidden error, for example
for logging.
Checkout the newly added example to both types.</p>
</li>
<li>
<p>Allow documenting the types generated by <code>serde_conv!</code>.
The <code>serde_conv!</code> macro now acceps outer attributes before
the optional visibility modifier.
This allow adding doc comments in the shape of <code>#[doc =
&quot;...&quot;]</code> or any other attributes, such as lint
modifiers.</p>
<pre lang="rust"><code>serde_conv!(
    #[doc = &quot;Serialize bools as string&quot;]
    #[allow(dead_code)]
    pub BoolAsString,
    bool,
    |x: &amp;bool| ::std::string::ToString::to_string(x),
    |x: ::std::string::String| x.parse()
);
</code></pre>
</li>
<li>
<p>Add support for <code>hashbrown</code> v0.16 (<a
href="https://redirect.github.com/jonasbb/serde_with/issues/877">#877</a>)</p>
<p>This extends the existing support for <code>hashbrown</code> v0.14
and v0.15 to the newly released version.</p>
</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Bump MSRV to 1.76, since that is required for <code>toml</code>
dev-dependency.</li>
</ul>
<h2>serde_with v3.14.1</h2>
<h3>Fixed</h3>
<ul>
<li>Show macro expansion in the docs.rs generated rustdoc.</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="8513323fda"><code>8513323</code></a>
Bump version to 3.16.1 (<a
href="https://redirect.github.com/jonasbb/serde_with/issues/908">#908</a>)</li>
<li><a
href="5392bbe75e"><code>5392bbe</code></a>
Bump version to 3.16.1</li>
<li><a
href="1e54f1cd38"><code>1e54f1c</code></a>
Fix duplicate schema set definitions for schemars 0.8, 0.9, and 1.0 (<a
href="https://redirect.github.com/jonasbb/serde_with/issues/907">#907</a>)</li>
<li><a
href="0650180645"><code>0650180</code></a>
Fix duplicate schema set definitions for schemars 0.8, 0.9, and 1.0</li>
<li><a
href="41d1033438"><code>41d1033</code></a>
Fix test conditions for schemars tests to include &quot;hex&quot;
feature</li>
<li><a
href="2eed58af05"><code>2eed58a</code></a>
Bump the github-actions group across 1 directory with 2 updates (<a
href="https://redirect.github.com/jonasbb/serde_with/issues/905">#905</a>)</li>
<li><a
href="ed040f2330"><code>ed040f2</code></a>
Bump the github-actions group across 1 directory with 2 updates</li>
<li><a
href="fa2129b1b9"><code>fa2129b</code></a>
Bump ron from 0.11.0 to 0.12.0 (<a
href="https://redirect.github.com/jonasbb/serde_with/issues/904">#904</a>)</li>
<li><a
href="b55cb99757"><code>b55cb99</code></a>
Bump ron from 0.11.0 to 0.12.0</li>
<li><a
href="066b9d4019"><code>066b9d4</code></a>
Bump version to 3.16.0 (<a
href="https://redirect.github.com/jonasbb/serde_with/issues/903">#903</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/jonasbb/serde_with/compare/v3.14.0...v3.16.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=serde_with&package-manager=cargo&previous-version=3.14.0&new-version=3.16.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Eric Traut <etraut@openai.com>
2025-11-30 20:35:32 -08:00
dependabot[bot]
ebd485b1a0 chore(deps): bump arboard from 3.6.0 to 3.6.1 in /codex-rs (#7426)
Bumps [arboard](https://github.com/1Password/arboard) from 3.6.0 to
3.6.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/1Password/arboard/releases">arboard's
releases</a>.</em></p>
<blockquote>
<h2>v3.6.1</h2>
<p>This release focuses on improving compatibility with data in the real
world and bug fixes. It also includes a new <code>Set</code> API for
working with file paths via drag-and-drop interfaces across Linux,
macOS, and Windows.</p>
<p>This release also marks the start of exclusively publishing
changelogs via GitHub Releases. The old <code>CHANGELOG.md</code> has
been removed due to maintenance overhead and duplication. <a
href="https://github.com/1Password/arboard/releases/tag/v3.6.0">v3.6.0</a>
is the last revision to include this file.</p>
<h3>Added</h3>
<ul>
<li>Add support for pasting lists of files via
<code>Set::file_list</code> interface by <a
href="https://github.com/Gae24"><code>@​Gae24</code></a> in <a
href="https://redirect.github.com/1Password/arboard/pull/181">1Password/arboard#181</a></li>
<li>Support <code>windows-sys</code> 0.60 in <code>arboard</code>'s
allowed version range by <a
href="https://github.com/complexspaces"><code>@​complexspaces</code></a>
in <a
href="https://redirect.github.com/1Password/arboard/pull/201">1Password/arboard#201</a></li>
</ul>
<h3>Changed</h3>
<ul>
<li>Fix grammar and typos by <a
href="https://github.com/complexspaces"><code>@​complexspaces</code></a>
and <a href="https://github.com/gagath"><code>@​gagath</code></a> in <a
href="https://redirect.github.com/1Password/arboard/pull/194">1Password/arboard#194</a>
and <a
href="https://redirect.github.com/1Password/arboard/pull/196">1Password/arboard#196</a></li>
<li>Prefer PNG when pasting images on Windows by <a
href="https://github.com/wcassels"><code>@​wcassels</code></a> in <a
href="https://redirect.github.com/1Password/arboard/pull/198">1Password/arboard#198</a>
<ul>
<li>Note: This change greatly increases compatibility for
&quot;complicated&quot; images that contain alpha values and/or
transparent pixels. Support for transparency in <code>BITMAP</code>
formats is ill-defined and inconsistently implemented in the wild, but
is consistent in <code>PNG</code>. Most applications loading images onto
the clipboard include <code>PNG</code>-encoded data already.</li>
</ul>
</li>
<li>Bitmap images pasted on Windows now use the <code>image</code> crate
instead of a homegrown internal parser.
<ul>
<li>This <strong>should not</strong> regress any existing Bitmap use
cases and instead will provide more consistent and robust parsing. If
you notice something now broken, please open an issue!</li>
</ul>
</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Remove silent dropping of file paths when non-UTF8 was mixed in on
Linux by <a href="https://github.com/Gae24"><code>@​Gae24</code></a> in
<a
href="https://redirect.github.com/1Password/arboard/pull/197">1Password/arboard#197</a></li>
<li>Fix parsing of 24-bit bitmaps on Windows by <a
href="https://github.com/wcassels"><code>@​wcassels</code></a> in <a
href="https://redirect.github.com/1Password/arboard/pull/198">1Password/arboard#198</a>
<ul>
<li>Example: Images with transparency copied by Firefox are now handled
correctly, among others.</li>
</ul>
</li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/gagath"><code>@​gagath</code></a> made
their first contribution in <a
href="https://redirect.github.com/1Password/arboard/pull/196">1Password/arboard#196</a></li>
<li><a href="https://github.com/wcassels"><code>@​wcassels</code></a>
made their first contribution in <a
href="https://redirect.github.com/1Password/arboard/pull/198">1Password/arboard#198</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/1Password/arboard/compare/v3.6.0...v3.6.1">https://github.com/1Password/arboard/compare/v3.6.0...v3.6.1</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="a3750c79a5"><code>a3750c7</code></a>
Release 3.6.1</li>
<li><a
href="edcce2cd6b"><code>edcce2c</code></a>
Remove CHANGELOG.md in favor of GitHub releases</li>
<li><a
href="26a96a6199"><code>26a96a6</code></a>
Bump windows-sys semver range to support 0.60.x</li>
<li><a
href="7bdd1c1175"><code>7bdd1c1</code></a>
Update errno for windows-sys 0.60 flexibility</li>
<li><a
href="55c0b260c4"><code>55c0b26</code></a>
read/write_unaligned rather than using manual field offsets</li>
<li><a
href="ff15a093d6"><code>ff15a09</code></a>
Return conversionFailure instead of adhoc errors</li>
<li><a
href="16ef18113f"><code>16ef181</code></a>
Implement fetching PNG on Windows and prefer over DIB when
available</li>
<li><a
href="a3c64f9a93"><code>a3c64f9</code></a>
Add a couple of end-to-end DIBV5 tests</li>
<li><a
href="e6008eaa91"><code>e6008ea</code></a>
Use image for reading DIB and try to make it do the right thing for
32-bit BI...</li>
<li><a
href="17ef05ce13"><code>17ef05c</code></a>
add <code>file_list</code> to <code>Set</code> interface (<a
href="https://redirect.github.com/1Password/arboard/issues/181">#181</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/1Password/arboard/compare/v3.6.0...v3.6.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=arboard&package-manager=cargo&previous-version=3.6.0&new-version=3.6.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-30 20:20:45 -08:00
jif-oai
457c9fdb87 chore: better session recycling (#7368) 2025-11-30 12:42:26 -08:00
jif-oai
6eeaf46ac1 fix: other flaky tests (#7372) 2025-11-28 15:29:44 +00:00
jif-oai
aaec8abf58 feat: detached review (#7292) 2025-11-28 11:34:57 +00:00
Job Chong
cbd7d0d543 chore: improve rollout session init errors (#7336)
Title: Improve rollout session initialization error messages

Issue: https://github.com/openai/codex/issues/7283

What: add targeted mapping for rollout/session initialization errors so
users get actionable messages when Codex cannot access session files.

Why: session creation previously returned a generic internal error,
hiding permissions/FS issues and making support harder.

How:
- Added rollout::error::map_session_init_error to translate the more
common io::Error kinds into user-facing hints (permission, missing dir,
file blocking, corruption). Others are passed through directly with
`CodexErr::Fatal`.
- Reused the mapper in Codex session creation to preserve root causes
instead of returning InternalAgentDied.
2025-11-27 00:20:33 -08:00
Eric Traut
fabdbfef9c Fixes two bugs in example-config.md documentation (#7324)
This PR is a modified version of [a
PR](https://github.com/openai/codex/pull/7316) submitted by @yydrowz3.
* Removes a redundant `experimental_sandbox_command_assessment` flag
* Moves `mcp_oauth_credentials_store` from the `[features]` table, where
it doesn't belong
2025-11-26 09:52:13 -08:00
lionel-oai
8b314e2d04 doc: fix relative links and add tips (#7319)
This PR is a documentation only one which:
- addresses the #7231 by adding a paragraph in `docs/getting-started.md`
in the tips category to encourage users to load everything needed in
their environment
- corrects link referencing in `docs/platform-sandboxing.md` so that the
page link opens at the right section
- removes the explicit heading IDs like {#my-id} in `docs/advanced.md`
which are not supported by GitHub and are **not** rendered in the UI:

<img width="1198" height="849" alt="Screenshot 2025-11-26 at 16 25 31"
src="https://github.com/user-attachments/assets/308d33c3-81d3-4785-a6c1-e9377e6d3ea6"
/>

This caused the following links in `README.md` to not work in `main` but
to work in this branch (you can test by going to
https://github.com/openai/codex/blob/docs/getting-started-enhancement/README.md)
- the MCP link goes straight to the correct section now:

```markdown
  - [**Advanced**](./docs/advanced.md)
  - [Tracing / verbose logging](./docs/advanced.md#tracing--verbose-logging)
  - [Model Context Protocol (MCP)](./docs/advanced.md#model-context-protocol-mcp)
```

---------

Signed-off-by: lionel-oai <lionel@openai.com>
Signed-off-by: lionelchg <lionel.cheng@hotmail.fr>
Co-authored-by: lionelchg <lionel.cheng@hotmail.fr>
2025-11-26 09:35:08 -08:00
jif-oai
963009737f nit: drop file (#7314) 2025-11-26 11:30:34 +00:00
Eric Traut
e953092949 Fixed regression in experimental "sandbox command assessment" feature (#7308)
Recent model updates caused the experimental "sandbox tool assessment"
to time out most of the time leaving the user without any risk
assessment or tool summary. This change explicitly sets the reasoning
effort to medium and bumps the timeout.

This change has no effect if the user hasn't enabled the
`experimental_sandbox_command_assessment` feature flag.
2025-11-25 16:15:13 -08:00
jif-oai
28ff364c3a feat: update process ID for event handling (#7261) 2025-11-25 14:21:05 -08:00
iceweasel-oai
981e2f742d correctly recognize WorkspaceWrite policy on /approvals (#7301)
the `/approvals` popup fails to recognize that the CLI is in
WorkspaceWrite mode if that policy has extra bits, like `writable_roots`
etc.

This change matches the policy, ignoring additional config aspects.
2025-11-25 12:41:00 -08:00
Celia Chen
401f94ca31 [app-server] add thread/tokenUsage/updated v2 event (#7268)
the TokenEvent event message becomes `thread/tokenUsage/updated` in v2.
before & after:
```
< {
<   "method": "codex/event/token_count",
<   "params": {
<     "conversationId": "019ab891-4c55-7790-9670-6c3b48c33281",
<     "id": "1",
<     "msg": {
<       "info": {
<         "last_token_usage": {
<           "cached_input_tokens": 3072,
<           "input_tokens": 5152,
<           "output_tokens": 16,
<           "reasoning_output_tokens": 0,
<           "total_tokens": 5168
<         },
<         "model_context_window": 258400,
<         "total_token_usage": {
<           "cached_input_tokens": 3072,
<           "input_tokens": 5152,
<           "output_tokens": 16,
<           "reasoning_output_tokens": 0,
<           "total_tokens": 5168
<         }
<       },
<       "rate_limits": {
<         "credits": null,
<         "primary": null,
<         "secondary": null
<       },
<       "type": "token_count"
<     }
<   }
< }
< {
<   "method": "thread/tokenUsage/updated",
<   "params": {
<     "threadId": "019ab891-4c55-7790-9670-6c3b48c33281",
<     "tokenUsage": {
<       "last": {
<         "cachedInputTokens": 3072,
<         "inputTokens": 5152,
<         "outputTokens": 16,
<         "reasoningOutputTokens": 0,
<         "totalTokens": 5168
<       },
<       "modelContextWindow": 258400,
<       "total": {
<         "cachedInputTokens": 3072,
<         "inputTokens": 5152,
<         "outputTokens": 16,
<         "reasoningOutputTokens": 0,
<         "totalTokens": 5168
<       }
<     },
<     "turnId": "1"
<   }
< }
```
2025-11-25 19:56:04 +00:00
jif-oai
865e225bde tmp: drop flaky ubuntu (#7300) 2025-11-25 18:15:19 +00:00
jif-oai
4502b1b263 chore: proper client extraction (#6996) 2025-11-25 18:06:12 +00:00
jif-oai
2845e2c006 fix: drop conversation when /new (#7297) 2025-11-25 17:20:25 +00:00
jif-oai
d8a7a63959 fix: Drop MacOS 13 (#7295) 2025-11-25 16:25:44 +00:00
Owen Lin
caf2749d5b [app-server] feat: add turn/diff/updated event (#7279)
This is the V2 version of `EventMsg::TurnDiff`.

I decided to expose this as a `turn/*` notification as opposed to an
Item to make it more explicit that the diff is accumulated throughout a
turn (every `apply_patch` call updates the running diff). Also, I don't
think it's worth persisting this diff as an Item because it can always
be recomputed from the actual `FileChange` Items.
2025-11-25 16:21:08 +00:00
jif-oai
9ba27cfa0a feat: add compaction event (#7289) 2025-11-25 16:12:14 +00:00
Owen Lin
157a16cefa [app-server] feat: add thread_id and turn_id to item and error notifications (#7124)
Add `thread_id` and `turn_id` to `item/started`, `item/completed`, and
`error` notifications. Otherwise the client will have a hard time
knowing which thread & turn (if multiple threads are running in
parallel) a new item/error is for.

Also add `thread_id` to `turn/started` and `turn/completed`.
2025-11-25 08:05:47 -08:00
jif-oai
37d83e075e feat: add custom env for unified exec process (#7286) 2025-11-25 10:35:35 +00:00
jif-oai
523b40a129 feat[app-serve]: config management (#7241) 2025-11-25 09:29:38 +00:00
Celia Chen
0dd822264a [app-server-test-client] add send-followup-v2 (#7271)
Add a new endpoint that allows us to test multi-turn behavior.

Tested with running:
```
RUST_LOG=codex_app_server=debug CODEX_BIN=target/debug/codex \
      cargo run -p codex-app-server-test-client -- \
      send-follow-up-v2 "hello" "and now a follow-up question"
```
2025-11-25 08:04:27 +00:00
Clifford Ressel
3308dc5e48 fix: Correct the stream error message (#7266)
Fixes a copy paste bug with the error handling in  `try_run_turn`

I have read the CLA Document and I hereby sign the CLA
2025-11-24 20:16:29 -08:00
jif-oai
fc2ff624ac fix: don't store early exit sessions (#7263) 2025-11-24 21:14:24 +00:00
jif-oai
b897880378 chore: dedup unified exec "waited" rendering (#7256)
From
<img width="477" height="283" alt="Screenshot 2025-11-24 at 18 02 25"
src="https://github.com/user-attachments/assets/724d1d68-c994-417e-9859-ada8eb173b4c"
/>
To
<img width="444" height="174" alt="Screenshot 2025-11-24 at 18 02 40"
src="https://github.com/user-attachments/assets/40f91247-6d55-4428-84d1-f39c912ac2e7"
/>
2025-11-24 20:45:40 +00:00
iceweasel-oai
99e5340c54 Windows Sandbox: treat <workspace_root>/.git as read-only in workspace-write mode (#7142)
this functionality is
[supported](https://github.com/openai/codex/blob/main/codex-rs/protocol/src/protocol.rs#L421-L422)
in the MacOs sandbox as well. Adding it to Windows for parity

This PR also changes `rust-ci.yaml` to work around a github `hashFiles`
issue. Others have done something
[similar](https://github.com/openai/superassistant/pull/32156) today
2025-11-24 12:41:21 -08:00
Josh McKinney
ec49b56874 chore: add cargo-deny configuration (#7119)
- add GitHub workflow running cargo-deny on push/PR
- document cargo-deny allowlist with workspace-dep notes and advisory
ignores
- align workspace crates to inherit version/edition/license for
consistent checks
2025-11-24 12:22:18 -08:00
Josh McKinney
4ed4c73d6b chore(ci): add cargo audit workflow and policy (#7108)
- add to ignore current unmaintained advisories (derivative, fxhash,
paste) so audits gate new issues only
- introduce GitHub Actions workflow to run on push/PR using to install
cargo-audit

Existing advisories (all "unmaintained"):
- https://rustsec.org/advisories/RUSTSEC-2024-0388
- https://rustsec.org/advisories/RUSTSEC-2025-0057
- https://rustsec.org/advisories/RUSTSEC-2024-0436
2025-11-24 12:20:55 -08:00
Priyadarshini Tamilselvan
523dabc1ee fix: custom prompt expansion with large pastes (#7154)
### **Summary of Changes**

**What?**
Fix for slash commands (e.g., /prompts:code-review) not being recognized
when large content (>3000 chars) is pasted.
[Bug Report](https://github.com/openai/codex/issues/7047)
**Why?**
With large pastes, slash commands were ignored, so custom prompts
weren't expanded and were submitted as literal text.

**How?**
Refactored the early return block in handle_key_event_without_popup
(lines 957-968).
Instead of returning early after replacing placeholders, the code now
replaces placeholders in the textarea and continues to the normal
submission flow.
This reuses the existing slash command detection and custom prompt
expansion logic (lines 981-1047), avoiding duplication.

**Changes:**
Modified codex-rs/tui/src/bottom_pane/chat_composer.rs: refactored early
return block to continue to normal flow instead of returning immediately
Added test: custom_prompt_with_large_paste_expands_correctly to verify
the fix

**Code Quality:**
No lint warnings
Code follows existing patterns and reuses existing logic
Atomic change focused on the bug fix
2025-11-24 12:18:32 -08:00
Gabriel Peal
3741f387e9 Allow enterprises to skip upgrade checks and messages (#7213)
This is a feature primarily for enterprises who centrally manage Codex
updates.
2025-11-24 15:04:49 -05:00
Eric Traut
2c885edefa Added alternate form of dependabot to CLA allow list (#7260)
This fixes the CI check for CLA, which recently started to flag the
GitHub dependabot as "not having signed the CLA".
2025-11-24 12:04:27 -08:00
dependabot[bot]
f6f4c8f5ee chore(deps): bump webbrowser from 1.0.5 to 1.0.6 in /codex-rs (#7225)
Bumps [webbrowser](https://github.com/amodm/webbrowser-rs) from 1.0.5 to
1.0.6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/amodm/webbrowser-rs/releases">webbrowser's
releases</a>.</em></p>
<blockquote>
<h2>v1.0.6</h2>
<h3>Fixed</h3>
<ul>
<li>Windows: fix browser opening when unicode characters exist in path.
See PR <a
href="https://redirect.github.com/amodm/webbrowser-rs/issues/108">#108</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/amodm/webbrowser-rs/blob/main/CHANGELOG.md">webbrowser's
changelog</a>.</em></p>
<blockquote>
<h2>[1.0.6] - 2025-10-15 <!-- raw HTML omitted --><!-- raw HTML omitted
--></h2>
<h3>Fixed</h3>
<ul>
<li>Windows: fix browser opening when unicode characters exist in path.
See PR <a
href="https://redirect.github.com/amodm/webbrowser-rs/issues/108">#108</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="659622914a"><code>6596229</code></a>
Release v1.0.6 [skip ci]</li>
<li><a
href="44908ca5f3"><code>44908ca</code></a>
ios: fix lint for objc2 invocation #build-ios</li>
<li><a
href="b76a217a07"><code>b76a217</code></a>
Merge branch 'Nodeigi-fix/107'</li>
<li><a
href="ee2b1cdf2e"><code>ee2b1cd</code></a>
fix opening a browser that is located in a path that contains unicode
characters</li>
<li><a
href="061e65e6b8"><code>061e65e</code></a>
ios: fix lints</li>
<li><a
href="85dd4a37fc"><code>85dd4a3</code></a>
macos: fix lints</li>
<li>See full diff in <a
href="https://github.com/amodm/webbrowser-rs/compare/v1.0.5...v1.0.6">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=webbrowser&package-manager=cargo&previous-version=1.0.5&new-version=1.0.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Eric Traut <etraut@openai.com>
2025-11-24 12:03:10 -08:00
dependabot[bot]
1875700220 chore(deps): bump libc from 0.2.175 to 0.2.177 in /codex-rs (#7224)
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.175 to 0.2.177.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/rust-lang/libc/releases">libc's
releases</a>.</em></p>
<blockquote>
<h2>0.2.177</h2>
<h3>Added</h3>
<ul>
<li>Apple: Add <code>TIOCGETA</code>, <code>TIOCSETA</code>,
<code>TIOCSETAW</code>, <code>TIOCSETAF</code> constants (<a
href="https://redirect.github.com/rust-lang/libc/pull/4736">#4736</a>)</li>
<li>Apple: Add <code>pthread_cond_timedwait_relative_np</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4719">#4719</a>)</li>
<li>BSDs: Add <code>_CS_PATH</code> constant (<a
href="https://redirect.github.com/rust-lang/libc/pull/4738">#4738</a>)</li>
<li>Linux-like: Add <code>SIGEMT</code> for mips* and sparc*
architectures (<a
href="https://redirect.github.com/rust-lang/libc/pull/4730">#4730</a>)</li>
<li>OpenBSD: Add <code>elf_aux_info</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4729">#4729</a>)</li>
<li>Redox: Add more sysconf constants (<a
href="https://redirect.github.com/rust-lang/libc/pull/4728">#4728</a>)</li>
<li>Windows: Add <code>wcsnlen</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4721">#4721</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>WASIP2: Invert conditional to include p2 APIs (<a
href="https://redirect.github.com/rust-lang/libc/pull/4733">#4733</a>)</li>
</ul>
<h2>0.2.176</h2>
<h3>Support</h3>
<ul>
<li>The default FreeBSD version has been raised from 11 to 12. This
matches <code>rustc</code> since 1.78. (<a
href="https://redirect.github.com/rust-lang/libc/pull/2406">#2406</a>)</li>
<li><code>Debug</code> is now always implemented, rather than being
gated behind the <code>extra_traits</code> feature. (<a
href="https://redirect.github.com/rust-lang/libc/pull/4624">#4624</a>)</li>
</ul>
<h3>Added</h3>
<ul>
<li>AIX: Restore some non-POSIX functions guarded by the
<code>_KERNEL</code> macro. (<a
href="https://redirect.github.com/rust-lang/libc/pull/4607">#4607</a>)</li>
<li>FreeBSD 14: Add <code>st_fileref</code> to <code>struct stat</code>
(<a
href="https://redirect.github.com/rust-lang/libc/pull/4642">#4642</a>)</li>
<li>Haiku: Add the <code>accept4</code> POSIX call (<a
href="https://redirect.github.com/rust-lang/libc/pull/4586">#4586</a>)</li>
<li>Introduce a wrapper for representing padding (<a
href="https://redirect.github.com/rust-lang/libc/pull/4632">#4632</a>)</li>
<li>Linux: Add <code>EM_RISCV</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4659">#4659</a>)</li>
<li>Linux: Add <code>MS_NOSYMFOLLOW</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4389">#4389</a>)</li>
<li>Linux: Add <code>backtrace_symbols(_fd)</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4668">#4668</a>)</li>
<li>Linux: Add missing <code>SOL_PACKET</code> optnames (<a
href="https://redirect.github.com/rust-lang/libc/pull/4669">#4669</a>)</li>
<li>Musl s390x: Add <code>SYS_mseal</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4549">#4549</a>)</li>
<li>NuttX: Add <code>__errno</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4687">#4687</a>)</li>
<li>Redox: Add <code>dirfd</code>, <code>VDISABLE</code>, and resource
consts (<a
href="https://redirect.github.com/rust-lang/libc/pull/4660">#4660</a>)</li>
<li>Redox: Add more <code>resource.h</code>, <code>fcntl.h</code>
constants (<a
href="https://redirect.github.com/rust-lang/libc/pull/4666">#4666</a>)</li>
<li>Redox: Enable <code>strftime</code> and <code>mkostemp[s]</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4629">#4629</a>)</li>
<li>Unix, Windows: Add <code>qsort_r</code> (Unix), and
<code>qsort(_s)</code> (Windows) (<a
href="https://redirect.github.com/rust-lang/libc/pull/4677">#4677</a>)</li>
<li>Unix: Add <code>dlvsym</code> for Linux-gnu, FreeBSD, and NetBSD (<a
href="https://redirect.github.com/rust-lang/libc/pull/4671">#4671</a>)</li>
<li>Unix: Add <code>sigqueue</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4620">#4620</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>FreeBSD 15: Mark <code>kinfo_proc</code> as non-exhaustive (<a
href="https://redirect.github.com/rust-lang/libc/pull/4553">#4553</a>)</li>
<li>FreeBSD: Set the ELF symbol version for <code>readdir_r</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4694">#4694</a>)</li>
<li>Linux: Correct the config for whether or not
<code>epoll_event</code> is packed (<a
href="https://redirect.github.com/rust-lang/libc/pull/4639">#4639</a>)</li>
<li>Tests: Replace the old <code>ctest</code> with the much more
reliable new implementation (<a
href="https://redirect.github.com/rust-lang/libc/pull/4655">#4655</a>
and many related PRs)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>AIX: Fix the type of the 4th arguement of <code>getgrnam_r</code>
([#4656](<a
href="https://redirect.github.com/rust-lang/libc/pull/4656">rust-lang/libc#4656</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/rust-lang/libc/blob/0.2.177/CHANGELOG.md">libc's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/rust-lang/libc/compare/0.2.176...0.2.177">0.2.177</a>
- 2025-10-09</h2>
<h3>Added</h3>
<ul>
<li>Apple: Add <code>TIOCGETA</code>, <code>TIOCSETA</code>,
<code>TIOCSETAW</code>, <code>TIOCSETAF</code> constants (<a
href="https://redirect.github.com/rust-lang/libc/pull/4736">#4736</a>)</li>
<li>Apple: Add <code>pthread_cond_timedwait_relative_np</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4719">#4719</a>)</li>
<li>BSDs: Add <code>_CS_PATH</code> constant (<a
href="https://redirect.github.com/rust-lang/libc/pull/4738">#4738</a>)</li>
<li>Linux-like: Add <code>SIGEMT</code> for mips* and sparc*
architectures (<a
href="https://redirect.github.com/rust-lang/libc/pull/4730">#4730</a>)</li>
<li>OpenBSD: Add <code>elf_aux_info</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4729">#4729</a>)</li>
<li>Redox: Add more sysconf constants (<a
href="https://redirect.github.com/rust-lang/libc/pull/4728">#4728</a>)</li>
<li>Windows: Add <code>wcsnlen</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4721">#4721</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>WASIP2: Invert conditional to include p2 APIs (<a
href="https://redirect.github.com/rust-lang/libc/pull/4733">#4733</a>)</li>
</ul>
<h2><a
href="https://github.com/rust-lang/libc/compare/0.2.175...0.2.176">0.2.176</a>
- 2025-09-23</h2>
<h3>Support</h3>
<ul>
<li>The default FreeBSD version has been raised from 11 to 12. This
matches <code>rustc</code> since 1.78. (<a
href="https://redirect.github.com/rust-lang/libc/pull/2406">#2406</a>)</li>
<li><code>Debug</code> is now always implemented, rather than being
gated behind the <code>extra_traits</code> feature. (<a
href="https://redirect.github.com/rust-lang/libc/pull/4624">#4624</a>)</li>
</ul>
<h3>Added</h3>
<ul>
<li>AIX: Restore some non-POSIX functions guarded by the
<code>_KERNEL</code> macro. (<a
href="https://redirect.github.com/rust-lang/libc/pull/4607">#4607</a>)</li>
<li>FreeBSD 14: Add <code>st_fileref</code> to <code>struct stat</code>
(<a
href="https://redirect.github.com/rust-lang/libc/pull/4642">#4642</a>)</li>
<li>Haiku: Add the <code>accept4</code> POSIX call (<a
href="https://redirect.github.com/rust-lang/libc/pull/4586">#4586</a>)</li>
<li>Introduce a wrapper for representing padding (<a
href="https://redirect.github.com/rust-lang/libc/pull/4632">#4632</a>)</li>
<li>Linux: Add <code>EM_RISCV</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4659">#4659</a>)</li>
<li>Linux: Add <code>MS_NOSYMFOLLOW</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4389">#4389</a>)</li>
<li>Linux: Add <code>backtrace_symbols(_fd)</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4668">#4668</a>)</li>
<li>Linux: Add missing <code>SOL_PACKET</code> optnames (<a
href="https://redirect.github.com/rust-lang/libc/pull/4669">#4669</a>)</li>
<li>Musl s390x: Add <code>SYS_mseal</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4549">#4549</a>)</li>
<li>NuttX: Add <code>__errno</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4687">#4687</a>)</li>
<li>Redox: Add <code>dirfd</code>, <code>VDISABLE</code>, and resource
consts (<a
href="https://redirect.github.com/rust-lang/libc/pull/4660">#4660</a>)</li>
<li>Redox: Add more <code>resource.h</code>, <code>fcntl.h</code>
constants (<a
href="https://redirect.github.com/rust-lang/libc/pull/4666">#4666</a>)</li>
<li>Redox: Enable <code>strftime</code> and <code>mkostemp[s]</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4629">#4629</a>)</li>
<li>Unix, Windows: Add <code>qsort_r</code> (Unix), and
<code>qsort(_s)</code> (Windows) (<a
href="https://redirect.github.com/rust-lang/libc/pull/4677">#4677</a>)</li>
<li>Unix: Add <code>dlvsym</code> for Linux-gnu, FreeBSD, and NetBSD (<a
href="https://redirect.github.com/rust-lang/libc/pull/4671">#4671</a>)</li>
<li>Unix: Add <code>sigqueue</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4620">#4620</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>FreeBSD 15: Mark <code>kinfo_proc</code> as non-exhaustive (<a
href="https://redirect.github.com/rust-lang/libc/pull/4553">#4553</a>)</li>
<li>FreeBSD: Set the ELF symbol version for <code>readdir_r</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4694">#4694</a>)</li>
<li>Linux: Correct the config for whether or not
<code>epoll_event</code> is packed (<a
href="https://redirect.github.com/rust-lang/libc/pull/4639">#4639</a>)</li>
<li>Tests: Replace the old <code>ctest</code> with the much more
reliable new implementation (<a
href="https://redirect.github.com/rust-lang/libc/pull/4655">#4655</a>
and many related PRs)</li>
</ul>
<h3>Fixed</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="9f598d245e"><code>9f598d2</code></a>
chore: release libc 0.2.177</li>
<li><a
href="329a5e77fd"><code>329a5e7</code></a>
Add missing TIOCGETA/TIOCSETA constants for macOS</li>
<li><a
href="72a40e2550"><code>72a40e2</code></a>
add <code>pthread_cond_timedwait_relative_np</code></li>
<li><a
href="2914d6f735"><code>2914d6f</code></a>
linux_like: add SIGEMT for mips* and sparc*</li>
<li><a
href="ff2ff25f15"><code>ff2ff25</code></a>
openbsd add elf_aux_info</li>
<li><a
href="4ae44a4494"><code>4ae44a4</code></a>
Update semver tests</li>
<li><a
href="d5737a0137"><code>d5737a0</code></a>
Define _CS_PATH on the BSDs</li>
<li><a
href="fe277da53e"><code>fe277da</code></a>
redox: more sysconf constants</li>
<li><a
href="bdad4264ce"><code>bdad426</code></a>
wasip2: Invert conditional to include p2 APIs</li>
<li><a
href="0af069dcbf"><code>0af069d</code></a>
Windows: add <code>wcsnlen</code></li>
<li>Additional commits viewable in <a
href="https://github.com/rust-lang/libc/compare/0.2.175...0.2.177">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=libc&package-manager=cargo&previous-version=0.2.175&new-version=0.2.177)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Eric Traut <etraut@openai.com>
2025-11-24 12:02:48 -08:00
Eric Traut
207d94b0e7 Removed streamable_shell from docs (#7235)
This config option no longer exists

Addresses #7207
2025-11-24 11:47:57 -08:00
Thomas Klausner
481c84e118 fix: Fix build process-hardening build on NetBSD (#7238)
This fixes the build of codex on NetBSD.
2025-11-24 11:46:43 -08:00
dependabot[bot]
6dd3606773 chore(deps): bump actions/checkout from 5 to 6 (#7230)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to
6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v6.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update README to include Node.js 24 support details and requirements
by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
<li>Persist creds to a separate file by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>v6-beta by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2298">actions/checkout#2298</a></li>
<li>update readme/changelog for v6 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2311">actions/checkout#2311</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v5.0.0...v6.0.0">https://github.com/actions/checkout/compare/v5.0.0...v6.0.0</a></p>
<h2>v6-beta</h2>
<h2>What's Changed</h2>
<p>Updated persist-credentials to store the credentials under
<code>$RUNNER_TEMP</code> instead of directly in the local git
config.</p>
<p>This requires a minimum Actions Runner version of <a
href="https://github.com/actions/runner/releases/tag/v2.329.0">v2.329.0</a>
to access the persisted credentials for <a
href="https://docs.github.com/en/actions/tutorials/use-containerized-services/create-a-docker-container-action">Docker
container action</a> scenarios.</p>
<h2>v5.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Port v6 cleanup to v5 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v5...v5.0.1">https://github.com/actions/checkout/compare/v5...v5.0.1</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>V6.0.0</h2>
<ul>
<li>Persist creds to a separate file by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>Update README to include Node.js 24 support details and requirements
by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
</ul>
<h2>V5.0.1</h2>
<ul>
<li>Port v6 cleanup to v5 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<h2>V5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>V4.3.1</h2>
<ul>
<li>Port v6 cleanup to v4 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2305">actions/checkout#2305</a></li>
</ul>
<h2>V4.3.0</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<h2>v4.2.0</h2>
<ul>
<li>Add Ref and Commit outputs by <a
href="https://github.com/lucacome"><code>@​lucacome</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1180">actions/checkout#1180</a></li>
<li>Dependency updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>- <a
href="https://redirect.github.com/actions/checkout/pull/1777">actions/checkout#1777</a>,
<a
href="https://redirect.github.com/actions/checkout/pull/1872">actions/checkout#1872</a></li>
</ul>
<h2>v4.1.7</h2>
<ul>
<li>Bump the minor-npm-dependencies group across 1 directory with 4
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1739">actions/checkout#1739</a></li>
<li>Bump actions/checkout from 3 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1697">actions/checkout#1697</a></li>
<li>Check out other refs/* by commit by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1774">actions/checkout#1774</a></li>
<li>Pin actions/checkout's own workflows to a known, good, stable
version. by <a href="https://github.com/jww3"><code>@​jww3</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1776">actions/checkout#1776</a></li>
</ul>
<h2>v4.1.6</h2>
<ul>
<li>Check platform to set archive extension appropriately by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1732">actions/checkout#1732</a></li>
</ul>
<h2>v4.1.5</h2>
<ul>
<li>Update NPM dependencies by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1703">actions/checkout#1703</a></li>
<li>Bump github/codeql-action from 2 to 3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1694">actions/checkout#1694</a></li>
<li>Bump actions/setup-node from 1 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1696">actions/checkout#1696</a></li>
<li>Bump actions/upload-artifact from 2 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1695">actions/checkout#1695</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="1af3b93b68"><code>1af3b93</code></a>
update readme/changelog for v6 (<a
href="https://redirect.github.com/actions/checkout/issues/2311">#2311</a>)</li>
<li><a
href="71cf2267d8"><code>71cf226</code></a>
v6-beta (<a
href="https://redirect.github.com/actions/checkout/issues/2298">#2298</a>)</li>
<li><a
href="069c695914"><code>069c695</code></a>
Persist creds to a separate file (<a
href="https://redirect.github.com/actions/checkout/issues/2286">#2286</a>)</li>
<li><a
href="ff7abcd0c3"><code>ff7abcd</code></a>
Update README to include Node.js 24 support details and requirements (<a
href="https://redirect.github.com/actions/checkout/issues/2248">#2248</a>)</li>
<li>See full diff in <a
href="https://github.com/actions/checkout/compare/v5...v6">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=5&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 11:45:57 -08:00
dependabot[bot]
35850caff6 chore(deps): bump actions/upload-artifact from 4 to 5 (#7229)
Bumps
[actions/upload-artifact](https://github.com/actions/upload-artifact)
from 4 to 5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/upload-artifact/releases">actions/upload-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v5.0.0</h2>
<h2>What's Changed</h2>
<p><strong>BREAKING CHANGE:</strong> this update supports Node
<code>v24.x</code>. This is not a breaking change per-se but we're
treating it as such.</p>
<ul>
<li>Update README.md by <a
href="https://github.com/GhadimiR"><code>@​GhadimiR</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/681">actions/upload-artifact#681</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/712">actions/upload-artifact#712</a></li>
<li>Readme: spell out the first use of GHES by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/upload-artifact/pull/727">actions/upload-artifact#727</a></li>
<li>Update GHES guidance to include reference to Node 20 version by <a
href="https://github.com/patrikpolyak"><code>@​patrikpolyak</code></a>
in <a
href="https://redirect.github.com/actions/upload-artifact/pull/725">actions/upload-artifact#725</a></li>
<li>Bump <code>@actions/artifact</code> to <code>v4.0.0</code></li>
<li>Prepare <code>v5.0.0</code> by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/upload-artifact/pull/734">actions/upload-artifact#734</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/GhadimiR"><code>@​GhadimiR</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/681">actions/upload-artifact#681</a></li>
<li><a href="https://github.com/nebuk89"><code>@​nebuk89</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/712">actions/upload-artifact#712</a></li>
<li><a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/727">actions/upload-artifact#727</a></li>
<li><a
href="https://github.com/patrikpolyak"><code>@​patrikpolyak</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/725">actions/upload-artifact#725</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v4...v5.0.0">https://github.com/actions/upload-artifact/compare/v4...v5.0.0</a></p>
<h2>v4.6.2</h2>
<h2>What's Changed</h2>
<ul>
<li>Update to use artifact 2.3.2 package &amp; prepare for new
upload-artifact release by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/685">actions/upload-artifact#685</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/685">actions/upload-artifact#685</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v4...v4.6.2">https://github.com/actions/upload-artifact/compare/v4...v4.6.2</a></p>
<h2>v4.6.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Update to use artifact 2.2.2 package by <a
href="https://github.com/yacaovsnc"><code>@​yacaovsnc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/673">actions/upload-artifact#673</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v4...v4.6.1">https://github.com/actions/upload-artifact/compare/v4...v4.6.1</a></p>
<h2>v4.6.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Expose env vars to control concurrency and timeout by <a
href="https://github.com/yacaovsnc"><code>@​yacaovsnc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/662">actions/upload-artifact#662</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v4...v4.6.0">https://github.com/actions/upload-artifact/compare/v4...v4.6.0</a></p>
<h2>v4.5.0</h2>
<h2>What's Changed</h2>
<ul>
<li>fix: deprecated <code>Node.js</code> version in action by <a
href="https://github.com/hamirmahal"><code>@​hamirmahal</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/578">actions/upload-artifact#578</a></li>
<li>Add new <code>artifact-digest</code> output by <a
href="https://github.com/bdehamer"><code>@​bdehamer</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/656">actions/upload-artifact#656</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/hamirmahal"><code>@​hamirmahal</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/578">actions/upload-artifact#578</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="330a01c490"><code>330a01c</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/734">#734</a>
from actions/danwkennedy/prepare-5.0.0</li>
<li><a
href="03f2824452"><code>03f2824</code></a>
Update <code>github.dep.yml</code></li>
<li><a
href="905a1ecb59"><code>905a1ec</code></a>
Prepare <code>v5.0.0</code></li>
<li><a
href="2d9f9cdfa9"><code>2d9f9cd</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/725">#725</a>
from patrikpolyak/patch-1</li>
<li><a
href="9687587dec"><code>9687587</code></a>
Merge branch 'main' into patch-1</li>
<li><a
href="2848b2cda0"><code>2848b2c</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/727">#727</a>
from danwkennedy/patch-1</li>
<li><a
href="9b511775fd"><code>9b51177</code></a>
Spell out the first use of GHES</li>
<li><a
href="cd231ca1ed"><code>cd231ca</code></a>
Update GHES guidance to include reference to Node 20 version</li>
<li><a
href="de65e23aa2"><code>de65e23</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/712">#712</a>
from actions/nebuk89-patch-1</li>
<li><a
href="8747d8cd76"><code>8747d8c</code></a>
Update README.md</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/upload-artifact/compare/v4...v5">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=4&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 11:45:29 -08:00
dependabot[bot]
e8b6fb7937 chore(deps): bump toml_edit from 0.23.4 to 0.23.5 in /codex-rs (#7223)
Bumps [toml_edit](https://github.com/toml-rs/toml) from 0.23.4 to
0.23.5.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="4695fb02fc"><code>4695fb0</code></a>
chore: Release</li>
<li><a
href="6a77ed71cf"><code>6a77ed7</code></a>
docs: Update changelog</li>
<li><a
href="c1e8197964"><code>c1e8197</code></a>
refactor: Switch serde dependency to serde_core (<a
href="https://redirect.github.com/toml-rs/toml/issues/1036">#1036</a>)</li>
<li><a
href="d85d6cd61c"><code>d85d6cd</code></a>
refactor: Switch serde dependency to serde_core</li>
<li>See full diff in <a
href="https://github.com/toml-rs/toml/compare/v0.23.4...v0.23.5">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=toml_edit&package-manager=cargo&previous-version=0.23.4&new-version=0.23.5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 11:44:14 -08:00
dependabot[bot]
be022be381 chore(deps): bump regex from 1.11.1 to 1.12.2 in /codex-rs (#7222)
Bumps [regex](https://github.com/rust-lang/regex) from 1.11.1 to 1.12.2.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/rust-lang/regex/blob/master/CHANGELOG.md">regex's
changelog</a>.</em></p>
<blockquote>
<h1>1.12.2 (2025-10-13)</h1>
<p>This release fixes a <code>cargo doc</code> breakage on nightly when
<code>--cfg docsrs</code> is
enabled. This caused documentation to fail to build on docs.rs.</p>
<p>Bug fixes:</p>
<ul>
<li>[BUG <a
href="https://redirect.github.com/rust-lang/regex/issues/1305">#1305</a>](<a
href="https://redirect.github.com/rust-lang/regex/issues/1305">rust-lang/regex#1305</a>):
Switches the <code>doc_auto_cfg</code> feature to <code>doc_cfg</code>
on nightly for docs.rs builds.</li>
</ul>
<h1>1.12.1 (2025-10-10)</h1>
<p>This release makes a bug fix in the new
<code>regex::Captures::get_match</code> API
introduced in <code>1.12.0</code>. There was an oversight with the
lifetime parameter
for the <code>Match</code> returned. This is technically a breaking
change, but given
that it was caught almost immediately and I've yanked the
<code>1.12.0</code> release,
I think this is fine.</p>
<h1>1.12.0 (2025-10-10)</h1>
<p>This release contains a smattering of bug fixes, a fix for excessive
memory
consumption in some cases and a new
<code>regex::Captures::get_match</code> API.</p>
<p>Improvements:</p>
<ul>
<li>[FEATURE <a
href="https://redirect.github.com/rust-lang/regex/issues/1146">#1146</a>](<a
href="https://redirect.github.com/rust-lang/regex/issues/1146">rust-lang/regex#1146</a>):
Add <code>Capture::get_match</code> for returning the overall match
without <code>unwrap()</code>.</li>
</ul>
<p>Bug fixes:</p>
<ul>
<li>[BUG <a
href="https://redirect.github.com/rust-lang/regex/issues/1083">#1083</a>](<a
href="https://redirect.github.com/rust-lang/regex/issues/1083">rust-lang/regex#1083</a>):
Fixes a panic in the lazy DFA (can only occur for especially large
regexes).</li>
<li>[BUG <a
href="https://redirect.github.com/rust-lang/regex/issues/1116">#1116</a>](<a
href="https://redirect.github.com/rust-lang/regex/issues/1116">rust-lang/regex#1116</a>):
Fixes a memory usage regression for large regexes (introduced in
<code>regex 1.9</code>).</li>
<li>[BUG <a
href="https://redirect.github.com/rust-lang/regex/issues/1195">#1195</a>](<a
href="https://redirect.github.com/rust-lang/regex/issues/1195">rust-lang/regex#1195</a>):
Fix universal start states in sparse DFA.</li>
<li>[BUG <a
href="https://redirect.github.com/rust-lang/regex/issues/1295">#1295</a>](<a
href="https://redirect.github.com/rust-lang/regex/pull/1295">rust-lang/regex#1295</a>):
Fixes a panic when deserializing a corrupted dense DFA.</li>
<li><a
href="8f5d9479d0">BUG
8f5d9479</a>:
Make <code>regex_automata::meta::Regex::find</code> consistently return
<code>None</code> when
<code>WhichCaptures::None</code> is used.</li>
</ul>
<h1>1.11.3 (2025-09-25)</h1>
<p>This is a small patch release with an improvement in memory usage in
some
cases.</p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="5ea3eb1e95"><code>5ea3eb1</code></a>
1.12.2</li>
<li><a
href="ab0b07171b"><code>ab0b071</code></a>
regex-automata-0.4.13</li>
<li><a
href="691d51457d"><code>691d514</code></a>
regex-syntax-0.8.8</li>
<li><a
href="1dd9077779"><code>1dd9077</code></a>
docs: swap <code>doc_auto_cfg</code> with <code>doc_cfg</code></li>
<li><a
href="0089034cb3"><code>0089034</code></a>
regex-cli-0.2.3</li>
<li><a
href="140f8949da"><code>140f894</code></a>
regex-lite-0.1.8</li>
<li><a
href="27d6d65263"><code>27d6d65</code></a>
1.12.1</li>
<li><a
href="85398ad500"><code>85398ad</code></a>
changelog: 1.12.1</li>
<li><a
href="764efbd305"><code>764efbd</code></a>
api: tweak the lifetime of <code>Captures::get_match</code></li>
<li><a
href="ee6aa55e01"><code>ee6aa55</code></a>
rure-0.2.4</li>
<li>Additional commits viewable in <a
href="https://github.com/rust-lang/regex/compare/1.11.1...1.12.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=regex&package-manager=cargo&previous-version=1.11.1&new-version=1.12.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 11:43:57 -08:00
Dylan Hurd
1e832b1438 fix(windows) support apply_patch parsing in powershell (#7221)
## Summary
Support powershell parsing of apply_patch

## Testing
- [x] Enable apply_patch unit tests

---------

Co-authored-by: jif-oai <jif@openai.com>
2025-11-24 19:32:47 +00:00
Matthew Zeng
c31663d745 [feedback] Add source info into feedback metadata. (#7140)
Verified the source info is correctly attached based on whether it's cli
or vscode.
2025-11-24 19:05:37 +00:00
jif-oai
35d89e820f fix: flaky test (#7257) 2025-11-24 18:45:41 +00:00
iceweasel-oai
486b1c4d9d consolidate world-writable-directories scanning. (#7234)
clean up the code for scanning for world writable directories

One path (selecting a sandbox mode from /approvals) was using an
incorrect method that did not use the new method of creating deny aces
to prevent writing to those directories. Now all paths are the same.
2025-11-24 09:51:58 -08:00
jif-oai
b2cddec3d7 feat: unified exec basic pruning strategy (#7239)
LRU + exited sessions first
2025-11-24 17:22:32 +00:00
jif-oai
920239f272 fix: codex delegate cancellation (#7092) 2025-11-24 16:59:09 +00:00
jif-oai
99bcb90353 chore: use proxy for encrypted summary (#7252) 2025-11-24 16:51:47 +00:00
Ahmed Ibrahim
b519267d05 Account for encrypted reasoning for auto compaction (#7113)
- The total token used returned from the api doesn't account for the
reasoning items before the assistant message
- Account for those for auto compaction
- Add the encrypted reasoning effort in the common tests utils
- Add a test to make sure it works as expected
2025-11-22 03:06:45 +00:00
zhao-oai
529eb4ff2a move execpolicy quickstart (#7127) 2025-11-21 19:13:51 -05:00
Michael Bolin
c6f68c9df8 feat: declare server capability in shell-tool-mcp (#7112)
This introduces a new feature to Codex when it operates as an MCP
_client_ where if an MCP _server_ replies that it has an entry named
`"codex/sandbox-state"` in its _server capabilities_, then Codex will
send it an MCP notification with the following structure:

```json
{
  "method": "codex/sandbox-state/update",
  "params": {
    "sandboxPolicy": {
      "type": "workspace-write",
      "network-access": false,
      "exclude-tmpdir-env-var": false
      "exclude-slash-tmp": false
    },
    "codexLinuxSandboxExe": null,
    "sandboxCwd": "/Users/mbolin/code/codex2"
  }
}
```

or with whatever values are appropriate for the initial `sandboxPolicy`.

**NOTE:** Codex _should_ continue to send the MCP server notifications
of the same format if these things change over the lifetime of the
thread, but that isn't wired up yet.

The result is that `shell-tool-mcp` can consume these values so that
when it calls `codex_core::exec::process_exec_tool_call()` in
`codex-rs/exec-server/src/posix/escalate_server.rs`, it is now sure to
call it with the correct values (whereas previously we relied on
hardcoded values).

While I would argue this is a supported use case within the MCP
protocol, the `rmcp` crate that we are using today does not support
custom notifications. As such, I had to patch it and I submitted it for
review, so hopefully it will be accepted in some form:

https://github.com/modelcontextprotocol/rust-sdk/pull/556

To test out this change from end-to-end:

- I ran `cargo build` in `~/code/codex2/codex-rs/exec-server`
- I built the fork of Bash in `~/code/bash/bash`
- I added the following to my `~/.codex/config.toml`:

```toml
# Use with `codex --disable shell_tool`.
[mcp_servers.execshell]
args = ["--bash", "/Users/mbolin/code/bash/bash"]
command = "/Users/mbolin/code/codex2/codex-rs/target/debug/codex-exec-mcp-server"
```

- From `~/code/codex2/codex-rs`, I ran `just codex --disable shell_tool`
- When the TUI started up, I verified that the sandbox mode is
`workspace-write`
- I ran `/mcp` to verify that the shell tool from the MCP is there:

<img width="1387" height="1400" alt="image"
src="https://github.com/user-attachments/assets/1a8addcc-5005-4e16-b59f-95cfd06fd4ab"
/>

- Then I asked it:

> what is the output of `gh issue list`

because this should be auto-approved with our existing dummy policy:


af63e6eccc/codex-rs/exec-server/src/posix.rs (L157-L164)

And it worked:

<img width="1387" height="1400" alt="image"
src="https://github.com/user-attachments/assets/7568d2f7-80da-4d68-86d0-c265a6f5e6c1"
/>
2025-11-21 16:11:01 -08:00
Michael Bolin
af63e6eccc fix: start publishing @openai/codex-shell-tool-mcp to npm (#7123)
Start publishing as part of the normal release process.
2025-11-21 15:03:50 -08:00
zhao-oai
87b211709e bypass sandbox for policy approved commands (#7110)
allowing cmds greenlit by execpolicy to bypass sandbox + minor refactor
for a world where we have execpolicy rules with specific sandbox
requirements
2025-11-21 18:03:23 -05:00
Michael Bolin
67975ed33a refactor: inline sandbox type lookup in process_exec_tool_call (#7122)
`process_exec_tool_call()` was taking `SandboxType` as a param, but in
practice, the only place it was constructed was in
`codex_message_processor.rs` where it was derived from the other
`sandbox_policy` param, so this PR inlines the logic that decides the
`SandboxType` into `process_exec_tool_call()`.



---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/7122).
* #7112
* __->__ #7122
2025-11-21 22:53:05 +00:00
Jeremy Rose
7561a6aaf0 support MCP elicitations (#6947)
No support for request schema yet, but we'll at least show the message
and allow accept/decline.

<img width="823" height="551" alt="Screenshot 2025-11-21 at 2 44 05 PM"
src="https://github.com/user-attachments/assets/6fbb892d-ca12-4765-921e-9ac4b217534d"
/>
2025-11-21 14:44:53 -08:00
Josh McKinney
3ea33a0616 fix(tui): Fail when stdin is not a terminal (#6382)
Piping to codex fails to do anything useful and locks up the process.
We currently check for stdout, but not stdin

```
❯ echo foo|just c
cargo run --bin codex -- "$@"
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.21s
     Running `target/debug/codex`
Error: stdin is not a terminal
error: Recipe `codex` failed on line 10 with exit code 1
```
2025-11-21 14:17:40 -08:00
Michael Bolin
e8ef6d3c16 feat: support login as an option on shell-tool-mcp (#7120)
The unified exec tool has a `login` option that defaults to `true`:


3bdcbc7292/codex-rs/core/src/tools/handlers/unified_exec.rs (L35-L36)

This updates the `ExecParams` for `shell-tool-mcp` to support the same
parameter. Note it is declared as `Option<bool>` to ensure it is marked
optional in the generated JSON schema.
2025-11-21 22:14:41 +00:00
pakrym-oai
e52cc38dfd Use use_model (#7121) 2025-11-21 22:10:52 +00:00
iceweasel-oai
3bdcbc7292 Windows: flag some invocations that launch browsers/URLs as dangerous (#7111)
Prevent certain Powershell/cmd invocations from reaching the sandbox
when they are trying to launch a browser, or run a command with a URL,
etc.
2025-11-21 13:36:17 -08:00
Owen Lin
a0434bbdb4 [app-server] doc: approvals (#7105)
Add documentation for shell and apply_patch approvals
2025-11-21 21:27:54 +00:00
Ahmed Ibrahim
d5f661c91d enable unified exec for experiments (#7118) 2025-11-21 13:10:01 -08:00
Ahmed Ibrahim
8ecaad948b feat: Add exp model to experiment with the tools (#7115) 2025-11-21 12:44:47 -08:00
Owen Lin
aa4e0d823e [app-server] feat: expose gitInfo/cwd/etc. on Thread (#7060)
Port the new additions from https://github.com/openai/codex/pull/6337 on
the legacy API to v2. Mainly need `gitInfo` and `cwd` for VSCE.
2025-11-21 10:37:12 -08:00
Dylan Hurd
0e051644a9 fix(scripts) next_minor_version should reset patch number (#7050)
## Summary
When incrementing the minor version, we should reset patch to 0, rather
than keeping it.

## Testing
- [x] tested locally with dry_run and `get_latest_release_version`
mocked out

---------

Co-authored-by: Michael Bolin <mbolin@openai.com>
2025-11-21 10:17:12 -08:00
Michael Bolin
40d14c0756 fix: clear out duplicate entries for bash in the GitHub release (#7103)
https://github.com/openai/codex/pull/7005 introduced a new part of the
release process that added multiple files named `bash` in the `dist/`
folder used as the basis of the GitHub Release. I believe that all file
names in a GitHub Release have to be unique, which is why the recent
release build failed:

https://github.com/openai/codex/actions/runs/19577669780/job/56070183504

Based on the output of the **List** step, I believe these are the
appropriate artifacts to delete as a quick fix.
2025-11-21 09:59:30 -08:00
jif-oai
af65666561 chore: drop model_max_output_tokens (#7100) 2025-11-21 17:42:54 +00:00
Owen Lin
2ae1f81d84 [app-server] feat: add Declined status for command exec (#7101)
Add a `Declined` status for when we request an approval from the user
and the user declines. This allows us to distinguish from commands that
actually ran, but failed.

This behaves similarly to apply_patch / FileChange, which does the same
thing.
2025-11-21 09:19:39 -08:00
Michael Bolin
d363a0968e feat: codex-shell-tool-mcp (#7005)
This adds a GitHub workflow for building a new npm module we are
experimenting with that contains an MCP server for running Bash
commands. The new workflow, `shell-tool-mcp`, is a dependency of the
general `release` workflow so that we continue to use one version number
for all artifacts across the project in one GitHub release.

`.github/workflows/shell-tool-mcp.yml` is the primary workflow
introduced by this PR, which does the following:

- builds the `codex-exec-mcp-server` and `codex-execve-wrapper`
executables for both arm64 and x64 versions of Mac and Linux (preferring
the MUSL version for Linux)
- builds Bash (dynamically linked) for a [comically] large number of
platforms (both x64 and arm64 for most) with a small patch specified by
`shell-tool-mcp/patches/bash-exec-wrapper.patch`:
  - `debian-11`
  - `debian-12`
  - `ubuntu-20.04`
  - `ubuntu-22.04`
  - `ubuntu-24.04`
  - `centos-9`
  - `macos-13` (x64 only)
  - `macos-14` (arm64 only)
  - `macos-15` (arm64 only)
- builds the TypeScript for the [new] Node module declared in the
`shell-tool-mcp/` folder, which creates `bin/mcp-server.js`
- adds all of the native binaries to `shell-tool-mcp/vendor/` folder;
`bin/mcp-server.js` does a runtime check to determine which ones to
execute
- uses `npm pack` to create the `.tgz` for the module
- if `publish: true` is set, invokes the `npm publish` call with the
`.tgz`

The justification for building Bash for so many different operating
systems is because, since it is dynamically linked, we want to increase
our confidence that the version we build is compatible with the glibc
whatever OS we end up running on. (Note this is less of a concern with
`codex-exec-mcp-server` and `codex-execve-wrapper` on Linux, as they are
statically linked.)

This PR also introduces the code for the npm module in `shell-tool-mcp/`
(the proposed module name is `@openai/codex-shell-tool-mcp`). Initially,
I intended the module to be a single file of vanilla JavaScript (like
[`codex-cli/bin/codex.js`](ab5972d447/codex-cli/bin/codex.js)),
but some of the logic seemed a bit tricky, so I decided to port it to
TypeScript and add unit tests.

`shell-tool-mcp/src/index.ts` defines the `main()` function for the
module, which performs runtime checks to determine the clang triple to
find the path to the Rust executables within the `vendor/` folder
(`resolveTargetTriple()`). It uses a combination of `readOsRelease()`
and `resolveBashPath()` to determine the correct Bash executable to run
in the environment. Ultimately, it spawns a command like the following:

```
codex-exec-mcp-server \
    --execve codex-execve-wrapper \
    --bash custom-bash "$@"
```

Note `.github/workflows/shell-tool-mcp-ci.yml` defines a fairly standard
CI job for the module (`format`/`build`/`test`).

To test this PR, I pushed this branch to my personal fork of Codex and
ran the CI job there:

https://github.com/bolinfest/codex/actions/runs/19564311320

Admittedly, the graph looks a bit wild now:

<img width="5115" height="2969" alt="Screenshot 2025-11-20 at 11 44
58 PM"
src="https://github.com/user-attachments/assets/cc5ef306-efc1-4ed7-a137-5347e394f393"
/>

But when it finished, I was able to download `codex-shell-tool-mcp-npm`
from the **Artifacts** for the workflow in an empty temp directory,
unzip the `.zip` and then the `.tgz` inside it, followed by `xattr -rc
.` to remove the quarantine bits. Then I ran:

```shell
npx @modelcontextprotocol/inspector node /private/tmp/foobar4/package/bin/mcp-server.js
```

which launched the MCP Inspector and I was able to use it as expected!
This bodes well that this should work once the package is published to
npm:

```shell
npx @modelcontextprotocol/inspector npx @openai/codex-shell-tool-mcp
```

Also, to verify the package contains what I expect:

```shell
/tmp/foobar4/package$ tree
.
├── bin
│   └── mcp-server.js
├── package.json
├── README.md
└── vendor
    ├── aarch64-apple-darwin
    │   ├── bash
    │   │   ├── macos-14
    │   │   │   └── bash
    │   │   └── macos-15
    │   │       └── bash
    │   ├── codex-exec-mcp-server
    │   └── codex-execve-wrapper
    ├── aarch64-unknown-linux-musl
    │   ├── bash
    │   │   ├── centos-9
    │   │   │   └── bash
    │   │   ├── debian-11
    │   │   │   └── bash
    │   │   ├── debian-12
    │   │   │   └── bash
    │   │   ├── ubuntu-20.04
    │   │   │   └── bash
    │   │   ├── ubuntu-22.04
    │   │   │   └── bash
    │   │   └── ubuntu-24.04
    │   │       └── bash
    │   ├── codex-exec-mcp-server
    │   └── codex-execve-wrapper
    ├── x86_64-apple-darwin
    │   ├── bash
    │   │   └── macos-13
    │   │       └── bash
    │   ├── codex-exec-mcp-server
    │   └── codex-execve-wrapper
    └── x86_64-unknown-linux-musl
        ├── bash
        │   ├── centos-9
        │   │   └── bash
        │   ├── debian-11
        │   │   └── bash
        │   ├── debian-12
        │   │   └── bash
        │   ├── ubuntu-20.04
        │   │   └── bash
        │   ├── ubuntu-22.04
        │   │   └── bash
        │   └── ubuntu-24.04
        │       └── bash
        ├── codex-exec-mcp-server
        └── codex-execve-wrapper

26 directories, 26 files
```
2025-11-21 08:16:36 -08:00
jif-oai
bce030ddb5 Revert "fix: read max_output_tokens param from config" (#7088)
Reverts openai/codex#4139
2025-11-21 11:40:02 +01:00
iceweasel-oai
f4af6e389e Windows Sandbox: support network_access and exclude_tmpdir_env_var (#7030) 2025-11-20 22:59:55 -08:00
Eric Traut
b315b22f7b Fixed the deduplicator github action (#7070)
It stopped working (found zero duplicates) starting three days ago when
the model was switched from `gpt-5` to `gpt-5.1`. I'm not sure why it
stopped working. This is an attempt to get it working again by using the
default model for the codex action (which is presumably
`gpt-5.1-codex-max`).
2025-11-20 22:46:55 -08:00
Yorling
c9e149fd5c fix: read max_output_tokens param from config (#4139)
Request param `max_output_tokens` is documented in
`https://github.com/openai/codex/blob/main/docs/config.md`,
but nowhere uses the item in config, this commit read it from config for
GPT responses API.

see https://github.com/openai/codex/issues/4138 for issue report.

Signed-off-by: Yorling <shallowcloud@yeah.net>
2025-11-20 22:46:34 -08:00
Eric Traut
bacdc004be Fixed two tests that can fail in some environments that have global git rewrite rules (#7068)
This fixes https://github.com/openai/codex/issues/7044
2025-11-20 22:45:40 -08:00
pakrym-oai
ab5972d447 Support all types of search actions (#7061)
Fixes the 

```
{
  "error": {
    "message": "Invalid value: 'other'. Supported values are: 'search', 'open_page', and 'find_in_page'.",
    "type": "invalid_request_error",
    "param": "input[150].action.type",
    "code": "invalid_value"
  }
```
error.


The actual-actual fix here is supporting absent `query` parameter.
2025-11-20 20:45:28 -08:00
pakrym-oai
767b66f407 Migrate coverage to shell_command (#7042) 2025-11-21 03:44:00 +00:00
pakrym-oai
830ab4ce20 Support full powershell paths in is_safe_command (#7055)
New shell implementation always uses full paths.
2025-11-20 19:29:15 -08:00
Dylan Hurd
3f73e2c892 fix(app-server) remove www warning (#7046)
### Summary
After #7022, we no longer need this warning. We should also clean up the
schema for the notification, but this is a quick fix to just stop the
behavior in the VSCE

## Testing
- [x] Ran locally
2025-11-20 19:18:39 -08:00
Dylan Hurd
1822ffe870 feat(tui): default reasoning selection to medium (#7040)
## Summary
- allow selection popups to request an initial highlighted row
- begin the /models reasoning selector focused on the default effort

## Testing
- just fmt
- just fix -p codex-tui
- cargo test -p codex-tui



https://github.com/user-attachments/assets/b322aeb1-e8f3-4578-92f7-5c2fa5ee4c98



------
[Codex
Task](https://chatgpt.com/codex/tasks/task_i_691f75e8fc188322a910fbe2138666ef)
2025-11-20 17:06:04 -08:00
Celia Chen
7e2165f394 [app-server] update doc with codex error info (#6941)
Document new codex error info. Also fixed the name from
`codex_error_code` to `codex_error_info`.
2025-11-21 01:02:37 +00:00
Michael Bolin
8e5f38c0f0 feat: waiting for an elicitation should not count against a shell tool timeout (#6973)
Previously, we were running into an issue where we would run the `shell`
tool call with a timeout of 10s, but it fired an elicitation asking for
user approval, the time the user took to respond to the elicitation was
counted agains the 10s timeout, so the `shell` tool call would fail with
a timeout error unless the user is very fast!

This PR addresses this issue by introducing a "stopwatch" abstraction
that is used to manage the timeout. The idea is:

- `Stopwatch::new()` is called with the _real_ timeout of the `shell`
tool call.
- `process_exec_tool_call()` is called with the `Cancellation` variant
of `ExecExpiration` because it should not manage its own timeout in this
case
- the `Stopwatch` expiration is wired up to the `cancel_rx` passed to
`process_exec_tool_call()`
- when an elicitation for the `shell` tool call is received, the
`Stopwatch` pauses
- because it is possible for multiple elicitations to arrive
concurrently, it keeps track of the number of "active pauses" and does
not resume until that counter goes down to zero

I verified that I can test the MCP server using
`@modelcontextprotocol/inspector` and specify `git status` as the
`command` with a timeout of 500ms and that the elicitation pops up and I
have all the time in the world to respond whereas previous to this PR,
that would not have been possible.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/6973).
* #7005
* __->__ #6973
* #6972
2025-11-20 16:45:38 -08:00
Ahmed Ibrahim
1388e99674 fix flaky tool_call_output_exceeds_limit_truncated_chars_limit (#7043)
I am suspecting this is flaky because of the wall time can become 0,
0.1, or 1.
2025-11-20 16:36:29 -08:00
Michael Bolin
f56d1dc8fc feat: update process_exec_tool_call() to take a cancellation token (#6972)
This updates `ExecParams` so that instead of taking `timeout_ms:
Option<u64>`, it now takes a more general cancellation mechanism,
`ExecExpiration`, which is an enum that includes a
`Cancellation(tokio_util::sync::CancellationToken)` variant.

If the cancellation token is fired, then `process_exec_tool_call()`
returns in the same way as if a timeout was exceeded.

This is necessary so that in #6973, we can manage the timeout logic
external to the `process_exec_tool_call()` because we want to "suspend"
the timeout when an elicitation from a human user is pending.








---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/6972).
* #7005
* #6973
* __->__ #6972
2025-11-20 16:29:57 -08:00
Ahmed Ibrahim
9be310041b migrate collect_tool_identifiers_for_model to test_codex (#7041)
Maybe it solved flakiness
2025-11-20 16:02:50 -08:00
Xiao-Yong Jin
0fbcdd77c8 core: make shell behavior portable on FreeBSD (#7039)
- Use /bin/sh instead of /bin/bash on FreeBSD/OpenBSD in the process
group timeout test to avoid command-not-found failures.

- Accept /usr/local/bin/bash as a valid SHELL path to match common
FreeBSD installations.

- Switch the shell serialization duration test to /bin/sh for improved
portability across Unix platforms.

With this change, `cargo test -p codex-core --lib` runs and passes on
FreeBSD.
2025-11-20 16:01:35 -08:00
Celia Chen
9bce050385 [app-server & core] introduce new codex error code and v2 app-server error events (#6938)
This PR does two things:
1. populate a new `codex_error_code` protocol in error events sent from
core to client;
2. old v1 core events `codex/event/stream_error` and `codex/event/error`
will now both become `error`. We also show codex error code for
turncompleted -> error status.

new events in app server test:
```
< {
<   "method": "codex/event/stream_error",
<   "params": {
<     "conversationId": "019aa34c-0c14-70e0-9706-98520a760d67",
<     "id": "0",
<     "msg": {
<       "codex_error_code": {
<         "response_stream_disconnected": {
<           "http_status_code": 401
<         }
<       },
<       "message": "Reconnecting... 2/5",
<       "type": "stream_error"
<     }
<   }
< }

 {
<   "method": "error",
<   "params": {
<     "error": {
<       "codexErrorCode": {
<         "responseStreamDisconnected": {
<           "httpStatusCode": 401
<         }
<       },
<       "message": "Reconnecting... 2/5"
<     }
<   }
< }

< {
<   "method": "turn/completed",
<   "params": {
<     "turn": {
<       "error": {
<         "codexErrorCode": {
<           "responseTooManyFailedAttempts": {
<             "httpStatusCode": 401
<           }
<         },
<         "message": "exceeded retry limit, last status: 401 Unauthorized, request id: 9a1b495a1a97ed3e-SJC"
<       },
<       "id": "0",
<       "items": [],
<       "status": "failed"
<     }
<   }
< }
```
2025-11-20 23:06:55 +00:00
iceweasel-oai
3f92ad4190 add deny ACEs for world writable dirs (#7022)
Our Restricted Token contains 3 SIDs (Logon, Everyone, {WorkspaceWrite
Capability || ReadOnly Capability})

because it must include Everyone, that left us vulnerable to directories
that allow writes to Everyone. Even though those directories do not have
ACEs that enable our capability SIDs to write to them, they could still
be written to even in ReadOnly mode, or even in WorkspaceWrite mode if
they are outside of a writable root.

A solution to this is to explicitly add *Deny* ACEs to these
directories, always for the ReadOnly Capability SID, and for the
WorkspaceWrite SID if the directory is outside of a workspace root.

Under a restricted token, Windows always checks Deny ACEs before Allow
ACEs so even though our restricted token would allow a write to these
directories due to the Everyone SID, it fails first because of the Deny
ACE on the capability SID
2025-11-20 14:50:33 -08:00
Ahmed Ibrahim
54ee302a06 Attempt to fix unified_exec_formats_large_output_summary flakiness (#7029)
second attempt to fix this test after
https://github.com/openai/codex/pull/6884. I think this flakiness is
happening because yield_time is too small for a 10,000 step loop in
python.
2025-11-20 14:38:04 -08:00
Ahmed Ibrahim
44fa06ae36 fix flaky test: approval_matrix_covers_all_modes (#7028)
looks like it sometimes flake around 30. let's give it more time.
2025-11-20 14:37:42 -08:00
pakrym-oai
856f97f449 Delete shell_command feature (#7024) 2025-11-20 14:14:56 -08:00
zhao-oai
fe7a3f0c2b execpolicycheck command in codex cli (#7012)
adding execpolicycheck tool onto codex cli

this is useful for validating policies (can be multiple) against
commands.

it will also surface errors in policy syntax:
<img width="1150" height="281" alt="Screenshot 2025-11-19 at 12 46
21 PM"
src="https://github.com/user-attachments/assets/8f99b403-564c-4172-acc9-6574a8d13dc3"
/>

this PR also changes output format when there's no match in the CLI.
instead of returning the raw string `noMatch`, we return
`{"noMatch":{}}`

this PR is a rewrite of: https://github.com/openai/codex/pull/6932 (due
to the numerous merge conflicts present in the original PR)

---------

Co-authored-by: Michael Bolin <mbolin@openai.com>
2025-11-20 16:44:31 -05:00
zhao-oai
c30ca0d5b6 increasing user shell timeout to 1 hour (#7025)
setting user shell timeout to an unreasonably high value since there
isn't an easy way to have a command run without timeouts

currently, user shell commands timeout is 10 seconds
2025-11-20 13:39:16 -08:00
Weiller Carvalho
a8a6cbdd1c fix: route feedback issue links by category (#6840)
## Summary
- TUI feedback note now only links to the bug-report template when the
category is bug/bad result.
- Good result/other feedback shows a thank-you+thread ID instead of
funneling people to file a bug.
- Added a helper + unit test so future changes keep the behavior
consistent.

## Testing
  - just fmt
  - just fix -p codex-tui
  - cargo test -p codex-tui

  Fixes #6839
2025-11-20 13:20:03 -08:00
Dmitri Khokhlov
e4257f432e codex-exec: allow resume --last to read prompt #6717 (#6719)
### Description

- codex exec --json resume --last "<prompt>" bailed out because clap
treated the prompt as SESSION_ID. I removed the conflicts_with flag and
reinterpret that positional as a prompt when
--last is set, so the flow now keeps working in JSON mode.
(codex-rs/exec/src/cli.rs:84-104, codex-rs/exec/src/lib.rs:75-130)
- Added a regression test that exercises resume --last in JSON mode to
ensure the prompt is accepted and the rollout file is updated.
(codex-rs/exec/tests/suite/resume.rs:126-178)

### Testing

  - just fmt
  - cargo test -p codex-exec
  - just fix -p codex-exec
  - cargo test -p codex-exec

#6717

Signed-off-by: Dmitri Khokhlov <dkhokhlov@cribl.io>
2025-11-20 13:10:49 -08:00
Jeremy Rose
2c793083f4 tui: centralize markdown styling and make inline code cyan (#7023)
<img width="762" height="271" alt="Screenshot 2025-11-20 at 12 54 06 PM"
src="https://github.com/user-attachments/assets/10021d63-27eb-407b-8fcc-43740e3bfb0f"
/>
2025-11-20 21:06:22 +00:00
Lionel Cheng
e150798baf Bumped number of fuzzy search results from 8 to 20 (#7013)
I just noticed that in the VSCode / Codex extension when you type @ the
number of results is around 70:

- small video of searching for `mod.rs` inside `codex` repository:
https://github.com/user-attachments/assets/46e53d31-adff-465e-b32b-051c4c1c298c

- while in the CLI the number of results is currently of 8 which is
quite small:
<img width="615" height="439" alt="Screenshot 2025-11-20 at 09 42 04"
src="https://github.com/user-attachments/assets/1c6d12cb-3b1f-4d5b-9ad3-6b12975eaaec"
/>

I bumped it to 20. I had several cases where I wanted a file and did not
find it because the number of results was too small

Signed-off-by: lionel-oai <lionel@openai.com>
Co-authored-by: lionel-oai <lionel@openai.com>
2025-11-20 12:33:12 -08:00
Kyuheon Kim
33a6cc66ab fix(cli): correct mcp add usage order (#6827)
## Summary
- add an explicit `override_usage` string to `AddArgs` so clap prints
`<NAME>` before the command/url choice, matching the actual parser and
docs

### Before

Usage: codex mcp add [OPTIONS] <COMMAND|--url <URL>> <NAME>


### After

Usage: codex mcp add [OPTIONS] <NAME> [--url <URL> | -- <COMMAND>...]

---------

Signed-off-by: kyuheon-kr <kyuheon.kr@gmail.com>
2025-11-20 12:32:12 -08:00
pakrym-oai
52d0ec4cd8 Delete tiktoken-rs (#7018) 2025-11-20 11:15:04 -08:00
LIHUA
397279d46e Fix: Improve text encoding for shell output in VSCode preview (#6178) (#6182)
## 🐛 Problem

Users running commands with non-ASCII characters (like Russian text
"пример") in Windows/WSL environments experience garbled text in
VSCode's shell preview window, with Unicode replacement characters (�)
appearing instead of the actual text.

**Issue**: https://github.com/openai/codex/issues/6178

## 🔧 Root Cause

The issue was in `StreamOutput<Vec<u8>>::from_utf8_lossy()` method in
`codex-rs/core/src/exec.rs`, which used `String::from_utf8_lossy()` to
convert shell output bytes to strings. This function immediately
replaces any invalid UTF-8 byte sequences with replacement characters,
without attempting to decode using other common encodings.

In Windows/WSL environments, shell output often uses encodings like:

- Windows-1252 (common Windows encoding)
- Latin-1/ISO-8859-1 (extended ASCII)

## 🛠️ Solution

Replaced the simple `String::from_utf8_lossy()` call with intelligent
encoding detection via a new `bytes_to_string_smart()` function that
tries multiple encoding strategies:

1. **UTF-8** (fast path for valid UTF-8)
2. **Windows-1252** (handles Windows-specific characters in 0x80-0x9F
range)
3. **Latin-1** (fallback for extended ASCII)
4. **Lossy UTF-8** (final fallback, same as before)

## 📁 Changes

### New Files

- `codex-rs/core/src/text_encoding.rs` - Smart encoding detection module
- `codex-rs/core/tests/suite/text_encoding_fix.rs` - Integration tests

### Modified Files

- `codex-rs/core/src/lib.rs` - Added text_encoding module
- `codex-rs/core/src/exec.rs` - Updated StreamOutput::from_utf8_lossy()
- `codex-rs/core/tests/suite/mod.rs` - Registered new test module

##  Testing

- **5 unit tests** covering UTF-8, Windows-1252, Latin-1, and fallback
scenarios
- **2 integration tests** simulating the exact Issue #6178 scenario
- **Demonstrates improvement** over the previous
`String::from_utf8_lossy()` approach

All tests pass:

```bash
cargo test -p codex-core text_encoding
cargo test -p codex-core test_shell_output_encoding_issue_6178
```

## 🎯 Impact

-  **Eliminates garbled text** in VSCode shell preview for non-ASCII
content
-  **Supports Windows/WSL environments** with proper encoding detection
-  **Zero performance impact** for UTF-8 text (fast path)
-  **Backward compatible** - UTF-8 content works exactly as before
-  **Handles edge cases** with robust fallback mechanism

## 🧪 Test Scenarios

The fix has been tested with:

- Russian text ("пример")
- Windows-1252 quotation marks (""test")
- Latin-1 accented characters ("café")
- Mixed encoding content
- Invalid byte sequences (graceful fallback)

## 📋 Checklist

- [X] Addresses the reported issue
- [X] Includes comprehensive tests
- [X] Maintains backward compatibility
- [X] Follows project coding conventions
- [X] No breaking changes

---------

Co-authored-by: Josh McKinney <joshka@openai.com>
2025-11-20 11:04:11 -08:00
pakrym-oai
30ca89424c Always fallback to real shell (#6953)
Either cmd.exe or `/bin/sh`.
2025-11-20 10:58:46 -08:00
Eric Traut
d909048a85 Added feature switch to disable animations in TUI (#6870)
This PR adds support for a new feature flag `tui.animations`. By
default, the TUI uses animations in its welcome screen, "working"
spinners, and "shimmer" effects. This animations can interfere with
screen readers, so it's good to provide a way to disable them.

This change is inspired by [a
PR](https://github.com/openai/codex/pull/4014) contributed by @Orinks.
That PR has faltered a bit, but I think the core idea is sound. This
version incorporates feedback from @aibrahim-oai. In particular:
1. It uses a feature flag (`tui.animations`) rather than the unqualified
CLI key `no-animations`. Feature flags are the preferred way to expose
boolean switches. They are also exposed via CLI command switches.
2. It includes more complete documentation.
3. It disables a few animations that the other PR omitted.
2025-11-20 10:40:08 -08:00
jif-oai
888c6dd9e7 fix: command formatting for user commands (#7002) 2025-11-20 17:29:15 +01:00
hanson-openai
b5dd189067 Allow unified_exec to early exit (if the process terminates before yield_time_ms) (#6867)
Thread through an `exit_notify` tokio `Notify` through to the
`UnifiedExecSession` so that we can return early if the command
terminates before `yield_time_ms`.

As Codex review correctly pointed out below 🙌 we also need a
`exit_signaled` flag so that commands which finish before we start
waiting can also exit early.

Since the default `yield_time_ms` is now 10s, this means that we don't
have to wait 10s for trivial commands like ls, sed, etc (which are the
majority of agent commands 😅)

---------

Co-authored-by: jif-oai <jif@openai.com>
2025-11-20 13:34:41 +01:00
Michael Bolin
54e6e4ac32 fix: when displaying execv, show file instead of arg0 (#6966)
After merging https://github.com/openai/codex/pull/6958, I realized that
the `command` I was displaying was not quite right. Since we know it, we
should show the _exact_ program being executed (the first arg to
`execve(3)`) rather than `arg0` to be more precise.

Below is the same command I used to test
https://github.com/openai/codex/pull/6958, but now you can see it shows
`/Users/mbolin/.openai/bin/git` instead of just `git`.

<img width="1526" height="1444" alt="image"
src="https://github.com/user-attachments/assets/428128d1-c658-456e-a64e-fc6a0009cb34"
/>
2025-11-19 22:42:58 -08:00
Michael Bolin
e8af41de8a fix: clean up elicitation used by exec-server (#6958)
Using appropriate message/title fields, I think this looks better now:

<img width="3370" height="3208" alt="image"
src="https://github.com/user-attachments/assets/e9bbf906-4ba8-4563-affc-62cdc6c97342"
/>

Though note that in the current version of the Inspector (`0.17.2`), you
cannot hit **Submit** until you fill out the field. I believe this is a
bug in the Inspector, as it does not properly handle the case when all
fields are optional. I put up a fix:

https://github.com/modelcontextprotocol/inspector/pull/926
2025-11-20 04:59:17 +00:00
Owen Lin
d6c30ed25e [app-server] feat: v2 apply_patch approval flow (#6760)
This PR adds the API V2 version of the apply_patch approval flow, which
centers around `ThreadItem::FileChange`.

This PR wires the new RPC (`item/fileChange/requestApproval`, V2 only)
and related events (`item/started`, `item/completed` for
`ThreadItem::FileChange`, which are emitted in both V1 and V2) through
the app-server
protocol. The new approval RPC is only sent when the user initiates a
turn with the new `turn/start` API so we don't break backwards
compatibility with VSCE.

Similar to https://github.com/openai/codex/pull/6758, the approach I
took was to make as few changes to the Codex core as possible,
leveraging existing `EventMsg` core events, and translating those in
app-server. I did have to add a few additional fields to
`EventMsg::PatchApplyBegin` and `EventMsg::PatchApplyEnd`, but those
were fairly lightweight.

However, the `EventMsg`s emitted by core are the following:
```
1) Auto-approved (no request for approval)

- EventMsg::PatchApplyBegin
- EventMsg::PatchApplyEnd

2) Approved by user
- EventMsg::ApplyPatchApprovalRequest
- EventMsg::PatchApplyBegin
- EventMsg::PatchApplyEnd

3) Declined by user
- EventMsg::ApplyPatchApprovalRequest
- EventMsg::PatchApplyBegin
- EventMsg::PatchApplyEnd
```

For a request triggering an approval, this would result in:
```
item/fileChange/requestApproval
item/started
item/completed
```

which is different from the `ThreadItem::CommandExecution` flow
introduced in https://github.com/openai/codex/pull/6758, which does the
below and is preferable:
```
item/started
item/commandExecution/requestApproval
item/completed
```

To fix this, we leverage `TurnSummaryStore` on codex_message_processor
to store a little bit of state, allowing us to fire `item/started` and
`item/fileChange/requestApproval` whenever we receive the underlying
`EventMsg::ApplyPatchApprovalRequest`, and no-oping when we receive the
`EventMsg::PatchApplyBegin` later.

This is much less invasive than modifying the order of EventMsg within
core (I tried).

The resulting payloads:
```
{
  "method": "item/started",
  "params": {
    "item": {
      "changes": [
        {
          "diff": "Hello from Codex!\n",
          "kind": "add",
          "path": "/Users/owen/repos/codex/codex-rs/APPROVAL_DEMO.txt"
        }
      ],
      "id": "call_Nxnwj7B3YXigfV6Mwh03d686",
      "status": "inProgress",
      "type": "fileChange"
    }
  }
}
```

```
{
  "id": 0,
  "method": "item/fileChange/requestApproval",
  "params": {
    "grantRoot": null,
    "itemId": "call_Nxnwj7B3YXigfV6Mwh03d686",
    "reason": null,
    "threadId": "019a9e11-8295-7883-a283-779e06502c6f",
    "turnId": "1"
  }
}
```

```
{
  "id": 0,
  "result": {
    "decision": "accept"
  }
}
```

```
{
  "method": "item/completed",
  "params": {
    "item": {
      "changes": [
        {
          "diff": "Hello from Codex!\n",
          "kind": "add",
          "path": "/Users/owen/repos/codex/codex-rs/APPROVAL_DEMO.txt"
        }
      ],
      "id": "call_Nxnwj7B3YXigfV6Mwh03d686",
      "status": "completed",
      "type": "fileChange"
    }
  }
}
```
2025-11-19 20:13:31 -08:00
zhao-oai
fb9849e1e3 migrating execpolicy -> execpolicy-legacy and execpolicy2 -> execpolicy (#6956) 2025-11-19 19:14:10 -08:00
Celia Chen
72a1453ac5 Revert "[core] add optional status_code to error events (#6865)" (#6955)
This reverts commit c2ec477d93.

# External (non-OpenAI) Pull Request Requirements

Before opening this Pull Request, please read the dedicated
"Contributing" markdown file or your PR may be closed:
https://github.com/openai/codex/blob/main/docs/contributing.md

If your PR conforms to our contribution guidelines, replace this text
with a detailed and high quality description of your changes.

Include a link to a bug report or enhancement request.
2025-11-20 01:26:14 +00:00
Ahmed Ibrahim
6d67b8b283 stop model migration screen after first time. (#6954)
it got serialized wrong.
2025-11-19 17:17:04 -08:00
zhao-oai
74a75679d9 update execpolicy quickstart readme (#6952) 2025-11-19 16:57:27 -08:00
pakrym-oai
92e3046733 Single pass truncation (#6914) 2025-11-19 16:56:37 -08:00
zhao-oai
65c13f1ae7 execpolicy2 core integration (#6641)
This PR threads execpolicy2 into codex-core.

activated via feature flag: exec_policy (on by default)

reads and parses all .codexpolicy files in `codex_home/codex`

refactored tool runtime API to integrate execpolicy logic

---------

Co-authored-by: Michael Bolin <mbolin@openai.com>
2025-11-19 16:50:43 -08:00
Dylan Hurd
b00a7cf40d fix(shell) fallback shells (#6948)
## Summary
Add fallbacks when user_shell_path does not resolve to a known shell
type

## Testing
- [x] Tests still pass
2025-11-19 16:41:38 -08:00
Michael Bolin
13d378f2ce chore: refactor exec-server to prepare it for standalone MCP use (#6944)
This PR reorganizes things slightly so that:

- Instead of a single multitool executable, `codex-exec-server`, we now
have two executables:
  - `codex-exec-mcp-server` to launch the MCP server
- `codex-execve-wrapper` is the `execve(2)` wrapper to use with the
`BASH_EXEC_WRAPPER` environment variable
- `BASH_EXEC_WRAPPER` must be a single executable: it cannot be a
command string composed of an executable with args (i.e., it no longer
adds the `escalate` subcommand, as before)
- `codex-exec-mcp-server` takes `--bash` and `--execve` as options.
Though if `--execve` is not specified, the MCP server will check the
directory containing `std::env::current_exe()` and attempt to use the
file named `codex-execve-wrapper` within it. In development, this works
out since these executables are side-by-side in the `target/debug`
folder.

With respect to testing, this also fixes an important bug in
`dummy_exec_policy()`, as I was using `ends_with()` as if it applied to
a `String`, but in this case, it is used with a `&Path`, so the
semantics are slightly different.

Putting this all together, I was able to test this by running the
following:

```
~/code/codex/codex-rs$ npx @modelcontextprotocol/inspector \
    ./target/debug/codex-exec-mcp-server --bash ~/code/bash/bash
```

If I try to run `git status` in `/Users/mbolin/code/codex` via the
`shell` tool from the MCP server:

<img width="1589" height="1335" alt="image"
src="https://github.com/user-attachments/assets/9db6aea8-7fbc-4675-8b1f-ec446685d6c4"
/>

then I get prompted with the following elicitation, as expected:

<img width="1589" height="1335" alt="image"
src="https://github.com/user-attachments/assets/21b68fe0-494d-4562-9bad-0ddc55fc846d"
/>

Though a current limitation is that the `shell` tool defaults to a
timeout of 10s, which means I only have 10s to respond to the
elicitation. Ideally, the time spent waiting for a response from a human
should not count against the timeout for the command execution. I will
address this in a subsequent PR.

---

Note `~/code/bash/bash` was created by doing:

```
cd ~/code
git clone https://github.com/bminor/bash
cd bash
git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
<apply the patch below>
./configure
make
```

The patch:

```
diff --git a/execute_cmd.c b/execute_cmd.c
index 070f5119..d20ad2b9 100644
--- a/execute_cmd.c
+++ b/execute_cmd.c
@@ -6129,6 +6129,19 @@ shell_execve (char *command, char **args, char **env)
   char sample[HASH_BANG_BUFSIZ];
   size_t larray;

+  char* exec_wrapper = getenv("BASH_EXEC_WRAPPER");
+  if (exec_wrapper && *exec_wrapper && !whitespace (*exec_wrapper))
+    {
+      char *orig_command = command;
+
+      larray = strvec_len (args);
+
+      memmove (args + 2, args, (++larray) * sizeof (char *));
+      args[0] = exec_wrapper;
+      args[1] = orig_command;
+      command = exec_wrapper;
+    }
+
```
2025-11-19 16:38:14 -08:00
Lionel Cheng
a6597a9958 Fix/correct reasoning display (#6749)
This closes #6748 by implementing fallback to
`model_family.default_reasoning_effort` in `reasoning_effort` display of
`/status` when no `model_reasoning_effort` is set in the configuration.

## common/src/config_summary.rs

- `create_config_summary_entries` now fills the "reasoning effort" entry
with the explicit `config.model_reasoning_effort` when present and falls
back to `config.model_family.default_reasoning_effort` when it is
`None`, instead of emitting the literal string `none`.
- This ensures downstream consumers such as `tui/src/status/helpers.rs`
continue to work unchanged while automatically picking up model-family
defaults when the user has not selected a reasoning effort.

## tui/src/status/helpers.rs / core/src/model_family.rs

`ModelFamily::default_reasoning_effort` metadata is set to `medium` for
both `gpt-5*-codex` and `gpt-5` models following the default behaviour
of the API and recommendation of the codebase:
- per https://platform.openai.com/docs/api-reference/responses/create
`gpt-5` defaults to `medium` reasoning when no preset is passed
- there is no mention of the preset for `gpt-5.1-codex` in the API docs
but `medium` is the default setting for `gpt-5.1-codex` as per
`codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__model_reasoning_selection_popup.snap`

---------

Signed-off-by: lionelchg <lionel.cheng@hotmail.fr>
Co-authored-by: Eric Traut <etraut@openai.com>
2025-11-19 15:52:24 -08:00
Beehive Innovations
692989c277 fix(context left after review): review footer context after /review (#5610)
## Summary
- show live review token usage while `/review` runs and restore the main
session indicator afterward
  - add regression coverage for the footer behavior

## Testing
  - just fmt
  - cargo test -p codex-tui

Fixes #5604

---------

Signed-off-by: Fahad <fahad@2doapp.com>
2025-11-19 22:50:07 +00:00
iceweasel-oai
2fde03b4a0 stop over-reporting world-writable directories (#6936)
Fix world-writable audit false positives by expanding generic
permissions with MapGenericMask and then checking only concrete write
bits. The earlier check looked for FILE_GENERIC_WRITE/generic masks
directly, which shares bits with read permissions and could flag an
Everyone read ACE as writable.
2025-11-19 13:59:17 -08:00
Michael Bolin
056c8f8279 fix: prepare ExecPolicy in exec-server for execpolicy2 cutover (#6888)
This PR introduces an extra layer of abstraction to prepare us for the
migration to execpolicy2:

- introduces a new trait, `EscalationPolicy`, whose `determine_action()`
method is responsible for producing the `EscalateAction`
- the existing `ExecPolicy` typedef is changed to return an intermediate
`ExecPolicyOutcome` instead of `EscalateAction`
- the default implementation of `EscalationPolicy`,
`McpEscalationPolicy`, composes `ExecPolicy`
- the `ExecPolicyOutcome` includes `codex_execpolicy2::Decision`, which
has a `Prompt` variant
- when `McpEscalationPolicy` gets `Decision::Prompt` back from
`ExecPolicy`, it prompts the user via an MCP elicitation and maps the
result into an `ElicitationAction`
- now that the end user can reply to an elicitation with `Decline` or
`Cancel`, we introduce a new variant, `EscalateAction::Deny`, which the
client handles by returning exit code `1` without running anything

Note the way the elicitation is created is still not quite right, but I
will fix that once we have things running end-to-end for real in a
follow-up PR.
2025-11-19 13:55:29 -08:00
Celia Chen
c2ec477d93 [core] add optional status_code to error events (#6865)
We want to better uncover error status code for clients. Add an optional
status_code to error events (thread error, error, stream error) so app
server could uncover the status code from the client side later.

in event log:
```
< {
<   "method": "codex/event/stream_error",
<   "params": {
<     "conversationId": "019a9a32-f576-7292-9711-8e57e8063536",
<     "id": "0",
<     "msg": {
<       "message": "Reconnecting... 5/5",
<       "status_code": 401,
<       "type": "stream_error"
<     }
<   }
< }
< {
<   "method": "codex/event/error",
<   "params": {
<     "conversationId": "019a9a32-f576-7292-9711-8e57e8063536",
<     "id": "0",
<     "msg": {
<       "message": "exceeded retry limit, last status: 401 Unauthorized, request id: 9a0cb03a485067f7-SJC",
<       "status_code": 401,
<       "type": "error"
<     }
<   }
< }
```
2025-11-19 19:51:21 +00:00
Dylan Hurd
20982d5c6a fix(app-server) move windows world writable warning (#6916)
## Summary
Move the app-server warning into the process_new_conversation

## Testing
- [x] Tested locally
2025-11-19 11:24:49 -08:00
pakrym-oai
64ae9aa3c3 Keep gpt-5.1-codex the default (#6922) 2025-11-19 11:08:10 -08:00
zhao-oai
72af589398 storing credits (#6858)
Expand the rate-limit cache/TUI: store credit snapshots alongside
primary and secondary windows, render “Credits” when the backend reports
they exist (unlimited vs rounded integer balances)
2025-11-19 10:49:35 -08:00
iceweasel-oai
b3d320433f have world_writable_warning_details accept cwd as a param (#6913)
this enables app-server to pass in the correct workspace cwd for the
current conversation
2025-11-19 10:10:03 -08:00
jif-oai
91a1d20e2d use another prompt (#6912) 2025-11-19 17:47:47 +00:00
jif-oai
87716e7cd0 NITs (#6911) 2025-11-19 17:43:51 +00:00
jif-oai
8976551f0d Fix ordering 2 (#6910) 2025-11-19 17:40:27 +00:00
jif-oai
f1d6767685 fix: ordering (#6909) 2025-11-19 17:39:07 +00:00
Ahmed Ibrahim
d62cab9a06 fix: don't truncate at new lines (#6907) 2025-11-19 17:05:48 +00:00
Ahmed Ibrahim
d5dfba2509 feat: arcticfox in the wild (#6906)
<img width="485" height="600" alt="image"
src="https://github.com/user-attachments/assets/4341740d-dd58-4a3e-b69a-33a3be0606c5"
/>

---------

Co-authored-by: jif-oai <jif@openai.com>
2025-11-19 16:31:06 +00:00
Owen Lin
1924500250 [app-server] populate thread>turns>items on thread/resume (#6848)
This PR allows clients to render historical messages when resuming a
thread via `thread/resume` by reading from the list of `EventMsg`
payloads loaded from the rollout, and then transforming them into Turns
and ThreadItems to be returned on the `Thread` object.

This is implemented by leveraging `SessionConfiguredNotification` which
returns this list of `EventMsg` objects when resuming a conversation,
and then applying a stateful `ThreadHistoryBuilder` that parses from
this EventMsg log and transforms it into Turns and ThreadItems.

Note that we only persist a subset of `EventMsg`s in a rollout as
defined in `policy.rs`, so we lose fidelity whenever we resume a thread
compared to when we streamed the thread's turns originally. However,
this behavior is at parity with the legacy API.
2025-11-19 15:58:09 +00:00
jif-oai
cfc57e14c7 nit: useless log to debug (#6898)
When you type too fast in most terminals, it gets interpreted as paste,
making this log spam
2025-11-19 12:32:53 +00:00
Dylan Hurd
15b5eb30ed fix(core) Support changing /approvals before conversation (#6836)
## Summary
Setting `/approvals` before the start of a conversation was not updating
the environment_context for a conversation. Not sure exactly when this
problem was introduced, but this should reduce model confusion
dramatically.

## Testing
- [x] Added unit test to reproduce bug, confirmed fix with update
- [x] Tested locally
2025-11-19 11:32:48 +00:00
jif-oai
3e9e1d993d chore: consolidate compaction token usage (#6894) 2025-11-19 11:26:01 +00:00
Dylan Hurd
44c747837a chore(app-server) world-writable windows notification (#6880)
## Summary
On app-server startup, detect whether the experimental sandbox is
enabled, and send a notification .

**Note**
New conversations will not respect the feature because we [ignore cli
overrides in
NewConversation](a75321a64c/codex-rs/app-server/src/codex_message_processor.rs (L1237-L1252)).
However, this should be okay, since we don't actually use config for
this, we use a [global
variable](87cce88f48/codex-rs/core/src/safety.rs (L105-L110)).
We should carefully unwind this setup at some point.


## Testing
- [ ] In progress: testing locally

---------

Co-authored-by: jif-oai <jif@openai.com>
2025-11-19 11:19:34 +00:00
jif-oai
4985a7a444 fix: parallel tool call instruction injection (#6893) 2025-11-19 11:01:57 +00:00
jif-oai
10d571f236 nit: stable (#6895) 2025-11-19 10:43:43 +00:00
jif-oai
956d3bfac6 feat: warning large commits (#6838) 2025-11-19 10:22:10 +00:00
Thibault Sottiaux
73488657cb fix label (#6892) 2025-11-19 10:11:30 +00:00
Ahmed Ibrahim
efebc62fb7 Move shell to use truncate_text (#6842)
Move shell to use the configurable `truncate_text`

---------

Co-authored-by: pakrym-oai <pakrym@openai.com>
2025-11-19 01:56:08 -08:00
pakrym-oai
75f38f16dd Run remote auto compaction (#6879) 2025-11-19 00:43:58 -08:00
Ahmed Ibrahim
0440a3f105 flaky-unified_exec_formats_large_output_summary (#6884)
# External (non-OpenAI) Pull Request Requirements

Before opening this Pull Request, please read the dedicated
"Contributing" markdown file or your PR may be closed:
https://github.com/openai/codex/blob/main/docs/contributing.md

If your PR conforms to our contribution guidelines, replace this text
with a detailed and high quality description of your changes.

Include a link to a bug report or enhancement request.
2025-11-19 00:00:37 -08:00
pakrym-oai
ee0484a98c shell_command returns freeform output (#6860)
Instead of returning structured out and then re-formatting it into
freeform, return the freeform output from shell_command tool.

Keep `shell` as the default tool for GPT-5.
2025-11-18 23:38:43 -08:00
Dylan Hurd
7e0e675db4 chore(core) arcticfox (#6876)
..
2025-11-18 23:38:08 -08:00
Dylan Hurd
84458f12f6 fix(tui) ghost snapshot notifications (#6881)
## Summary
- avoid surfacing ghost snapshot warnings in the TUI when snapshot
creation fails, logging the conditions instead
- continue to capture successful ghost snapshots without changing
existing behavior

## Testing
- `cargo test -p codex-core` *(fails:
default_client::tests::test_create_client_sets_default_headers,
default_client::tests::test_get_codex_user_agent,
exec::tests::kill_child_process_group_kills_grandchildren_on_timeout)*

------
[Codex
Task](https://chatgpt.com/codex/tasks/task_i_691c02238db08322927c47b8c2d72c4c)
2025-11-18 23:23:00 -08:00
Ahmed Ibrahim
793063070b fix: typos in model picker (#6859)
# External (non-OpenAI) Pull Request Requirements

Before opening this Pull Request, please read the dedicated
"Contributing" markdown file or your PR may be closed:
https://github.com/openai/codex/blob/main/docs/contributing.md

If your PR conforms to our contribution guidelines, replace this text
with a detailed and high quality description of your changes.

Include a link to a bug report or enhancement request.
2025-11-19 06:29:02 +00:00
ae
030d1d5b1c chore: update windows docs url (#6877)
- Testing: None
2025-11-19 06:24:17 +00:00
ae
7e6316d4aa feat: tweak windows sandbox strings (#6875)
New strings:
1. Approval mode picker just says "Select Approval Mode"
1. Updated "Auto" to "Agent"
1. When you select "Agent", you get "Agent mode on Windows uses an
experimental sandbox to limit network and filesystem access. [Learn
more]"
1. Updated world-writable warning to "The Windows sandbox cannot protect
writes to folders that are writable by Everyone. Consider removing write
access for Everyone from the following folders: {folders}"

---------

Co-authored-by: iceweasel-oai <iceweasel@openai.com>
2025-11-19 06:00:06 +00:00
Michael Bolin
a75321a64c fix: add more fields to ThreadStartResponse and ThreadResumeResponse (#6847)
This adds the following fields to `ThreadStartResponse` and
`ThreadResumeResponse`:

```rust
    pub model: String,
    pub model_provider: String,
    pub cwd: PathBuf,
    pub approval_policy: AskForApproval,
    pub sandbox: SandboxPolicy,
    pub reasoning_effort: Option<ReasoningEffort>,
```

This is important because these fields are optional in
`ThreadStartParams` and `ThreadResumeParams`, so the caller needs to be
able to determine what values were ultimately used to start/resume the
conversation. (Though note that any of these could be changed later
between turns in the conversation.)

Though to get this information reliably, it must be read from the
internal `SessionConfiguredEvent` that is created in response to the
start of a conversation. Because `SessionConfiguredEvent` (as defined in
`codex-rs/protocol/src/protocol.rs`) did not have all of these fields, a
number of them had to be added as part of this PR.

Because `SessionConfiguredEvent` is referenced in many tests, test
instances of `SessionConfiguredEvent` had to be updated, as well, which
is why this PR touches so many files.
2025-11-18 21:18:43 -08:00
ae
7508e4fd2d chore: update windows sandbox docs (#6872) 2025-11-18 21:02:04 -08:00
pakrym-oai
cac0a6a29d Remote compaction on by-default (#6866) 2025-11-19 02:21:57 +00:00
Celia Chen
b395dc1be6 [app-server] introduce turn/completed v2 event (#6800)
similar to logic in
`codex/codex-rs/exec/src/event_processor_with_jsonl_output.rs`.
translation of v1 -> v2 events:
`codex/event/task_complete` -> `turn/completed`
`codex/event/turn_aborted` -> `turn/completed` with `interrupted` status
`codex/event/error` -> `turn/completed` with `error` status

this PR also makes `items` field in `Turn` optional. For now, we only
populate it when we resume a thread, and leave it as None for all other
places until we properly rewrite core to keep track of items.

tested using the codex app server client. example new event:
```
< {
<   "method": "turn/completed",
<   "params": {
<     "turn": {
<       "id": "0",
<       "items": [],
<       "status": "interrupted"
<     }
<   }
< }
```
2025-11-19 01:55:24 +00:00
zhao-oai
4288091f63 update credit status details (#6862) 2025-11-19 01:40:22 +00:00
Jeremy Rose
526eb3ff82 tui: add branch to 'codex resume', filter by cwd (#6232)
By default, show only sessions that shared a cwd with the current cwd.
`--all` shows all sessions in all cwds. Also, show the branch name from
the rollout metadata.

<img width="1091" height="638" alt="Screenshot 2025-11-04 at 3 30 47 PM"
src="https://github.com/user-attachments/assets/aae90308-6115-455f-aff7-22da5f1d9681"
/>
2025-11-19 00:47:37 +00:00
iceweasel-oai
b952bd2649 smoketest for browser vuln, rough draft of Windows security doc (#6822) 2025-11-18 16:43:34 -08:00
iceweasel-oai
cf57320b9f windows sandbox: support multiple workspace roots (#6854)
The Windows sandbox did not previously support multiple workspace roots
via config. Now it does
2025-11-18 16:35:00 -08:00
zhao-oai
4fb714fb46 updating codex backend models (#6855) 2025-11-18 16:25:50 -08:00
Jeremy Rose
c1391b9f94 exec-server (#6630) 2025-11-19 00:20:19 +00:00
Eric Traut
9275e93364 Fix tests so they don't emit an extraneous config.toml in the source tree (#6853)
This PR fixes the `release_event_does_not_change_selection` test so it
doesn't cause an extra `config.toml` to be emitted in the sources when
running the tests locally. Prior to this fix, I needed to delete this
file every time I ran the tests to prevent it from showing up as an
uncommitted source file.
2025-11-18 15:27:45 -08:00
Owen Lin
b3a824ae3c [app-server-test-client] feat: auto approve command (#6852) 2025-11-18 15:25:02 -08:00
Eric Traut
ab30453dee Improved runtime of generated_ts_has_no_optional_nullable_fields test (#6851)
The `generated_ts_has_no_optional_nullable_fields` test was occasionally
failing on slow CI nodes because of a timeout. This change reduces the
work done by the test. It adds some "options" for the `generate_ts`
function so it can skip work that's not needed for the test.
2025-11-18 15:24:32 -08:00
jif-oai
c56d0c159b fix: local compaction (#6844) 2025-11-18 22:18:10 +00:00
simister
0bf857bc91 Fix typo in config.md for MCP server (#6845)
# External (non-OpenAI) Pull Request Requirements

Before opening this Pull Request, please read the dedicated
"Contributing" markdown file or your PR may be closed:
https://github.com/openai/codex/blob/main/docs/contributing.md

If your PR conforms to our contribution guidelines, replace this text
with a detailed and high quality description of your changes.

Include a link to a bug report or enhancement request.
2025-11-18 14:06:13 -08:00
Anton Panasenko
f7a921039c [codex][otel] support mtls configuration (#6228)
fix for https://github.com/openai/codex/issues/6153

supports mTLS configuration and includes TLS features in the library
build to enable secure HTTPS connections with custom root certificates.

grpc:
https://docs.rs/tonic/0.13.1/src/tonic/transport/channel/endpoint.rs.html#63
https:
https://docs.rs/reqwest/0.12.23/src/reqwest/async_impl/client.rs.html#516
2025-11-18 14:01:01 -08:00
jif-oai
8ddae8cde3 feat: review in app server (#6613) 2025-11-18 21:58:54 +00:00
Dylan Hurd
29ca89c414 chore(config) enable shell_command (#6843)
## Summary
Enables shell_command as default for `gpt-5*` and `codex-*` models.

## Testing
- [x] Updated unit tests
2025-11-18 12:46:02 -08:00
iceweasel-oai
4bada5a84d Prompt to turn on windows sandbox when auto mode selected. (#6618)
- stop prompting users to install WSL 
- prompt users to turn on Windows sandbox when auto mode requested.

<img width="1660" height="195" alt="Screenshot 2025-11-17 110612"
src="https://github.com/user-attachments/assets/c67fc239-a227-417e-94bb-599a8ed8f11e"
/>
<img width="1684" height="168" alt="Screenshot 2025-11-17 110637"
src="https://github.com/user-attachments/assets/d18c3370-830d-4971-8746-04757ae2f709"
/>
<img width="1655" height="293" alt="Screenshot 2025-11-17 110719"
src="https://github.com/user-attachments/assets/d21f6ce9-c23e-4842-baf6-8938b77c16db"
/>
2025-11-18 11:38:18 -08:00
Ahmed Ibrahim
3de8790714 Add the utility to truncate by tokens (#6746)
- This PR is to make it on path for truncating by tokens. This path will
be initially used by unified exec and context manager (responsible for
MCP calls mainly).
- We are exposing new config `calls_output_max_tokens`
- Use `tokens` as the main budget unit but truncate based on the model
family by Introducing `TruncationPolicy`.
- Introduce `truncate_text` as a router for truncation based on the
mode.

In next PRs:
- remove truncate_with_line_bytes_budget
- Add the ability to the model to override the token budget.
2025-11-18 11:36:23 -08:00
Alejandro Peña
b035c604b0 Update faq.md section on supported models (#6832)
Update faq.md to recommend usage of GPT-5.1 Codex, the latest Codex
model from OpenAI.
2025-11-18 09:38:45 -08:00
zhao-oai
e9e644a119 fixing localshell tool calls (#6823)
- Local-shell tool responses were always tagged as
`ExecCommandSource::UserShell` because handler would call
`run_exec_like` with `is_user_shell_cmd` set to true.
- Treat `ToolPayload::LocalShell` the same as other model generated
shell tool calls by deleting `is_user_shell_cmd` from `run_exec_like`
(since actual user shell commands follow a separate code path)
2025-11-18 17:28:26 +00:00
jif-oai
f5d9939cda feat: enable parallel tool calls (#6796) 2025-11-18 17:10:14 +00:00
jif-oai
838531d3e4 feat: remote compaction (#6795)
Co-authored-by: pakrym-oai <pakrym@openai.com>
2025-11-18 16:51:16 +00:00
jif-oai
0eb2e6f9ee nit: app server (#6830) 2025-11-18 16:34:13 +00:00
jif-oai
c20df79a38 nit: mark ghost commit as stable (#6833) 2025-11-18 16:05:49 +00:00
jif-oai
fc55fd7a81 feat: git branch tooling (#6831) 2025-11-18 15:26:09 +00:00
Lael
f3d4e210d8 🐛 fix(rmcp-client): refresh OAuth tokens using expires_at (#6574)
## Summary
- persist OAuth credential expiry timestamps and rehydrate `expires_in`
- proactively refresh rmcp OAuth tokens when `expires_at` is near, then
persist

## Testing
- just fmt
- just fix -p codex-rmcp-client
- cargo test -p codex-rmcp-client

Fixes #6572
2025-11-18 02:16:58 -05:00
Dylan Hurd
28ebe1c97a fix(windows) shell_command on windows, minor parsing (#6811)
## Summary
Enables shell_command for windows users, and starts adding some basic
command parsing here, to at least remove powershell prefixes. We'll
follow this up with command parsing but I wanted to land this change
separately with some basic UX.

**NOTE**: This implementation parses bash and powershell on both
platforms. In theory this is possible, since you can use git bash on
windows or powershell on linux. In practice, this may not be worth the
complexity of supporting, so I don't feel strongly about the current
approach vs. platform-specific branching.

## Testing
- [x] Added a bunch of tests 
- [x] Ran on both windows and os x
2025-11-17 22:23:53 -08:00
Dylan Hurd
2b7378ac77 chore(core) Add shell_serialization coverage (#6810)
## Summary
Similar to #6545, this PR updates the shell_serialization test suite to
cover the various `shell` tool invocations we have. Note that this does
not cover unified_exec, which has its own suite of tests. This should
provide some test coverage for when we eventually consolidate
serialization logic.

## Testing
- [x] These are tests
2025-11-17 19:10:56 -08:00
Ahmed Ibrahim
ddcc60a085 Update defaults to gpt-5.1 (#6652)
## Summary
- update documentation, example configs, and automation defaults to
reference gpt-5.1 / gpt-5.1-codex
- bump the CLI and core configuration defaults, model presets, and error
messaging to the new models while keeping the model-family/tool coverage
for legacy slugs
- refresh tests, fixtures, and TUI snapshots so they expect the upgraded
defaults

## Testing
- `cargo test -p codex-core
config::tests::test_precedence_fixture_with_gpt5_profile`


------
[Codex
Task](https://chatgpt.com/codex/tasks/task_i_6916c5b3c2b08321ace04ee38604fc6b)
2025-11-17 17:40:11 -08:00
cassirer-openai
8465f1f2f4 Demote function call payload log to debug to avoid noisy error-level stderr (#6808) 2025-11-18 01:16:11 +00:00
zhao-oai
7ab45487dd execpolicy2 extension (#6627)
- enabling execpolicy2 parser to parse multiple policy files to build a
combined `Policy` (useful if codex detects many `.codexpolicy` files)
- adding functionality to `Policy` to allow evaluation of multiple cmds
at once (useful when we have chained commands)
2025-11-17 16:44:41 -08:00
Owen Lin
cecbd5b021 [app-server] feat: add v2 command execution approval flow (#6758)
This PR adds the API V2 version of the command‑execution approval flow
for the shell tool.

This PR wires the new RPC (`item/commandExecution/requestApproval`, V2
only) and related events (`item/started`, `item/completed`, and
`item/commandExecution/delta`, which are emitted in both V1 and V2)
through the app-server
protocol. The new approval RPC is only sent when the user initiates a
turn with the new `turn/start` API so we don't break backwards
compatibility with VSCE.

The approach I took was to make as few changes to the Codex core as
possible, leveraging existing `EventMsg` core events, and translating
those in app-server. I did have to add additional fields to
`EventMsg::ExecCommandEndEvent` to capture the command's input so that
app-server can statelessly transform these events to a
`ThreadItem::CommandExecution` item for the `item/completed` event.

Once we stabilize the API and it's complete enough for our partners, we
can work on migrating the core to be aware of command execution items as
a first-class concept.

**Note**: We'll need followup work to make sure these APIs work for the
unified exec tool, but will wait til that's stable and landed before
doing a pass on app-server.

Example payloads below:
```
{
  "method": "item/started",
  "params": {
    "item": {
      "aggregatedOutput": null,
      "command": "/bin/zsh -lc 'touch /tmp/should-trigger-approval'",
      "cwd": "/Users/owen/repos/codex/codex-rs",
      "durationMs": null,
      "exitCode": null,
      "id": "call_lNWWsbXl1e47qNaYjFRs0dyU",
      "parsedCmd": [
        {
          "cmd": "touch /tmp/should-trigger-approval",
          "type": "unknown"
        }
      ],
      "status": "inProgress",
      "type": "commandExecution"
    }
  }
}
```

```
{
  "id": 0,
  "method": "item/commandExecution/requestApproval",
  "params": {
    "itemId": "call_lNWWsbXl1e47qNaYjFRs0dyU",
    "parsedCmd": [
      {
        "cmd": "touch /tmp/should-trigger-approval",
        "type": "unknown"
      }
    ],
    "reason": "Need to create file in /tmp which is outside workspace sandbox",
    "risk": null,
    "threadId": "019a93e8-0a52-7fe3-9808-b6bc40c0989a",
    "turnId": "1"
  }
}
```

```
{
  "id": 0,
  "result": {
    "acceptSettings": {
      "forSession": false
    },
    "decision": "accept"
  }
}
```

```
{
  "params": {
    "item": {
      "aggregatedOutput": null,
      "command": "/bin/zsh -lc 'touch /tmp/should-trigger-approval'",
      "cwd": "/Users/owen/repos/codex/codex-rs",
      "durationMs": 224,
      "exitCode": 0,
      "id": "call_lNWWsbXl1e47qNaYjFRs0dyU",
      "parsedCmd": [
        {
          "cmd": "touch /tmp/should-trigger-approval",
          "type": "unknown"
        }
      ],
      "status": "completed",
      "type": "commandExecution"
    }
  }
}
```
2025-11-18 00:23:54 +00:00
zhao-oai
4000e26303 background rate limits fetch (#6789)
fetching rate limits every minute asynchronously
2025-11-17 16:06:26 -08:00
iceweasel-oai
e032d338f2 move cap_sid file into ~/.codex so the sandbox cannot overwrite it (#6798)
The `cap_sid` file contains the IDs of the two custom SIDs that the
Windows sandbox creates/manages to implement read-only and
workspace-write sandbox policies.

It previously lived in `<cwd>/.codex` which means that the sandbox could
write to it, which could degrade the efficacy of the sandbox. This
change moves it to `~/.codex/` (or wherever `CODEX_HOME` points to) so
that it is outside the workspace.
2025-11-17 15:49:41 -08:00
Eric Traut
8bebe86a47 Fix TUI issues with Alt-Gr on Windows (#6799)
This PR fixes keyboard handling for the Right Alt (aka "Alt-Gr") key on
Windows. This key appears on keyboards in Central and Eastern Europe.
Codex has effectively never worked for Windows users in these regions
because the code didn't properly handle this key, which is used for
typing common symbols like `\` and `@`.

A few days ago, I merged a [community-authored
PR](https://github.com/openai/codex/pull/6720) that supplied a partial
fix for this issue. Upon closer inspect, that PR was 1) too broad (not
scoped to Windows only) and 2) incomplete (didn't fix all relevant code
paths, so paste was still broken).

This improvement is based on another [community-provided
PR](https://github.com/openai/codex/pull/3241) by @marektomas-cz. He
submitted it back in September and later closed it because it didn't
receive any attention.

This fix addresses the following bugs: #5922, #3046, #3092, #3519,
#5684, #5843.
2025-11-17 15:18:16 -08:00
Jeremy Rose
ab2e7499f8 core: add a feature to disable the shell tool (#6481)
`--disable shell_tool` disables the built-in shell tool. This is useful
for MCP-only operation.

---------

Co-authored-by: Michael Bolin <mbolin@openai.com>
2025-11-17 22:56:19 +00:00
Dylan Hurd
daf77b8452 chore(core) Update shell instructions (#6679)
## Summary
Consolidates `shell` and `shell_command` tool instructions.
## Testing 
- [x] Updated tests, tested locally
2025-11-17 13:05:15 -08:00
Owen Lin
03a6e853c0 fix: annotate all app server v2 types with camelCase (#6791) 2025-11-17 12:02:52 -08:00
rugvedS07
837bc98a1d LM Studio OSS Support (#2312)
## Overview

Adds LM Studio OSS support. Closes #1883


### Changes
This PR enhances the behavior of `--oss` flag to support LM Studio as a
provider. Additionally, it introduces a new flag`--local-provider` which
can take in `lmstudio` or `ollama` as values if the user wants to
explicitly choose which one to use.

If no provider is specified `codex --oss` will auto-select the provider
based on whichever is running.

#### Additional enhancements 
The default can be set using `oss-provider` in config like:

```
oss_provider = "lmstudio"
```

For non-interactive users, they will need to either provide the provider
as an arg or have it in their `config.toml`

### Notes
For best performance, [set the default context
length](https://lmstudio.ai/docs/app/advanced/per-model) for gpt-oss to
the maximum your machine can support

---------

Co-authored-by: Matt Clayton <matt@lmstudio.ai>
Co-authored-by: Eric Traut <etraut@openai.com>
2025-11-17 11:49:09 -08:00
Celia Chen
842a1b7fe7 [app-server] add events to readme (#6690)
add table of contents, lifecycle and events to readme.
2025-11-17 19:28:05 +00:00
Jeremy Rose
03ffe4d595 core/tui: non-blocking MCP startup (#6334)
This makes MCP startup not block TUI startup. Messages sent while MCPs
are booting will be queued.


https://github.com/user-attachments/assets/96e1d234-5d8f-4932-a935-a675d35c05e0


Fixes #6317

---------

Co-authored-by: pakrym-oai <pakrym@openai.com>
2025-11-17 11:26:11 -08:00
Owen Lin
ae2a084fae chore: delete chatwidget::tests::binary_size_transcript_snapshot tui test (#6759)
We're running into quite a bit of drag maintaining this test, since
every time we add fields to an EventMsg that happened to be dumped into
the `binary-size-log.jsonl` fixture, this test starts to fail. The fix
is usually to either manually update the `binary-size-log.jsonl` fixture
file, or update the `upgrade_event_payload_for_tests` function to map
the data in that file into something workable.

Eason says it's fine to delete this test, so let's just delete it
2025-11-17 11:11:41 -08:00
zhao-oai
a941ae7632 feat: execpolicy v2 (#6467)
## Summary
- Introduces the `codex-execpolicy2` crate.
- This PR covers only the prefix-rule subset of the planned execpolicy
v2 language; a richer language will follow.

## Policy
- Policy language centers on `prefix_rule(pattern=[...], decision?,
match?, not_match?)`, where `pattern` is an ordered list of tokens; any
element may be a list to denote alternatives. `decision` defaults to
`allow`; valid values are `allow`, `prompt`, and `forbidden`. `match` /
`not_match` hold example commands that are tokenized and validated at
load time (think of these as unit tests).

## Policy shapes
- Prefix rules use Starlark syntax:
```starlark
prefix_rule(
    pattern = ["cmd", ["alt1", "alt2"]], # ordered tokens; list entries denote alternatives
    decision = "prompt",                # allow | prompt | forbidden; defaults to allow
    match = [["cmd", "alt1"]],          # examples that must match this rule (enforced at compile time)
    not_match = [["cmd", "oops"]],      # examples that must not match this rule (enforced at compile time)
)
```

## Response shapes
- Match:

```json
{
  "match": {
    "decision": "allow|prompt|forbidden",
    "matchedRules": [
      {
        "prefixRuleMatch": {
          "matchedPrefix": ["<token>", "..."],
          "decision": "allow|prompt|forbidden"
        }
      }
    ]
  }
}
```

- No match:

```json
"noMatch"
```

- `matchedRules` lists every rule whose prefix matched the command;
`matchedPrefix` is the exact prefix that matched.
- The effective `decision` is the strictest severity across all matches
(`forbidden` > `prompt` > `allow`).

---------

Co-authored-by: Michael Bolin <mbolin@openai.com>
2025-11-17 10:15:45 -08:00
jif-oai
2c665fb1dd nit: personal git ignore (#6787) 2025-11-17 17:45:52 +00:00
jif-oai
98a90a3bb2 tmp: drop sccache for windows 2 (#6775) 2025-11-17 16:39:15 +00:00
jif-oai
7c8d333980 feat: placeholder for image that can't be decoded to prevent 400 (#6773) 2025-11-17 16:10:53 +00:00
Dylan Hurd
497fb4a19c fix(core) serialize shell_command (#6744)
## Summary
Ensures we're serializing calls to `shell_command`

## Testing
- [x] Added unit test
2025-11-16 23:16:51 -08:00
Xiao-Yong Jin
5860481bc4 Fix FreeBSD/OpenBSD builds: target-specific keyring features and BSD hardening (#6680)
## Summary
Builds on FreeBSD and OpenBSD were failing due to globally enabled
Linux-specific keyring features and hardening code paths not gated by
OS. This PR scopes keyring native backends to the
appropriate targets, disables default features at the workspace root,
and adds a BSD-specific hardening function. Linux/macOS/Windows behavior
remains unchanged, while FreeBSD/OpenBSD
  now build and run with a supported backend.

## Key Changes

  - Keyring features:
- Disable keyring default features at the workspace root to avoid
pulling Linux backends on non-Linux.
- Move native backend features into target-specific sections in the
affected crates:
          - Linux: linux-native-async-persistent
          - macOS: apple-native
          - Windows: windows-native
          - FreeBSD/OpenBSD: sync-secret-service
  - Process hardening:
      - Add pre_main_hardening_bsd() for FreeBSD/OpenBSD, applying:
          - Set RLIMIT_CORE to 0
          - Clear LD_* environment variables
- Simplify process-hardening Cargo deps to unconditional libc (avoid
conflicting OS fragments).
  - No changes to CODEX_SANDBOX_* behavior.

## Rationale

- Previously, enabling keyring native backends globally pulled
Linux-only features on BSD, causing build errors.
- Hardening logic was tailored for Linux/macOS; BSD builds lacked a
gated path with equivalent safeguards.
- Target-scoped features and BSD hardening make the crates portable
across these OSes without affecting existing behavior elsewhere.

## Impact by Platform

  - Linux: No functional change; backends now selected via target cfg.
  - macOS: No functional change; explicit apple-native mapping.
  - Windows: No functional change; explicit windows-native mapping.
- FreeBSD/OpenBSD: Builds succeed using sync-secret-service; BSD
hardening applied during startup.

## Testing

- Verified compilation across affected crates with target-specific
features.
- Smoke-checked that Linux/macOS/Windows feature sets remain identical
functionally after scoping.
- On BSD, confirmed keyring resolves to sync-secret-service and
hardening compiles.

## Risks / Compatibility

  - Minimal risk: only feature scoping and OS-gated additions.
- No public API changes in the crates; runtime behavior on non-BSD
platforms is preserved.
- On BSD, the new hardening clears LD_*; this is consistent with
security posture on other Unix platforms.

## Reviewer Notes

- Pay attention to target-specific sections for keyring in the affected
Cargo.toml files.
- Confirm pre_main_hardening_bsd() mirrors the safe subset of
Linux/macOS hardening without introducing Linux-only calls.
- Confirm no references to CODEX_SANDBOX_ENV_VAR or
CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR were added/modified.

## Checklist

  - Disable keyring default features at workspace root.
- Target-specific keyring features mapped per OS
(Linux/macOS/Windows/BSD).
  - Add BSD hardening (RLIMIT_CORE=0, clear LD_*).
  - Simplify process-hardening dependencies to unconditional libc.
  - No changes to sandbox env var code.
  - Formatting and linting: just fmt + just fix -p for changed crates.
  - Project tests pass for changed crates; broader suite unchanged.

---------

Co-authored-by: celia-oai <celia@openai.com>
2025-11-17 05:07:34 +00:00
Eric Traut
a52cf4d2b4 Exempt the "codex" github user from signing the CLA (#6724)
This fixes bug #6697
2025-11-16 20:49:31 -08:00
dependabot[bot]
e70c52a3af chore(deps): bump actions/github-script from 7 to 8 (#6755)
Bumps [actions/github-script](https://github.com/actions/github-script)
from 7 to 8.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/github-script/releases">actions/github-script's
releases</a>.</em></p>
<blockquote>
<h2>v8.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update Node.js version support to 24.x by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/637">actions/github-script#637</a></li>
<li>README for updating actions/github-script from v7 to v8 by <a
href="https://github.com/sneha-krip"><code>@​sneha-krip</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/653">actions/github-script#653</a></li>
</ul>
<h2>⚠️ Minimum Compatible Runner Version</h2>
<p><strong>v2.327.1</strong><br />
<a
href="https://github.com/actions/runner/releases/tag/v2.327.1">Release
Notes</a></p>
<p>Make sure your runner is updated to this version or newer to use this
release.</p>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/637">actions/github-script#637</a></li>
<li><a
href="https://github.com/sneha-krip"><code>@​sneha-krip</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/653">actions/github-script#653</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/github-script/compare/v7.1.0...v8.0.0">https://github.com/actions/github-script/compare/v7.1.0...v8.0.0</a></p>
<h2>v7.1.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Upgrade husky to v9 by <a
href="https://github.com/benelan"><code>@​benelan</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/482">actions/github-script#482</a></li>
<li>Add workflow file for publishing releases to immutable action
package by <a
href="https://github.com/Jcambass"><code>@​Jcambass</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/485">actions/github-script#485</a></li>
<li>Upgrade IA Publish by <a
href="https://github.com/Jcambass"><code>@​Jcambass</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/486">actions/github-script#486</a></li>
<li>Fix workflow status badges by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/497">actions/github-script#497</a></li>
<li>Update usage of <code>actions/upload-artifact</code> by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/512">actions/github-script#512</a></li>
<li>Clear up package name confusion by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/514">actions/github-script#514</a></li>
<li>Update dependencies with <code>npm audit fix</code> by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/515">actions/github-script#515</a></li>
<li>Specify that the used script is JavaScript by <a
href="https://github.com/timotk"><code>@​timotk</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/478">actions/github-script#478</a></li>
<li>chore: Add Dependabot for NPM and Actions by <a
href="https://github.com/nschonni"><code>@​nschonni</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/472">actions/github-script#472</a></li>
<li>Define <code>permissions</code> in workflows and update actions by
<a href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in
<a
href="https://redirect.github.com/actions/github-script/pull/531">actions/github-script#531</a></li>
<li>chore: Add Dependabot for .github/actions/install-dependencies by <a
href="https://github.com/nschonni"><code>@​nschonni</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/532">actions/github-script#532</a></li>
<li>chore: Remove .vscode settings by <a
href="https://github.com/nschonni"><code>@​nschonni</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/533">actions/github-script#533</a></li>
<li>ci: Use github/setup-licensed by <a
href="https://github.com/nschonni"><code>@​nschonni</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/473">actions/github-script#473</a></li>
<li>make octokit instance available as octokit on top of github, to make
it easier to seamlessly copy examples from GitHub rest api or octokit
documentations by <a
href="https://github.com/iamstarkov"><code>@​iamstarkov</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/508">actions/github-script#508</a></li>
<li>Remove <code>octokit</code> README updates for v7 by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/557">actions/github-script#557</a></li>
<li>docs: add &quot;exec&quot; usage examples by <a
href="https://github.com/neilime"><code>@​neilime</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/546">actions/github-script#546</a></li>
<li>Bump ruby/setup-ruby from 1.213.0 to 1.222.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/github-script/pull/563">actions/github-script#563</a></li>
<li>Bump ruby/setup-ruby from 1.222.0 to 1.229.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/github-script/pull/575">actions/github-script#575</a></li>
<li>Clearly document passing inputs to the <code>script</code> by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/603">actions/github-script#603</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/610">actions/github-script#610</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/benelan"><code>@​benelan</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/482">actions/github-script#482</a></li>
<li><a href="https://github.com/Jcambass"><code>@​Jcambass</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/485">actions/github-script#485</a></li>
<li><a href="https://github.com/timotk"><code>@​timotk</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/478">actions/github-script#478</a></li>
<li><a
href="https://github.com/iamstarkov"><code>@​iamstarkov</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/508">actions/github-script#508</a></li>
<li><a href="https://github.com/neilime"><code>@​neilime</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/546">actions/github-script#546</a></li>
<li><a href="https://github.com/nebuk89"><code>@​nebuk89</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/610">actions/github-script#610</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/github-script/compare/v7...v7.1.0">https://github.com/actions/github-script/compare/v7...v7.1.0</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="ed597411d8"><code>ed59741</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/github-script/issues/653">#653</a>
from actions/sneha-krip/readme-for-v8</li>
<li><a
href="2dc352e4ba"><code>2dc352e</code></a>
Bold minimum Actions Runner version in README</li>
<li><a
href="01e118c8d0"><code>01e118c</code></a>
Update README for Node 24 runtime requirements</li>
<li><a
href="8b222ac82e"><code>8b222ac</code></a>
Apply suggestion from <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a></li>
<li><a
href="adc0eeac99"><code>adc0eea</code></a>
README for updating actions/github-script from v7 to v8</li>
<li><a
href="20fe497b3f"><code>20fe497</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/github-script/issues/637">#637</a>
from actions/node24</li>
<li><a
href="e7b7f222b1"><code>e7b7f22</code></a>
update licenses</li>
<li><a
href="2c81ba05f3"><code>2c81ba0</code></a>
Update Node.js version support to 24.x</li>
<li>See full diff in <a
href="https://github.com/actions/github-script/compare/v7...v8">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/github-script&package-manager=github_actions&previous-version=7&new-version=8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-16 19:53:19 -08:00
dulikaifazr
de1768d3ba Fix: Claude models return incomplete responses due to empty finish_reason handling (#6728)
## Summary
Fixes streaming issue where Claude models return only 1-4 characters
instead of full responses when used through certain API
providers/proxies.

## Environment
- **OS**: Windows
- **Models affected**: Claude models (e.g., claude-haiku-4-5-20251001)
- **API Provider**: AAAI API proxy (https://api.aaai.vip/v1)
- **Working models**: GLM, Google models work correctly

## Problem
When using Claude models in both TUI and exec modes, only 1-4 characters
are displayed despite the backend receiving the full response. Debug
logs revealed that some API providers send SSE chunks with an empty
string finish_reason during active streaming, rather than null or
omitting the field entirely.

The current code treats any non-null finish_reason as a termination
signal, causing the stream to exit prematurely after the first chunk.
The problematic chunks contain finish_reason with an empty string
instead of null.

## Solution
Fix empty finish_reason handling in chat_completions.rs by adding a
check to only process non-empty finish_reason values. This ensures empty
strings are ignored and streaming continues normally.

## Testing
- Tested on Windows with Claude Haiku model via AAAI API proxy
- Full responses now received and displayed correctly in both TUI and
exec modes
- Other models (GLM, Google) continue to work as expected
- No regression in existing functionality

## Impact
- Improves compatibility with API providers that send empty
finish_reason during streaming
- Enables Claude models to work correctly in Windows environment
- No breaking changes to existing functionality

## Related Issues
This fix resolves the issue where Claude models appeared to return
incomplete responses. The root cause was identified as a compatibility
issue in parsing SSE responses from certain API providers/proxies,
rather than a model-specific problem. This change improves overall
robustness when working with various API endpoints.

---------

Co-authored-by: Eric Traut <etraut@openai.com>
2025-11-16 19:50:36 -08:00
Akrelion45
702238f004 Fix AltGr/backslash input on Windows Codex terminal (#6720)
### Summary

- Treat AltGr chords (Ctrl+Alt) as literal character input in the Codex
TUI textarea so Windows terminals that report
    backslash and other characters via AltGr insert correctly.
- Add regression test altgr_ctrl_alt_char_inserts_literal to ensure
Ctrl+Alt char events append the character and
    advance the cursor.

 ### Motivation

On US/UK keyboard layouts, backslash is produced by a plain key, so
Ctrl+Alt handling is never exercised and the
bug isn’t visible. On many non‑US layouts (e.g., German), backslash and
other symbols require AltGr, which terminals
report as Ctrl+Alt+<char>. Our textarea previously filtered these chords
like navigation bindings, so AltGr input was
dropped on affected layouts. This change treats AltGr chords as literal
input so backslash and similar symbols work on
  Windows terminals.

This fixes multiple reported Issues where the \ symbol got cut off.
Like:
C:\Users\Admin
became
C:UsersAdmin

Co-authored-by: Eric Traut <etraut@openai.com>
2025-11-16 19:15:06 -08:00
Eric Traut
fa5f6e76c9 Revert "tmp: drop sccache for windows (#6673)" (#6751)
This reverts commit 4719cba19a
2025-11-16 18:37:12 -08:00
Joonsoo Lee
f828cd2897 fix: resolve Windows MCP server execution for script-based tools (#3828)
## What?

Fixes MCP server initialization failures on Windows when using
script-based tools like `npx`, `pnpm`, and `yarn` that rely on
`.cmd`/`.bat` files rather than `.exe` binaries.

Fixes #2945

## Why?

Windows users encounter "program not found" errors when configuring MCP
servers with commands like `npx` in their `~/.codex/config.toml`. This
happens because:

- Tools like `npx` are batch scripts (`npx.cmd`) on Windows, not
executable binaries
- Rust's `std::process::Command` bypasses the shell and cannot execute
these scripts directly
- The Windows shell normally handles this by checking `PATHEXT` for
executable extensions

Without this fix, Windows users must specify full paths or add `.cmd`
extensions manually, which breaks cross-platform compatibility.

## How?

Added platform-specific program resolution using the `which` crate to
find the correct executable path:

- **Windows**: Resolves programs through PATH/PATHEXT to find
`.cmd`/`.bat` scripts
- **Unix**: Returns the program unchanged (no-op, as Unix handles
scripts natively)

### Changes

- Added `which = "6"` dependency to `mcp-client/Cargo.toml`
- Implemented `program_resolver` module in `mcp_client.rs` with
platform-specific resolution
- Added comprehensive tests for both Windows and Unix behavior

### Testing

Added platform-specific tests to verify:
- Unix systems execute scripts without extensions
- Windows fails without proper extensions
- Windows succeeds with explicit extensions
- Cross-platform resolution enables successful execution

**Tested on:**
- Windows 11 (NT 10.0.26100.0 x64)
- PowerShell 5.1 & 7+, CMD, Git Bash
- MCP servers: playwright, context7, supabase
- WSL (verified no regression)

**Local checks passed:**
```bash
cargo test && cargo clippy --tests && cargo fmt -- --config imports_granularity=Item
```

### Results

**Before:**
```
🖐 MCP client for `playwright` failed to start: program not found
```

**After:**
```
🖐 MCP client for `playwright` failed to start: request timed out
```

Windows users can now use simple commands like `npx` in their config
without specifying full paths or extensions. The timeout issue is a
separate concern that will be addressed in a follow-up PR.

---------

Co-authored-by: Eric Traut <etraut@openai.com>
2025-11-16 13:41:10 -08:00
Abkari Mohammed Sayeem
326c1e0a7e Fix documentation errors for Custom Prompts named arguments and add canonical examples (#5910)
The Custom Prompts documentation (docs/prompts.md) was incomplete for
named arguments:

1. **Documentation for custom prompts was incomplete** - named argument
usage was mentioned briefly but lacked comprehensive canonical examples
showing proper syntax and behavior.

2. **Fixed by adding canonical, tested syntax and examples:**
   - Example 1: Basic named arguments with TICKET_ID and TICKET_TITLE
   - Example 2: Mixed positional and named arguments with FILE and FOCUS
   - Example 3: Using positional arguments
- Example 4: Updated draftpr example to use proper $FEATURE_NAME syntax
   - Added clear usage examples showing KEY=value syntax
   - Added expanded prompt examples showing the result
   - Documented error handling and validation requirements

3. **Added Implementation Reference section** that references the
relevant feature implementation from the codebase (PRs #4470 and #4474
for initial implementation, #5332 and #5403 for clarifications).

This addresses issue #5039 by providing complete, accurate documentation
for named argument usage in custom prompts.

---------

Co-authored-by: Eric Traut <etraut@openai.com>
2025-11-15 09:25:46 -08:00
Ahmed Ibrahim
3f1c4b9add Tighten panic on double truncation (#6701) 2025-11-15 07:28:59 +00:00
Ahmed Ibrahim
0b28e72b66 Improve compact (#6692)
This PR does the following:
- Add compact prefix to the summary
- Change the compaction prompt
- Allow multiple compaction for long running tasks
- Filter out summary messages on the following compaction

Considerations:
- Filtering out the summary message isn't the most clean
- Theoretically, we can end up in infinite compaction loop if the user
messages > compaction limit . However, that's not possible in today's
code because we have hard cap on user messages.
- We need to address having multiple user messages because it confuses
the model.

Testing:
- Making sure that after compact we always end up with one user message
(task) and one summary, even on multiple compaction.
2025-11-15 07:17:51 +00:00
Ahmed Ibrahim
94dfb211af Refactor truncation helpers into its own file (#6683)
That's to centralize the truncation in one place. Next step would be to
make only two methods public: one with bytes/lines and one with tokens.
2025-11-15 06:44:23 +00:00
Ahmed Ibrahim
b560c5cef1 Revert "templates and build step for validating/submitting winget package" (#6696)
Reverts openai/codex#6485
2025-11-15 03:47:58 +00:00
Josh McKinney
4ae986967c ci: only run CLA assistant for openai org repos (#6687)
This prevents notifications coming from PRs on forked repos
2025-11-14 17:34:14 -08:00
Vinicius da Motta
89ecc00b79 Handle "Don't Trust" directory selection in onboarding (#4941)
Fixes #4940
Fixes #4892

When selecting "No, ask me to approve edits and commands" during
onboarding, the code wasn't applying the correct approval policy,
causing Codex to block all write operations instead of requesting
approval.

This PR fixes the issue by persisting the "DontTrust" decision in
config.toml as `trust_level = "untrusted"` and handling it in the
sandbox and approval policy logic, so Codex correctly asks for approval
before making changes.

## Before (bug)
<img width="709" height="500" alt="bef"
src="https://github.com/user-attachments/assets/5aced26d-d810-4754-879a-89d9e4e0073b"
/>

## After (fixed)
<img width="713" height="359" alt="aft"
src="https://github.com/user-attachments/assets/9887bbcb-a9a5-4e54-8e76-9125a782226b"
/>

---------

Co-authored-by: Eric Traut <etraut@openai.com>
2025-11-14 15:23:35 -08:00
pakrym-oai
018a2d2e50 Ignore unified_exec_respects_workdir_override (#6693) 2025-11-14 15:00:31 -08:00
pakrym-oai
cfcc87a953 Order outputs before inputs (#6691)
For better caching performance all output items should be rendered in
the order they were produced before all new input items (for example,
all function_call before all function_call_output).
2025-11-14 14:54:11 -08:00
Owen Lin
c3951e505d feat: add app-server-test-client crate for internal use (#5391)
For app-server development it's been helpful to be able to trigger some
test flows end-to-end and print the JSON-RPC messages sent between
client and server.
2025-11-14 12:39:58 -08:00
iceweasel-oai
abb7b79701 fix codex detection, add new security-focused smoketests. (#6682)
Fix 'codex' detection to look for debug build, then release build, then
installed.

Adds more smoketests around security from @viyatb-oai
2025-11-14 12:08:59 -08:00
Ryan Lopopolo
936650001f feat(ts-sdk): allow overriding CLI environment (#6648)
## Summary
- add an `env` option for the TypeScript Codex client and plumb it into
`CodexExec` so the CLI can run without inheriting `process.env`
- extend the test spy to capture spawn environments, add coverage for
the new option, and document how to use it

## Testing
- `pnpm test` *(fails: corepack cannot download pnpm because outbound
network access is blocked in the sandbox)*

------
[Codex
Task](https://chatgpt.com/codex/tasks/task_i_6916b2d7c7548322a72d61d91a2dac85)
2025-11-14 19:44:19 +00:00
iceweasel-oai
37fba28ac3 templates and build step for validating/submitting winget package (#6485) 2025-11-14 11:06:44 -08:00
pakrym-oai
4ba562d2dd Add test timeout (#6612)
Add an overall test timeout of 30s.
2025-11-14 09:30:37 -08:00
Jeremy Rose
799364de87 Enable TUI notifications by default (#6633)
## Summary
- default the `tui.notifications` setting to enabled so desktop
notifications work out of the box
- update configuration tests and documentation to reflect the new
default

## Testing
- `cargo test -p codex-core` *(fails:
`exec::tests::kill_child_process_group_kills_grandchildren_on_timeout`
is flaky in this sandbox because the spawned grandchild process stays
alive)*
- `cargo test -p codex-core
exec::tests::kill_child_process_group_kills_grandchildren_on_timeout`
*(fails: same sandbox limitation as above)*

------
[Codex
Task](https://chatgpt.com/codex/tasks/task_i_69166f811144832c9e8aaf8ee2642373)
2025-11-14 09:28:09 -08:00
jif-oai
4719cba19a tmp: drop sccache for windows (#6673) 2025-11-14 17:29:05 +01:00
Celia Chen
526777c9b4 [App server] add mcp tool call item started/completed events (#6642)
this PR does two things:
1. refactor `apply_bespoke_event_handling` into a separate file as it's
getting kind of long;
2. add mcp tool call `item/started` and `item/completed` events. To roll
out app server events asap we didn't properly migrate mcp core events to
use TurnItem for mcp tool calls - this will be a follow-up PR.

real events generated in log:
```
{
  "method": "codex/event/mcp_tool_call_end",
  "params": {
    "conversationId": "019a8021-26af-7c20-83db-21ca81e44d68",
    "id": "0",
    "msg": {
      "call_id": "call_7EjRQkD9HnfyMWf7tGrT9FKA",
      "duration": {
        "nanos": 92708,
        "secs": 0
      },
      "invocation": {
        "arguments": {
          "server": ""
        },
        "server": "codex",
        "tool": "list_mcp_resources"
      },
      "result": {
        "Ok": {
          "content": [
            {
              "text": "{\"resources\":[]}",
              "type": "text"
            }
          ],
          "isError": false
        }
      },
      "type": "mcp_tool_call_end"
    }
  }
}

{
  "method": "item/completed",
  "params": {
    "item": {
      "arguments": {
        "server": ""
      },
      "error": null,
      "id": "call_7EjRQkD9HnfyMWf7tGrT9FKA",
      "result": {
        "content": [
          {
            "text": "{\"resources\":[]}",
            "type": "text"
          }
        ],
        "structuredContent": null
      },
      "server": "codex",
      "status": "completed",
      "tool": "list_mcp_resources",
      "type": "mcpToolCall"
    }
  }
}
```
2025-11-14 08:08:43 -08:00
jif-oai
f17b392470 feat: cache tokenizer (#6609) 2025-11-14 17:05:00 +01:00
jif-oai
63c8c01f40 feat: better UI for unified_exec (#6515)
<img width="376" height="132" alt="Screenshot 2025-11-12 at 17 36 22"
src="https://github.com/user-attachments/assets/ce693f0d-5ca0-462e-b170-c20811dcc8d5"
/>
2025-11-14 16:31:12 +01:00
jif-oai
4788fb179a feat: add resume logs when doing /new (#6660)
<img width="769" height="803" alt="Screenshot 2025-11-14 at 10 25 49"
src="https://github.com/user-attachments/assets/12fbc21e-cab9-4d0a-a484-1aeb60219f96"
/>
2025-11-14 11:42:16 +01:00
pakrym-oai
6c384eb9c6 tests: replace mount_sse_once_match with mount_sse_once for SSE mocking (#6640) 2025-11-13 18:04:05 -08:00
Ahmed Ibrahim
2a6e9b20df Promote shared helpers for suite tests (#6460)
## Summary
- add `TestCodex::submit_turn_with_policies` and extend the response
helpers with reusable tool-call utilities
- update the grep_files, read_file, list_dir, shell_serialization, and
tools suites to rely on the shared helpers instead of local copies
- make the list_dir helper return `anyhow::Result` so clippy no longer
warns about `expect`

## Testing
- `just fix -p codex-core`
- `cargo test -p codex-core --test all
suite::grep_files::grep_files_tool_collects_matches`
- `cargo test -p codex-core
suite::grep_files::grep_files_tool_collects_matches -- --ignored`
(filter requests ignored tests so nothing runs, but the build stays
clean)


------
[Codex
Task](https://chatgpt.com/codex/tasks/task_i_69112d53abac83219813cab4d7cb6446)
2025-11-13 17:12:10 -08:00
Ahmed Ibrahim
f3c6b1334b Use shared network gating helper in chat completion tests (#6461)
## Summary
- replace the bespoke network check in the chat completion payload and
SSE tests with the existing `skip_if_no_network!` helper so they follow
the same gating convention as the rest of the suite

## Testing
- `just fmt`


------
[Codex
Task](https://chatgpt.com/codex/tasks/task_i_69112d4cb9f08321ba773e8ccf39778e)
2025-11-13 17:11:43 -08:00
Ahmed Ibrahim
9890ceb939 Avoid double truncation (#6631)
1. Avoid double truncation by giving 10% above the tool default constant
2. Add tests that fails when const = 1
2025-11-13 16:59:31 -08:00
pakrym-oai
7b027e7536 Revert "Revert "Overhaul shell detection and centralize command generation for unified exec"" (#6607)
Reverts openai/codex#6606
2025-11-13 16:45:17 -08:00
Owen Lin
db2aa57d73 [app-server] small fixes for JSON schema export and one-of types (#6614)
A partner is consuming our generated JSON schema bundle for app-server
and identified a few issues:
- not all polymorphic / one-of types have a type descriminator
- `"$ref": "#/definitions/v2/SandboxPolicy"` is missing
- "Option<>" is an invalid schema name, and also unnecessary

This PR:
- adds the type descriminator to the various types that are missing it
except for `SessionSource` and `SubAgentSource` because they are
serialized to disk (adding this would break backwards compat for
resume), and they should not be necessary to consume for an integration
with app-server.
- removes the special handling in `export.rs` of various types like
SandboxPolicy, which turned out to be unnecessary and incorrect
- filters out `Option<>` which was auto-generated for request params
that don't need a body

For context, we currently pull in wayyy more types than we need through
the `EventMsg` god object which we are **not** planning to expose in API
v2 (this is how I suspect `SessionSource` and `SubAgentSource` are being
pulled in). But until we have all the necessary v2 notifications in
place that will allow us to remove `EventMsg`, we will keep exporting it
for now.
2025-11-13 16:25:17 -08:00
Celia Chen
b8ec97c0ef [App-server] add new v2 events:item/reasoning/delta, item/agentMessage/delta & item/reasoning/summaryPartAdded (#6559)
core event to app server event mapping:
1. `codex/event/reasoning_content_delta` ->
`item/reasoning/summaryTextDelta`.
2. `codex/event/reasoning_raw_content_delta` ->
`item/reasoning/textDelta`
3. `codex/event/agent_message_content_delta` →
`item/agentMessage/delta`.
4. `codex/event/agent_reasoning_section_break` ->
`item/reasoning/summaryPartAdded`.

Also added a change in core to pass down content index, summary index
and item id from events.

Tested with the `git checkout owen/app_server_test_client && cargo run
-p codex-app-server-test-client -- send-message-v2 "hello"` and verified
that new events are emitted correctly.
2025-11-14 00:25:01 +00:00
Dylan Hurd
2c1b693da4 chore(core) Consolidate apply_patch tests (#6545)
## Summary
Consolidates our apply_patch tests into one suite, and ensures each test
case tests the various ways the harness supports apply_patch:
1. Freeform custom tool call
2. JSON function tool
3. Simple shell call
4. Heredoc shell call

There are a few test cases that are specific to a particular variant,
I've left those alone.

## Testing
- [x] This adds a significant number of tests
2025-11-13 15:52:39 -08:00
pakrym-oai
547be54ee8 Only list failed tests (#6619)
Makes output easier to parse
2025-11-13 13:50:33 -08:00
Dan Hernandez
b4a53aef47 feat: Add support for --add-dir to exec and TypeScript SDK (#6565)
## Summary

Adds support for specifying additional directories in the TypeScript SDK
through a new `additionalDirectories` option in `ThreadOptions`.

## Changes

- Added `additionalDirectories` parameter to `ThreadOptions` interface
- Updated `CodexExec` to accept and pass through additional directories
via the `--config` flag for `sandbox_workspace_write.writable_roots`
- Added comprehensive test coverage for the new functionality

## Test plan

- Added test case that verifies `additionalDirectories` is correctly
passed as repeated flags
- Existing tests continue to pass

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-13 13:47:10 -08:00
Dan Hernandez
439bc5dbbe Add AbortSignal support to TypeScript SDK (#6378)
## Summary
Adds AbortSignal support to the TypeScript SDK for canceling thread
execution using AbortController.

## Changes
- Add `signal?: AbortSignal` property to `TurnOptions` type
- Pass signal through Thread class methods to exec layer  
- Add signal parameter to `CodexExecArgs`
- Leverage Node.js native `spawn()` signal support for automatic
cancellation
- Add comprehensive test coverage (6 tests covering all abort scenarios)

## Implementation
The implementation uses Node.js's built-in AbortSignal support in
`spawn()` (available since Node v15, SDK requires >=18), which
automatically handles:
- Checking if already aborted before starting
- Killing the child process when abort is triggered
- Emitting appropriate error events
- All cleanup operations

This is a one-line change to the core implementation (`signal:
args.signal` passed to spawn), making it simple, reliable, and
maintainable.

## Usage Example
```typescript
import { Codex } from '@openai/codex-sdk';

const codex = new Codex({ apiKey: 'your-api-key' });
const thread = codex.startThread();

// Create AbortController
const controller = new AbortController();

// Run with abort signal
const resultPromise = thread.run("Your prompt here", {
  signal: controller.signal
});

// Cancel anytime
controller.abort('User requested cancellation');
```

## Testing
All tests pass (23 total across SDK):
-  Aborts when signal is already aborted (both run and runStreamed)
-  Aborts during execution/iteration
-  Completes normally when not aborted
-  Backward compatible (signal is optional)

Tests verified to fail correctly when signal support is removed (no
false positives).

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: pakrym-oai <pakrym@openai.com>
2025-11-13 13:35:42 -08:00
pakrym-oai
c95bd345ea Enable close-stale-contributor-prs.yml workflow (#6615)
Tested on https://github.com/openai/codex/pull/3036
2025-11-13 11:50:54 -08:00
pakrym-oai
0792a7953d Update default yield time (#6610)
10s for exec and 250ms for write_stdin
2025-11-13 10:24:41 -08:00
pakrym-oai
6cda3de3a4 Close stale PRs workflow (#6594)
Closes stale PRs from OpenAI employees.
2025-11-13 10:01:51 -08:00
pakrym-oai
041d6ad902 Migrate prompt caching tests to test_codex (#6605)
To hopefully fix the flakiness
2025-11-13 09:19:38 -08:00
pakrym-oai
e6995174c1 Revert "Overhaul shell detection and centralize command generation for unified exec" (#6606)
Reverts openai/codex#6577
2025-11-13 08:43:00 -08:00
pakrym-oai
d28e912214 Overhaul shell detection and centralize command generation for unified exec (#6577)
This fixes command display for unified exec. All `cd`s and `ls`es are
now parsed.

<img width="452" height="237" alt="image"
src="https://github.com/user-attachments/assets/ce92d81f-f74c-485a-9b34-1eaa29290ec6"
/>

Deletes a ton of tests that were doing nothing from shell.rs.

---------

Co-authored-by: Pavel Krymets <pavel@krymets.com>
2025-11-13 08:28:09 -08:00
Ahmed Ibrahim
ba74cee6f7 fix model picker wrapping (#6589)
Previously the popup measured rows using the full content width while
the renderer drew them with 2 columns of padding, so at certain widths
the layout allocated too little vertical space and hid the third option.
Now both desired_height and render call a shared helper that subtracts
the padding before measuring, so the height we reserve always matches
what we draw and the menu doesn't drops entries.


https://github.com/user-attachments/assets/59058fd9-1e34-4325-b5fe-fc888dfcb6bc
2025-11-13 08:09:13 -08:00
jif-oai
2a417c47ac feat: proxy context left after compaction (#6597) 2025-11-13 16:54:03 +01:00
Dylan Hurd
8dcbd29edd chore(core) Update prompt for gpt-5.1 (#6588)
## Summary
Updates the prompt for GPT-5.1
2025-11-13 07:51:28 -08:00
pakrym-oai
34621166d5 Default to explicit medium reasoning for 5.1 (#6593) 2025-11-13 07:58:42 +00:00
pakrym-oai
e3dd362c94 Reasoning level update (#6586)
Automatically update reasoning levels when migrating between models
2025-11-13 06:24:36 +00:00
Ahmed Ibrahim
305fe73d83 copy for model migration nudge (#6585) 2025-11-13 05:56:30 +00:00
Ahmed Ibrahim
e3aaee00c8 feat: show gpt mini (#6583) 2025-11-13 05:21:00 +00:00
Ahmed Ibrahim
b1979b70a8 remove porcupine model slug (#6580) 2025-11-13 04:43:31 +00:00
Eric Traut
73ed30d7e5 Avoid hang when tool's process spawns grandchild that shares stderr/stdout (#6575)
We've received many reports of codex hanging when calling certain tools.
[Here](https://github.com/openai/codex/issues/3204) is one example. This
is likely a major cause. The problem occurs when
`consume_truncated_output` waits for `stdout` and `stderr` to be closed
once the child process terminates. This normally works fine, but it
doesn't handle the case where the child has spawned grandchild processes
that inherits `stdout` and `stderr`.

The fix was originally written by @md-oai in [this
PR](https://github.com/openai/codex/pull/1852), which has gone stale.
I've copied the original fix (which looks sound to me) and added an
integration test to prevent future regressions.
2025-11-12 20:08:12 -08:00
Ahmed Ibrahim
ad7eaa80f9 Change model picker to include gpt5.1 (#6569)
- Change the presets
- Change the tests that make sure we keep the list of tools updated
- Filter out deprecated models
2025-11-12 19:44:53 -08:00
Ahmed Ibrahim
966d71c02a Update subtitle of model picker as part of the nux (#6567) 2025-11-12 18:30:43 -08:00
pakrym-oai
f97874093e Set verbosity to low for 5.1 (#6568)
And improve test coverage
2025-11-13 01:40:52 +00:00
Ahmed Ibrahim
e63ab0dd65 NUX for gpt5.1 (#6561)
- Introducing a screen to inform users of model changes. 
- Config name is being passed to be able to reuse this component in the
future for future models
2025-11-13 01:24:21 +00:00
Owen Lin
964220ac94 [app-server] feat: thread/resume supports history, path, and overrides (#6483)
This updates `thread/resume` to be at parity with v1's
`ResumeConversationParams`. Turns out history is useful for codex cloud
and path is useful for the VSCode extension. And config overrides are
always useful.
2025-11-12 22:02:43 +00:00
pakrym-oai
2f58e69997 Do not double encode request bodies in logging (#6558) 2025-11-12 21:28:42 +00:00
pakrym-oai
ec69a4a810 Add gpt-5.1 model definitions (#6551) 2025-11-12 12:44:36 -08:00
Eric Traut
ad09c138b9 Fixed status output to use auth information from AuthManager (#6529)
This PR addresses https://github.com/openai/codex/issues/6360. The root
problem is that the TUI was directly loading the `auth.json` file to
access the auth information. It should instead be using the AuthManager,
which records the current auth information. The `auth.json` file can be
overwritten at any time by other instances of the CLI or extension, so
its information can be out of sync with the current instance. The
`/status` command should always report the auth information associated
with the current instance.

An alternative fix for this bug was submitted by @chojs23 in [this
PR](https://github.com/openai/codex/pull/6495). That approach was only a
partial fix.
2025-11-12 10:26:50 -08:00
jif-oai
e00eb50db3 feat: only wait for mutating tools for ghost commit (#6534) 2025-11-12 18:16:32 +00:00
pakrym-oai
7d9ad3effd Fix otel tests (#6541)
Mount responses only once, remove unneeded retries and add a final
assistant messages to complete the turn.
2025-11-12 16:35:34 +00:00
Michael Bolin
c3a710ee14 chore: verify boolean values can be parsed as config overrides (#6516)
This is important to ensure that this:

```
codex --enable unified_exec
```

and this:

```
codex --config features.unified_exec=true
```

are equivalent. Also that when it is passed programmatically:


807e2c27f0/codex-rs/app-server-protocol/src/protocol/v1.rs (L55)

then this should work for `config`:

```json
{"features": {"shell_command_tool": true}}
```

though I believe also this:

```json
{"features.shell_command_tool": true}
```
2025-11-12 08:19:16 -08:00
Michael Bolin
29364f3a9b feat: shell_command tool (#6510)
This adds support for a new variant of the shell tool behind a flag. To
test, run `codex` with `--enable shell_command_tool`, which will
register the tool with Codex under the name `shell_command` that accepts
the following shape:

```python
{
  command: str
  workdir: str | None,
  timeout_ms: int | None,
  with_escalated_permissions: bool | None,
  justification: str | None,
}
```

This is comparable to the existing tool registered under
`shell`/`container.exec`. The primary difference is that it accepts
`command` as a `str` instead of a `str[]`. The `shell_command` tool
executes by running `execvp(["bash", "-lc", command])`, though the exact
arguments to `execvp(3)` depend on the user's default shell.

The hypothesis is that this will simplify things for the model. For
example, on Windows, instead of generating:

```json
{"command": ["pwsh.exe", "-NoLogo", "-Command", "ls -Name"]}
```

The model could simply generate:

```json
{"command": "ls -Name"}
```

As part of this change, I extracted some logic out of `user_shell.rs` as
`Shell::derive_exec_args()` so that it can be reused in
`codex-rs/core/src/tools/handlers/shell.rs`. Note the original code
generated exec arg lists like:

```javascript
["bash", "-lc", command]
["zsh", "-lc", command]
["pwsh.exe", "-NoProfile", "-Command", command]
```

Using `-l` for Bash and Zsh, but then specifying `-NoProfile` for
PowerShell seemed inconsistent to me, so I changed this in the new
implementation while also adding a `use_login_shell: bool` option to
make this explicit. If we decide to add a `login: bool` to
`ShellCommandToolCallParams` like we have for unified exec:


807e2c27f0/codex-rs/core/src/tools/handlers/unified_exec.rs (L33-L34)

Then this should make it straightforward to support.
2025-11-12 08:18:57 -08:00
jif-oai
530db0ad73 feat: warning switch model on resume (#6507)
<img width="1259" height="40" alt="Screenshot 2025-11-11 at 14 01 41"
src="https://github.com/user-attachments/assets/48ead3d2-d89c-4d8a-a578-82d9663dbd88"
/>
2025-11-12 11:13:37 +00:00
Gabriel Peal
424bfecd0b Re-add prettier log-level=warn to generate-ts (#6528)
I added it in https://github.com/openai/codex/pull/6342 but it was
removed in
https://github.com/openai/codex/pull/5063/files#diff-e2aa6dad1e886b7765158a27aefd1be5de99baa71b44f6bc5ce3fe462b9ae5d3R135
as a result of a bad diamond merge
2025-11-11 21:30:01 -05:00
Lionel Cheng
eb1c651c00 Update full-auto description with on-request (#6523)
This PR fixes #6522 by correcting the comment for `full-auto` in both
`codex-rs/exec/src/cli.rs` and `codex-rs/tui/src/cli.rs` from `-a
on-failure` to `-a on-request` to make it coherent with
`codex-rs/tui/src/lib.rs:97-105`:

```rust
pub async fn run_main(
    mut cli: Cli,
    codex_linux_sandbox_exe: Option<PathBuf>,
) -> std::io::Result<AppExitInfo> {
    let (sandbox_mode, approval_policy) = if cli.full_auto {
        (
            Some(SandboxMode::WorkspaceWrite),
            Some(AskForApproval::OnRequest),
        )
```

Running `just codex --help` or `just codex exec --help` should now yield
the correct description of `full-auto` CLI argument.

Signed-off-by: lionelchg <lionel.cheng@hotmail.fr>
2025-11-11 15:59:20 -08:00
Celia Chen
e357fc723d [app-server] add item started/completed events for turn items (#6517)
This one should be quite straightforward, as it's just a translation of
TurnItem events we already emit to ThreadItem that app-server exposes to
customers.

To test, cp my change to owen/app_server_test_client and do the
following:
```
cargo build -p codex-cli
RUST_LOG=codex_app_server=info CODEX_BIN=target/debug/codex cargo run -p codex-app-server-test-client -- send-message-v2 "hello"
```

example event before (still kept there for backward compatibility):
```
{
<   "method": "codex/event/item_completed",
<   "params": {
<     "conversationId": "019a74cc-fad9-7ab3-83a3-f42827b7b074",
<     "id": "0",
<     "msg": {
<       "item": {
<         "Reasoning": {
<           "id": "rs_03d183492e07e20a016913a936eb8c81a1a7671a103fee8afc",
<           "raw_content": [],
<           "summary_text": [
<             "Hey! What would you like to work on? I can explore the repo, run specific tests, or implement a change. Let's keep it short and straightforward. There's no need for a lengthy introduction or elaborate planning, just a friendly greeting and an open offer to help. I want to make sure the user feels welcomed and understood right from the start. It's all about keeping the tone friendly and concise!"
<           ]
<         }
<       },
<       "thread_id": "019a74cc-fad9-7ab3-83a3-f42827b7b074",
<       "turn_id": "0",
<       "type": "item_completed"
<     }
<   }
< }
```

after (v2):
```
< {
<   "method": "item/completed",
<   "params": {
<     "item": {
<       "id": "rs_03d183492e07e20a016913a936eb8c81a1a7671a103fee8afc",
<       "text": "Hey! What would you like to work on? I can explore the repo, run specific tests, or implement a change. Let's keep it short and straightforward. There's no need for a lengthy introduction or elaborate planning, just a friendly greeting and an open offer to help. I want to make sure the user feels welcomed and understood right from the start. It's all about keeping the tone friendly and concise!",
<       "type": "reasoning"
<     }
<   }
< }
```
2025-11-11 22:43:24 +00:00
pakrym-oai
807e2c27f0 Add unified exec escalation handling and tests (#6492)
Similar implementation to the shell tool
2025-11-11 08:19:35 -08:00
jif-oai
ad279eacdc nit: logs to trace (#6503) 2025-11-11 13:37:06 +00:00
jif-oai
052b052832 Enable ghost_commit feature by default (#6041)
## Summary
- enable the ghost_commit feature flag by default

## Testing
- just fmt

------
https://chatgpt.com/codex/tasks/task_i_6904ce2d0370832dbb3c2c09a90fb188
2025-11-11 09:20:46 +00:00
Celia Chen
6951872776 [hygiene][app-server] have a helper function for duplicate code in turn APIs (#6488)
turn_start and turn_interrupt have some logic that can be shared. have a
helper function for it.
2025-11-11 02:44:47 +00:00
pakrym-oai
bb7b0213a8 Colocate more of bash parsing (#6489)
Move a few callsites that were detecting `bash -lc` into a shared
helper.
2025-11-11 02:38:36 +00:00
pakrym-oai
6c36318bd8 Use codex-linux-sandbox in unified exec (#6480)
Unified exec isn't working on Linux because we don't provide the correct
arg0.

The library we use for pty management doesn't allow setting arg0
separately from executable. Use the same aliasing strategy we use for
`apply_patch` for `codex-linux-sandbox`.

Use `#[ctor]` hack to dispatch codex-linux-sandbox calls.


Addresses https://github.com/openai/codex/issues/6450
2025-11-10 17:17:09 -08:00
zhao-oai
930f81a17b flip rate limit status bar (#6482)
flipping rate limit status bar to match chat.com/codex/settings/usage

<img width="848" height="420" alt="Screenshot 2025-11-10 at 4 53 41 PM"
src="https://github.com/user-attachments/assets/e326db3f-4405-412d-9e62-337282ec9a35"
/>
2025-11-11 01:13:10 +00:00
iceweasel-oai
9aff64e017 upload Windows .exe file artifacts for CLI releases (#6478)
This PR is to unlock future WinGet installation. WinGet struggles to
create command aliases when installing from nested ZIPs on some clients,
so adding raw Windows x64/Arm64 executables lets the manifest use
InstallerType: portable with direct EXEs, which reliably registers the
codex alias. This makes “winget install → codex” work out of the box
without PATH changes.

---------

Co-authored-by: Michael Bolin <mbolin@openai.com>
2025-11-10 23:31:06 +00:00
Owen Lin
3838d6739c [app-server] update macro to make renaming methods less boilerplate-y (#6470)
We already do this for notification definitions and it's really nice.

Verified there are no changes to actual exported files by diff'ing
before and after this change.
2025-11-10 15:15:08 -08:00
Josh McKinney
60deb6773a refactor(tui): job-control for Ctrl-Z handling (#6477)
- Moved the unix-only suspend/resume logic into a dedicated job_control
module housing SuspendContext, replacing scattered cfg-gated fields and
helpers in tui.rs.
- Tui now holds a single suspend_context (Arc-backed) instead of
multiple atomics, and the event stream uses it directly for Ctrl-Z
handling.
- Added detailed docs around the suspend/resume flow, cursor tracking,
and the Arc/atomic ownership model for the 'static event stream.
- Renamed the process-level SIGTSTP helper to suspend_process and the
cursor tracker to set_cursor_y to better reflect their roles.
2025-11-10 23:13:43 +00:00
Jeremy Rose
0271c20d8f add codex debug seatbelt --log-denials (#4098)
This adds a debugging tool for analyzing why certain commands fail to
execute under the sandbox.

Example output:

```
$ codex debug seatbelt --log-denials bash -lc "(echo foo > ~/foo.txt)"
bash: /Users/nornagon/foo.txt: Operation not permitted

=== Sandbox denials ===
(bash) file-write-data /dev/tty
(bash) file-write-data /dev/ttys001
(bash) sysctl-read kern.ngroups
(bash) file-write-create /Users/nornagon/foo.txt
```

It operates by:

1. spawning `log stream` to watch system logs, and
2. tracking all descendant PIDs using kqueue + proc_listchildpids.

this is a "best-effort" technique, as `log stream` may drop logs(?), and
kqueue + proc_listchildpids isn't atomic and can end up missing very
short-lived processes. But it works well enough in my testing to be
useful :)
2025-11-10 22:48:14 +00:00
George Nesterenok
52e97b9b6b Fix wayland image paste error (#4824)
## Summary
- log and surface clipboard failures instead of silently ignoring them
when `Ctrl+V` pastes an image (`paste_image_to_temp_png()` now feeds an
error history cell)
- enable `arboard`’s `wayland-data-control` feature so native Wayland
sessions can deliver image data without XWayland
- keep the success path unchanged: valid images still attach and show
the `[image …]` placeholder as before

Fixes #4818

---------

Co-authored-by: Eric Traut <etraut@openai.com>
Co-authored-by: Jeremy Rose <172423086+nornagon-openai@users.noreply.github.com>
2025-11-10 14:35:30 -08:00
Owen Lin
2ac49fea58 [app-server] chore: move initialize out of deprecated API section (#6468)
Self-explanatory - `initialize` is not a deprecated API and works
equally well with the v2 APIs.
2025-11-10 20:24:36 +00:00
jif-oai
f01f2ec9ee feat: add workdir to unified_exec (#6466) 2025-11-10 19:53:36 +00:00
zhao-oai
980886498c Add user command event types (#6246)
adding new user command event, logic in TUI to render user command
events
2025-11-10 19:18:45 +00:00
Ahmed Ibrahim
e743d251a7 Add opt-out for rate limit model nudge (#6433)
## Summary
- add a `hide_rate_limit_model_nudge` notice flag plus config edit
plumbing so the rate limit reminder preference is persisted and
documented
- extend the chat widget prompt with a "never show again" option, and
wire new app events so selecting it hides future nudges immediately and
writes the config
- add unit coverage and refresh the snapshot for the three-option prompt

## Testing
- `just fmt`
- `just fix -p codex-tui`
- `just fix -p codex-core`
- `cargo test -p codex-tui`
- `cargo test -p codex-core` *(fails at
`exec::tests::kill_child_process_group_kills_grandchildren_on_timeout`:
grandchild process still alive)*

------
[Codex
Task](https://chatgpt.com/codex/tasks/task_i_6910d7f407748321b2661fc355416994)
2025-11-10 09:21:53 -08:00
Shijie Rao
788badd221 fix: update brew auto update version check (#6238)
### Summary
* Use
`https://github.com/Homebrew/homebrew-cask/blob/main/Casks/c/codex.rb`
to get the latest available version for brew usage.
2025-11-10 09:05:00 -08:00
Owen Lin
fbdedd9a06 [app-server] feat: add command to generate json schema (#6406)
Add a `codex generate-json-schema` command for generating a JSON schema
bundle of app-server types, analogous to the existing `codex
generate-ts` command for Typescript.
2025-11-10 16:59:14 +00:00
Eric Traut
5916153157 Don't lock PRs that have been closed without merging (#6422)
The CLA action is designed to automatically lock a PR when it is closed.
This preserves the CLA agreement statements, preventing the contributor
from deleting them after the fact. However, this action is currently
locking PRs that are closed without merging. I'd like to keep such PRs
open so the contributor can respond with additional comments. I'm
currently manually unlocking PRs that I close, but I'd like to eliminate
this manual step.
2025-11-10 08:46:11 -08:00
Eric Traut
b46012e483 Support exiting from the login menu (#6419)
I recently fixed a bug in [this
PR](https://github.com/openai/codex/pull/6285) that prevented Ctrl+C
from dismissing the login menu in the TUI and leaving the user unauthed.

A [user pointed out](https://github.com/openai/codex/issues/6418) that
this makes Ctrl+C can no longer be used to exit the app. This PR changes
the behavior so we exit the app rather than ignoring the Ctrl+C.
2025-11-10 08:43:11 -08:00
Owen Lin
42683dadfb fix: use generate_ts from app_server_protocol (#6407)
Update `codex generate-ts` to use the TS export code from
`app-server-protocol/src/export.rs`.

I realized there were two duplicate implementations of Typescript export
code:
- `app-server-protocol/src/export.rs`
- the `codex-protocol-ts` crate

The `codex-protocol-ts` crate that `codex generate-ts` uses is out of
date now since it doesn't handle the V2 namespace from:
https://github.com/openai/codex/pull/6212.
2025-11-10 08:08:12 -08:00
Eric Traut
65cb1a1b77 Updated docs to reflect recent changes in web_search configuration (#6376)
This is a simplified version of [a
PR](https://github.com/openai/codex/pull/6134) supplied by a community
member.

It updates the docs to reflect a recent config deprecation.
2025-11-10 07:57:56 -08:00
jif-oai
50a77dc138 Move compact (#6454) 2025-11-10 11:59:48 +00:00
Raduan A.
557ac63094 Fix config documentation: correct TOML parsing description (#6424)
The CLI help text and inline comments incorrectly stated that -c
key=value flag parses values as JSON, when the implementation actually
uses TOML parsing via parse_toml_value(). This caused confusion when
users attempted to configure MCP servers using JSON syntax based on the
documentation.

Changes:
- Updated help text to correctly state TOML parsing instead of JSON

Fixes #4531
2025-11-09 22:58:32 -08:00
Andrew Nikolin
131c384361 Fix warning message phrasing (#6446)
Small fix for sentence phrasing in the warning message

Co-authored-by: AndrewNikolin <877163+AndrewNikolin@users.noreply.github.com>
2025-11-09 22:12:28 -08:00
dependabot[bot]
e2598f5094 chore(deps): bump zeroize from 1.8.1 to 1.8.2 in /codex-rs (#6444)
Bumps [zeroize](https://github.com/RustCrypto/utils) from 1.8.1 to
1.8.2.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="c100874101"><code>c100874</code></a>
zeroize v1.8.2 (<a
href="https://redirect.github.com/RustCrypto/utils/issues/1229">#1229</a>)</li>
<li><a
href="3940ccbebd"><code>3940ccb</code></a>
Switch from <code>doc_auto_cfg</code> to <code>doc_cfg</code> (<a
href="https://redirect.github.com/RustCrypto/utils/issues/1228">#1228</a>)</li>
<li><a
href="c68a5204b2"><code>c68a520</code></a>
Fix Nightly warnings (<a
href="https://redirect.github.com/RustCrypto/utils/issues/1080">#1080</a>)</li>
<li><a
href="b15cc6c1cd"><code>b15cc6c</code></a>
cargo: point <code>repository</code> metadata to clonable URLs (<a
href="https://redirect.github.com/RustCrypto/utils/issues/1079">#1079</a>)</li>
<li><a
href="3db6690f7b"><code>3db6690</code></a>
zeroize: fix <code>homepage</code>/<code>repository</code> in Cargo.toml
(<a
href="https://redirect.github.com/RustCrypto/utils/issues/1076">#1076</a>)</li>
<li>See full diff in <a
href="https://github.com/RustCrypto/utils/compare/zeroize-v1.8.1...zeroize-v1.8.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=zeroize&package-manager=cargo&previous-version=1.8.1&new-version=1.8.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-09 22:03:36 -08:00
dependabot[bot]
78b2aeea55 chore(deps): bump askama from 0.12.1 to 0.14.0 in /codex-rs (#6443)
Bumps [askama](https://github.com/askama-rs/askama) from 0.12.1 to
0.14.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/askama-rs/askama/releases">askama's
releases</a>.</em></p>
<blockquote>
<h2>v0.14.0</h2>
<h2>Added Features</h2>
<ul>
<li>Implement <code>Values</code> on tuple by <a
href="https://github.com/GuillaumeGomez"><code>@​GuillaumeGomez</code></a>
in <a
href="https://redirect.github.com/askama-rs/askama/pull/391">askama-rs/askama#391</a></li>
<li>Pass variables to sub-templates more reliably even if indirectly by
<a href="https://github.com/Kijewski"><code>@​Kijewski</code></a> in <a
href="https://redirect.github.com/askama-rs/askama/pull/397">askama-rs/askama#397</a></li>
<li>Implement <code>first</code> and <code>blank</code> arguments for
<code>|indent</code> by <a
href="https://github.com/Kijewski"><code>@​Kijewski</code></a> in <a
href="https://redirect.github.com/askama-rs/askama/pull/401">askama-rs/askama#401</a></li>
<li>Add named arguments for builtin filters by <a
href="https://github.com/Kijewski"><code>@​Kijewski</code></a> in <a
href="https://redirect.github.com/askama-rs/askama/pull/403">askama-rs/askama#403</a></li>
<li>Add <code>unique</code> filter by <a
href="https://github.com/GuillaumeGomez"><code>@​GuillaumeGomez</code></a>
in <a
href="https://redirect.github.com/askama-rs/askama/pull/405">askama-rs/askama#405</a></li>
</ul>
<h2>Bug Fixes And Consistency</h2>
<ul>
<li><code>askama_derive</code> accidentally exposed as a feature by <a
href="https://github.com/Kijewski"><code>@​Kijewski</code></a> in <a
href="https://redirect.github.com/askama-rs/askama/pull/384">askama-rs/askama#384</a></li>
<li>Track config files by <a
href="https://github.com/GuillaumeGomez"><code>@​GuillaumeGomez</code></a>
in <a
href="https://redirect.github.com/askama-rs/askama/pull/385">askama-rs/askama#385</a></li>
<li>If using local variable as value when creating a new variable, do
not put it behind a reference by <a
href="https://github.com/GuillaumeGomez"><code>@​GuillaumeGomez</code></a>
in <a
href="https://redirect.github.com/askama-rs/askama/pull/392">askama-rs/askama#392</a></li>
<li>generator: make <code>CARGO_MANIFEST_DIR</code> part of
<code>ConfigKey</code> by <a
href="https://github.com/strickczq"><code>@​strickczq</code></a> in <a
href="https://redirect.github.com/askama-rs/askama/pull/395">askama-rs/askama#395</a></li>
<li>Do not put question mark initialization expressions behind a
reference by <a
href="https://github.com/GuillaumeGomez"><code>@​GuillaumeGomez</code></a>
in <a
href="https://redirect.github.com/askama-rs/askama/pull/400">askama-rs/askama#400</a></li>
<li>Update to more current rust version on readthedocs by <a
href="https://github.com/Kijewski"><code>@​Kijewski</code></a> in <a
href="https://redirect.github.com/askama-rs/askama/pull/410">askama-rs/askama#410</a></li>
<li>Fix <code>unique</code> filter implementation by <a
href="https://github.com/GuillaumeGomez"><code>@​GuillaumeGomez</code></a>
in <a
href="https://redirect.github.com/askama-rs/askama/pull/417">askama-rs/askama#417</a></li>
<li>Add <code>|titlecase</code> as alias for <code>|title</code> by <a
href="https://github.com/Kijewski"><code>@​Kijewski</code></a> in <a
href="https://redirect.github.com/askama-rs/askama/pull/416">askama-rs/askama#416</a></li>
</ul>
<h2>Further Changes</h2>
<ul>
<li>book: add page about <code>FastWritable</code> by <a
href="https://github.com/Kijewski"><code>@​Kijewski</code></a> in <a
href="https://redirect.github.com/askama-rs/askama/pull/407">askama-rs/askama#407</a></li>
<li>Add throughput to derive benchmark by <a
href="https://github.com/Kijewski"><code>@​Kijewski</code></a> in <a
href="https://redirect.github.com/askama-rs/askama/pull/413">askama-rs/askama#413</a></li>
<li>Move <code>FastWritable</code> into <code>askama</code> root by <a
href="https://github.com/GuillaumeGomez"><code>@​GuillaumeGomez</code></a>
in <a
href="https://redirect.github.com/askama-rs/askama/pull/411">askama-rs/askama#411</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/strickczq"><code>@​strickczq</code></a>
made their first contribution in <a
href="https://redirect.github.com/askama-rs/askama/pull/395">askama-rs/askama#395</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/askama-rs/askama/compare/v0.13.0...v0.14.0">https://github.com/askama-rs/askama/compare/v0.13.0...v0.14.0</a></p>
<h2>v0.13.1</h2>
<h2>What's Changed</h2>
<ul>
<li><code>askama_derive</code> accidentally exposed as a feature by <a
href="https://github.com/Kijewski"><code>@​Kijewski</code></a> in <a
href="https://redirect.github.com/askama-rs/askama/pull/384">askama-rs/askama#384</a></li>
<li>Track config files by <a
href="https://github.com/GuillaumeGomez"><code>@​GuillaumeGomez</code></a>
in <a
href="https://redirect.github.com/askama-rs/askama/pull/385">askama-rs/askama#385</a></li>
<li>Implement <code>Values</code> on tuple by <a
href="https://github.com/GuillaumeGomez"><code>@​GuillaumeGomez</code></a>
in <a
href="https://redirect.github.com/askama-rs/askama/pull/391">askama-rs/askama#391</a></li>
<li>generator: make <code>CARGO_MANIFEST_DIR</code> part of
<code>ConfigKey</code> by <a
href="https://github.com/strickczq"><code>@​strickczq</code></a> in <a
href="https://redirect.github.com/askama-rs/askama/pull/395">askama-rs/askama#395</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/strickczq"><code>@​strickczq</code></a>
made their first contribution in <a
href="https://redirect.github.com/askama-rs/askama/pull/395">askama-rs/askama#395</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/askama-rs/askama/compare/v0.13.0...v0.13.1">https://github.com/askama-rs/askama/compare/v0.13.0...v0.13.1</a></p>
<h2>v0.13.0 – Rinja is Askama, again!</h2>
<p>With this release, the <a
href="https://blog.guillaume-gomez.fr/articles/2024-07-31+docs.rs+switching+jinja+template+framework+from+tera+to+rinja">fork</a>
rinja got merged back into the main project. Please have a look at our
<a
href="https://blog.guillaume-gomez.fr/articles/2025-03-19+Askama+and+Rinja+merge">blog
post</a> for more information about the split and the merge.</p>
<h2>What's Changed</h2>
<p>This release (v0.13.0), when <a
href="https://github.com/askama-rs/askama/compare/0.12.1...v0.13.0">compared
to</a> the last stable askama release (v0.12.1), consists of:</p>
<ul>
<li>over 1000 commits</li>
<li>with changes in over 500 files</li>
<li>with over 40k additions and 8000 deletions</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="95867ac8ce"><code>95867ac</code></a>
Merge pull request <a
href="https://redirect.github.com/askama-rs/askama/issues/416">#416</a>
from Kijewski/pr-upgrading-0.14</li>
<li><a
href="61b7422497"><code>61b7422</code></a>
Add <code>|titlecase</code> as alias for <code>|title</code></li>
<li><a
href="79be271593"><code>79be271</code></a>
Run doctests</li>
<li><a
href="72bbe3ede1"><code>72bbe3e</code></a>
Bump version number to v0.14.0</li>
<li><a
href="57750338fa"><code>5775033</code></a>
book: update <code>upgrading.md</code></li>
<li><a
href="a5b43c0aa2"><code>a5b43c0</code></a>
Fix <code>unique</code> filter implementation</li>
<li><a
href="7fccbdf1d7"><code>7fccbdf</code></a>
Remove usage of <code>nextest</code></li>
<li><a
href="6a16256f24"><code>6a16256</code></a>
Fix new clippy lints</li>
<li><a
href="04a4d5b020"><code>04a4d5b</code></a>
Update MSRV to 1.83</li>
<li><a
href="d2a788a740"><code>d2a788a</code></a>
Add doc about <code>unique</code> filter</li>
<li>Additional commits viewable in <a
href="https://github.com/askama-rs/askama/compare/0.12.1...v0.14.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=askama&package-manager=cargo&previous-version=0.12.1&new-version=0.14.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-09 22:02:26 -08:00
dependabot[bot]
082d2fa19a chore(deps): bump taiki-e/install-action from 2.60.0 to 2.62.49 (#6438)
Bumps
[taiki-e/install-action](https://github.com/taiki-e/install-action) from
2.60.0 to 2.62.49.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/taiki-e/install-action/releases">taiki-e/install-action's
releases</a>.</em></p>
<blockquote>
<h2>2.62.49</h2>
<ul>
<li>
<p>Update <code>cargo-binstall@latest</code> to 1.15.11.</p>
</li>
<li>
<p>Update <code>cargo-auditable@latest</code> to 0.7.2.</p>
</li>
<li>
<p>Update <code>vacuum@latest</code> to 0.20.2.</p>
</li>
</ul>
<h2>2.62.48</h2>
<ul>
<li>
<p>Update <code>mise@latest</code> to 2025.11.3.</p>
</li>
<li>
<p>Update <code>cargo-audit@latest</code> to 0.22.0.</p>
</li>
<li>
<p>Update <code>vacuum@latest</code> to 0.20.1.</p>
</li>
<li>
<p>Update <code>uv@latest</code> to 0.9.8.</p>
</li>
<li>
<p>Update <code>cargo-udeps@latest</code> to 0.1.60.</p>
</li>
<li>
<p>Update <code>zizmor@latest</code> to 1.16.3.</p>
</li>
</ul>
<h2>2.62.47</h2>
<ul>
<li>
<p>Update <code>vacuum@latest</code> to 0.20.0.</p>
</li>
<li>
<p>Update <code>cargo-nextest@latest</code> to 0.9.111.</p>
</li>
<li>
<p>Update <code>cargo-shear@latest</code> to 1.6.2.</p>
</li>
</ul>
<h2>2.62.46</h2>
<ul>
<li>
<p>Update <code>vacuum@latest</code> to 0.19.5.</p>
</li>
<li>
<p>Update <code>syft@latest</code> to 1.37.0.</p>
</li>
<li>
<p>Update <code>mise@latest</code> to 2025.11.2.</p>
</li>
<li>
<p>Update <code>knope@latest</code> to 0.21.5.</p>
</li>
</ul>
<h2>2.62.45</h2>
<ul>
<li>
<p>Update <code>zizmor@latest</code> to 1.16.2.</p>
</li>
<li>
<p>Update <code>cargo-binstall@latest</code> to 1.15.10.</p>
</li>
<li>
<p>Update <code>ubi@latest</code> to 0.8.4.</p>
</li>
<li>
<p>Update <code>mise@latest</code> to 2025.11.1.</p>
</li>
<li>
<p>Update <code>cargo-semver-checks@latest</code> to 0.45.0.</p>
</li>
</ul>
<h2>2.62.44</h2>
<ul>
<li>Update <code>mise@latest</code> to 2025.11.0.</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md">taiki-e/install-action's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<p>All notable changes to this project will be documented in this
file.</p>
<p>This project adheres to <a href="https://semver.org">Semantic
Versioning</a>.</p>
<!-- raw HTML omitted -->
<h2>[Unreleased]</h2>
<h2>[2.62.49] - 2025-11-09</h2>
<ul>
<li>
<p>Update <code>cargo-binstall@latest</code> to 1.15.11.</p>
</li>
<li>
<p>Update <code>cargo-auditable@latest</code> to 0.7.2.</p>
</li>
<li>
<p>Update <code>vacuum@latest</code> to 0.20.2.</p>
</li>
</ul>
<h2>[2.62.48] - 2025-11-08</h2>
<ul>
<li>
<p>Update <code>mise@latest</code> to 2025.11.3.</p>
</li>
<li>
<p>Update <code>cargo-audit@latest</code> to 0.22.0.</p>
</li>
<li>
<p>Update <code>vacuum@latest</code> to 0.20.1.</p>
</li>
<li>
<p>Update <code>uv@latest</code> to 0.9.8.</p>
</li>
<li>
<p>Update <code>cargo-udeps@latest</code> to 0.1.60.</p>
</li>
<li>
<p>Update <code>zizmor@latest</code> to 1.16.3.</p>
</li>
</ul>
<h2>[2.62.47] - 2025-11-05</h2>
<ul>
<li>
<p>Update <code>vacuum@latest</code> to 0.20.0.</p>
</li>
<li>
<p>Update <code>cargo-nextest@latest</code> to 0.9.111.</p>
</li>
<li>
<p>Update <code>cargo-shear@latest</code> to 1.6.2.</p>
</li>
</ul>
<h2>[2.62.46] - 2025-11-04</h2>
<ul>
<li>
<p>Update <code>vacuum@latest</code> to 0.19.5.</p>
</li>
<li>
<p>Update <code>syft@latest</code> to 1.37.0.</p>
</li>
<li>
<p>Update <code>mise@latest</code> to 2025.11.2.</p>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="44c6d64aa6"><code>44c6d64</code></a>
Release 2.62.49</li>
<li><a
href="3a701df4c2"><code>3a701df</code></a>
Update <code>cargo-binstall@latest</code> to 1.15.11</li>
<li><a
href="4242e04eb8"><code>4242e04</code></a>
Update <code>cargo-auditable@latest</code> to 0.7.2</li>
<li><a
href="3df5533ef8"><code>3df5533</code></a>
Update <code>vacuum@latest</code> to 0.20.2</li>
<li><a
href="e797ba6a25"><code>e797ba6</code></a>
Release 2.62.48</li>
<li><a
href="bcf91e02ac"><code>bcf91e0</code></a>
Update <code>mise@latest</code> to 2025.11.3</li>
<li><a
href="e78113b60c"><code>e78113b</code></a>
Update <code>cargo-audit@latest</code> to 0.22.0</li>
<li><a
href="0ef486444e"><code>0ef4864</code></a>
Update <code>vacuum@latest</code> to 0.20.1</li>
<li><a
href="5eda7b1985"><code>5eda7b1</code></a>
Update <code>uv@latest</code> to 0.9.8</li>
<li><a
href="3853a413e6"><code>3853a41</code></a>
Update <code>cargo-udeps@latest</code> to 0.1.60</li>
<li>Additional commits viewable in <a
href="0c5db7f7f8...44c6d64aa6">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=taiki-e/install-action&package-manager=github_actions&previous-version=2.60.0&new-version=2.62.49)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-09 22:00:08 -08:00
dependabot[bot]
7c7c7567d5 chore(deps): bump codespell-project/actions-codespell from 2.1 to 2.2 (#6437)
Bumps
[codespell-project/actions-codespell](https://github.com/codespell-project/actions-codespell)
from 2.1 to 2.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/codespell-project/actions-codespell/releases">codespell-project/actions-codespell's
releases</a>.</em></p>
<blockquote>
<h2>v2.2</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<ul>
<li>Add the config file option and tests by <a
href="https://github.com/rdimaio"><code>@​rdimaio</code></a> in <a
href="https://redirect.github.com/codespell-project/actions-codespell/pull/80">codespell-project/actions-codespell#80</a></li>
<li>Use <code>pip install</code> with <code>--no-cache-dir</code> in the
Dockerfile by <a
href="https://github.com/PeterDaveHello"><code>@​PeterDaveHello</code></a>
in <a
href="https://redirect.github.com/codespell-project/actions-codespell/pull/89">codespell-project/actions-codespell#89</a></li>
<li>Upgrade to Python 3.13 by <a
href="https://github.com/candrews"><code>@​candrews</code></a> in <a
href="https://redirect.github.com/codespell-project/actions-codespell/pull/82">codespell-project/actions-codespell#82</a></li>
<li>Add checkout action and problem matcher to README by <a
href="https://github.com/vadi2"><code>@​vadi2</code></a> in <a
href="https://redirect.github.com/codespell-project/actions-codespell/pull/32">codespell-project/actions-codespell#32</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/rdimaio"><code>@​rdimaio</code></a> made
their first contribution in <a
href="https://redirect.github.com/codespell-project/actions-codespell/pull/80">codespell-project/actions-codespell#80</a></li>
<li><a
href="https://github.com/PeterDaveHello"><code>@​PeterDaveHello</code></a>
made their first contribution in <a
href="https://redirect.github.com/codespell-project/actions-codespell/pull/89">codespell-project/actions-codespell#89</a></li>
<li><a href="https://github.com/candrews"><code>@​candrews</code></a>
made their first contribution in <a
href="https://redirect.github.com/codespell-project/actions-codespell/pull/82">codespell-project/actions-codespell#82</a></li>
<li><a href="https://github.com/vadi2"><code>@​vadi2</code></a> made
their first contribution in <a
href="https://redirect.github.com/codespell-project/actions-codespell/pull/32">codespell-project/actions-codespell#32</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/codespell-project/actions-codespell/compare/v2...v2.2">https://github.com/codespell-project/actions-codespell/compare/v2...v2.2</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="8f01853be1"><code>8f01853</code></a>
MAINT: Release notes</li>
<li><a
href="23a4abea24"><code>23a4abe</code></a>
Add checkout action and problem matcher to README (<a
href="https://redirect.github.com/codespell-project/actions-codespell/issues/32">#32</a>)</li>
<li><a
href="906f13fba1"><code>906f13f</code></a>
Upgrade to Python 3.13 (<a
href="https://redirect.github.com/codespell-project/actions-codespell/issues/82">#82</a>)</li>
<li><a
href="df0bba344d"><code>df0bba3</code></a>
[pre-commit.ci] pre-commit autoupdate (<a
href="https://redirect.github.com/codespell-project/actions-codespell/issues/85">#85</a>)</li>
<li><a
href="c460eef33e"><code>c460eef</code></a>
Use <code>pip install</code> with <code>--no-cache-dir</code> in the
Dockerfile (<a
href="https://redirect.github.com/codespell-project/actions-codespell/issues/89">#89</a>)</li>
<li><a
href="037a23a348"><code>037a23a</code></a>
Add the config file option and tests (<a
href="https://redirect.github.com/codespell-project/actions-codespell/issues/80">#80</a>)</li>
<li><a
href="8d1a4b1bd9"><code>8d1a4b1</code></a>
Bump actions/setup-python from 5 to 6 (<a
href="https://redirect.github.com/codespell-project/actions-codespell/issues/92">#92</a>)</li>
<li><a
href="71286cb40f"><code>71286cb</code></a>
Bump actions/checkout from 4 to 5 (<a
href="https://redirect.github.com/codespell-project/actions-codespell/issues/91">#91</a>)</li>
<li><a
href="fad9339798"><code>fad9339</code></a>
[pre-commit.ci] pre-commit autoupdate (<a
href="https://redirect.github.com/codespell-project/actions-codespell/issues/84">#84</a>)</li>
<li>See full diff in <a
href="406322ec52...8f01853be1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=codespell-project/actions-codespell&package-manager=github_actions&previous-version=2.1&new-version=2.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-09 21:58:51 -08:00
iceweasel-oai
625f2208c4 For npm upgrade on Windows, go through cmd.exe to get path traversal working (#6387)
On Windows, `npm` by itself does not resolve under std::process::Command
which does not consider PATHEXT to resolve it to `npm.cmd` in the PATH.
By running the npm upgrade command via cmd.exe we get proper path
semantics so it actually works.
2025-11-09 21:07:44 -08:00
kinopeee
5f1fab0e7c fix(cloud-tasks): respect cli_auth_credentials_store config (#5856)
## Problem

`codex cloud` always instantiated `AuthManager` with `File` mode,
ignoring the user's actual `cli_auth_credentials_store` setting. This
caused users with `cli_auth_credentials_store = "keyring"` (or `"auto"`)
to see "Not signed in" errors even when they had valid credentials
stored in the system keyring.

## Root cause

The code called `Config::load_from_base_config_with_overrides()` with an
empty `ConfigToml::default()`, which always returned `File` as the
default store mode instead of loading the actual user configuration.

## Solution

- **Added `util::load_cli_auth_manager()` helper**  
Properly loads user config via
`load_config_as_toml_with_cli_overrides()` and extracts the
`cli_auth_credentials_store` setting before creating `AuthManager`.

- **Updated callers**  
  - `init_backend()` - used when starting cloud tasks UI
  - `build_chatgpt_headers()` - used for API requests

## Testing

-  `just fmt`
-  `just fix -p codex-cloud-tasks`
-  `cargo test -p codex-cloud-tasks`

## Files changed

- `codex-rs/cloud-tasks/src/lib.rs`
- `codex-rs/cloud-tasks/src/util.rs`

## Verification

Users with keyring-based auth can now run `codex cloud` successfully
without "Not signed in" errors.

---------

Co-authored-by: Eric Traut <etraut@openai.com>
Co-authored-by: celia-oai <celia@openai.com>
2025-11-10 00:35:08 +00:00
Oliver Mannion
c07461e6f3 fix(seatbelt): Allow reading hw.physicalcpu (#6421)
Allow reading `hw.physicalcpu` so numpy can be imported when running in
the sandbox.
 
resolves #6420
2025-11-09 08:53:36 -08:00
Raduan A.
8b80a0a269 Fix SDK documentation: replace 'file diffs' with 'file change notifications' (#6425)
The TypeScript SDK's README incorrectly claimed that runStreamed() emits
"file diffs". However, the FileChangeItem type only contains metadata
(path, kind, status) without actual diff content.

Updated line 36 to accurately describe the SDK as providing "file change
notifications" instead of "file diffs" to match the actual
implementation in items.ts.

Fixes #5850
2025-11-09 08:37:16 -08:00
iceweasel-oai
a47181e471 more world-writable warning improvements (#6389)
3 improvements:
1. show up to 3 actual paths that are world-writable
2. do the scan/warning for Read-Only mode too, because it also applies
there
3. remove the "Cancel" option since it doesn't always apply (like on
startup)
2025-11-08 11:35:43 -08:00
Raduan A.
5beb6167c8 feat(tui): Display keyboard shortcuts inline for approval options (#5889)
Shows single-key shortcuts (y, a, n) next to approval options to make
them more discoverable. Previously these shortcuts worked but were
hidden, making the feature hard to discover.

Changes:
- "Yes, proceed" now shows "y" shortcut
- "Yes, and don't ask again" now shows "a" shortcut
- "No, and tell Codex..." continues to show "esc" shortcut

This improves UX by surfacing the quick keyboard shortcuts that were
already functional but undiscoverable in the UI.

---

Update:

added parentheses for better visual clarity 
<img width="1540" height="486" alt="CleanShot 2025-11-05 at 11 47 07@2x"
src="https://github.com/user-attachments/assets/f951c34a-9ec8-4b81-b151-7b2ccba94658"
/>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Eric Traut <etraut@openai.com>
2025-11-08 09:08:42 -08:00
iceweasel-oai
917f39ec12 Improve world-writable scan (#6381)
1. scan many more directories since it's much faster than the original
implementation
2. limit overall scan time to 2s
3. skip some directories that are noisy - ApplicationData, Installer,
etc.
2025-11-07 21:28:55 -08:00
Luca King
a2fdfce02a Kill shell tool process groups on timeout (#5258)
## Summary
- launch shell tool processes in their own process group so Codex owns
the full tree
- on timeout or ctrl-c, send SIGKILL to the process group before
terminating the tracked child
- document that the default shell/unified_exec timeout remains 1000 ms

## Original Bug
Long-lived shell tool commands hang indefinitely because the timeout
handler only terminated the direct child process; any grandchildren it
spawned kept running and held the PTY open, preventing Codex from
regaining control.

## Repro Original Bug
Install next.js and run `next dev` (which is a long-running shell
process with children). On openai:main, it will cause the agent to
permanently get stuck here until human intervention. On this branch,
this command will be terminated successfully after timeout_ms which will
unblock the agent. This is a critical fix for unmonitored / lightly
monitored agents that don't have immediate human observation to unblock
them.

---------

Co-authored-by: Michael Bolin <mbolin@openai.com>
Co-authored-by: Michael Bolin <bolinfest@gmail.com>
2025-11-07 17:54:35 -08:00
pakrym-oai
91b16b8682 Don't request approval for safe commands in unified exec (#6380) 2025-11-07 16:36:04 -08:00
Alexander Smirnov
183fc8e01a core: replace Cloudflare 403 HTML with friendly message (#6252)
### Motivation

When Codex is launched from a region where Cloudflare blocks access (for
example, Russia), the CLI currently dumps Cloudflare’s entire HTML error
page. This isn’t actionable and makes it hard for users to understand
what happened. We want to detect the Cloudflare block and show a
concise, user-friendly explanation instead.

### What Changed

- Added CLOUDFLARE_BLOCKED_MESSAGE and a friendly_message() helper to
UnexpectedResponseError. Whenever we see a 403 whose body contains the
Cloudflare block notice, we now emit a single-line message (Access
blocked by Cloudflare…) while preserving the HTTP status and request id.
All other responses keep the original behaviour.
- Added two focused unit tests:
- unexpected_status_cloudflare_html_is_simplified ensures the Cloudflare
HTML case yields the friendly message.
- unexpected_status_non_html_is_unchanged confirms plain-text 403s still
return the raw body.

### Testing

- cargo build -p codex-cli
- cargo test -p codex-core
- just fix -p codex-core
- cargo test --all-features

---------

Co-authored-by: Eric Traut <etraut@openai.com>
2025-11-07 15:55:16 -08:00
Josh McKinney
9fba811764 refactor(terminal): cleanup deprecated flush logic (#6373)
Removes flush logic that was leftover to test against ratatui's flush
Cleaned up the flush logic so it's a bit more intent revealing.
DrawCommand now owns the Cells that it draws as this works around a
borrow checker problem.
2025-11-07 15:54:07 -08:00
Celia Chen
db408b9e62 [App-server] add initialization to doc (#6377)
Address comments in #6353.
2025-11-07 23:52:20 +00:00
Jakob Malmo
2eecc1a2e4 fix(wsl): normalize Windows paths during update (#6086) (#6097)
When running under WSL, the update command could receive Windows-style
absolute paths (e.g., `C:\...`) and pass them to Linux processes
unchanged, which fails because WSL expects those paths in
`/mnt/<drive>/...` form.

This patch adds a tiny helper in the CLI (`cli/src/wsl_paths.rs`) that:
- Detects WSL (`WSL_DISTRO_NAME` or `"microsoft"` in `/proc/version`)  
- Converts `X:\...` → `/mnt/x/...`  

`run_update_action` now normalizes the package-manager command and
arguments under WSL before spawning.
Non-WSL platforms are unaffected.  

Includes small unit tests for the converter.  

**Fixes:** #6086, #6084

Co-authored-by: Eric Traut <etraut@openai.com>
2025-11-07 14:49:17 -08:00
Dan Hernandez
c76528ca1f [SDK] Add network_access and web_search options to TypeScript SDK (#6367)
## Summary

This PR adds two new optional boolean fields to `ThreadOptions` in the
TypeScript SDK:

- **`networkAccess`**: Enables network access in the sandbox by setting
`sandbox_workspace_write.network_access` config
- **`webSearch`**: Enables the web search tool by setting
`tools.web_search` config

These options map to existing Codex configuration options and are
properly threaded through the SDK layers:
1. `ThreadOptions` (threadOptions.ts) - User-facing API
2. `CodexExecArgs` (exec.ts) - Internal execution args  
3. CLI flags via `--config` in the `codex exec` command

## Changes

- `sdk/typescript/src/threadOptions.ts`: Added `networkAccess` and
`webSearch` fields to `ThreadOptions` type
- `sdk/typescript/src/exec.ts`: Added fields to `CodexExecArgs` and CLI
flag generation
- `sdk/typescript/src/thread.ts`: Pass options through to exec layer

## Test Plan

- [x] Build succeeds (`pnpm build`)
- [x] Linter passes (`pnpm lint`)
- [x] Type definitions are properly exported
- [ ] Manual testing with sample code (to be done by reviewer)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-07 13:19:34 -08:00
Michael Bolin
bb47f2226f feat: add --promote-alpha option to create_github_release script (#6370)
Historically, running `create_github_release --publish-release` would
always publish a new release from latest `main`, which isn't always the
best idea. We should really publish an alpha, let it bake, and then
promote it.

This PR introduces a new flag, `--promote-alpha`, which does exactly
that. It also works with `--dry-run`, so you can sanity check the commit
it will use as the base commit for the new release before running it for
real.

```shell
$ ./codex-rs/scripts/create_github_release --dry-run --promote-alpha 0.56.0-alpha.2
Publishing version 0.56.0
Running gh api GET /repos/openai/codex/git/refs/tags/rust-v0.56.0-alpha.2
Running gh api GET /repos/openai/codex/git/tags/7d4ef77bc35b011aa0c76c5cbe6cd7d3e53f1dfe
Running gh api GET /repos/openai/codex/compare/main...8b49211e67d3c863df5ecc13fc5f88516a20fa69
Would publish version 0.56.0 using base commit 62474a30e8 derived from rust-v0.56.0-alpha.2.
```
2025-11-07 20:05:22 +00:00
Jeremy Rose
c6ab92bc50 tui: add comments to tui.rs (#6369) 2025-11-07 18:17:52 +00:00
pakrym-oai
4c1a6f0ee0 Promote shell config tool to model family config (#6351) 2025-11-07 10:11:11 -08:00
Owen Lin
361d43b969 [app-server] doc: update README for threads and turns (#6368)
Self explanatory!
2025-11-07 17:02:49 +00:00
Celia Chen
2e81f1900d [App-server] Add auth v2 doc & update codex mcp interface auth section (#6353)
Added doc for auth v2 endpoints. Updated the auth section in Codex MCP
interface doc too.
2025-11-07 08:17:19 -08:00
Owen Lin
2030b28083 [app-server] feat: expose additional fields on Thread (#6338)
Add the following fields to Thread:

```
    pub preview: String,
    pub model_provider: String,
    pub created_at: i64,
```

Will prob need another PR once this lands:
https://github.com/openai/codex/pull/6337
2025-11-07 04:08:45 +00:00
Celia Chen
e84e39940b [App-server] Implement account/read endpoint (#6336)
This PR does two things:
1. add a new function in core that maps the core-internal plan type to
the external plan type;
2. implement account/read that get account status (v2 of
`getAuthStatus`).
2025-11-06 19:43:13 -08:00
pakrym-oai
e8905f6d20 Prefer wait_for_event over wait_for_event_with_timeout (#6349) 2025-11-06 18:11:11 -08:00
Shane Vitarana
316352be94 Fix apply_patch rename move path resolution (#5486)
Fixes https://github.com/openai/codex/issues/5485.

Fixed rename hunks so `apply_patch` resolves the destination path using
the verifier’s effective cwd, ensuring patches that run under `cd
<worktree> && apply_patch` stay inside the worktree.

Added a regression test
(`test_apply_patch_resolves_move_path_with_effective_cwd`) that
reproduced the old behavior (dest path resolved in the main repo) and
now passes.

Related to https://github.com/openai/codex/issues/5483.

Co-authored-by: Eric Traut <etraut@openai.com>
2025-11-06 17:02:09 -08:00
pakrym-oai
f8b30af6dc Prefer wait_for_event over wait_for_event_with_timeout. (#6346)
No need to specify the timeout in most cases.
2025-11-06 16:14:43 -08:00
Eric Traut
039a4b070e Updated the AI labeler rules to match the most recent issue tracker labels (#6347)
This PR updates the AI prompt used for the workflow that adds automated
labels to incoming issues. I've been updating and refining the list of
labels as I work through the issue backlog, and the old prompt was
becoming somewhat outdated.
2025-11-06 16:02:12 -08:00
pakrym-oai
c368c6aeea Remove shell tool when unified exec is enabled (#6345)
Also drop streameable shell that's just an alias for unified exec.
2025-11-06 15:46:24 -08:00
Eric Traut
0c647bc566 Don't retry "insufficient_quota" errors (#6340)
This PR makes an "insufficient quota" error fatal so we don't attempt to
retry it multiple times in the agent loop.

We have multiple bug reports from users about intermittent retry
behaviors, and this could explain some of them. With this change, we'll
eliminate the retries and surface a clear error message.

The PR is a nearly identical copy of [this
PR](https://github.com/openai/codex/pull/4837) contributed by
@abimaelmartell. The original PR has gone stale. Rather than wait for
the contributor to resolve merge conflicts, I wanted to get this change
in.
2025-11-06 15:12:01 -08:00
Ejaz Ahmed
e30f65118d feat: Enable CTRL-n and CTRL-p for navigating slash commands, files, history (#1994)
Adds CTRL-n and CTRL-p navigation for slash commands, files, and
history.
Closes #1992

Co-authored-by: Eric Traut <etraut@openai.com>
2025-11-06 14:58:18 -08:00
Jeremy Rose
1bd2d7a659 tui: fix backtracking past /status (#6335)
Fixes https://github.com/openai/codex/issues/4722

Supersedes https://github.com/openai/codex/pull/5058

Ideally we'd have a clearer way of separating history per-session than
by detecting a specific history cell type, but this is a fairly minimal
fix for now.
2025-11-06 14:50:07 -08:00
Gabriel Peal
65d53fd4b1 Make generate_ts prettier output warn-only (#6342)
Before, every file would be outputted with the time prettier spent
formatting it. This made downstream scripts way too noisy.
2025-11-06 17:45:51 -05:00
pakrym-oai
b5349202e9 Freeform unified exec output formatting (#6233) 2025-11-06 22:14:27 +00:00
Gabriel Peal
1b8cc8b625 [App Server] Add more session metadata to listConversations (#6337)
This unlocks a few new product experience for app server consumers
2025-11-06 17:13:24 -05:00
Jeremy Rose
8501b0b768 core: widen sandbox to allow certificate ops when network is enabled (#5980)
This allows `gh api` to work in the workspace-write sandbox w/ network
enabled. Without this we see e.g.

```
$ codex debug seatbelt --full-auto gh api repos/openai/codex/pulls --paginate -X GET -F state=all
Get "https://api.github.com/repos/openai/codex/pulls?per_page=100&state=all": tls: failed to verify certificate: x509: OSStatus -26276
```
2025-11-06 12:47:20 -08:00
Eric Traut
fe7eb18104 Updated contributing guidelines and PR template to request link to bug report in PR notes (#6332)
Some PRs are being submitted without reference to existing bug reports
or feature requests. This updates the PR template and contributing
guidelines to request that all PRs from the community contain such a
link. This provides additional context and helps prioritize, track, and
assess PRs.
2025-11-06 12:02:39 -08:00
Thibault Sottiaux
8c75ed39d5 feat: clarify that gpt-5-codex should not amend commits unless requested (#6333) 2025-11-06 11:42:47 -08:00
Owen Lin
fdb9fa301e chore: move relevant tests to app-server/tests/suite/v2 (#6289)
These are technically app-server v2 APIs, so move them to the same
directory as the others.
2025-11-06 10:53:17 -08:00
iceweasel-oai
871d442b8e Windows Sandbox: Show Everyone-writable directory warning (#6283)
Show a warning when Auto Sandbox mode becomes enabled, if we detect
Everyone-writable directories, since they cannot be protected by the
current implementation of the Sandbox.

This PR also includes changes to how we detect Everyone-writable to be
*much* faster
2025-11-06 10:44:42 -08:00
Ahmed Ibrahim
dbad5eeec6 chore: fix grammar mistakes (#6326) 2025-11-06 09:48:59 -08:00
vladislav doster
4b4252210b docs: Fix code fence and typo in advanced guide (#6295)
- add `bash` to code fence
- fix spelling of `JavaScript`
2025-11-06 09:00:28 -08:00
Owen Lin
6582554926 [app-server] feat: v2 Turn APIs (#6216)
Implements:
```
turn/start
turn/interrupt
```

along with their integration tests. These are relatively light wrappers
around the existing core logic, and changes to core logic are minimal.

However, an improvement made for developer ergonomics:
- `turn/start` replaces both `SendUserMessage` (no turn overrides) and
`SendUserTurn` (can override model, approval policy, etc.)
2025-11-06 16:36:36 +00:00
Thibault Sottiaux
649ce520c4 chore: rename for clarity (#6319)
Co-authored-by: Ahmed Ibrahim <aibrahim@openai.com>
2025-11-06 08:32:57 -08:00
Thibault Sottiaux
667e841d3e feat: support models with single reasoning effort (#6300) 2025-11-05 23:06:45 -08:00
Ahmed Ibrahim
63e1ef25af feat: add model nudge for queries (#6286) 2025-11-06 03:42:59 +00:00
Celia Chen
229d18f4d2 [App-server] Add account/login/cancel v2 endpoint (#6288)
Add `account/login/cancel` v2 endpoint for auth. this is similar
implementation to `cancelLoginChatgpt` v1 endpoint.
2025-11-06 01:13:55 +00:00
wizard
4a1a7f9685 fix: ToC so it doesn’t include itself or duplicate the end marker (#4388)
turns out the ToC was including itself when generating, which messed up
comparisons and sometimes made the file rewrite endlessly.

also fixed the slice so `<!-- End ToC -->` doesn’t get duplicated when
we insert the new ToC.

should behave nicely now - no extra rewrites, no doubled markers.

Co-authored-by: Eric Traut <etraut@openai.com>
2025-11-05 14:52:51 -08:00
Eric Traut
86c149ae8e Prevent dismissal of login menu in TUI (#6285)
We currently allow the user to dismiss the login menu via Ctrl+C. This
leaves them in a bad state where they're not auth'ed but have an input
prompt. In the extension, this isn't a problem because we don't allow
the user to dismiss the login screen.

Testing: I confirmed that Ctrl+C no longer dismisses the login menu.

This is an alternative (simpler) fix for a [community
PR](https://github.com/openai/codex/pull/3234).
2025-11-05 14:25:58 -08:00
Celia Chen
05f0b4f590 [App-server] Implement v2 for account/login/start and account/login/completed (#6183)
This PR implements `account/login/start` and `account/login/completed`.
Instead of having separate endpoints for login with chatgpt and api, we
have a single enum handling different login methods. For sync auth
methods like sign in with api key, we still send a `completed`
notification back to be compatible with the async login flow.
2025-11-05 13:52:50 -08:00
easong-openai
d4eda9d10b stop capturing r when environment selection modal is open (#6249)
This fixes an issue where you can't select environments with an r in them when the selection modal is open
2025-11-05 13:23:46 -08:00
Eric Traut
d7953aed74 Fixes intermittent test failures in CI (#6282)
I'm seeing two tests fail intermittently in CI. This PR attempts to
address (or at least mitigate) the flakiness.

* summarize_context_three_requests_and_instructions - The test snapshots
server.received_requests() immediately after observing TaskComplete.
Because the OpenAI /v1/responses call is streamed, the HTTP request can
still be draining when that event fires, so wiremock occasionally
reports only two captured requests. Fix is to wait for async activity to
complete.
* archive_conversation_moves_rollout_into_archived_directory - times out
on a slow CI run. Mitigation is to increase timeout value from 10s to
20s.
2025-11-05 13:12:25 -08:00
Owen Lin
2ab1650d4d [app-server] feat: v2 Thread APIs (#6214)
Implements:
```
thread/list
thread/start
thread/resume
thread/archive
```

along with their integration tests. These are relatively light wrappers
around the existing core logic, and changes to core logic are minimal.

However, an improvement made for developer ergonomics:
- `thread/start` and `thread/resume` automatically attaches a
conversation listener internally, so clients don't have to make a
separate `AddConversationListener` call like they do today.

For consistency, also updated `model/list` and `feedback/upload` (naming
conventions, list API params).
2025-11-05 20:28:43 +00:00
Gabriel Peal
79aa83ee39 Update rmcp to 0.8.5 (#6261)
Picks up https://github.com/modelcontextprotocol/rust-sdk/pull/511 which
should fix todoist and some other MCP server oauth and may further
resolve issues in https://github.com/openai/codex/issues/5045
2025-11-05 14:20:30 -05:00
Eric Traut
c4ebe4b078 Improved token refresh handling to address "Re-connecting" behavior (#6231)
Currently, when the access token expires, we attempt to use the refresh
token to acquire a new access token. This works most of the time.
However, there are situations where the refresh token is expired,
exhausted (already used to perform a refresh), or revoked. In those
cases, the current logic treats the error as transient and attempts to
retry it repeatedly.

This PR changes the token refresh logic to differentiate between
permanent and transient errors. It also changes callers to treat the
permanent errors as fatal rather than retrying them. And it provides
better error messages to users so they understand how to address the
problem. These error messages should also help us further understand why
we're seeing examples of refresh token exhaustion.

Here is the error message in the CLI. The same text appears within the
extension.

<img width="863" height="38" alt="image"
src="https://github.com/user-attachments/assets/7ffc0d08-ebf0-4900-b9a9-265064202f4f"
/>

I also correct the spelling of "Re-connecting", which shouldn't have a
hyphen in it.

Testing: I manually tested these code paths by adding temporary code to
programmatically cause my refresh token to be exhausted (by calling the
token refresh endpoint in a tight loop more than 50 times). I then
simulated an access token expiration, which caused the token refresh
logic to be invoked. I confirmed that the updated logic properly handled
the error condition.

Note: We earlier discussed the idea of forcefully logging out the user
at the point where token refresh failed. I made several attempts to do
this, and all of them resulted in a bad UX. It's important to surface
this error to users in a way that explains the problem and tells them
that they need to log in again. We also previously discussed deleting
the auth.json file when this condition is detected. That also creates
problems because it effectively changes the auth status from logged in
to logged out, and this causes odd failures and inconsistent UX. I think
it's therefore better not to delete auth.json in this case. If the user
closes the CLI or VSCE and starts it again, we properly detect that the
access token is expired and the refresh token is "dead", and we force
the user to go through the login flow at that time.

This should address aspects of #6191, #5679, and #5505
2025-11-05 10:51:57 -08:00
Ahmed Ibrahim
1a89f70015 refactor Conversation history file into its own directory (#6229)
This is just a refactor of `conversation_history` file by breaking it up
into multiple smaller ones with helper. This refactor will help us move
more functionality related to context management here. in a clean way.
2025-11-05 10:49:35 -08:00
Jeremy Rose
62474a30e8 tui: refactor ChatWidget and BottomPane to use Renderables (#5565)
- introduce RenderableItem to support both owned and borrowed children
in composite Renderables
- refactor some of our gnarlier manual layouts, BottomPane and
ChatWidget, to use ColumnRenderable
- Renderable and friends now handle cursor_pos()
2025-11-05 09:50:40 -08:00
Dan Hernandez
9a10e80ab7 Add modelReasoningEffort option to TypeScript SDK (#6237)
## Summary
- Adds `ModelReasoningEffort` type to TypeScript SDK with values:
`minimal`, `low`, `medium`, `high`
- Adds `modelReasoningEffort` option to `ThreadOptions`
- Forwards the option to the codex CLI via `--config
model_reasoning_effort="<value>"`
- Includes test coverage for the new option

## Changes
- `sdk/typescript/src/threadOptions.ts`: Define `ModelReasoningEffort`
type and add to `ThreadOptions`
- `sdk/typescript/src/index.ts`: Export `ModelReasoningEffort` type
- `sdk/typescript/src/exec.ts`: Forward `modelReasoningEffort` to CLI as
config flag
- `sdk/typescript/src/thread.ts`: Pass option through to exec (+ debug
logging)
- `sdk/typescript/tests/run.test.ts`: Add test for
`modelReasoningEffort` flag forwarding

---------

Co-authored-by: Eric Traut <etraut@openai.com>
2025-11-05 08:51:03 -08:00
Gabriel Peal
9b538a8672 Upgrade rmcp to 0.8.4 (#6234)
Picks up https://github.com/modelcontextprotocol/rust-sdk/pull/509 which
fixes https://github.com/openai/codex/issues/6164
2025-11-05 00:23:24 -05:00
Andrew Dirksen
95af417923 allow codex to be run from pid 1 (#4200)
Previously it was not possible for codex to run commands as the init
process (pid 1) in linux. Commands run in containers tend to see their
own pid as 1. See https://github.com/openai/codex/issues/4198

This pr implements the solution mentioned in that issue.

Co-authored-by: Eric Traut <etraut@openai.com>
2025-11-04 17:54:46 -08:00
Soroush Yousefpour
fff576cf98 fix(core): load custom prompts from symlinked Markdown files (#3643)
- Discover prompts via fs::metadata to follow symlinks

- Add Unix-only symlink test in custom_prompts.rs

- Update docs/prompts.md to mention symlinks

Fixes #3637

---------

Signed-off-by: Soroush Yousefpour <h.yusefpour@gmail.com>
Co-authored-by: dedrisian-oai <dedrisian@openai.com>
Co-authored-by: Eric Traut <etraut@openai.com>
2025-11-04 17:44:02 -08:00
Lukas
1575f0504c Fix nix build (#6230)
Previously, the `nix build .#default` command fails due to a missing
output hash in the `./codex-rs/default.nix` for `crossterm-0.28.1`:

```
error: No hash was found while vendoring the git dependency crossterm-0.28.1. You can add
a hash through the `outputHashes` argument of `importCargoLock`:

outputHashes = {
 "crossterm-0.28.1" = "<hash>";
};

If you use `buildRustPackage`, you can add this attribute to the `cargoLock`
attribute set.
```

This PR adds the missing hash:

```diff
cargoLock.outputHashes = {
  "ratatui-0.29.0" = "sha256-HBvT5c8GsiCxMffNjJGLmHnvG77A6cqEL+1ARurBXho=";
+ "crossterm-0.28.1" = "sha256-6qCtfSMuXACKFb9ATID39XyFDIEMFDmbx6SSmNe+728=";
};
```

With this change, `nix build .#default` succeeds:

```
> nix build .#default --max-jobs 1 --cores 2

warning: Git tree '/home/lukas/r/github.com/lukasl-dev/codex' is dirty
[1/0/1 built] building codex-rs-0.1.0 (buildPhase)[1/0/1 built] building codex-rs-0.1.0 (buildP[1/0/1 built] building codex-rs-0.1.0 (buildPhase):    [1/0/1 built] building codex-rs-0.1.0 (b[1/0/1 built] building codex-rs-0.1.0 (buildPhase):    Compi[1/0/1 built] building codex-rs-0.1

> ./result/bin/codex
  You are running Codex in /home/lukas/r/github.com/lukasl-dev/codex

  Since this folder is version controlled, you may wish to allow Codex to work in this folder without asking for approval.
  ...
```
2025-11-04 17:07:37 -08:00
Owen Lin
edf4c3f627 [app-server] feat: export.rs supports a v2 namespace, initial v2 notifications (#6212)
**Typescript and JSON schema exports**
While working on Thread/Turn/Items type definitions, I realize we will
run into name conflicts between v1 and v2 APIs (e.g. `RateLimitWindow`
which won't be reusable since v1 uses `RateLimitWindow` from `protocol/`
which uses snake_case, but we want to expose camelCase everywhere, so
we'll define a V2 version of that struct that serializes as camelCase).

To set us up for a clean and isolated v2 API, generate types into a
`v2/` namespace for both typescript and JSON schema.
- TypeScript: v2 types emit under `out_dir/v2/*.ts`, and root index.ts
now re-exports them via `export * as v2 from "./v2"`;.
- JSON Schemas: v2 definitions bundle under `#/definitions/v2/*` rather
than the root.

The location for the original types (v1 and types pulled from
`protocol/` and other core crates) haven't changed and are still at the
root. This is for backwards compatibility: no breaking changes to
existing usages of v1 APIs and types.

**Notifications**
While working on export.rs, I:
- refactored server/client notifications with macros (like we already do
for methods) so they also get exported (I noticed they weren't being
exported at all).
- removed the hardcoded list of types to export as JSON schema by
leveraging the existing macros instead
- and took a stab at API V2 notifications. These aren't wired up yet,
and I expect to iterate on these this week.
2025-11-05 01:02:39 +00:00
Ahmed Ibrahim
d40a6b7f73 fix: Update the deprecation message to link to the docs (#6211)
The deprecation message is currently a bit confusing. Users may not
understand what is `[features].x`. I updated the docs and the
deprecation message for more guidance.

---------

Co-authored-by: Gabriel Peal <gpeal@users.noreply.github.com>
2025-11-04 21:02:27 +00:00
Dylan Hurd
3a22018edd Revert "fix: pin musl 1.2.5 for DNS fixes" (#6222)
Reverts openai/codex#6189
2025-11-04 11:56:40 -08:00
Ahmed Ibrahim
fe54c216a3 ignore deltas in codex_delegate (#6208)
ignore legacy deltas in codex-delegate to avoid this
[issue](https://github.com/openai/codex/pull/6202).
2025-11-04 19:21:35 +00:00
Dylan Hurd
cb6584de46 fix: pin musl 1.2.5 for DNS fixes (#6189)
## Summary
musl 1.2.5 includes [several fixes to DNS over
TCP](https://www.openwall.com/lists/musl/2024/03/01/2), which appears to
be the root cause of #6116.

This approach is a bit janky, but according to codex:
> On the Ubuntu 24.04 runners we use, apt-cache policy musl-tools shows
only the distro build (1.2.4-2ubuntu2)"

We should build with this version and confirm.

## Testing
- [ ] TODO: test and see if this fixes Azure issues
2025-11-04 09:17:16 -08:00
Ahmed Ibrahim
7e068e1094 fix: ignore reasoning deltas because we send it with turn item (#6202)
should fix this:

<img width="2418" height="242" alt="image"
src="https://github.com/user-attachments/assets/f818d00b-ed3a-479b-94a7-e4bc5db6326e"
/>
2025-11-04 08:27:16 -08:00
Celia Chen
d3187dbc17 [App-server] v2 for account/updated and account/logout (#6175)
V2 for `account/updated` and `account/logout` for app server. correspond
to old `authStatusChange` and `LogoutChatGpt` respectively. Followup PRs
will make other v2 endpoints call `account/updated` instead of
`authStatusChange` too.
2025-11-03 22:01:33 -08:00
Robby He
dc2f26f7b5 Fix is_api_message to correctly exclude reasoning messages (#6156)
## Problem

The `is_api_message` function in `conversation_history.rs` had a
misalignment between its documentation and implementation:

- **Comment stated**: "Anything that is not a system message or
'reasoning' message is considered an API message"
- **Code behavior**: Was returning `true` for `ResponseItem::Reasoning`,
meaning reasoning messages were incorrectly treated as API messages

This inconsistency could lead to reasoning messages being persisted in
conversation history when they should be filtered out.

## Root Cause

Investigation revealed that reasoning messages are explicitly excluded
throughout the codebase:

1. **Chat completions API** (lines 267-272 in `chat_completions.rs`)
omits reasoning from conversation history:
   ```rust
   ResponseItem::Reasoning { .. } | ResponseItem::Other => {
       // Omit these items from the conversation history.
       continue;
   }
   ```

2. **Existing tests** like `drops_reasoning_when_last_role_is_user` and
`ignores_reasoning_before_last_user` validate that reasoning should be
excluded from API payloads

## Solution

Fixed the `is_api_message` function to align with its documentation and
the rest of the codebase:

```rust
// Before: Reasoning was incorrectly returning true
ResponseItem::Reasoning { .. } | ResponseItem::WebSearchCall { .. } => true,

// After: Reasoning correctly returns false  
ResponseItem::WebSearchCall { .. } => true,
ResponseItem::Reasoning { .. } | ResponseItem::Other => false,
```

## Testing

- Enhanced existing test to verify reasoning messages are properly
filtered out
- All 264 core tests pass, including 8 chat completions tests that
validate reasoning behavior
- No regressions introduced

This ensures reasoning messages are consistently excluded from API
message processing across the entire codebase.
2025-11-03 20:55:41 -08:00
Ricardo Ander-Egg
553db8def1 Follow symlinks during file search (#4453)
I have read the CLA Document and I hereby sign the CLA

Closes #4452

This fixes a usability issue where users with symlinked folders in their
working directory couldn't search those files using the `@` file search
feature.

## Rationale

The "bug" was in the file search implementation in
`codex-rs/file-search/src/lib.rs`. The `WalkBuilder` was using default
settings which don't follow symlinks, causing two related issues:

1. Partial search results: The `@` search would find symlinked
directories but couldn't find files inside them
2. Inconsistent behavior: Users expect symlinked folders to behave like
regular folders in search results.

## Root cause

The `ignore` crate's `WalkBuilder` defaults to `.follow_links(false)`
[[source](9802945e63/crates/ignore/src/walk.rs (L532))],
so when traversing the file system, it would:

- Detect symlinked directories as directory entries
- But not traverse into them to index their contents
- The `get_file_path` function would then filter out actual directories,
leaving only the symlinked folder itself as a result

Fix: Added `.follow_links(true)` to the `WalkBuilder` configuration,
making the file search follow symlinks and index their contents just
like regular directories.

This change maintains backward compatibility since symlink following is
generally expected behavior for file search tools, and it aligns with
how users expect the `@` search feature to work.

Co-authored-by: Eric Traut <etraut@openai.com>
2025-11-03 20:28:33 -08:00
Lucas Freire Sangoi
ab63a47173 docs: add example config.toml (#5175)
I was missing an example config.toml, and following docs/config.md alone
was slower. I had GPT-5 scan the codebase for every accepted config key,
check the defaults, and generate a single example config.toml with
annotations. It lists all keys Codex reads from TOML, sets each to its
effective default where it exists, leaves optional ones commented, and
adds short comments on purpose and valid values. This should make
onboarding faster and reduce configuration errors. I can rename it to
config.example.toml or move it under docs/ if you prefer.
2025-11-03 18:19:26 -08:00
Ahmed Ibrahim
e658c6c73b fix: --search shouldn't show deprecation message (#6180)
Use the new feature flags instead of the old config.
2025-11-04 00:11:50 +00:00
Eric Traut
1e0e553304 Fixed notify handler so it passes correct input_messages details (#6143)
This fixes bug #6121. 

The `input_messages` field passed to the notify handler is currently
empty because the logic is incorrectly including the OutputText rather
than InputText. I've fixed that and added proper filtering to remove
messages associated with AGENTS.md and other context injected by the
harness.

Testing: I wrote a notify handler and verified that the user prompt is
correctly passed through to the handler.
2025-11-03 14:23:04 -08:00
iceweasel-oai
07b7d28937 log sandbox commands to $CODEX_HOME instead of cwd (#6171)
Logging commands in the Windows Sandbox is temporary, but while we are
doing it, let's always write to CODEX_HOME instead of dirtying the cwd.
2025-11-03 13:12:33 -08:00
Ahmed Ibrahim
6ee7fbcfff feat: add the time after aborting (#5996)
Tell the model how much time passed after the user aborted the call.
2025-11-03 11:44:06 -08:00
Jeremy Rose
5f3a0473f1 tui: refine text area word separator handling (#5541)
## Summary
- replace the word part enum with a simple `is_word_separator` helper
- keep word-boundary logic aligned with the helper and punctuation-aware
behavior
- extend forward/backward deletion tests to cover whitespace around
separators

## Testing
- just fix -p codex-tui
- cargo test -p codex-tui


------
https://chatgpt.com/codex/tasks/task_i_68f91c71d838832ca2a3c4f0ec1b55d4
2025-11-03 11:33:34 -08:00
iceweasel-oai
2eda75a8ee Do not skip trust prompt on Windows if sandbox is enabled. (#6167)
If the experimental windows sandbox is enabled, the trust prompt should
show on Windows.
2025-11-03 11:27:45 -08:00
Michael Bolin
e1f098b9b7 feat: add options to responses-api-proxy to support Azure (#6129)
This PR introduces an `--upstream-url` option to the proxy CLI that
determines the URL that Responses API requests should be forwarded to.
To preserve existing behavior, the default value is
`"https://api.openai.com/v1/responses"`.

The motivation for this change is that the [Codex GitHub
Action](https://github.com/openai/codex-action) should support those who
use the OpenAI Responses API via Azure. Relevant issues:

- https://github.com/openai/codex-action/issues/28
- https://github.com/openai/codex-action/issues/38
- https://github.com/openai/codex-action/pull/44

Though rather than introduce a bunch of new Azure-specific logic in the
action as https://github.com/openai/codex-action/pull/44 proposes, we
should leverage our Responses API proxy to get the _hardening_ benefits
it provides:


d5853d9c47/codex-rs/responses-api-proxy/README.md (hardening-details)

This PR should make this straightforward to incorporate in the action.
To see how the updated version of the action would consume these new
options, see https://github.com/openai/codex-action/pull/47.
2025-11-03 10:06:00 -08:00
pakrym-oai
e5e13479d0 Include reasoning tokens in the context window calculation (#6161)
This value is used to determine whether mid-turn compaction is required.
Reasoning items are only excluded between turns (and soon will start to
be preserved even across turns) so it's incorrect to subtract
reasoning_output_tokens mid term.

This will result in higher values reported between turns but we are also
looking into preserving reasoning items for the entire conversation to
improve performance and caching.
2025-11-03 10:02:23 -08:00
Jeremy Rose
7bc3ca9e40 Fix rmcp client feature flag reference (#6051)
## Summary
- update the OAuth login error message to reference
`[features].rmcp_client` in config

## Testing
- cargo test -p codex-cli

------
https://chatgpt.com/codex/tasks/task_i_69050365dc84832ca298f863c879a59a
2025-11-03 09:59:19 -08:00
Mark Hemmings
4d8b71d412 Fix typo in error message for OAuth login (#6159)
Error message for attempting to OAuth with a remote RCP is incorrect and
misleading. The correct config is

```
[features]
rmcp_client = true
```

Co-authored-by: Eric Traut <etraut@openai.com>
2025-11-03 08:59:00 -08:00
pygarap
b484672961 Add documentation for slash commands in docs/slash_commands.md. (#5685)
This pull request adds a new documentation section to explain the
available slash commands in Codex. The update introduces a clear
overview and a reference table for built-in commands, making it easier
for users to understand and utilize these features.

Documentation updates:

* Added a new section to `docs/slash_commands.md` describing what slash
commands are and listing all built-in commands with their purposes in a
formatted table.
2025-11-03 08:27:13 -08:00
Vinh Nguyen
a1ee10b438 fix: improve usage URLs in status card and snapshots (#6111)
Hi OpenAI Codex team, currently "Visit chatgpt.com/codex/settings/usage
for up-to-date information on rate limits and credits" message in status
card and error messages. For now, without the "https://" prefix, the
link cannot be clicked directly from most terminals or chat interfaces.

<img width="636" height="127" alt="Screenshot 2025-11-02 at 22 47 06"
src="https://github.com/user-attachments/assets/5ea11e8b-fb74-451c-85dc-f4d492b2678b"
/>

---

The fix is intent to improve this issue:

- It makes the link clickable in terminals that support it, hence better
accessibility
- It follows standard URL formatting practices
- It maintains consistency with other links in the application (like the
existing "https://openai.com/chatgpt/pricing" links)

Thank you!
2025-11-02 21:44:59 -08:00
Eric Traut
dccce34d84 Fix "archive conversation" on Windows (#6124)
Addresses issue https://github.com/openai/codex/issues/3582 where an
"archive conversation" command in the extension fails on Windows.

The problem is that the `archive_conversation` api server call is not
canonicalizing the path to the rollout path when performing its check to
verify that the rollout path is in the sessions directory. This causes
it to fail 100% of the time on Windows.

Testing: I was able to repro the error on Windows 100% prior to this
change. After the change, I'm no longer able to repro.
2025-11-02 21:41:05 -08:00
dependabot[bot]
f5945d7c03 chore(deps): bump actions/upload-artifact from 4 to 5 (#6137)
Bumps
[actions/upload-artifact](https://github.com/actions/upload-artifact)
from 4 to 5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/upload-artifact/releases">actions/upload-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v5.0.0</h2>
<h2>What's Changed</h2>
<p><strong>BREAKING CHANGE:</strong> this update supports Node
<code>v24.x</code>. This is not a breaking change per-se but we're
treating it as such.</p>
<ul>
<li>Update README.md by <a
href="https://github.com/GhadimiR"><code>@​GhadimiR</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/681">actions/upload-artifact#681</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/712">actions/upload-artifact#712</a></li>
<li>Readme: spell out the first use of GHES by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/upload-artifact/pull/727">actions/upload-artifact#727</a></li>
<li>Update GHES guidance to include reference to Node 20 version by <a
href="https://github.com/patrikpolyak"><code>@​patrikpolyak</code></a>
in <a
href="https://redirect.github.com/actions/upload-artifact/pull/725">actions/upload-artifact#725</a></li>
<li>Bump <code>@actions/artifact</code> to <code>v4.0.0</code></li>
<li>Prepare <code>v5.0.0</code> by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/upload-artifact/pull/734">actions/upload-artifact#734</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/GhadimiR"><code>@​GhadimiR</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/681">actions/upload-artifact#681</a></li>
<li><a href="https://github.com/nebuk89"><code>@​nebuk89</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/712">actions/upload-artifact#712</a></li>
<li><a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/727">actions/upload-artifact#727</a></li>
<li><a
href="https://github.com/patrikpolyak"><code>@​patrikpolyak</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/725">actions/upload-artifact#725</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v4...v5.0.0">https://github.com/actions/upload-artifact/compare/v4...v5.0.0</a></p>
<h2>v4.6.2</h2>
<h2>What's Changed</h2>
<ul>
<li>Update to use artifact 2.3.2 package &amp; prepare for new
upload-artifact release by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/685">actions/upload-artifact#685</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/685">actions/upload-artifact#685</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v4...v4.6.2">https://github.com/actions/upload-artifact/compare/v4...v4.6.2</a></p>
<h2>v4.6.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Update to use artifact 2.2.2 package by <a
href="https://github.com/yacaovsnc"><code>@​yacaovsnc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/673">actions/upload-artifact#673</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v4...v4.6.1">https://github.com/actions/upload-artifact/compare/v4...v4.6.1</a></p>
<h2>v4.6.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Expose env vars to control concurrency and timeout by <a
href="https://github.com/yacaovsnc"><code>@​yacaovsnc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/662">actions/upload-artifact#662</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v4...v4.6.0">https://github.com/actions/upload-artifact/compare/v4...v4.6.0</a></p>
<h2>v4.5.0</h2>
<h2>What's Changed</h2>
<ul>
<li>fix: deprecated <code>Node.js</code> version in action by <a
href="https://github.com/hamirmahal"><code>@​hamirmahal</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/578">actions/upload-artifact#578</a></li>
<li>Add new <code>artifact-digest</code> output by <a
href="https://github.com/bdehamer"><code>@​bdehamer</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/656">actions/upload-artifact#656</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/hamirmahal"><code>@​hamirmahal</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/578">actions/upload-artifact#578</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="330a01c490"><code>330a01c</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/734">#734</a>
from actions/danwkennedy/prepare-5.0.0</li>
<li><a
href="03f2824452"><code>03f2824</code></a>
Update <code>github.dep.yml</code></li>
<li><a
href="905a1ecb59"><code>905a1ec</code></a>
Prepare <code>v5.0.0</code></li>
<li><a
href="2d9f9cdfa9"><code>2d9f9cd</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/725">#725</a>
from patrikpolyak/patch-1</li>
<li><a
href="9687587dec"><code>9687587</code></a>
Merge branch 'main' into patch-1</li>
<li><a
href="2848b2cda0"><code>2848b2c</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/727">#727</a>
from danwkennedy/patch-1</li>
<li><a
href="9b511775fd"><code>9b51177</code></a>
Spell out the first use of GHES</li>
<li><a
href="cd231ca1ed"><code>cd231ca</code></a>
Update GHES guidance to include reference to Node 20 version</li>
<li><a
href="de65e23aa2"><code>de65e23</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/712">#712</a>
from actions/nebuk89-patch-1</li>
<li><a
href="8747d8cd76"><code>8747d8c</code></a>
Update README.md</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/upload-artifact/compare/v4...v5">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=4&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-02 20:38:45 -08:00
Rohan Godha
5fcf923c19 fix: pasting api key stray character (#4903)
When signing in with an API key, pasting (with command+v on mac) adds a
stray `v` character to the end of the api key.



demo video (where I'm pasting in `sk-something-super-secret`)


https://github.com/user-attachments/assets/b2b34b5f-c7e4-4760-9657-c35686dd8bb8
2025-11-03 04:19:08 +00:00
Eric Traut
0c7efa0cfd Fix incorrect "deprecated" message about experimental config key (#6131)
When I enable `experimental_sandbox_command_assessment`, I get an
incorrect deprecation warning: "experimental_sandbox_command_assessment
is deprecated. Use experimental_sandbox_command_assessment instead."

This PR fixes this error.
2025-11-02 16:33:09 -08:00
Eric Traut
d5853d9c47 Changes to sandbox command assessment feature based on initial experiment feedback (#6091)
* Removed sandbox risk categories; feedback indicates that these are not
that useful and "less is more"
* Tweaked the assessment prompt to generate terser answers
* Fixed bug in orchestrator that prevents this feature from being
exposed in the extension
2025-11-01 14:52:23 -07:00
567 changed files with 53036 additions and 39659 deletions

View File

@@ -1,3 +1,3 @@
model = "gpt-5"
model = "gpt-5.1"
# Consider setting [mcp_servers] here!

View File

@@ -4,3 +4,5 @@ Before opening this Pull Request, please read the dedicated "Contributing" markd
https://github.com/openai/codex/blob/main/docs/contributing.md
If your PR conforms to our contribution guidelines, replace this text with a detailed and high quality description of your changes.
Include a link to a bug report or enhancement request.

26
.github/workflows/cargo-deny.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: cargo-deny
on:
pull_request:
push:
branches:
- main
jobs:
cargo-deny:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./codex-rs
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Run cargo-deny
uses: EmbarkStudios/cargo-deny-action@v1
with:
rust-version: stable
manifest-path: ./codex-rs/Cargo.toml

View File

@@ -12,7 +12,7 @@ jobs:
NODE_OPTIONS: --max-old-space-size=4096
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v4
@@ -46,7 +46,7 @@ jobs:
echo "pack_output=$PACK_OUTPUT" >> "$GITHUB_OUTPUT"
- name: Upload staged npm package artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: codex-npm-staging
path: ${{ steps.stage_npm_package.outputs.pack_output }}

View File

@@ -13,17 +13,37 @@ permissions:
jobs:
cla:
# Only run the CLA assistant for the canonical openai repo so forks are not blocked
# and contributors who signed previously do not receive duplicate CLA notifications.
if: ${{ github.repository_owner == 'openai' }}
runs-on: ubuntu-latest
steps:
- uses: contributor-assistant/github-action@v2.6.1
# Run on close only if the PR was merged. This will lock the PR to preserve
# the CLA agreement. We don't want to lock PRs that have been closed without
# merging because the contributor may want to respond with additional comments.
# This action has a "lock-pullrequest-aftermerge" option that can be set to false,
# but that would unconditionally skip locking even in cases where the PR was merged.
if: |
github.event_name == 'pull_request_target' ||
github.event.comment.body == 'recheck' ||
github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA'
(
github.event_name == 'pull_request_target' &&
(
github.event.action == 'opened' ||
github.event.action == 'synchronize' ||
(github.event.action == 'closed' && github.event.pull_request.merged == true)
)
) ||
(
github.event_name == 'issue_comment' &&
(
github.event.comment.body == 'recheck' ||
github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA'
)
)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
path-to-document: https://github.com/openai/codex/blob/main/docs/CLA.md
path-to-signatures: signatures/cla.json
branch: cla-signatures
allowlist: dependabot[bot]
allowlist: codex,dependabot,dependabot[bot],github-actions[bot]

View File

@@ -0,0 +1,105 @@
name: Close stale contributor PRs
on:
workflow_dispatch:
schedule:
- cron: "0 6 * * *"
permissions:
contents: read
issues: write
pull-requests: write
jobs:
close-stale-contributor-prs:
runs-on: ubuntu-latest
steps:
- name: Close inactive PRs from contributors
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const DAYS_INACTIVE = 14;
const cutoff = new Date(Date.now() - DAYS_INACTIVE * 24 * 60 * 60 * 1000);
const { owner, repo } = context.repo;
const dryRun = false;
const stalePrs = [];
core.info(`Dry run mode: ${dryRun}`);
const prs = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: "open",
per_page: 100,
sort: "updated",
direction: "asc",
});
for (const pr of prs) {
const lastUpdated = new Date(pr.updated_at);
if (lastUpdated > cutoff) {
core.info(`PR ${pr.number} is fresh`);
continue;
}
if (!pr.user || pr.user.type !== "User") {
core.info(`PR ${pr.number} wasn't created by a user`);
continue;
}
let permission;
try {
const permissionResponse = await github.rest.repos.getCollaboratorPermissionLevel({
owner,
repo,
username: pr.user.login,
});
permission = permissionResponse.data.permission;
} catch (error) {
if (error.status === 404) {
core.info(`Author ${pr.user.login} is not a collaborator; skipping #${pr.number}`);
continue;
}
throw error;
}
const hasContributorAccess = ["admin", "maintain", "write"].includes(permission);
if (!hasContributorAccess) {
core.info(`Author ${pr.user.login} has ${permission} access; skipping #${pr.number}`);
continue;
}
stalePrs.push(pr);
}
if (!stalePrs.length) {
core.info("No stale contributor pull requests found.");
return;
}
for (const pr of stalePrs) {
const issue_number = pr.number;
const closeComment = `Closing this pull request because it has had no updates for more than ${DAYS_INACTIVE} days. If you plan to continue working on it, feel free to reopen or open a new PR.`;
if (dryRun) {
core.info(`[dry-run] Would close contributor PR #${issue_number} from ${pr.user.login}`);
continue;
}
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body: closeComment,
});
await github.rest.pulls.update({
owner,
repo,
pull_number: issue_number,
state: "closed",
});
core.info(`Closed contributor PR #${issue_number} from ${pr.user.login}`);
}

View File

@@ -18,10 +18,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Annotate locations with typos
uses: codespell-project/codespell-problem-matcher@b80729f885d32f78a716c2f107b4db1025001c42 # v1
- name: Codespell
uses: codespell-project/actions-codespell@406322ec52dd7b488e48c1c4b82e2a8b3a1bf630 # v2.1
uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2
with:
ignore_words_file: .codespellignore

View File

@@ -16,7 +16,7 @@ jobs:
outputs:
codex_output: ${{ steps.codex.outputs.final-message }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Prepare Codex inputs
env:
@@ -46,7 +46,6 @@ jobs:
with:
openai-api-key: ${{ secrets.CODEX_OPENAI_API_KEY }}
allow-users: "*"
model: gpt-5
prompt: |
You are an assistant that triages new GitHub issues by identifying potential duplicates.

View File

@@ -16,7 +16,7 @@ jobs:
outputs:
codex_output: ${{ steps.codex.outputs.final-message }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- id: codex
uses: openai/codex-action@main
@@ -26,21 +26,36 @@ jobs:
prompt: |
You are an assistant that reviews GitHub issues for the repository.
Your job is to choose the most appropriate existing labels for the issue described later in this prompt.
Your job is to choose the most appropriate labels for the issue described later in this prompt.
Follow these rules:
- Only pick labels out of the list below.
- Prefer a small set of precise labels over many broad ones.
Labels to apply:
- Add one (and only one) of the following three labels to distinguish the type of issue. Default to "bug" if unsure.
1. bug — Reproducible defects in Codex products (CLI, VS Code extension, web, auth).
2. enhancement — Feature requests or usability improvements that ask for new capabilities, better ergonomics, or quality-of-life tweaks.
3. extension — VS Code (or other IDE) extension-specific issues.
4. windows-os — Bugs or friction specific to Windows environments (always when PowerShell is mentioned, path handling, copy/paste, OS-specific auth or tooling failures).
5. mcp — Topics involving Model Context Protocol servers/clients.
6. codex-web — Issues targeting the Codex web UI/Cloud experience.
8. azure — Problems or requests tied to Azure OpenAI deployments.
9. documentation — Updates or corrections needed in docs/README/config references (broken links, missing examples, outdated keys, clarification requests).
10. model-behaviorUndesirable LLM behavior: forgetting goals, refusing work, hallucinating environment details, quota misreports, or other reasoning/performance anomalies.
3. documentation — Updates or corrections needed in docs/README/config references (broken links, missing examples, outdated keys, clarification requests).
- If applicable, add one of the following labels to specify which sub-product or product surface the issue relates to.
1. CLI — the Codex command line interface.
2. extension — VS Code (or other IDE) extension-specific issues.
3. codex-web — Issues targeting the Codex web UI/Cloud experience.
4. github-actionIssues with the Codex GitHub action.
5. iOS — Issues with the Codex iOS app.
- Additionally add zero or more of the following labels that are relevant to the issue content. Prefer a small set of precise labels over many broad ones.
1. windows-os — Bugs or friction specific to Windows environments (always when PowerShell is mentioned, path handling, copy/paste, OS-specific auth or tooling failures).
2. mcp — Topics involving Model Context Protocol servers/clients.
3. mcp-server — Problems related to the codex mcp-server command, where codex runs as an MCP server.
4. azure — Problems or requests tied to Azure OpenAI deployments.
5. model-behavior — Undesirable LLM behavior: forgetting goals, refusing work, hallucinating environment details, quota misreports, or other reasoning/performance anomalies.
6. code-review — Issues related to the code review feature or functionality.
7. auth - Problems related to authentication, login, or access tokens.
8. codex-exec - Problems related to the "codex exec" command or functionality.
9. context-management - Problems related to compaction, context windows, or available context reporting.
10. custom-model - Problems that involve using custom model providers, local models, or OSS models.
11. rate-limits - Problems related to token limits, rate limits, or token usage reporting.
12. sandbox - Issues related to local sandbox environments or tool call approvals to override sandbox restrictions.
13. tool-calls - Problems related to specific tool call invocations including unexpected errors, failures, or hangs.
14. TUI - Problems with the terminal user interface (TUI) including keyboard shortcuts, copy & pasting, menus, or screen update issues.
Issue number: ${{ github.event.issue.number }}

View File

@@ -17,7 +17,7 @@ jobs:
codex: ${{ steps.detect.outputs.codex }}
workflows: ${{ steps.detect.outputs.workflows }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Detect changed paths (no external action)
@@ -56,7 +56,7 @@ jobs:
run:
working-directory: codex-rs
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@1.90
with:
components: rustfmt
@@ -74,9 +74,9 @@ jobs:
run:
working-directory: codex-rs
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@1.90
- uses: taiki-e/install-action@0c5db7f7f897c03b771660e91d065338615679f4 # v2
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
with:
tool: cargo-shear
version: 1.5.1
@@ -95,8 +95,8 @@ jobs:
run:
working-directory: codex-rs
env:
# Speed up repeated builds across CI runs by caching compiled objects.
RUSTC_WRAPPER: sccache
# Speed up repeated builds across CI runs by caching compiled objects (non-Windows).
USE_SCCACHE: ${{ startsWith(matrix.runner, 'windows') && 'false' || 'true' }}
CARGO_INCREMENTAL: "0"
SCCACHE_CACHE_SIZE: 10G
@@ -147,12 +147,21 @@ jobs:
profile: release
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@1.90
with:
targets: ${{ matrix.target }}
components: clippy
- name: Compute lockfile hash
id: lockhash
working-directory: codex-rs
shell: bash
run: |
set -euo pipefail
echo "hash=$(sha256sum Cargo.lock | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
echo "toolchain_hash=$(sha256sum rust-toolchain.toml | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
# Explicit cache restore: split cargo home vs target, so we can
# avoid caching the large target dir on the gnu-dev job.
- name: Restore cargo home cache
@@ -164,18 +173,20 @@ jobs:
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('codex-rs/rust-toolchain.toml') }}
key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
restore-keys: |
cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
# Install and restore sccache cache
- name: Install sccache
uses: taiki-e/install-action@0c5db7f7f897c03b771660e91d065338615679f4 # v2
if: ${{ env.USE_SCCACHE == 'true' }}
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
with:
tool: sccache
version: 0.7.5
- name: Configure sccache backend
if: ${{ env.USE_SCCACHE == 'true' }}
shell: bash
run: |
set -euo pipefail
@@ -188,15 +199,20 @@ jobs:
echo "Using sccache local disk + actions/cache fallback"
fi
- name: Enable sccache wrapper
if: ${{ env.USE_SCCACHE == 'true' }}
shell: bash
run: echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
- name: Restore sccache cache (fallback)
if: ${{ env.SCCACHE_GHA_ENABLED != 'true' }}
if: ${{ env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true' }}
id: cache_sccache_restore
uses: actions/cache/restore@v4
with:
path: ${{ github.workspace }}/.sccache/
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ hashFiles('**/Cargo.lock') }}-${{ github.run_id }}
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
restore-keys: |
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ hashFiles('**/Cargo.lock') }}-
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
@@ -228,7 +244,7 @@ jobs:
- name: Install cargo-chef
if: ${{ matrix.profile == 'release' }}
uses: taiki-e/install-action@0c5db7f7f897c03b771660e91d065338615679f4 # v2
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
with:
tool: cargo-chef
version: 0.1.71
@@ -271,23 +287,23 @@ jobs:
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('codex-rs/rust-toolchain.toml') }}
key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
- name: Save sccache cache (fallback)
if: always() && !cancelled() && env.SCCACHE_GHA_ENABLED != 'true'
if: always() && !cancelled() && env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true'
continue-on-error: true
uses: actions/cache/save@v4
with:
path: ${{ github.workspace }}/.sccache/
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ hashFiles('**/Cargo.lock') }}-${{ github.run_id }}
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
- name: sccache stats
if: always()
if: always() && env.USE_SCCACHE == 'true'
continue-on-error: true
run: sccache --show-stats || true
- name: sccache summary
if: always()
if: always() && env.USE_SCCACHE == 'true'
shell: bash
run: |
{
@@ -326,7 +342,8 @@ jobs:
run:
working-directory: codex-rs
env:
RUSTC_WRAPPER: sccache
# Speed up repeated builds across CI runs by caching compiled objects (non-Windows).
USE_SCCACHE: ${{ startsWith(matrix.runner, 'windows') && 'false' || 'true' }}
CARGO_INCREMENTAL: "0"
SCCACHE_CACHE_SIZE: 10G
@@ -351,11 +368,20 @@ jobs:
profile: dev
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@1.90
with:
targets: ${{ matrix.target }}
- name: Compute lockfile hash
id: lockhash
working-directory: codex-rs
shell: bash
run: |
set -euo pipefail
echo "hash=$(sha256sum Cargo.lock | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
echo "toolchain_hash=$(sha256sum rust-toolchain.toml | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
- name: Restore cargo home cache
id: cache_cargo_home_restore
uses: actions/cache/restore@v4
@@ -365,17 +391,19 @@ jobs:
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('codex-rs/rust-toolchain.toml') }}
key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
restore-keys: |
cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
- name: Install sccache
uses: taiki-e/install-action@0c5db7f7f897c03b771660e91d065338615679f4 # v2
if: ${{ env.USE_SCCACHE == 'true' }}
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
with:
tool: sccache
version: 0.7.5
- name: Configure sccache backend
if: ${{ env.USE_SCCACHE == 'true' }}
shell: bash
run: |
set -euo pipefail
@@ -388,28 +416,33 @@ jobs:
echo "Using sccache local disk + actions/cache fallback"
fi
- name: Enable sccache wrapper
if: ${{ env.USE_SCCACHE == 'true' }}
shell: bash
run: echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
- name: Restore sccache cache (fallback)
if: ${{ env.SCCACHE_GHA_ENABLED != 'true' }}
if: ${{ env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true' }}
id: cache_sccache_restore
uses: actions/cache/restore@v4
with:
path: ${{ github.workspace }}/.sccache/
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ hashFiles('**/Cargo.lock') }}-${{ github.run_id }}
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
restore-keys: |
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ hashFiles('**/Cargo.lock') }}-
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
- uses: taiki-e/install-action@0c5db7f7f897c03b771660e91d065338615679f4 # v2
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
with:
tool: nextest
version: 0.9.103
- name: tests
id: test
continue-on-error: true
run: cargo nextest run --all-features --no-fail-fast --target ${{ matrix.target }} --cargo-profile ci-test
env:
RUST_BACKTRACE: 1
NEXTEST_STATUS_LEVEL: leak
- name: Save cargo home cache
if: always() && !cancelled() && steps.cache_cargo_home_restore.outputs.cache-hit != 'true'
@@ -421,23 +454,23 @@ jobs:
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('codex-rs/rust-toolchain.toml') }}
key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
- name: Save sccache cache (fallback)
if: always() && !cancelled() && env.SCCACHE_GHA_ENABLED != 'true'
if: always() && !cancelled() && env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true'
continue-on-error: true
uses: actions/cache/save@v4
with:
path: ${{ github.workspace }}/.sccache/
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ hashFiles('**/Cargo.lock') }}-${{ github.run_id }}
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
- name: sccache stats
if: always()
if: always() && env.USE_SCCACHE == 'true'
continue-on-error: true
run: sccache --show-stats || true
- name: sccache summary
if: always()
if: always() && env.USE_SCCACHE == 'true'
shell: bash
run: |
{

View File

@@ -19,7 +19,7 @@ jobs:
tag-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Validate tag matches Cargo.toml version
shell: bash
@@ -76,7 +76,7 @@ jobs:
target: aarch64-pc-windows-msvc
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@1.90
with:
targets: ${{ matrix.target }}
@@ -295,6 +295,15 @@ jobs:
# ${{ matrix.target }}
dest="dist/${{ matrix.target }}"
# We want to ship the raw Windows executables in the GitHub Release
# in addition to the compressed archives. Keep the originals for
# Windows targets; remove them elsewhere to limit the number of
# artifacts that end up in the GitHub Release.
keep_originals=false
if [[ "${{ matrix.runner }}" == windows* ]]; then
keep_originals=true
fi
# For compatibility with environments that lack the `zstd` tool we
# additionally create a `.tar.gz` for all platforms and `.zip` for
# Windows alongside every single binary that we publish. The end result is:
@@ -324,7 +333,11 @@ jobs:
# Also create .zst (existing behaviour) *and* remove the original
# uncompressed binary to keep the directory small.
zstd -T0 -19 --rm "$dest/$base"
zstd_args=(-T0 -19)
if [[ "${keep_originals}" == false ]]; then
zstd_args+=(--rm)
fi
zstd "${zstd_args[@]}" "$dest/$base"
done
- name: Remove signing keychain
@@ -350,7 +363,7 @@ jobs:
fi
fi
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
with:
name: ${{ matrix.target }}
# Upload the per-binary .zst files as well as the new .tar.gz
@@ -358,8 +371,19 @@ jobs:
path: |
codex-rs/dist/${{ matrix.target }}/*
shell-tool-mcp:
name: shell-tool-mcp
needs: tag-check
uses: ./.github/workflows/shell-tool-mcp.yml
with:
release-tag: ${{ github.ref_name }}
publish: true
secrets: inherit
release:
needs: build
needs:
- build
- shell-tool-mcp
name: release
runs-on: ubuntu-latest
permissions:
@@ -373,7 +397,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- uses: actions/download-artifact@v4
with:
@@ -382,6 +406,14 @@ jobs:
- name: List
run: ls -R dist/
# This is a temporary fix: we should modify shell-tool-mcp.yml so these
# files do not end up in dist/ in the first place.
- name: Delete entries from dist/ that should not go in the release
run: |
rm -rf dist/shell-tool-mcp*
ls -R dist/
- name: Define release name
id: release_name
run: |

View File

@@ -11,7 +11,7 @@ jobs:
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v4

48
.github/workflows/shell-tool-mcp-ci.yml vendored Normal file
View File

@@ -0,0 +1,48 @@
name: shell-tool-mcp CI
on:
push:
paths:
- "shell-tool-mcp/**"
- ".github/workflows/shell-tool-mcp-ci.yml"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
pull_request:
paths:
- "shell-tool-mcp/**"
- ".github/workflows/shell-tool-mcp-ci.yml"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
env:
NODE_VERSION: 22
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Format check
run: pnpm --filter @openai/codex-shell-tool-mcp run format
- name: Run tests
run: pnpm --filter @openai/codex-shell-tool-mcp test
- name: Build
run: pnpm --filter @openai/codex-shell-tool-mcp run build

405
.github/workflows/shell-tool-mcp.yml vendored Normal file
View File

@@ -0,0 +1,405 @@
name: shell-tool-mcp
on:
workflow_call:
inputs:
release-version:
description: Version to publish (x.y.z or x.y.z-alpha.N). Defaults to GITHUB_REF_NAME when it starts with rust-v.
required: false
type: string
release-tag:
description: Tag name to use when downloading release artifacts (defaults to rust-v<version>).
required: false
type: string
publish:
description: Whether to publish to npm when the version is releasable.
required: false
default: true
type: boolean
env:
NODE_VERSION: 22
jobs:
metadata:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.compute.outputs.version }}
release_tag: ${{ steps.compute.outputs.release_tag }}
should_publish: ${{ steps.compute.outputs.should_publish }}
npm_tag: ${{ steps.compute.outputs.npm_tag }}
steps:
- name: Compute version and tags
id: compute
run: |
set -euo pipefail
version="${{ inputs.release-version }}"
release_tag="${{ inputs.release-tag }}"
if [[ -z "$version" ]]; then
if [[ -n "$release_tag" && "$release_tag" =~ ^rust-v.+ ]]; then
version="${release_tag#rust-v}"
elif [[ "${GITHUB_REF_NAME:-}" =~ ^rust-v.+ ]]; then
version="${GITHUB_REF_NAME#rust-v}"
release_tag="${GITHUB_REF_NAME}"
else
echo "release-version is required when GITHUB_REF_NAME is not a rust-v tag."
exit 1
fi
fi
if [[ -z "$release_tag" ]]; then
release_tag="rust-v${version}"
fi
npm_tag=""
should_publish="false"
if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
should_publish="true"
elif [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then
should_publish="true"
npm_tag="alpha"
fi
echo "version=${version}" >> "$GITHUB_OUTPUT"
echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT"
echo "npm_tag=${npm_tag}" >> "$GITHUB_OUTPUT"
echo "should_publish=${should_publish}" >> "$GITHUB_OUTPUT"
rust-binaries:
name: Build Rust - ${{ matrix.target }}
needs: metadata
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
defaults:
run:
working-directory: codex-rs
strategy:
fail-fast: false
matrix:
include:
- runner: macos-15-xlarge
target: aarch64-apple-darwin
- runner: macos-15-xlarge
target: x86_64-apple-darwin
- runner: ubuntu-24.04
target: x86_64-unknown-linux-musl
install_musl: true
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
install_musl: true
steps:
- name: Checkout repository
uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@1.90
with:
targets: ${{ matrix.target }}
- if: ${{ matrix.install_musl }}
name: Install musl build dependencies
run: |
sudo apt-get update
sudo apt-get install -y musl-tools pkg-config
- name: Build exec server binaries
run: cargo build --release --target ${{ matrix.target }} --bin codex-exec-mcp-server --bin codex-execve-wrapper
- name: Stage exec server binaries
run: |
dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}"
mkdir -p "$dest"
cp "target/${{ matrix.target }}/release/codex-exec-mcp-server" "$dest/"
cp "target/${{ matrix.target }}/release/codex-execve-wrapper" "$dest/"
- uses: actions/upload-artifact@v5
with:
name: shell-tool-mcp-rust-${{ matrix.target }}
path: artifacts/**
if-no-files-found: error
bash-linux:
name: Build Bash (Linux) - ${{ matrix.variant }} - ${{ matrix.target }}
needs: metadata
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
container:
image: ${{ matrix.image }}
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-24.04
target: x86_64-unknown-linux-musl
variant: ubuntu-24.04
image: ubuntu:24.04
- runner: ubuntu-24.04
target: x86_64-unknown-linux-musl
variant: ubuntu-22.04
image: ubuntu:22.04
- runner: ubuntu-24.04
target: x86_64-unknown-linux-musl
variant: debian-12
image: debian:12
- runner: ubuntu-24.04
target: x86_64-unknown-linux-musl
variant: debian-11
image: debian:11
- runner: ubuntu-24.04
target: x86_64-unknown-linux-musl
variant: centos-9
image: quay.io/centos/centos:stream9
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
variant: ubuntu-24.04
image: arm64v8/ubuntu:24.04
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
variant: ubuntu-22.04
image: arm64v8/ubuntu:22.04
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
variant: ubuntu-20.04
image: arm64v8/ubuntu:20.04
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
variant: debian-12
image: arm64v8/debian:12
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
variant: debian-11
image: arm64v8/debian:11
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
variant: centos-9
image: quay.io/centos/centos:stream9
steps:
- name: Install build prerequisites
shell: bash
run: |
set -euo pipefail
if command -v apt-get >/dev/null 2>&1; then
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y git build-essential bison autoconf gettext
elif command -v dnf >/dev/null 2>&1; then
dnf install -y git gcc gcc-c++ make bison autoconf gettext
elif command -v yum >/dev/null 2>&1; then
yum install -y git gcc gcc-c++ make bison autoconf gettext
else
echo "Unsupported package manager in container"
exit 1
fi
- name: Checkout repository
uses: actions/checkout@v6
- name: Build patched Bash
shell: bash
run: |
set -euo pipefail
git clone --depth 1 https://github.com/bminor/bash /tmp/bash
cd /tmp/bash
git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch"
./configure --without-bash-malloc
cores="$(command -v nproc >/dev/null 2>&1 && nproc || getconf _NPROCESSORS_ONLN)"
make -j"${cores}"
dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}"
mkdir -p "$dest"
cp bash "$dest/bash"
- uses: actions/upload-artifact@v5
with:
name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }}
path: artifacts/**
if-no-files-found: error
bash-darwin:
name: Build Bash (macOS) - ${{ matrix.variant }} - ${{ matrix.target }}
needs: metadata
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- runner: macos-15-xlarge
target: aarch64-apple-darwin
variant: macos-15
- runner: macos-14
target: aarch64-apple-darwin
variant: macos-14
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Build patched Bash
shell: bash
run: |
set -euo pipefail
git clone --depth 1 https://github.com/bminor/bash /tmp/bash
cd /tmp/bash
git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch"
./configure --without-bash-malloc
cores="$(getconf _NPROCESSORS_ONLN)"
make -j"${cores}"
dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}"
mkdir -p "$dest"
cp bash "$dest/bash"
- uses: actions/upload-artifact@v5
with:
name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }}
path: artifacts/**
if-no-files-found: error
package:
name: Package npm module
needs:
- metadata
- rust-binaries
- bash-linux
- bash-darwin
runs-on: ubuntu-latest
env:
PACKAGE_VERSION: ${{ needs.metadata.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.8.1
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install JavaScript dependencies
run: pnpm install --frozen-lockfile
- name: Build (shell-tool-mcp)
run: pnpm --filter @openai/codex-shell-tool-mcp run build
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Assemble staging directory
id: staging
shell: bash
run: |
set -euo pipefail
staging="${STAGING_DIR}"
mkdir -p "$staging" "$staging/vendor"
cp shell-tool-mcp/README.md "$staging/"
cp shell-tool-mcp/package.json "$staging/"
cp -R shell-tool-mcp/bin "$staging/"
found_vendor="false"
shopt -s nullglob
for vendor_dir in artifacts/*/vendor; do
rsync -av "$vendor_dir/" "$staging/vendor/"
found_vendor="true"
done
if [[ "$found_vendor" == "false" ]]; then
echo "No vendor payloads were downloaded."
exit 1
fi
node - <<'NODE'
import fs from "node:fs";
import path from "node:path";
const stagingDir = process.env.STAGING_DIR;
const version = process.env.PACKAGE_VERSION;
const pkgPath = path.join(stagingDir, "package.json");
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
pkg.version = version;
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
NODE
echo "dir=$staging" >> "$GITHUB_OUTPUT"
env:
STAGING_DIR: ${{ runner.temp }}/shell-tool-mcp
- name: Ensure binaries are executable
run: |
set -euo pipefail
staging="${{ steps.staging.outputs.dir }}"
chmod +x \
"$staging"/vendor/*/codex-exec-mcp-server \
"$staging"/vendor/*/codex-execve-wrapper \
"$staging"/vendor/*/bash/*/bash
- name: Create npm tarball
shell: bash
run: |
set -euo pipefail
mkdir -p dist/npm
staging="${{ steps.staging.outputs.dir }}"
pack_info=$(cd "$staging" && npm pack --ignore-scripts --json --pack-destination "${GITHUB_WORKSPACE}/dist/npm")
filename=$(PACK_INFO="$pack_info" node -e 'const data = JSON.parse(process.env.PACK_INFO); console.log(data[0].filename);')
mv "dist/npm/${filename}" "dist/npm/codex-shell-tool-mcp-npm-${PACKAGE_VERSION}.tgz"
- uses: actions/upload-artifact@v5
with:
name: codex-shell-tool-mcp-npm
path: dist/npm/codex-shell-tool-mcp-npm-${{ env.PACKAGE_VERSION }}.tgz
if-no-files-found: error
publish:
name: Publish npm package
needs:
- metadata
- package
if: ${{ inputs.publish && needs.metadata.outputs.should_publish == 'true' }}
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.8.1
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: ${{ env.NODE_VERSION }}
registry-url: https://registry.npmjs.org
scope: "@openai"
- name: Update npm
run: npm install -g npm@latest
- name: Download npm tarball
uses: actions/download-artifact@v4
with:
name: codex-shell-tool-mcp-npm
path: dist/npm
- name: Publish to npm
env:
NPM_TAG: ${{ needs.metadata.outputs.npm_tag }}
VERSION: ${{ needs.metadata.outputs.version }}
shell: bash
run: |
set -euo pipefail
tag_args=()
if [[ -n "${NPM_TAG}" ]]; then
tag_args+=(--tag "${NPM_TAG}")
fi
npm publish "dist/npm/codex-shell-tool-mcp-npm-${VERSION}.tgz" "${tag_args[@]}"

3
.gitignore vendored
View File

@@ -64,6 +64,9 @@ apply_patch/
# coverage
coverage/
# personal files
personal/
# os
.DS_Store
Thumbs.db

View File

@@ -84,6 +84,8 @@ If you dont have the tool:
- Use `ResponseMock::single_request()` when a test should only issue one POST, or `ResponseMock::requests()` to inspect every captured `ResponsesRequest`.
- `ResponsesRequest` exposes helpers (`body_json`, `input`, `function_call_output`, `custom_tool_call_output`, `call_output`, `header`, `path`, `query_param`) so assertions can target structured payloads instead of manual JSON digging.
- Build SSE payloads with the provided `ev_*` constructors and the `sse(...)`.
- Prefer `wait_for_event` over `wait_for_event_with_timeout`.
- Prefer `mount_sse_once` over `mount_sse_once_match` or `mount_sse_sequence`
- Typical pattern:

View File

@@ -69,18 +69,23 @@ Codex can access MCP servers. To configure them, refer to the [config docs](./do
Codex CLI supports a rich set of configuration options, with preferences stored in `~/.codex/config.toml`. For full configuration options, see [Configuration](./docs/config.md).
---
### Execpolicy
See the [Execpolicy quickstart](./docs/execpolicy.md) to set up rules that govern what commands Codex can execute.
### Docs & FAQ
- [**Getting started**](./docs/getting-started.md)
- [CLI usage](./docs/getting-started.md#cli-usage)
- [Slash Commands](./docs/slash_commands.md)
- [Running with a prompt as input](./docs/getting-started.md#running-with-a-prompt-as-input)
- [Example prompts](./docs/getting-started.md#example-prompts)
- [Custom prompts](./docs/prompts.md)
- [Memory with AGENTS.md](./docs/getting-started.md#memory-with-agentsmd)
- [**Configuration**](./docs/config.md)
- [Example config](./docs/example-config.md)
- [**Sandbox & approvals**](./docs/sandbox.md)
- [**Execpolicy quickstart**](./docs/execpolicy.md)
- [**Authentication**](./docs/authentication.md)
- [Auth methods](./docs/authentication.md#forcing-a-specific-auth-method-advanced)
- [Login on a "Headless" machine](./docs/authentication.md#connecting-on-a-headless-machine)

View File

@@ -0,0 +1,6 @@
[advisories]
ignore = [
"RUSTSEC-2024-0388", # derivative 2.2.0 via starlark; upstream crate is unmaintained
"RUSTSEC-2025-0057", # fxhash 0.2.1 via starlark_map; upstream crate is unmaintained
"RUSTSEC-2024-0436", # paste 1.0.15 via starlark/ratatui; upstream crate is unmaintained
]

View File

@@ -0,0 +1,13 @@
[profile.default]
# Do not increase, fix your test instead
slow-timeout = { period = "15s", terminate-after = 2 }
[[profile.default.overrides]]
# Do not add new tests here
filter = 'test(rmcp_client) | test(humanlike_typing_1000_chars_appears_live_no_placeholder)'
slow-timeout = { period = "1m", terminate-after = 4 }
[[profile.default.overrides]]
filter = 'test(approval_matrix_covers_all_modes)'
slow-timeout = { period = "30s", terminate-after = 2 }

View File

@@ -0,0 +1,26 @@
name: Cargo audit
on:
pull_request:
push:
branches:
- main
permissions:
contents: read
jobs:
audit:
runs-on: ubuntu-latest
defaults:
run:
working-directory: codex-rs
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Install cargo-audit
uses: taiki-e/install-action@v2
with:
tool: cargo-audit
- name: Run cargo audit
run: cargo audit --deny warnings

564
codex-rs/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@ members = [
"async-utils",
"app-server",
"app-server-protocol",
"app-server-test-client",
"apply-patch",
"arg0",
"feedback",
@@ -15,17 +16,19 @@ members = [
"common",
"core",
"exec",
"exec-server",
"execpolicy",
"execpolicy-legacy",
"keyring-store",
"file-search",
"linux-sandbox",
"lmstudio",
"login",
"mcp-server",
"mcp-types",
"ollama",
"process-hardening",
"protocol",
"protocol-ts",
"rmcp-client",
"responses-api-proxy",
"stdio-to-uds",
@@ -38,7 +41,8 @@ members = [
"utils/pty",
"utils/readiness",
"utils/string",
"utils/tokenizer",
"codex-client",
"codex-api",
]
resolver = "2"
@@ -49,11 +53,13 @@ version = "0.0.0"
# crates created with `cargo new -w ...` automatically inherit the 2024
# edition.
edition = "2024"
license = "Apache-2.0"
[workspace.dependencies]
# Internal
app_test_support = { path = "app-server/tests/common" }
codex-ansi-escape = { path = "ansi-escape" }
codex-api = { path = "codex-api" }
codex-app-server = { path = "app-server" }
codex-app-server-protocol = { path = "app-server-protocol" }
codex-apply-patch = { path = "apply-patch" }
@@ -61,21 +67,23 @@ codex-arg0 = { path = "arg0" }
codex-async-utils = { path = "async-utils" }
codex-backend-client = { path = "backend-client" }
codex-chatgpt = { path = "chatgpt" }
codex-client = { path = "codex-client" }
codex-common = { path = "common" }
codex-core = { path = "core" }
codex-exec = { path = "exec" }
codex-execpolicy = { path = "execpolicy" }
codex-feedback = { path = "feedback" }
codex-file-search = { path = "file-search" }
codex-git = { path = "utils/git" }
codex-keyring-store = { path = "keyring-store" }
codex-linux-sandbox = { path = "linux-sandbox" }
codex-lmstudio = { path = "lmstudio" }
codex-login = { path = "login" }
codex-mcp-server = { path = "mcp-server" }
codex-ollama = { path = "ollama" }
codex-otel = { path = "otel" }
codex-process-hardening = { path = "process-hardening" }
codex-protocol = { path = "protocol" }
codex-protocol-ts = { path = "protocol-ts" }
codex-responses-api-proxy = { path = "responses-api-proxy" }
codex-rmcp-client = { path = "rmcp-client" }
codex-stdio-to-uds = { path = "stdio-to-uds" }
@@ -86,8 +94,7 @@ codex-utils-json-to-toml = { path = "utils/json-to-toml" }
codex-utils-pty = { path = "utils/pty" }
codex-utils-readiness = { path = "utils/readiness" }
codex-utils-string = { path = "utils/string" }
codex-utils-tokenizer = { path = "utils/tokenizer" }
codex-windows-sandbox = { path = "windows-sandbox" }
codex-windows-sandbox = { path = "windows-sandbox-rs" }
core_test_support = { path = "core/tests/common" }
mcp-types = { path = "mcp-types" }
mcp_test_support = { path = "mcp-server/tests/common" }
@@ -96,8 +103,8 @@ mcp_test_support = { path = "mcp-server/tests/common" }
allocative = "0.3.3"
ansi-to-tui = "7.0.0"
anyhow = "1"
arboard = "3"
askama = "0.12"
arboard = { version = "3", features = ["wayland-data-control"] }
askama = "0.14"
assert_cmd = "2"
assert_matches = "1.5.0"
async-channel = "2.3.1"
@@ -106,6 +113,7 @@ async-trait = "0.1.89"
axum = { version = "0.8", default-features = false }
base64 = "0.22.1"
bytes = "1.10.1"
chardetng = "0.1.17"
chrono = "0.4.42"
clap = "4"
clap_complete = "4"
@@ -117,6 +125,7 @@ diffy = "0.4.2"
dirs = "6"
dotenvy = "0.15.7"
dunce = "1.0.4"
encoding_rs = "0.8.35"
env-flags = "0.1.1"
env_logger = "0.11.5"
escargot = "0.5"
@@ -124,17 +133,17 @@ eventsource-stream = "0.2.3"
futures = { version = "0.3", default-features = false }
http = "1.3.1"
icu_decimal = "2.1"
icu_provider = { version = "2.1", features = ["sync"] }
icu_locale_core = "2.1"
icu_provider = { version = "2.1", features = ["sync"] }
ignore = "0.4.23"
image = { version = "^0.25.8", default-features = false }
image = { version = "^0.25.9", default-features = false }
indexmap = "2.12.0"
insta = "1.43.2"
itertools = "0.14.0"
keyring = "3.6"
keyring = { version = "3.6", default-features = false }
landlock = "0.4.1"
lazy_static = "1"
libc = "0.2.175"
libc = "0.2.177"
log = "0.4"
lru = "0.12.5"
maplit = "1.0.2"
@@ -142,6 +151,7 @@ mime_guess = "2.0.5"
multimap = "0.10.0"
notify = "8.2.0"
nucleo-matcher = "0.3.1"
once_cell = "1.20.2"
openssl-sys = "*"
opentelemetry = "0.30.0"
opentelemetry-appender-tracing = "0.30.0"
@@ -150,7 +160,6 @@ opentelemetry-semantic-conventions = "0.30.0"
opentelemetry_sdk = "0.30.0"
os_info = "3.12.0"
owo-colors = "4.2.0"
paste = "1.0.15"
path-absolutize = "3.1.1"
pathdiff = "0.2"
portable-pty = "0.9.0"
@@ -160,20 +169,22 @@ pulldown-cmark = "0.10"
rand = "0.9"
ratatui = "0.29.0"
ratatui-macros = "0.6.0"
regex = "1.12.2"
regex-lite = "0.1.7"
reqwest = "0.12"
rmcp = { version = "0.8.3", default-features = false }
rmcp = { version = "0.10.0", default-features = false }
schemars = "0.8.22"
seccompiler = "0.5.0"
sentry = "0.34.0"
serde = "1"
serde_json = "1"
serde_with = "3.14"
serde_with = "3.16"
serial_test = "3.2.0"
sha1 = "0.10.6"
sha2 = "0.10"
shlex = "1.3.0"
similar = "2.7.0"
socket2 = "0.6.0"
starlark = "0.13.0"
strum = "0.27.2"
strum_macros = "0.27.2"
@@ -190,9 +201,9 @@ tokio-stream = "0.1.17"
tokio-test = "0.4"
tokio-util = "0.7.16"
toml = "0.9.5"
toml_edit = "0.23.4"
toml_edit = "0.23.5"
tonic = "0.13.1"
tracing = "0.1.41"
tracing = "0.1.43"
tracing-appender = "0.2.3"
tracing-subscriber = "0.3.20"
tracing-test = "0.2.5"
@@ -213,7 +224,7 @@ which = "6"
wildmatch = "2.5.0"
wiremock = "0.6"
zeroize = "1.8.1"
zeroize = "1.8.2"
[workspace.lints]
rust = {}
@@ -256,7 +267,7 @@ unwrap_used = "deny"
# cargo-shear cannot see the platform-specific openssl-sys usage, so we
# silence the false positive here instead of deleting a real dependency.
[workspace.metadata.cargo-shear]
ignored = ["icu_provider", "openssl-sys", "codex-utils-readiness", "codex-utils-tokenizer"]
ignored = ["icu_provider", "openssl-sys", "codex-utils-readiness"]
[profile.release]
lto = "fat"
@@ -275,8 +286,8 @@ opt-level = 0
[patch.crates-io]
# Uncomment to debug local changes.
# ratatui = { path = "../../ratatui" }
ratatui = { git = "https://github.com/nornagon/ratatui", branch = "nornagon-v0.29.0-patch" }
crossterm = { git = "https://github.com/nornagon/crossterm", branch = "nornagon/color-query" }
ratatui = { git = "https://github.com/nornagon/ratatui", branch = "nornagon-v0.29.0-patch" }
# Uncomment to debug local changes.
# rmcp = { path = "../../rust-sdk/crates/rmcp" }

View File

@@ -58,7 +58,7 @@ To test to see what happens when a command is run under the sandbox provided by
```
# macOS
codex sandbox macos [--full-auto] [COMMAND]...
codex sandbox macos [--full-auto] [--log-denials] [COMMAND]...
# Linux
codex sandbox linux [--full-auto] [COMMAND]...
@@ -67,7 +67,7 @@ codex sandbox linux [--full-auto] [COMMAND]...
codex sandbox windows [--full-auto] [COMMAND]...
# Legacy aliases
codex debug seatbelt [--full-auto] [COMMAND]...
codex debug seatbelt [--full-auto] [--log-denials] [COMMAND]...
codex debug landlock [--full-auto] [COMMAND]...
```

View File

@@ -1,7 +1,8 @@
[package]
edition = "2024"
name = "codex-ansi-escape"
version = { workspace = true }
version.workspace = true
edition.workspace = true
license.workspace = true
[lib]
name = "codex_ansi_escape"

View File

@@ -1,7 +1,8 @@
[package]
edition = "2024"
name = "codex-app-server-protocol"
version = { workspace = true }
version.workspace = true
edition.workspace = true
license.workspace = true
[lib]
name = "codex_app_server_protocol"
@@ -14,11 +15,12 @@ workspace = true
anyhow = { workspace = true }
clap = { workspace = true, features = ["derive"] }
codex-protocol = { workspace = true }
paste = { workspace = true }
mcp-types = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
strum_macros = { workspace = true }
thiserror = { workspace = true }
ts-rs = { workspace = true }
uuid = { workspace = true, features = ["serde", "v7"] }

View File

@@ -2,20 +2,24 @@ use crate::ClientNotification;
use crate::ClientRequest;
use crate::ServerNotification;
use crate::ServerRequest;
use crate::export_client_notification_schemas;
use crate::export_client_param_schemas;
use crate::export_client_response_schemas;
use crate::export_client_responses;
use crate::export_server_notification_schemas;
use crate::export_server_param_schemas;
use crate::export_server_response_schemas;
use crate::export_server_responses;
use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
use codex_protocol::protocol::EventMsg;
use schemars::JsonSchema;
use schemars::schema::RootSchema;
use schemars::schema_for;
use serde::Serialize;
use serde_json::Map;
use serde_json::Value;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::HashSet;
use std::ffi::OsStr;
use std::fs;
@@ -28,91 +32,64 @@ use ts_rs::TS;
const HEADER: &str = "// GENERATED CODE! DO NOT MODIFY BY HAND!\n\n";
macro_rules! for_each_schema_type {
($macro:ident) => {
$macro!(crate::RequestId);
$macro!(crate::JSONRPCMessage);
$macro!(crate::JSONRPCRequest);
$macro!(crate::JSONRPCNotification);
$macro!(crate::JSONRPCResponse);
$macro!(crate::JSONRPCError);
$macro!(crate::JSONRPCErrorError);
$macro!(crate::AddConversationListenerParams);
$macro!(crate::AddConversationSubscriptionResponse);
$macro!(crate::ApplyPatchApprovalParams);
$macro!(crate::ApplyPatchApprovalResponse);
$macro!(crate::ArchiveConversationParams);
$macro!(crate::ArchiveConversationResponse);
$macro!(crate::AuthMode);
$macro!(crate::AuthStatusChangeNotification);
$macro!(crate::CancelLoginChatGptParams);
$macro!(crate::CancelLoginChatGptResponse);
$macro!(crate::ClientInfo);
$macro!(crate::ClientNotification);
$macro!(crate::ClientRequest);
$macro!(crate::ConversationSummary);
$macro!(crate::ExecCommandApprovalParams);
$macro!(crate::ExecCommandApprovalResponse);
$macro!(crate::ExecOneOffCommandParams);
$macro!(crate::ExecOneOffCommandResponse);
$macro!(crate::FuzzyFileSearchParams);
$macro!(crate::FuzzyFileSearchResponse);
$macro!(crate::FuzzyFileSearchResult);
$macro!(crate::GetAuthStatusParams);
$macro!(crate::GetAuthStatusResponse);
$macro!(crate::GetUserAgentResponse);
$macro!(crate::GetUserSavedConfigResponse);
$macro!(crate::GitDiffToRemoteParams);
$macro!(crate::GitDiffToRemoteResponse);
$macro!(crate::GitSha);
$macro!(crate::InitializeParams);
$macro!(crate::InitializeResponse);
$macro!(crate::InputItem);
$macro!(crate::InterruptConversationParams);
$macro!(crate::InterruptConversationResponse);
$macro!(crate::ListConversationsParams);
$macro!(crate::ListConversationsResponse);
$macro!(crate::LoginApiKeyParams);
$macro!(crate::LoginApiKeyResponse);
$macro!(crate::LoginChatGptCompleteNotification);
$macro!(crate::LoginChatGptResponse);
$macro!(crate::LogoutChatGptParams);
$macro!(crate::LogoutChatGptResponse);
$macro!(crate::NewConversationParams);
$macro!(crate::NewConversationResponse);
$macro!(crate::Profile);
$macro!(crate::RemoveConversationListenerParams);
$macro!(crate::RemoveConversationSubscriptionResponse);
$macro!(crate::ResumeConversationParams);
$macro!(crate::ResumeConversationResponse);
$macro!(crate::SandboxSettings);
$macro!(crate::SendUserMessageParams);
$macro!(crate::SendUserMessageResponse);
$macro!(crate::SendUserTurnParams);
$macro!(crate::SendUserTurnResponse);
$macro!(crate::ServerNotification);
$macro!(crate::ServerRequest);
$macro!(crate::SessionConfiguredNotification);
$macro!(crate::SetDefaultModelParams);
$macro!(crate::SetDefaultModelResponse);
$macro!(crate::Tools);
$macro!(crate::UserInfoResponse);
$macro!(crate::UserSavedConfig);
$macro!(codex_protocol::protocol::EventMsg);
$macro!(codex_protocol::protocol::FileChange);
$macro!(codex_protocol::parse_command::ParsedCommand);
$macro!(codex_protocol::protocol::SandboxPolicy);
};
#[derive(Clone)]
pub struct GeneratedSchema {
namespace: Option<String>,
logical_name: String,
value: Value,
in_v1_dir: bool,
}
impl GeneratedSchema {
fn namespace(&self) -> Option<&str> {
self.namespace.as_deref()
}
fn logical_name(&self) -> &str {
&self.logical_name
}
fn value(&self) -> &Value {
&self.value
}
}
type JsonSchemaEmitter = fn(&Path) -> Result<GeneratedSchema>;
pub fn generate_types(out_dir: &Path, prettier: Option<&Path>) -> Result<()> {
generate_ts(out_dir, prettier)?;
generate_json(out_dir)?;
Ok(())
}
#[derive(Clone, Copy, Debug)]
pub struct GenerateTsOptions {
pub generate_indices: bool,
pub ensure_headers: bool,
pub run_prettier: bool,
}
impl Default for GenerateTsOptions {
fn default() -> Self {
Self {
generate_indices: true,
ensure_headers: true,
run_prettier: true,
}
}
}
pub fn generate_ts(out_dir: &Path, prettier: Option<&Path>) -> Result<()> {
generate_ts_with_options(out_dir, prettier, GenerateTsOptions::default())
}
pub fn generate_ts_with_options(
out_dir: &Path,
prettier: Option<&Path>,
options: GenerateTsOptions,
) -> Result<()> {
let v2_out_dir = out_dir.join("v2");
ensure_dir(out_dir)?;
ensure_dir(&v2_out_dir)?;
ClientRequest::export_all_to(out_dir)?;
export_client_responses(out_dir)?;
@@ -122,18 +99,34 @@ pub fn generate_ts(out_dir: &Path, prettier: Option<&Path>) -> Result<()> {
export_server_responses(out_dir)?;
ServerNotification::export_all_to(out_dir)?;
generate_index_ts(out_dir)?;
let ts_files = ts_files_in(out_dir)?;
for file in &ts_files {
prepend_header_if_missing(file)?;
if options.generate_indices {
generate_index_ts(out_dir)?;
generate_index_ts(&v2_out_dir)?;
}
if let Some(prettier_bin) = prettier
// Ensure our header is present on all TS files (root + subdirs like v2/).
let mut ts_files = Vec::new();
let should_collect_ts_files =
options.ensure_headers || (options.run_prettier && prettier.is_some());
if should_collect_ts_files {
ts_files = ts_files_in_recursive(out_dir)?;
}
if options.ensure_headers {
for file in &ts_files {
prepend_header_if_missing(file)?;
}
}
// Optionally run Prettier on all generated TS files.
if options.run_prettier
&& let Some(prettier_bin) = prettier
&& !ts_files.is_empty()
{
let status = Command::new(prettier_bin)
.arg("--write")
.arg("--log-level")
.arg("warn")
.args(ts_files.iter().map(|p| p.as_os_str()))
.status()
.with_context(|| format!("Failed to invoke Prettier at {}", prettier_bin.display()))?;
@@ -147,51 +140,115 @@ pub fn generate_ts(out_dir: &Path, prettier: Option<&Path>) -> Result<()> {
pub fn generate_json(out_dir: &Path) -> Result<()> {
ensure_dir(out_dir)?;
let mut bundle: BTreeMap<String, RootSchema> = BTreeMap::new();
let envelope_emitters: &[JsonSchemaEmitter] = &[
|d| write_json_schema_with_return::<crate::RequestId>(d, "RequestId"),
|d| write_json_schema_with_return::<crate::JSONRPCMessage>(d, "JSONRPCMessage"),
|d| write_json_schema_with_return::<crate::JSONRPCRequest>(d, "JSONRPCRequest"),
|d| write_json_schema_with_return::<crate::JSONRPCNotification>(d, "JSONRPCNotification"),
|d| write_json_schema_with_return::<crate::JSONRPCResponse>(d, "JSONRPCResponse"),
|d| write_json_schema_with_return::<crate::JSONRPCError>(d, "JSONRPCError"),
|d| write_json_schema_with_return::<crate::JSONRPCErrorError>(d, "JSONRPCErrorError"),
|d| write_json_schema_with_return::<crate::ClientRequest>(d, "ClientRequest"),
|d| write_json_schema_with_return::<crate::ServerRequest>(d, "ServerRequest"),
|d| write_json_schema_with_return::<crate::ClientNotification>(d, "ClientNotification"),
|d| write_json_schema_with_return::<crate::ServerNotification>(d, "ServerNotification"),
|d| write_json_schema_with_return::<EventMsg>(d, "EventMsg"),
];
macro_rules! add_schema {
($ty:path) => {{
let name = type_basename(stringify!($ty));
let schema = write_json_schema_with_return::<$ty>(out_dir, &name)?;
bundle.insert(name, schema);
}};
let mut schemas: Vec<GeneratedSchema> = Vec::new();
for emit in envelope_emitters {
schemas.push(emit(out_dir)?);
}
for_each_schema_type!(add_schema);
schemas.extend(export_client_param_schemas(out_dir)?);
schemas.extend(export_client_response_schemas(out_dir)?);
schemas.extend(export_server_param_schemas(out_dir)?);
schemas.extend(export_server_response_schemas(out_dir)?);
schemas.extend(export_client_notification_schemas(out_dir)?);
schemas.extend(export_server_notification_schemas(out_dir)?);
export_client_response_schemas(out_dir)?;
export_server_response_schemas(out_dir)?;
let bundle = build_schema_bundle(schemas)?;
write_pretty_json(
out_dir.join("codex_app_server_protocol.schemas.json"),
&bundle,
)?;
let mut definitions = Map::new();
Ok(())
}
fn build_schema_bundle(schemas: Vec<GeneratedSchema>) -> Result<Value> {
const SPECIAL_DEFINITIONS: &[&str] = &[
"ClientNotification",
"ClientRequest",
"EventMsg",
"FileChange",
"InputItem",
"ParsedCommand",
"SandboxPolicy",
"ServerNotification",
"ServerRequest",
];
const IGNORED_DEFINITIONS: &[&str] = &["Option<()>"];
for (name, schema) in bundle {
let mut schema_value = serde_json::to_value(schema)?;
annotate_schema(&mut schema_value, Some(name.as_str()));
let namespaced_types = collect_namespaced_types(&schemas);
let mut definitions = Map::new();
if let Value::Object(ref mut obj) = schema_value
for schema in schemas {
let GeneratedSchema {
namespace,
logical_name,
mut value,
in_v1_dir,
} = schema;
if IGNORED_DEFINITIONS.contains(&logical_name.as_str()) {
continue;
}
if let Some(ref ns) = namespace {
rewrite_refs_to_namespace(&mut value, ns);
}
let mut forced_namespace_refs: Vec<(String, String)> = Vec::new();
if let Value::Object(ref mut obj) = value
&& let Some(defs) = obj.remove("definitions")
&& let Value::Object(defs_obj) = defs
{
for (def_name, mut def_schema) in defs_obj {
if !SPECIAL_DEFINITIONS.contains(&def_name.as_str()) {
annotate_schema(&mut def_schema, Some(def_name.as_str()));
if IGNORED_DEFINITIONS.contains(&def_name.as_str()) {
continue;
}
if SPECIAL_DEFINITIONS.contains(&def_name.as_str()) {
continue;
}
annotate_schema(&mut def_schema, Some(def_name.as_str()));
let target_namespace = match namespace {
Some(ref ns) => Some(ns.clone()),
None => namespace_for_definition(&def_name, &namespaced_types)
.cloned()
.filter(|_| !in_v1_dir),
};
if let Some(ref ns) = target_namespace {
if namespace.as_deref() == Some(ns.as_str()) {
rewrite_refs_to_namespace(&mut def_schema, ns);
insert_into_namespace(&mut definitions, ns, def_name.clone(), def_schema)?;
} else if !forced_namespace_refs
.iter()
.any(|(name, existing_ns)| name == &def_name && existing_ns == ns)
{
forced_namespace_refs.push((def_name.clone(), ns.clone()));
}
} else {
definitions.insert(def_name, def_schema);
}
}
}
definitions.insert(name, schema_value);
for (name, ns) in forced_namespace_refs {
rewrite_named_ref_to_namespace(&mut value, &ns, &name);
}
if let Some(ref ns) = namespace {
insert_into_namespace(&mut definitions, ns, logical_name.clone(), value)?;
} else {
definitions.insert(logical_name, value);
}
}
let mut root = Map::new();
@@ -206,15 +263,28 @@ pub fn generate_json(out_dir: &Path) -> Result<()> {
root.insert("type".to_string(), Value::String("object".into()));
root.insert("definitions".to_string(), Value::Object(definitions));
write_pretty_json(
out_dir.join("codex_app_server_protocol.schemas.json"),
&Value::Object(root),
)?;
Ok(())
Ok(Value::Object(root))
}
fn write_json_schema_with_return<T>(out_dir: &Path, name: &str) -> Result<RootSchema>
fn insert_into_namespace(
definitions: &mut Map<String, Value>,
namespace: &str,
name: String,
schema: Value,
) -> Result<()> {
let entry = definitions
.entry(namespace.to_string())
.or_insert_with(|| Value::Object(Map::new()));
match entry {
Value::Object(map) => {
map.insert(name, schema);
Ok(())
}
_ => Err(anyhow!("expected namespace {namespace} to be an object")),
}
}
fn write_json_schema_with_return<T>(out_dir: &Path, name: &str) -> Result<GeneratedSchema>
where
T: JsonSchema,
{
@@ -222,17 +292,37 @@ where
let schema = schema_for!(T);
let mut schema_value = serde_json::to_value(schema)?;
annotate_schema(&mut schema_value, Some(file_stem));
write_pretty_json(out_dir.join(format!("{file_stem}.json")), &schema_value)
// If the name looks like a namespaced path (e.g., "v2::Type"), mirror
// the TypeScript layout and write to out_dir/v2/Type.json. Otherwise
// write alongside the legacy files.
let (raw_namespace, logical_name) = split_namespace(file_stem);
let out_path = if let Some(ns) = raw_namespace {
let dir = out_dir.join(ns);
ensure_dir(&dir)?;
dir.join(format!("{logical_name}.json"))
} else {
out_dir.join(format!("{file_stem}.json"))
};
write_pretty_json(out_path, &schema_value)
.with_context(|| format!("Failed to write JSON schema for {file_stem}"))?;
let annotated_schema = serde_json::from_value(schema_value)?;
Ok(annotated_schema)
let namespace = match raw_namespace {
Some("v1") | None => None,
Some(ns) => Some(ns.to_string()),
};
Ok(GeneratedSchema {
in_v1_dir: raw_namespace == Some("v1"),
namespace,
logical_name: logical_name.to_string(),
value: schema_value,
})
}
pub(crate) fn write_json_schema<T>(out_dir: &Path, name: &str) -> Result<()>
pub(crate) fn write_json_schema<T>(out_dir: &Path, name: &str) -> Result<GeneratedSchema>
where
T: JsonSchema,
{
write_json_schema_with_return::<T>(out_dir, name).map(|_| ())
write_json_schema_with_return::<T>(out_dir, name)
}
fn write_pretty_json(path: PathBuf, value: &impl Serialize) -> Result<()> {
@@ -241,13 +331,73 @@ fn write_pretty_json(path: PathBuf, value: &impl Serialize) -> Result<()> {
fs::write(&path, json).with_context(|| format!("Failed to write {}", path.display()))?;
Ok(())
}
fn type_basename(type_path: &str) -> String {
type_path
.rsplit_once("::")
.map(|(_, name)| name)
.unwrap_or(type_path)
.trim()
.to_string()
/// Split a fully-qualified type name like "v2::Type" into its namespace and logical name.
fn split_namespace(name: &str) -> (Option<&str>, &str) {
name.split_once("::")
.map_or((None, name), |(ns, rest)| (Some(ns), rest))
}
/// Recursively rewrite $ref values that point at "#/definitions/..." so that
/// they point to a namespaced location under the bundle.
fn rewrite_refs_to_namespace(value: &mut Value, ns: &str) {
match value {
Value::Object(obj) => {
if let Some(Value::String(r)) = obj.get_mut("$ref")
&& let Some(suffix) = r.strip_prefix("#/definitions/")
{
let prefix = format!("{ns}/");
if !suffix.starts_with(&prefix) {
*r = format!("#/definitions/{ns}/{suffix}");
}
}
for v in obj.values_mut() {
rewrite_refs_to_namespace(v, ns);
}
}
Value::Array(items) => {
for v in items.iter_mut() {
rewrite_refs_to_namespace(v, ns);
}
}
_ => {}
}
}
fn collect_namespaced_types(schemas: &[GeneratedSchema]) -> HashMap<String, String> {
let mut types = HashMap::new();
for schema in schemas {
if let Some(ns) = schema.namespace() {
types
.entry(schema.logical_name().to_string())
.or_insert_with(|| ns.to_string());
if let Some(Value::Object(defs)) = schema.value().get("definitions") {
for key in defs.keys() {
types.entry(key.clone()).or_insert_with(|| ns.to_string());
}
}
if let Some(Value::Object(defs)) = schema.value().get("$defs") {
for key in defs.keys() {
types.entry(key.clone()).or_insert_with(|| ns.to_string());
}
}
}
}
types
}
fn namespace_for_definition<'a>(
name: &str,
types: &'a HashMap<String, String>,
) -> Option<&'a String> {
if let Some(ns) = types.get(name) {
return Some(ns);
}
let trimmed = name.trim_end_matches(|c: char| c.is_ascii_digit());
if trimmed != name {
return types.get(trimmed);
}
None
}
fn variant_definition_name(base: &str, variant: &Value) -> Option<String> {
@@ -269,14 +419,6 @@ fn variant_definition_name(base: &str, variant: &Value) -> Option<String> {
});
}
if let Some(mode_literal) = literal_from_property(props, "mode") {
let pascal = to_pascal_case(mode_literal);
return Some(match base {
"SandboxPolicy" => format!("{pascal}SandboxPolicy"),
_ => format!("{pascal}{base}"),
});
}
if props.len() == 1
&& let Some(key) = props.keys().next()
{
@@ -467,6 +609,33 @@ fn ensure_dir(dir: &Path) -> Result<()> {
.with_context(|| format!("Failed to create output directory {}", dir.display()))
}
fn rewrite_named_ref_to_namespace(value: &mut Value, ns: &str, name: &str) {
let direct = format!("#/definitions/{name}");
let prefixed = format!("{direct}/");
let replacement = format!("#/definitions/{ns}/{name}");
let replacement_prefixed = format!("{replacement}/");
match value {
Value::Object(obj) => {
if let Some(Value::String(reference)) = obj.get_mut("$ref") {
if reference == &direct {
*reference = replacement;
} else if let Some(rest) = reference.strip_prefix(&prefixed) {
*reference = format!("{replacement_prefixed}{rest}");
}
}
for child in obj.values_mut() {
rewrite_named_ref_to_namespace(child, ns, name);
}
}
Value::Array(items) => {
for child in items {
rewrite_named_ref_to_namespace(child, ns, name);
}
}
_ => {}
}
}
fn prepend_header_if_missing(path: &Path) -> Result<()> {
let mut content = String::new();
{
@@ -504,6 +673,28 @@ fn ts_files_in(dir: &Path) -> Result<Vec<PathBuf>> {
Ok(files)
}
fn ts_files_in_recursive(dir: &Path) -> Result<Vec<PathBuf>> {
let mut files = Vec::new();
let mut stack = vec![dir.to_path_buf()];
while let Some(d) = stack.pop() {
for entry in
fs::read_dir(&d).with_context(|| format!("Failed to read dir {}", d.display()))?
{
let entry = entry?;
let path = entry.path();
if path.is_dir() {
stack.push(path);
} else if path.is_file() && path.extension() == Some(OsStr::new("ts")) {
files.push(path);
}
}
}
files.sort();
Ok(files)
}
/// Generate an index.ts file that re-exports all generated types.
/// This allows consumers to import all types from a single file.
fn generate_index_ts(out_dir: &Path) -> Result<PathBuf> {
let mut entries: Vec<String> = Vec::new();
let mut stems: Vec<String> = ts_files_in(out_dir)?
@@ -520,6 +711,14 @@ fn generate_index_ts(out_dir: &Path) -> Result<PathBuf> {
entries.push(format!("export type {{ {name} }} from \"./{name}\";\n"));
}
// If this is the root out_dir and a ./v2 folder exists with TS files,
// expose it as a namespace to avoid symbol collisions at the root.
let v2_dir = out_dir.join("v2");
let has_v2_ts = ts_files_in(&v2_dir).map(|v| !v.is_empty()).unwrap_or(false);
if has_v2_ts {
entries.push("export * as v2 from \"./v2\";\n".to_string());
}
let mut content =
String::with_capacity(HEADER.len() + entries.iter().map(String::len).sum::<usize>());
content.push_str(HEADER);
@@ -546,6 +745,7 @@ mod tests {
#[test]
fn generated_ts_has_no_optional_nullable_fields() -> Result<()> {
// Assert that there are no types of the form "?: T | null" in the generated TS files.
let output_dir = std::env::temp_dir().join(format!("codex_ts_types_{}", Uuid::now_v7()));
fs::create_dir(&output_dir)?;
@@ -559,7 +759,13 @@ mod tests {
let _guard = TempDirGuard(output_dir.clone());
generate_ts(&output_dir, None)?;
// Avoid doing more work than necessary to keep the test from timing out.
let options = GenerateTsOptions {
generate_indices: false,
ensure_headers: false,
run_prettier: false,
};
generate_ts_with_options(&output_dir, None, options)?;
let mut undefined_offenders = Vec::new();
let mut optional_nullable_offenders = BTreeSet::new();

View File

@@ -7,5 +7,6 @@ pub use export::generate_ts;
pub use export::generate_types;
pub use jsonrpc_lite::*;
pub use protocol::common::*;
pub use protocol::thread_history::*;
pub use protocol::v1::*;
pub use protocol::v2::*;

View File

@@ -1,18 +1,12 @@
use std::collections::HashMap;
use std::path::PathBuf;
use std::path::Path;
use crate::JSONRPCNotification;
use crate::JSONRPCRequest;
use crate::RequestId;
use crate::export::GeneratedSchema;
use crate::export::write_json_schema;
use crate::protocol::v1;
use crate::protocol::v2;
use codex_protocol::ConversationId;
use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::protocol::FileChange;
use codex_protocol::protocol::RateLimitSnapshot;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::SandboxCommandAssessment;
use paste::paste;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
@@ -44,7 +38,7 @@ macro_rules! client_request_definitions {
(
$(
$(#[$variant_meta:meta])*
$variant:ident {
$variant:ident $(=> $wire:literal)? {
params: $(#[$params_meta:meta])* $params:ty,
response: $response:ty,
}
@@ -56,6 +50,7 @@ macro_rules! client_request_definitions {
pub enum ClientRequest {
$(
$(#[$variant_meta])*
$(#[serde(rename = $wire)] #[ts(rename = $wire)])?
$variant {
#[serde(rename = "id")]
request_id: RequestId,
@@ -74,66 +69,120 @@ macro_rules! client_request_definitions {
Ok(())
}
#[allow(clippy::vec_init_then_push)]
pub fn export_client_response_schemas(
out_dir: &::std::path::Path,
) -> ::anyhow::Result<()> {
) -> ::anyhow::Result<Vec<GeneratedSchema>> {
let mut schemas = Vec::new();
$(
crate::export::write_json_schema::<$response>(out_dir, stringify!($response))?;
schemas.push(write_json_schema::<$response>(out_dir, stringify!($response))?);
)*
Ok(())
Ok(schemas)
}
#[allow(clippy::vec_init_then_push)]
pub fn export_client_param_schemas(
out_dir: &::std::path::Path,
) -> ::anyhow::Result<Vec<GeneratedSchema>> {
let mut schemas = Vec::new();
$(
schemas.push(write_json_schema::<$params>(out_dir, stringify!($params))?);
)*
Ok(schemas)
}
};
}
client_request_definitions! {
/// NEW APIs
#[serde(rename = "model/list")]
#[ts(rename = "model/list")]
ListModels {
params: v2::ListModelsParams,
response: v2::ListModelsResponse,
},
#[serde(rename = "account/login")]
#[ts(rename = "account/login")]
LoginAccount {
params: v2::LoginAccountParams,
response: v2::LoginAccountResponse,
},
#[serde(rename = "account/logout")]
#[ts(rename = "account/logout")]
LogoutAccount {
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
response: v2::LogoutAccountResponse,
},
#[serde(rename = "account/rateLimits/read")]
#[ts(rename = "account/rateLimits/read")]
GetAccountRateLimits {
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
response: v2::GetAccountRateLimitsResponse,
},
#[serde(rename = "feedback/upload")]
#[ts(rename = "feedback/upload")]
UploadFeedback {
params: v2::UploadFeedbackParams,
response: v2::UploadFeedbackResponse,
},
#[serde(rename = "account/read")]
#[ts(rename = "account/read")]
GetAccount {
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
response: v2::GetAccountResponse,
},
/// DEPRECATED APIs below
Initialize {
params: v1::InitializeParams,
response: v1::InitializeResponse,
},
/// NEW APIs
// Thread lifecycle
ThreadStart => "thread/start" {
params: v2::ThreadStartParams,
response: v2::ThreadStartResponse,
},
ThreadResume => "thread/resume" {
params: v2::ThreadResumeParams,
response: v2::ThreadResumeResponse,
},
ThreadArchive => "thread/archive" {
params: v2::ThreadArchiveParams,
response: v2::ThreadArchiveResponse,
},
ThreadList => "thread/list" {
params: v2::ThreadListParams,
response: v2::ThreadListResponse,
},
ThreadCompact => "thread/compact" {
params: v2::ThreadCompactParams,
response: v2::ThreadCompactResponse,
},
TurnStart => "turn/start" {
params: v2::TurnStartParams,
response: v2::TurnStartResponse,
},
TurnInterrupt => "turn/interrupt" {
params: v2::TurnInterruptParams,
response: v2::TurnInterruptResponse,
},
ReviewStart => "review/start" {
params: v2::ReviewStartParams,
response: v2::ReviewStartResponse,
},
ModelList => "model/list" {
params: v2::ModelListParams,
response: v2::ModelListResponse,
},
LoginAccount => "account/login/start" {
params: v2::LoginAccountParams,
response: v2::LoginAccountResponse,
},
CancelLoginAccount => "account/login/cancel" {
params: v2::CancelLoginAccountParams,
response: v2::CancelLoginAccountResponse,
},
LogoutAccount => "account/logout" {
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
response: v2::LogoutAccountResponse,
},
GetAccountRateLimits => "account/rateLimits/read" {
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
response: v2::GetAccountRateLimitsResponse,
},
FeedbackUpload => "feedback/upload" {
params: v2::FeedbackUploadParams,
response: v2::FeedbackUploadResponse,
},
ConfigRead => "config/read" {
params: v2::ConfigReadParams,
response: v2::ConfigReadResponse,
},
ConfigValueWrite => "config/value/write" {
params: v2::ConfigValueWriteParams,
response: v2::ConfigWriteResponse,
},
ConfigBatchWrite => "config/batchWrite" {
params: v2::ConfigBatchWriteParams,
response: v2::ConfigWriteResponse,
},
GetAccount => "account/read" {
params: v2::GetAccountParams,
response: v2::GetAccountResponse,
},
/// DEPRECATED APIs below
NewConversation {
params: v1::NewConversationParams,
response: v1::NewConversationResponse,
@@ -188,6 +237,7 @@ client_request_definitions! {
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
response: v1::LoginChatGptResponse,
},
// DEPRECATED in favor of CancelLoginAccount
CancelLoginChatGpt {
params: v1::CancelLoginChatGptParams,
response: v1::CancelLoginChatGptResponse,
@@ -196,6 +246,7 @@ client_request_definitions! {
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
response: v1::LogoutChatGptResponse,
},
/// DEPRECATED in favor of GetAccount
GetAuthStatus {
params: v1::GetAuthStatusParams,
response: v1::GetAuthStatusResponse,
@@ -235,34 +286,36 @@ macro_rules! server_request_definitions {
(
$(
$(#[$variant_meta:meta])*
$variant:ident
$variant:ident $(=> $wire:literal)? {
params: $params:ty,
response: $response:ty,
}
),* $(,)?
) => {
paste! {
/// Request initiated from the server and sent to the client.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(tag = "method", rename_all = "camelCase")]
pub enum ServerRequest {
$(
$(#[$variant_meta])*
$variant {
#[serde(rename = "id")]
request_id: RequestId,
params: [<$variant Params>],
},
)*
}
/// Request initiated from the server and sent to the client.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(tag = "method", rename_all = "camelCase")]
pub enum ServerRequest {
$(
$(#[$variant_meta])*
$(#[serde(rename = $wire)] #[ts(rename = $wire)])?
$variant {
#[serde(rename = "id")]
request_id: RequestId,
params: $params,
},
)*
}
#[derive(Debug, Clone, PartialEq, JsonSchema)]
pub enum ServerRequestPayload {
$( $variant([<$variant Params>]), )*
}
#[derive(Debug, Clone, PartialEq, JsonSchema)]
pub enum ServerRequestPayload {
$( $variant($params), )*
}
impl ServerRequestPayload {
pub fn request_with_id(self, request_id: RequestId) -> ServerRequest {
match self {
$(Self::$variant(params) => ServerRequest::$variant { request_id, params },)*
}
impl ServerRequestPayload {
pub fn request_with_id(self, request_id: RequestId) -> ServerRequest {
match self {
$(Self::$variant(params) => ServerRequest::$variant { request_id, params },)*
}
}
}
@@ -270,19 +323,113 @@ macro_rules! server_request_definitions {
pub fn export_server_responses(
out_dir: &::std::path::Path,
) -> ::std::result::Result<(), ::ts_rs::ExportError> {
paste! {
$(<[<$variant Response>] as ::ts_rs::TS>::export_all_to(out_dir)?;)*
}
$(
<$response as ::ts_rs::TS>::export_all_to(out_dir)?;
)*
Ok(())
}
#[allow(clippy::vec_init_then_push)]
pub fn export_server_response_schemas(
out_dir: &::std::path::Path,
) -> ::anyhow::Result<()> {
paste! {
$(crate::export::write_json_schema::<[<$variant Response>]>(out_dir, stringify!([<$variant Response>]))?;)*
out_dir: &Path,
) -> ::anyhow::Result<Vec<GeneratedSchema>> {
let mut schemas = Vec::new();
$(
schemas.push(crate::export::write_json_schema::<$response>(
out_dir,
concat!(stringify!($variant), "Response"),
)?);
)*
Ok(schemas)
}
#[allow(clippy::vec_init_then_push)]
pub fn export_server_param_schemas(
out_dir: &Path,
) -> ::anyhow::Result<Vec<GeneratedSchema>> {
let mut schemas = Vec::new();
$(
schemas.push(crate::export::write_json_schema::<$params>(
out_dir,
concat!(stringify!($variant), "Params"),
)?);
)*
Ok(schemas)
}
};
}
/// Generates `ServerNotification` enum and helpers, including a JSON Schema
/// exporter for each notification.
macro_rules! server_notification_definitions {
(
$(
$(#[$variant_meta:meta])*
$variant:ident $(=> $wire:literal)? ( $payload:ty )
),* $(,)?
) => {
/// Notification sent from the server to the client.
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS, Display)]
#[serde(tag = "method", content = "params", rename_all = "camelCase")]
#[strum(serialize_all = "camelCase")]
pub enum ServerNotification {
$(
$(#[$variant_meta])*
$(#[serde(rename = $wire)] #[ts(rename = $wire)] #[strum(serialize = $wire)])?
$variant($payload),
)*
}
impl ServerNotification {
pub fn to_params(self) -> Result<serde_json::Value, serde_json::Error> {
match self {
$(Self::$variant(params) => serde_json::to_value(params),)*
}
}
Ok(())
}
impl TryFrom<JSONRPCNotification> for ServerNotification {
type Error = serde_json::Error;
fn try_from(value: JSONRPCNotification) -> Result<Self, serde_json::Error> {
serde_json::from_value(serde_json::to_value(value)?)
}
}
#[allow(clippy::vec_init_then_push)]
pub fn export_server_notification_schemas(
out_dir: &::std::path::Path,
) -> ::anyhow::Result<Vec<GeneratedSchema>> {
let mut schemas = Vec::new();
$(schemas.push(crate::export::write_json_schema::<$payload>(out_dir, stringify!($payload))?);)*
Ok(schemas)
}
};
}
/// Notifications sent from the client to the server.
macro_rules! client_notification_definitions {
(
$(
$(#[$variant_meta:meta])*
$variant:ident $( ( $payload:ty ) )?
),* $(,)?
) => {
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS, Display)]
#[serde(tag = "method", content = "params", rename_all = "camelCase")]
#[strum(serialize_all = "camelCase")]
pub enum ClientNotification {
$(
$(#[$variant_meta])*
$variant $( ( $payload ) )?,
)*
}
pub fn export_client_notification_schemas(
_out_dir: &::std::path::Path,
) -> ::anyhow::Result<Vec<GeneratedSchema>> {
let schemas = Vec::new();
$( $(schemas.push(crate::export::write_json_schema::<$payload>(_out_dir, stringify!($payload))?);)? )*
Ok(schemas)
}
};
}
@@ -296,49 +443,34 @@ impl TryFrom<JSONRPCRequest> for ServerRequest {
}
server_request_definitions! {
/// NEW APIs
/// Sent when approval is requested for a specific command execution.
/// This request is used for Turns started via turn/start.
CommandExecutionRequestApproval => "item/commandExecution/requestApproval" {
params: v2::CommandExecutionRequestApprovalParams,
response: v2::CommandExecutionRequestApprovalResponse,
},
/// Sent when approval is requested for a specific file change.
/// This request is used for Turns started via turn/start.
FileChangeRequestApproval => "item/fileChange/requestApproval" {
params: v2::FileChangeRequestApprovalParams,
response: v2::FileChangeRequestApprovalResponse,
},
/// DEPRECATED APIs below
/// Request to approve a patch.
ApplyPatchApproval,
/// This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage).
ApplyPatchApproval {
params: v1::ApplyPatchApprovalParams,
response: v1::ApplyPatchApprovalResponse,
},
/// Request to exec a command.
ExecCommandApproval,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
pub struct ApplyPatchApprovalParams {
pub conversation_id: ConversationId,
/// Use to correlate this with [codex_core::protocol::PatchApplyBeginEvent]
/// and [codex_core::protocol::PatchApplyEndEvent].
pub call_id: String,
pub file_changes: HashMap<PathBuf, FileChange>,
/// Optional explanatory reason (e.g. request for extra write access).
pub reason: Option<String>,
/// When set, the agent is asking the user to allow writes under this root
/// for the remainder of the session (unclear if this is honored today).
pub grant_root: Option<PathBuf>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
pub struct ExecCommandApprovalParams {
pub conversation_id: ConversationId,
/// Use to correlate this with [codex_core::protocol::ExecCommandBeginEvent]
/// and [codex_core::protocol::ExecCommandEndEvent].
pub call_id: String,
pub command: Vec<String>,
pub cwd: PathBuf,
pub reason: Option<String>,
pub risk: Option<SandboxCommandAssessment>,
pub parsed_cmd: Vec<ParsedCommand>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
pub struct ExecCommandApprovalResponse {
pub decision: ReviewDecision,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
pub struct ApplyPatchApprovalResponse {
pub decision: ReviewDecision,
/// This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage).
ExecCommandApproval {
params: v1::ExecCommandApprovalParams,
response: v1::ExecCommandApprovalResponse,
},
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -366,52 +498,45 @@ pub struct FuzzyFileSearchResponse {
pub files: Vec<FuzzyFileSearchResult>,
}
/// Notification sent from the server to the client.
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS, Display)]
#[serde(tag = "method", content = "params", rename_all = "camelCase")]
#[strum(serialize_all = "camelCase")]
pub enum ServerNotification {
server_notification_definitions! {
/// NEW NOTIFICATIONS
#[serde(rename = "account/rateLimits/updated")]
#[ts(rename = "account/rateLimits/updated")]
#[strum(serialize = "account/rateLimits/updated")]
AccountRateLimitsUpdated(RateLimitSnapshot),
Error => "error" (v2::ErrorNotification),
ThreadStarted => "thread/started" (v2::ThreadStartedNotification),
ThreadTokenUsageUpdated => "thread/tokenUsage/updated" (v2::ThreadTokenUsageUpdatedNotification),
TurnStarted => "turn/started" (v2::TurnStartedNotification),
TurnCompleted => "turn/completed" (v2::TurnCompletedNotification),
TurnDiffUpdated => "turn/diff/updated" (v2::TurnDiffUpdatedNotification),
TurnPlanUpdated => "turn/plan/updated" (v2::TurnPlanUpdatedNotification),
ItemStarted => "item/started" (v2::ItemStartedNotification),
ItemCompleted => "item/completed" (v2::ItemCompletedNotification),
AgentMessageDelta => "item/agentMessage/delta" (v2::AgentMessageDeltaNotification),
CommandExecutionOutputDelta => "item/commandExecution/outputDelta" (v2::CommandExecutionOutputDeltaNotification),
FileChangeOutputDelta => "item/fileChange/outputDelta" (v2::FileChangeOutputDeltaNotification),
McpToolCallProgress => "item/mcpToolCall/progress" (v2::McpToolCallProgressNotification),
AccountUpdated => "account/updated" (v2::AccountUpdatedNotification),
AccountRateLimitsUpdated => "account/rateLimits/updated" (v2::AccountRateLimitsUpdatedNotification),
ReasoningSummaryTextDelta => "item/reasoning/summaryTextDelta" (v2::ReasoningSummaryTextDeltaNotification),
ReasoningSummaryPartAdded => "item/reasoning/summaryPartAdded" (v2::ReasoningSummaryPartAddedNotification),
ReasoningTextDelta => "item/reasoning/textDelta" (v2::ReasoningTextDeltaNotification),
ContextCompacted => "thread/compacted" (v2::ContextCompactedNotification),
/// Notifies the user of world-writable directories on Windows, which cannot be protected by the sandbox.
WindowsWorldWritableWarning => "windows/worldWritableWarning" (v2::WindowsWorldWritableWarningNotification),
#[serde(rename = "account/login/completed")]
#[ts(rename = "account/login/completed")]
#[strum(serialize = "account/login/completed")]
AccountLoginCompleted(v2::AccountLoginCompletedNotification),
/// DEPRECATED NOTIFICATIONS below
/// Authentication status changed
AuthStatusChange(v1::AuthStatusChangeNotification),
/// ChatGPT login flow completed
/// Deprecated: use `account/login/completed` instead.
LoginChatGptComplete(v1::LoginChatGptCompleteNotification),
/// The special session configured event for a new or resumed conversation.
SessionConfigured(v1::SessionConfiguredNotification),
}
impl ServerNotification {
pub fn to_params(self) -> Result<serde_json::Value, serde_json::Error> {
match self {
ServerNotification::AccountRateLimitsUpdated(params) => serde_json::to_value(params),
ServerNotification::AuthStatusChange(params) => serde_json::to_value(params),
ServerNotification::LoginChatGptComplete(params) => serde_json::to_value(params),
ServerNotification::SessionConfigured(params) => serde_json::to_value(params),
}
}
}
impl TryFrom<JSONRPCNotification> for ServerNotification {
type Error = serde_json::Error;
fn try_from(value: JSONRPCNotification) -> Result<Self, Self::Error> {
serde_json::from_value(serde_json::to_value(value)?)
}
}
/// Notification sent from the client to the server.
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS, Display)]
#[serde(tag = "method", content = "params", rename_all = "camelCase")]
#[strum(serialize_all = "camelCase")]
pub enum ClientNotification {
client_notification_definitions! {
Initialized,
}
@@ -419,17 +544,20 @@ pub enum ClientNotification {
mod tests {
use super::*;
use anyhow::Result;
use codex_protocol::ConversationId;
use codex_protocol::account::PlanType;
use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::protocol::AskForApproval;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::path::PathBuf;
#[test]
fn serialize_new_conversation() -> Result<()> {
let request = ClientRequest::NewConversation {
request_id: RequestId::Integer(42),
params: v1::NewConversationParams {
model: Some("gpt-5-codex".to_string()),
model: Some("gpt-5.1-codex-max".to_string()),
model_provider: None,
profile: None,
cwd: None,
@@ -447,7 +575,7 @@ mod tests {
"method": "newConversation",
"id": 42,
"params": {
"model": "gpt-5-codex",
"model": "gpt-5.1-codex-max",
"modelProvider": null,
"profile": null,
"cwd": null,
@@ -502,7 +630,7 @@ mod tests {
#[test]
fn serialize_server_request() -> Result<()> {
let conversation_id = ConversationId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?;
let params = ExecCommandApprovalParams {
let params = v1::ExecCommandApprovalParams {
conversation_id,
call_id: "call-42".to_string(),
command: vec!["echo".to_string(), "hello".to_string()],
@@ -571,7 +699,7 @@ mod tests {
};
assert_eq!(
json!({
"method": "account/login",
"method": "account/login/start",
"id": 2,
"params": {
"type": "apiKey",
@@ -587,11 +715,11 @@ mod tests {
fn serialize_account_login_chatgpt() -> Result<()> {
let request = ClientRequest::LoginAccount {
request_id: RequestId::Integer(3),
params: v2::LoginAccountParams::ChatGpt,
params: v2::LoginAccountParams::Chatgpt,
};
assert_eq!(
json!({
"method": "account/login",
"method": "account/login/start",
"id": 3,
"params": {
"type": "chatgpt"
@@ -622,12 +750,17 @@ mod tests {
fn serialize_get_account() -> Result<()> {
let request = ClientRequest::GetAccount {
request_id: RequestId::Integer(5),
params: None,
params: v2::GetAccountParams {
refresh_token: false,
},
};
assert_eq!(
json!({
"method": "account/read",
"id": 5,
"params": {
"refreshToken": false
}
}),
serde_json::to_value(&request)?,
);
@@ -636,19 +769,16 @@ mod tests {
#[test]
fn account_serializes_fields_in_camel_case() -> Result<()> {
let api_key = v2::Account::ApiKey {
api_key: "secret".to_string(),
};
let api_key = v2::Account::ApiKey {};
assert_eq!(
json!({
"type": "apiKey",
"apiKey": "secret",
}),
serde_json::to_value(&api_key)?,
);
let chatgpt = v2::Account::ChatGpt {
email: Some("user@example.com".to_string()),
let chatgpt = v2::Account::Chatgpt {
email: "user@example.com".to_string(),
plan_type: PlanType::Plus,
};
assert_eq!(
@@ -665,16 +795,16 @@ mod tests {
#[test]
fn serialize_list_models() -> Result<()> {
let request = ClientRequest::ListModels {
let request = ClientRequest::ModelList {
request_id: RequestId::Integer(6),
params: v2::ListModelsParams::default(),
params: v2::ModelListParams::default(),
};
assert_eq!(
json!({
"method": "model/list",
"id": 6,
"params": {
"pageSize": null,
"limit": null,
"cursor": null
}
}),

View File

@@ -2,5 +2,6 @@
// Exposes protocol pieces used by `lib.rs` via `pub use protocol::common::*;`.
pub mod common;
pub mod thread_history;
pub mod v1;
pub mod v2;

View File

@@ -0,0 +1,409 @@
use crate::protocol::v2::ThreadItem;
use crate::protocol::v2::Turn;
use crate::protocol::v2::TurnStatus;
use crate::protocol::v2::UserInput;
use codex_protocol::protocol::AgentReasoningEvent;
use codex_protocol::protocol::AgentReasoningRawContentEvent;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::TurnAbortedEvent;
use codex_protocol::protocol::UserMessageEvent;
/// Convert persisted [`EventMsg`] entries into a sequence of [`Turn`] values.
///
/// The purpose of this is to convert the EventMsgs persisted in a rollout file
/// into a sequence of Turns and ThreadItems, which allows the client to render
/// the historical messages when resuming a thread.
pub fn build_turns_from_event_msgs(events: &[EventMsg]) -> Vec<Turn> {
let mut builder = ThreadHistoryBuilder::new();
for event in events {
builder.handle_event(event);
}
builder.finish()
}
struct ThreadHistoryBuilder {
turns: Vec<Turn>,
current_turn: Option<PendingTurn>,
next_turn_index: i64,
next_item_index: i64,
}
impl ThreadHistoryBuilder {
fn new() -> Self {
Self {
turns: Vec::new(),
current_turn: None,
next_turn_index: 1,
next_item_index: 1,
}
}
fn finish(mut self) -> Vec<Turn> {
self.finish_current_turn();
self.turns
}
/// This function should handle all EventMsg variants that can be persisted in a rollout file.
/// See `should_persist_event_msg` in `codex-rs/core/rollout/policy.rs`.
fn handle_event(&mut self, event: &EventMsg) {
match event {
EventMsg::UserMessage(payload) => self.handle_user_message(payload),
EventMsg::AgentMessage(payload) => self.handle_agent_message(payload.message.clone()),
EventMsg::AgentReasoning(payload) => self.handle_agent_reasoning(payload),
EventMsg::AgentReasoningRawContent(payload) => {
self.handle_agent_reasoning_raw_content(payload)
}
EventMsg::TokenCount(_) => {}
EventMsg::EnteredReviewMode(_) => {}
EventMsg::ExitedReviewMode(_) => {}
EventMsg::UndoCompleted(_) => {}
EventMsg::TurnAborted(payload) => self.handle_turn_aborted(payload),
_ => {}
}
}
fn handle_user_message(&mut self, payload: &UserMessageEvent) {
self.finish_current_turn();
let mut turn = self.new_turn();
let id = self.next_item_id();
let content = self.build_user_inputs(payload);
turn.items.push(ThreadItem::UserMessage { id, content });
self.current_turn = Some(turn);
}
fn handle_agent_message(&mut self, text: String) {
if text.is_empty() {
return;
}
let id = self.next_item_id();
self.ensure_turn()
.items
.push(ThreadItem::AgentMessage { id, text });
}
fn handle_agent_reasoning(&mut self, payload: &AgentReasoningEvent) {
if payload.text.is_empty() {
return;
}
// If the last item is a reasoning item, add the new text to the summary.
if let Some(ThreadItem::Reasoning { summary, .. }) = self.ensure_turn().items.last_mut() {
summary.push(payload.text.clone());
return;
}
// Otherwise, create a new reasoning item.
let id = self.next_item_id();
self.ensure_turn().items.push(ThreadItem::Reasoning {
id,
summary: vec![payload.text.clone()],
content: Vec::new(),
});
}
fn handle_agent_reasoning_raw_content(&mut self, payload: &AgentReasoningRawContentEvent) {
if payload.text.is_empty() {
return;
}
// If the last item is a reasoning item, add the new text to the content.
if let Some(ThreadItem::Reasoning { content, .. }) = self.ensure_turn().items.last_mut() {
content.push(payload.text.clone());
return;
}
// Otherwise, create a new reasoning item.
let id = self.next_item_id();
self.ensure_turn().items.push(ThreadItem::Reasoning {
id,
summary: Vec::new(),
content: vec![payload.text.clone()],
});
}
fn handle_turn_aborted(&mut self, _payload: &TurnAbortedEvent) {
let Some(turn) = self.current_turn.as_mut() else {
return;
};
turn.status = TurnStatus::Interrupted;
}
fn finish_current_turn(&mut self) {
if let Some(turn) = self.current_turn.take() {
if turn.items.is_empty() {
return;
}
self.turns.push(turn.into());
}
}
fn new_turn(&mut self) -> PendingTurn {
PendingTurn {
id: self.next_turn_id(),
items: Vec::new(),
status: TurnStatus::Completed,
}
}
fn ensure_turn(&mut self) -> &mut PendingTurn {
if self.current_turn.is_none() {
let turn = self.new_turn();
return self.current_turn.insert(turn);
}
if let Some(turn) = self.current_turn.as_mut() {
return turn;
}
unreachable!("current turn must exist after initialization");
}
fn next_turn_id(&mut self) -> String {
let id = format!("turn-{}", self.next_turn_index);
self.next_turn_index += 1;
id
}
fn next_item_id(&mut self) -> String {
let id = format!("item-{}", self.next_item_index);
self.next_item_index += 1;
id
}
fn build_user_inputs(&self, payload: &UserMessageEvent) -> Vec<UserInput> {
let mut content = Vec::new();
if !payload.message.trim().is_empty() {
content.push(UserInput::Text {
text: payload.message.clone(),
});
}
if let Some(images) = &payload.images {
for image in images {
content.push(UserInput::Image { url: image.clone() });
}
}
content
}
}
struct PendingTurn {
id: String,
items: Vec<ThreadItem>,
status: TurnStatus,
}
impl From<PendingTurn> for Turn {
fn from(value: PendingTurn) -> Self {
Self {
id: value.id,
items: value.items,
status: value.status,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use codex_protocol::protocol::AgentMessageEvent;
use codex_protocol::protocol::AgentReasoningEvent;
use codex_protocol::protocol::AgentReasoningRawContentEvent;
use codex_protocol::protocol::TurnAbortReason;
use codex_protocol::protocol::TurnAbortedEvent;
use codex_protocol::protocol::UserMessageEvent;
use pretty_assertions::assert_eq;
#[test]
fn builds_multiple_turns_with_reasoning_items() {
let events = vec![
EventMsg::UserMessage(UserMessageEvent {
message: "First turn".into(),
images: Some(vec!["https://example.com/one.png".into()]),
}),
EventMsg::AgentMessage(AgentMessageEvent {
message: "Hi there".into(),
}),
EventMsg::AgentReasoning(AgentReasoningEvent {
text: "thinking".into(),
}),
EventMsg::AgentReasoningRawContent(AgentReasoningRawContentEvent {
text: "full reasoning".into(),
}),
EventMsg::UserMessage(UserMessageEvent {
message: "Second turn".into(),
images: None,
}),
EventMsg::AgentMessage(AgentMessageEvent {
message: "Reply two".into(),
}),
];
let turns = build_turns_from_event_msgs(&events);
assert_eq!(turns.len(), 2);
let first = &turns[0];
assert_eq!(first.id, "turn-1");
assert_eq!(first.status, TurnStatus::Completed);
assert_eq!(first.items.len(), 3);
assert_eq!(
first.items[0],
ThreadItem::UserMessage {
id: "item-1".into(),
content: vec![
UserInput::Text {
text: "First turn".into(),
},
UserInput::Image {
url: "https://example.com/one.png".into(),
}
],
}
);
assert_eq!(
first.items[1],
ThreadItem::AgentMessage {
id: "item-2".into(),
text: "Hi there".into(),
}
);
assert_eq!(
first.items[2],
ThreadItem::Reasoning {
id: "item-3".into(),
summary: vec!["thinking".into()],
content: vec!["full reasoning".into()],
}
);
let second = &turns[1];
assert_eq!(second.id, "turn-2");
assert_eq!(second.items.len(), 2);
assert_eq!(
second.items[0],
ThreadItem::UserMessage {
id: "item-4".into(),
content: vec![UserInput::Text {
text: "Second turn".into()
}],
}
);
assert_eq!(
second.items[1],
ThreadItem::AgentMessage {
id: "item-5".into(),
text: "Reply two".into(),
}
);
}
#[test]
fn splits_reasoning_when_interleaved() {
let events = vec![
EventMsg::UserMessage(UserMessageEvent {
message: "Turn start".into(),
images: None,
}),
EventMsg::AgentReasoning(AgentReasoningEvent {
text: "first summary".into(),
}),
EventMsg::AgentReasoningRawContent(AgentReasoningRawContentEvent {
text: "first content".into(),
}),
EventMsg::AgentMessage(AgentMessageEvent {
message: "interlude".into(),
}),
EventMsg::AgentReasoning(AgentReasoningEvent {
text: "second summary".into(),
}),
];
let turns = build_turns_from_event_msgs(&events);
assert_eq!(turns.len(), 1);
let turn = &turns[0];
assert_eq!(turn.items.len(), 4);
assert_eq!(
turn.items[1],
ThreadItem::Reasoning {
id: "item-2".into(),
summary: vec!["first summary".into()],
content: vec!["first content".into()],
}
);
assert_eq!(
turn.items[3],
ThreadItem::Reasoning {
id: "item-4".into(),
summary: vec!["second summary".into()],
content: Vec::new(),
}
);
}
#[test]
fn marks_turn_as_interrupted_when_aborted() {
let events = vec![
EventMsg::UserMessage(UserMessageEvent {
message: "Please do the thing".into(),
images: None,
}),
EventMsg::AgentMessage(AgentMessageEvent {
message: "Working...".into(),
}),
EventMsg::TurnAborted(TurnAbortedEvent {
reason: TurnAbortReason::Replaced,
}),
EventMsg::UserMessage(UserMessageEvent {
message: "Let's try again".into(),
images: None,
}),
EventMsg::AgentMessage(AgentMessageEvent {
message: "Second attempt complete.".into(),
}),
];
let turns = build_turns_from_event_msgs(&events);
assert_eq!(turns.len(), 2);
let first_turn = &turns[0];
assert_eq!(first_turn.status, TurnStatus::Interrupted);
assert_eq!(first_turn.items.len(), 2);
assert_eq!(
first_turn.items[0],
ThreadItem::UserMessage {
id: "item-1".into(),
content: vec![UserInput::Text {
text: "Please do the thing".into()
}],
}
);
assert_eq!(
first_turn.items[1],
ThreadItem::AgentMessage {
id: "item-2".into(),
text: "Working...".into(),
}
);
let second_turn = &turns[1];
assert_eq!(second_turn.status, TurnStatus::Completed);
assert_eq!(second_turn.items.len(), 2);
assert_eq!(
second_turn.items[0],
ThreadItem::UserMessage {
id: "item-3".into(),
content: vec![UserInput::Text {
text: "Let's try again".into()
}],
}
);
assert_eq!(
second_turn.items[1],
ThreadItem::AgentMessage {
id: "item-4".into(),
text: "Second attempt complete.".into(),
}
);
}
}

View File

@@ -8,9 +8,14 @@ use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::config_types::Verbosity;
use codex_protocol::models::ResponseItem;
use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::FileChange;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::SandboxCommandAssessment;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::TurnAbortReason;
use schemars::JsonSchema;
use serde::Deserialize;
@@ -113,6 +118,18 @@ pub struct ConversationSummary {
pub preview: String,
pub timestamp: Option<String>,
pub model_provider: String,
pub cwd: PathBuf,
pub cli_version: String,
pub source: SessionSource,
pub git_info: Option<ConversationGitInfo>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub struct ConversationGitInfo {
pub sha: Option<String>,
pub branch: Option<String>,
pub origin_url: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -178,6 +195,46 @@ pub struct GitDiffToRemoteResponse {
pub diff: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
pub struct ApplyPatchApprovalParams {
pub conversation_id: ConversationId,
/// Use to correlate this with [codex_core::protocol::PatchApplyBeginEvent]
/// and [codex_core::protocol::PatchApplyEndEvent].
pub call_id: String,
pub file_changes: HashMap<PathBuf, FileChange>,
/// Optional explanatory reason (e.g. request for extra write access).
pub reason: Option<String>,
/// When set, the agent is asking the user to allow writes under this root
/// for the remainder of the session (unclear if this is honored today).
pub grant_root: Option<PathBuf>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
pub struct ApplyPatchApprovalResponse {
pub decision: ReviewDecision,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
pub struct ExecCommandApprovalParams {
pub conversation_id: ConversationId,
/// Use to correlate this with [codex_core::protocol::ExecCommandBeginEvent]
/// and [codex_core::protocol::ExecCommandEndEvent].
pub call_id: String,
pub command: Vec<String>,
pub cwd: PathBuf,
pub reason: Option<String>,
pub risk: Option<SandboxCommandAssessment>,
pub parsed_cmd: Vec<ParsedCommand>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
pub struct ExecCommandApprovalResponse {
pub decision: ReviewDecision,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
pub struct CancelLoginChatGptParams {
@@ -374,10 +431,9 @@ pub enum InputItem {
LocalImage { path: PathBuf },
}
// Deprecated notifications (v1)
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
/// Deprecated in favor of AccountLoginCompletedNotification.
pub struct LoginChatGptCompleteNotification {
#[schemars(with = "String")]
pub login_id: Uuid,
@@ -400,6 +456,7 @@ pub struct SessionConfiguredNotification {
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
/// Deprecated notification. Use AccountUpdatedNotification instead.
pub struct AuthStatusChangeNotification {
pub auth_method: Option<AuthMode>,
}

File diff suppressed because it is too large Load Diff

1298
codex-rs/app-server-test-client/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
[package]
name = "codex-app-server-test-client"
version.workspace = true
edition.workspace = true
license.workspace = true
[lints]
workspace = true
[dependencies]
anyhow = { workspace = true }
clap = { workspace = true, features = ["derive", "env"] }
codex-app-server-protocol = { workspace = true }
codex-protocol = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
uuid = { workspace = true, features = ["v4"] }

View File

@@ -0,0 +1,2 @@
# App Server Test Client
Exercises simple `codex app-server` flows end-to-end, logging JSON-RPC messages sent between client and server to stdout.

View File

@@ -0,0 +1,860 @@
use std::collections::VecDeque;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Write;
use std::process::Child;
use std::process::ChildStdin;
use std::process::ChildStdout;
use std::process::Command;
use std::process::Stdio;
use std::thread;
use std::time::Duration;
use anyhow::Context;
use anyhow::Result;
use anyhow::bail;
use clap::Parser;
use clap::Subcommand;
use codex_app_server_protocol::AddConversationListenerParams;
use codex_app_server_protocol::AddConversationSubscriptionResponse;
use codex_app_server_protocol::ApprovalDecision;
use codex_app_server_protocol::AskForApproval;
use codex_app_server_protocol::ClientInfo;
use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::CommandExecutionRequestAcceptSettings;
use codex_app_server_protocol::CommandExecutionRequestApprovalParams;
use codex_app_server_protocol::CommandExecutionRequestApprovalResponse;
use codex_app_server_protocol::FileChangeRequestApprovalParams;
use codex_app_server_protocol::FileChangeRequestApprovalResponse;
use codex_app_server_protocol::GetAccountRateLimitsResponse;
use codex_app_server_protocol::InitializeParams;
use codex_app_server_protocol::InitializeResponse;
use codex_app_server_protocol::InputItem;
use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::JSONRPCRequest;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::LoginChatGptCompleteNotification;
use codex_app_server_protocol::LoginChatGptResponse;
use codex_app_server_protocol::NewConversationParams;
use codex_app_server_protocol::NewConversationResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::SandboxPolicy;
use codex_app_server_protocol::SendUserMessageParams;
use codex_app_server_protocol::SendUserMessageResponse;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerRequest;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
use codex_app_server_protocol::TurnStartParams;
use codex_app_server_protocol::TurnStartResponse;
use codex_app_server_protocol::TurnStatus;
use codex_app_server_protocol::UserInput as V2UserInput;
use codex_protocol::ConversationId;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg;
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::Value;
use uuid::Uuid;
/// Minimal launcher that initializes the Codex app-server and logs the handshake.
#[derive(Parser)]
#[command(author = "Codex", version, about = "Bootstrap Codex app-server", long_about = None)]
struct Cli {
/// Path to the `codex` CLI binary.
#[arg(long, env = "CODEX_BIN", default_value = "codex")]
codex_bin: String,
#[command(subcommand)]
command: CliCommand,
}
#[derive(Subcommand)]
enum CliCommand {
/// Send a user message through the Codex app-server.
SendMessage {
/// User message to send to Codex.
#[arg()]
user_message: String,
},
/// Send a user message through the app-server V2 thread/turn APIs.
SendMessageV2 {
/// User message to send to Codex.
#[arg()]
user_message: String,
},
/// Start a V2 turn that elicits an ExecCommand approval.
#[command(name = "trigger-cmd-approval")]
TriggerCmdApproval {
/// Optional prompt; defaults to a simple python command.
#[arg()]
user_message: Option<String>,
},
/// Start a V2 turn that elicits an ApplyPatch approval.
#[command(name = "trigger-patch-approval")]
TriggerPatchApproval {
/// Optional prompt; defaults to creating a file via apply_patch.
#[arg()]
user_message: Option<String>,
},
/// Start a V2 turn that should not elicit an ExecCommand approval.
#[command(name = "no-trigger-cmd-approval")]
NoTriggerCmdApproval,
/// Send two sequential V2 turns in the same thread to test follow-up behavior.
SendFollowUpV2 {
/// Initial user message for the first turn.
#[arg()]
first_message: String,
/// Follow-up user message for the second turn.
#[arg()]
follow_up_message: String,
},
/// Trigger the ChatGPT login flow and wait for completion.
TestLogin,
/// Fetch the current account rate limits from the Codex app-server.
GetAccountRateLimits,
}
fn main() -> Result<()> {
let Cli { codex_bin, command } = Cli::parse();
match command {
CliCommand::SendMessage { user_message } => send_message(codex_bin, user_message),
CliCommand::SendMessageV2 { user_message } => send_message_v2(codex_bin, user_message),
CliCommand::TriggerCmdApproval { user_message } => {
trigger_cmd_approval(codex_bin, user_message)
}
CliCommand::TriggerPatchApproval { user_message } => {
trigger_patch_approval(codex_bin, user_message)
}
CliCommand::NoTriggerCmdApproval => no_trigger_cmd_approval(codex_bin),
CliCommand::SendFollowUpV2 {
first_message,
follow_up_message,
} => send_follow_up_v2(codex_bin, first_message, follow_up_message),
CliCommand::TestLogin => test_login(codex_bin),
CliCommand::GetAccountRateLimits => get_account_rate_limits(codex_bin),
}
}
fn send_message(codex_bin: String, user_message: String) -> Result<()> {
let mut client = CodexClient::spawn(codex_bin)?;
let initialize = client.initialize()?;
println!("< initialize response: {initialize:?}");
let conversation = client.new_conversation()?;
println!("< newConversation response: {conversation:?}");
let subscription = client.add_conversation_listener(&conversation.conversation_id)?;
println!("< addConversationListener response: {subscription:?}");
let send_response = client.send_user_message(&conversation.conversation_id, &user_message)?;
println!("< sendUserMessage response: {send_response:?}");
client.stream_conversation(&conversation.conversation_id)?;
client.remove_conversation_listener(subscription.subscription_id)?;
Ok(())
}
fn send_message_v2(codex_bin: String, user_message: String) -> Result<()> {
send_message_v2_with_policies(codex_bin, user_message, None, None)
}
fn trigger_cmd_approval(codex_bin: String, user_message: Option<String>) -> Result<()> {
let default_prompt =
"Run `touch /tmp/should-trigger-approval` so I can confirm the file exists.";
let message = user_message.unwrap_or_else(|| default_prompt.to_string());
send_message_v2_with_policies(
codex_bin,
message,
Some(AskForApproval::OnRequest),
Some(SandboxPolicy::ReadOnly),
)
}
fn trigger_patch_approval(codex_bin: String, user_message: Option<String>) -> Result<()> {
let default_prompt =
"Create a file named APPROVAL_DEMO.txt containing a short hello message using apply_patch.";
let message = user_message.unwrap_or_else(|| default_prompt.to_string());
send_message_v2_with_policies(
codex_bin,
message,
Some(AskForApproval::OnRequest),
Some(SandboxPolicy::ReadOnly),
)
}
fn no_trigger_cmd_approval(codex_bin: String) -> Result<()> {
let prompt = "Run `touch should_not_trigger_approval.txt`";
send_message_v2_with_policies(codex_bin, prompt.to_string(), None, None)
}
fn send_message_v2_with_policies(
codex_bin: String,
user_message: String,
approval_policy: Option<AskForApproval>,
sandbox_policy: Option<SandboxPolicy>,
) -> Result<()> {
let mut client = CodexClient::spawn(codex_bin)?;
let initialize = client.initialize()?;
println!("< initialize response: {initialize:?}");
let thread_response = client.thread_start(ThreadStartParams::default())?;
println!("< thread/start response: {thread_response:?}");
let mut turn_params = TurnStartParams {
thread_id: thread_response.thread.id.clone(),
input: vec![V2UserInput::Text { text: user_message }],
..Default::default()
};
turn_params.approval_policy = approval_policy;
turn_params.sandbox_policy = sandbox_policy;
let turn_response = client.turn_start(turn_params)?;
println!("< turn/start response: {turn_response:?}");
client.stream_turn(&thread_response.thread.id, &turn_response.turn.id)?;
Ok(())
}
fn send_follow_up_v2(
codex_bin: String,
first_message: String,
follow_up_message: String,
) -> Result<()> {
let mut client = CodexClient::spawn(codex_bin)?;
let initialize = client.initialize()?;
println!("< initialize response: {initialize:?}");
let thread_response = client.thread_start(ThreadStartParams::default())?;
println!("< thread/start response: {thread_response:?}");
let first_turn_params = TurnStartParams {
thread_id: thread_response.thread.id.clone(),
input: vec![V2UserInput::Text {
text: first_message,
}],
..Default::default()
};
let first_turn_response = client.turn_start(first_turn_params)?;
println!("< turn/start response (initial): {first_turn_response:?}");
client.stream_turn(&thread_response.thread.id, &first_turn_response.turn.id)?;
let follow_up_params = TurnStartParams {
thread_id: thread_response.thread.id.clone(),
input: vec![V2UserInput::Text {
text: follow_up_message,
}],
..Default::default()
};
let follow_up_response = client.turn_start(follow_up_params)?;
println!("< turn/start response (follow-up): {follow_up_response:?}");
client.stream_turn(&thread_response.thread.id, &follow_up_response.turn.id)?;
Ok(())
}
fn test_login(codex_bin: String) -> Result<()> {
let mut client = CodexClient::spawn(codex_bin)?;
let initialize = client.initialize()?;
println!("< initialize response: {initialize:?}");
let login_response = client.login_chat_gpt()?;
println!("< loginChatGpt response: {login_response:?}");
println!(
"Open the following URL in your browser to continue:\n{}",
login_response.auth_url
);
let completion = client.wait_for_login_completion(&login_response.login_id)?;
println!("< loginChatGptComplete notification: {completion:?}");
if completion.success {
println!("Login succeeded.");
Ok(())
} else {
bail!(
"login failed: {}",
completion
.error
.as_deref()
.unwrap_or("unknown error from loginChatGptComplete")
);
}
}
fn get_account_rate_limits(codex_bin: String) -> Result<()> {
let mut client = CodexClient::spawn(codex_bin)?;
let initialize = client.initialize()?;
println!("< initialize response: {initialize:?}");
let response = client.get_account_rate_limits()?;
println!("< account/rateLimits/read response: {response:?}");
Ok(())
}
struct CodexClient {
child: Child,
stdin: Option<ChildStdin>,
stdout: BufReader<ChildStdout>,
pending_notifications: VecDeque<JSONRPCNotification>,
}
impl CodexClient {
fn spawn(codex_bin: String) -> Result<Self> {
let mut codex_app_server = Command::new(&codex_bin)
.arg("app-server")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.with_context(|| format!("failed to start `{codex_bin}` app-server"))?;
let stdin = codex_app_server
.stdin
.take()
.context("codex app-server stdin unavailable")?;
let stdout = codex_app_server
.stdout
.take()
.context("codex app-server stdout unavailable")?;
Ok(Self {
child: codex_app_server,
stdin: Some(stdin),
stdout: BufReader::new(stdout),
pending_notifications: VecDeque::new(),
})
}
fn initialize(&mut self) -> Result<InitializeResponse> {
let request_id = self.request_id();
let request = ClientRequest::Initialize {
request_id: request_id.clone(),
params: InitializeParams {
client_info: ClientInfo {
name: "codex-toy-app-server".to_string(),
title: Some("Codex Toy App Server".to_string()),
version: env!("CARGO_PKG_VERSION").to_string(),
},
},
};
self.send_request(request, request_id, "initialize")
}
fn new_conversation(&mut self) -> Result<NewConversationResponse> {
let request_id = self.request_id();
let request = ClientRequest::NewConversation {
request_id: request_id.clone(),
params: NewConversationParams::default(),
};
self.send_request(request, request_id, "newConversation")
}
fn add_conversation_listener(
&mut self,
conversation_id: &ConversationId,
) -> Result<AddConversationSubscriptionResponse> {
let request_id = self.request_id();
let request = ClientRequest::AddConversationListener {
request_id: request_id.clone(),
params: AddConversationListenerParams {
conversation_id: *conversation_id,
experimental_raw_events: false,
},
};
self.send_request(request, request_id, "addConversationListener")
}
fn remove_conversation_listener(&mut self, subscription_id: Uuid) -> Result<()> {
let request_id = self.request_id();
let request = ClientRequest::RemoveConversationListener {
request_id: request_id.clone(),
params: codex_app_server_protocol::RemoveConversationListenerParams { subscription_id },
};
self.send_request::<codex_app_server_protocol::RemoveConversationSubscriptionResponse>(
request,
request_id,
"removeConversationListener",
)?;
Ok(())
}
fn send_user_message(
&mut self,
conversation_id: &ConversationId,
message: &str,
) -> Result<SendUserMessageResponse> {
let request_id = self.request_id();
let request = ClientRequest::SendUserMessage {
request_id: request_id.clone(),
params: SendUserMessageParams {
conversation_id: *conversation_id,
items: vec![InputItem::Text {
text: message.to_string(),
}],
},
};
self.send_request(request, request_id, "sendUserMessage")
}
fn thread_start(&mut self, params: ThreadStartParams) -> Result<ThreadStartResponse> {
let request_id = self.request_id();
let request = ClientRequest::ThreadStart {
request_id: request_id.clone(),
params,
};
self.send_request(request, request_id, "thread/start")
}
fn turn_start(&mut self, params: TurnStartParams) -> Result<TurnStartResponse> {
let request_id = self.request_id();
let request = ClientRequest::TurnStart {
request_id: request_id.clone(),
params,
};
self.send_request(request, request_id, "turn/start")
}
fn login_chat_gpt(&mut self) -> Result<LoginChatGptResponse> {
let request_id = self.request_id();
let request = ClientRequest::LoginChatGpt {
request_id: request_id.clone(),
params: None,
};
self.send_request(request, request_id, "loginChatGpt")
}
fn get_account_rate_limits(&mut self) -> Result<GetAccountRateLimitsResponse> {
let request_id = self.request_id();
let request = ClientRequest::GetAccountRateLimits {
request_id: request_id.clone(),
params: None,
};
self.send_request(request, request_id, "account/rateLimits/read")
}
fn stream_conversation(&mut self, conversation_id: &ConversationId) -> Result<()> {
loop {
let notification = self.next_notification()?;
if !notification.method.starts_with("codex/event/") {
continue;
}
if let Some(event) = self.extract_event(notification, conversation_id)? {
match &event.msg {
EventMsg::AgentMessage(event) => {
println!("{}", event.message);
}
EventMsg::AgentMessageDelta(event) => {
print!("{}", event.delta);
std::io::stdout().flush().ok();
}
EventMsg::TaskComplete(event) => {
println!("\n[task complete: {event:?}]");
break;
}
EventMsg::TurnAborted(event) => {
println!("\n[turn aborted: {:?}]", event.reason);
break;
}
EventMsg::Error(event) => {
println!("[error] {event:?}");
}
_ => {
println!("[UNKNOWN EVENT] {:?}", event.msg);
}
}
}
}
Ok(())
}
fn wait_for_login_completion(
&mut self,
expected_login_id: &Uuid,
) -> Result<LoginChatGptCompleteNotification> {
loop {
let notification = self.next_notification()?;
if let Ok(server_notification) = ServerNotification::try_from(notification) {
match server_notification {
ServerNotification::LoginChatGptComplete(completion) => {
if &completion.login_id == expected_login_id {
return Ok(completion);
}
println!(
"[ignoring loginChatGptComplete for unexpected login_id: {}]",
completion.login_id
);
}
ServerNotification::AuthStatusChange(status) => {
println!("< authStatusChange notification: {status:?}");
}
ServerNotification::AccountRateLimitsUpdated(snapshot) => {
println!("< accountRateLimitsUpdated notification: {snapshot:?}");
}
ServerNotification::SessionConfigured(_) => {
// SessionConfigured notifications are unrelated to login; skip.
}
_ => {}
}
}
// Not a server notification (likely a conversation event); keep waiting.
}
}
fn stream_turn(&mut self, thread_id: &str, turn_id: &str) -> Result<()> {
loop {
let notification = self.next_notification()?;
let Ok(server_notification) = ServerNotification::try_from(notification) else {
continue;
};
match server_notification {
ServerNotification::ThreadStarted(payload) => {
if payload.thread.id == thread_id {
println!("< thread/started notification: {:?}", payload.thread);
}
}
ServerNotification::TurnStarted(payload) => {
if payload.turn.id == turn_id {
println!("< turn/started notification: {:?}", payload.turn.status);
}
}
ServerNotification::AgentMessageDelta(delta) => {
print!("{}", delta.delta);
std::io::stdout().flush().ok();
}
ServerNotification::CommandExecutionOutputDelta(delta) => {
print!("{}", delta.delta);
std::io::stdout().flush().ok();
}
ServerNotification::ItemStarted(payload) => {
println!("\n< item started: {:?}", payload.item);
}
ServerNotification::ItemCompleted(payload) => {
println!("< item completed: {:?}", payload.item);
}
ServerNotification::TurnCompleted(payload) => {
if payload.turn.id == turn_id {
println!("\n< turn/completed notification: {:?}", payload.turn.status);
if let TurnStatus::Failed { error } = &payload.turn.status {
println!("[turn error] {}", error.message);
}
break;
}
}
ServerNotification::McpToolCallProgress(payload) => {
println!("< MCP tool progress: {}", payload.message);
}
_ => {
println!("[UNKNOWN SERVER NOTIFICATION] {server_notification:?}");
}
}
}
Ok(())
}
fn extract_event(
&self,
notification: JSONRPCNotification,
conversation_id: &ConversationId,
) -> Result<Option<Event>> {
let params = notification
.params
.context("event notification missing params")?;
let mut map = match params {
Value::Object(map) => map,
other => bail!("unexpected params shape: {other:?}"),
};
let conversation_value = map
.remove("conversationId")
.context("event missing conversationId")?;
let notification_conversation: ConversationId = serde_json::from_value(conversation_value)
.context("conversationId was not a valid UUID")?;
if &notification_conversation != conversation_id {
return Ok(None);
}
let event_value = Value::Object(map);
let event: Event =
serde_json::from_value(event_value).context("failed to decode event payload")?;
Ok(Some(event))
}
fn send_request<T>(
&mut self,
request: ClientRequest,
request_id: RequestId,
method: &str,
) -> Result<T>
where
T: DeserializeOwned,
{
self.write_request(&request)?;
self.wait_for_response(request_id, method)
}
fn write_request(&mut self, request: &ClientRequest) -> Result<()> {
let request_json = serde_json::to_string(request)?;
let request_pretty = serde_json::to_string_pretty(request)?;
print_multiline_with_prefix("> ", &request_pretty);
if let Some(stdin) = self.stdin.as_mut() {
writeln!(stdin, "{request_json}")?;
stdin
.flush()
.context("failed to flush request to codex app-server")?;
} else {
bail!("codex app-server stdin closed");
}
Ok(())
}
fn wait_for_response<T>(&mut self, request_id: RequestId, method: &str) -> Result<T>
where
T: DeserializeOwned,
{
loop {
let message = self.read_jsonrpc_message()?;
match message {
JSONRPCMessage::Response(JSONRPCResponse { id, result }) => {
if id == request_id {
return serde_json::from_value(result)
.with_context(|| format!("{method} response missing payload"));
}
}
JSONRPCMessage::Error(err) => {
if err.id == request_id {
bail!("{method} failed: {err:?}");
}
}
JSONRPCMessage::Notification(notification) => {
self.pending_notifications.push_back(notification);
}
JSONRPCMessage::Request(request) => {
self.handle_server_request(request)?;
}
}
}
}
fn next_notification(&mut self) -> Result<JSONRPCNotification> {
if let Some(notification) = self.pending_notifications.pop_front() {
return Ok(notification);
}
loop {
let message = self.read_jsonrpc_message()?;
match message {
JSONRPCMessage::Notification(notification) => return Ok(notification),
JSONRPCMessage::Response(_) | JSONRPCMessage::Error(_) => {
// No outstanding requests, so ignore stray responses/errors for now.
continue;
}
JSONRPCMessage::Request(request) => {
self.handle_server_request(request)?;
}
}
}
}
fn read_jsonrpc_message(&mut self) -> Result<JSONRPCMessage> {
loop {
let mut response_line = String::new();
let bytes = self
.stdout
.read_line(&mut response_line)
.context("failed to read from codex app-server")?;
if bytes == 0 {
bail!("codex app-server closed stdout");
}
let trimmed = response_line.trim();
if trimmed.is_empty() {
continue;
}
let parsed: Value =
serde_json::from_str(trimmed).context("response was not valid JSON-RPC")?;
let pretty = serde_json::to_string_pretty(&parsed)?;
print_multiline_with_prefix("< ", &pretty);
let message: JSONRPCMessage = serde_json::from_value(parsed)
.context("response was not a valid JSON-RPC message")?;
return Ok(message);
}
}
fn request_id(&self) -> RequestId {
RequestId::String(Uuid::new_v4().to_string())
}
fn handle_server_request(&mut self, request: JSONRPCRequest) -> Result<()> {
let server_request = ServerRequest::try_from(request)
.context("failed to deserialize ServerRequest from JSONRPCRequest")?;
match server_request {
ServerRequest::CommandExecutionRequestApproval { request_id, params } => {
self.handle_command_execution_request_approval(request_id, params)?;
}
ServerRequest::FileChangeRequestApproval { request_id, params } => {
self.approve_file_change_request(request_id, params)?;
}
other => {
bail!("received unsupported server request: {other:?}");
}
}
Ok(())
}
fn handle_command_execution_request_approval(
&mut self,
request_id: RequestId,
params: CommandExecutionRequestApprovalParams,
) -> Result<()> {
let CommandExecutionRequestApprovalParams {
thread_id,
turn_id,
item_id,
reason,
risk,
} = params;
println!(
"\n< commandExecution approval requested for thread {thread_id}, turn {turn_id}, item {item_id}"
);
if let Some(reason) = reason.as_deref() {
println!("< reason: {reason}");
}
if let Some(risk) = risk.as_ref() {
println!("< risk assessment: {risk:?}");
}
let response = CommandExecutionRequestApprovalResponse {
decision: ApprovalDecision::Accept,
accept_settings: Some(CommandExecutionRequestAcceptSettings { for_session: false }),
};
self.send_server_request_response(request_id, &response)?;
println!("< approved commandExecution request for item {item_id}");
Ok(())
}
fn approve_file_change_request(
&mut self,
request_id: RequestId,
params: FileChangeRequestApprovalParams,
) -> Result<()> {
let FileChangeRequestApprovalParams {
thread_id,
turn_id,
item_id,
reason,
grant_root,
} = params;
println!(
"\n< fileChange approval requested for thread {thread_id}, turn {turn_id}, item {item_id}"
);
if let Some(reason) = reason.as_deref() {
println!("< reason: {reason}");
}
if let Some(grant_root) = grant_root.as_deref() {
println!("< grant root: {}", grant_root.display());
}
let response = FileChangeRequestApprovalResponse {
decision: ApprovalDecision::Accept,
};
self.send_server_request_response(request_id, &response)?;
println!("< approved fileChange request for item {item_id}");
Ok(())
}
fn send_server_request_response<T>(&mut self, request_id: RequestId, response: &T) -> Result<()>
where
T: Serialize,
{
let message = JSONRPCMessage::Response(JSONRPCResponse {
id: request_id,
result: serde_json::to_value(response)?,
});
self.write_jsonrpc_message(message)
}
fn write_jsonrpc_message(&mut self, message: JSONRPCMessage) -> Result<()> {
let payload = serde_json::to_string(&message)?;
let pretty = serde_json::to_string_pretty(&message)?;
print_multiline_with_prefix("> ", &pretty);
if let Some(stdin) = self.stdin.as_mut() {
writeln!(stdin, "{payload}")?;
stdin
.flush()
.context("failed to flush response to codex app-server")?;
return Ok(());
}
bail!("codex app-server stdin closed")
}
}
fn print_multiline_with_prefix(prefix: &str, payload: &str) {
for line in payload.lines() {
println!("{prefix}{line}");
}
}
impl Drop for CodexClient {
fn drop(&mut self) {
let _ = self.stdin.take();
if let Ok(Some(status)) = self.child.try_wait() {
println!("[codex app-server exited: {status}]");
return;
}
thread::sleep(Duration::from_millis(100));
if let Ok(Some(status)) = self.child.try_wait() {
println!("[codex app-server exited: {status}]");
return;
}
let _ = self.child.kill();
let _ = self.child.wait();
}
}

View File

@@ -1,7 +1,8 @@
[package]
edition = "2024"
name = "codex-app-server"
version = { workspace = true }
version.workspace = true
edition.workspace = true
license.workspace = true
[[bin]]
name = "codex-app-server"
@@ -29,6 +30,9 @@ codex-utils-json-to-toml = { workspace = true }
chrono = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sha2 = { workspace = true }
tempfile = { workspace = true }
toml = { workspace = true }
tokio = { workspace = true, features = [
"io-std",
"macros",
@@ -46,9 +50,9 @@ app_test_support = { workspace = true }
assert_cmd = { workspace = true }
base64 = { workspace = true }
core_test_support = { workspace = true }
mcp-types = { workspace = true }
os_info = { workspace = true }
pretty_assertions = { workspace = true }
serial_test = { workspace = true }
tempfile = { workspace = true }
toml = { workspace = true }
wiremock = { workspace = true }
shlex = { workspace = true }

View File

@@ -1,6 +1,16 @@
# codex-app-server
`codex app-server` is the harness Codex uses to power rich interfaces such as the [Codex VS Code extension](https://marketplace.visualstudio.com/items?itemName=openai.chatgpt). The message schema is currently unstable, but those who wish to build experimental UIs on top of Codex may find it valuable.
`codex app-server` is the interface Codex uses to power rich interfaces such as the [Codex VS Code extension](https://marketplace.visualstudio.com/items?itemName=openai.chatgpt). The message schema is currently unstable, but those who wish to build experimental UIs on top of Codex may find it valuable.
## Table of Contents
- [Protocol](#protocol)
- [Message Schema](#message-schema)
- [Lifecycle Overview](#lifecycle-overview)
- [Initialization](#initialization)
- [Core primitives](#core-primitives)
- [Thread & turn endpoints](#thread--turn-endpoints)
- [Events (work-in-progress)](#events-work-in-progress)
- [Auth endpoints](#auth-endpoints)
## Protocol
@@ -8,8 +18,407 @@ Similar to [MCP](https://modelcontextprotocol.io/), `codex app-server` supports
## Message Schema
Currently, you can dump a TypeScript version of the schema using `codex generate-ts`. It is specific to the version of Codex you used to run `generate-ts`, so the two are guaranteed to be compatible.
Currently, you can dump a TypeScript version of the schema using `codex app-server generate-ts`, or a JSON Schema bundle via `codex app-server generate-json-schema`. Each output is specific to the version of Codex you used to run the command, so the generated artifacts are guaranteed to match that version.
```
codex generate-ts --out DIR
codex app-server generate-ts --out DIR
codex app-server generate-json-schema --out DIR
```
## Lifecycle Overview
- Initialize once: Immediately after launching the codex app-server process, send an `initialize` request with your client metadata, then emit an `initialized` notification. Any other request before this handshake gets rejected.
- Start (or resume) a thread: Call `thread/start` to open a fresh conversation. The response returns the thread object and youll also get a `thread/started` notification. If youre continuing an existing conversation, call `thread/resume` with its ID instead.
- Begin a turn: To send user input, call `turn/start` with the target `threadId` and the user's input. Optional fields let you override model, cwd, sandbox policy, etc. This immediately returns the new turn object and triggers a `turn/started` notification.
- Stream events: After `turn/start`, keep reading JSON-RPC notifications on stdout. Youll see `item/started`, `item/completed`, deltas like `item/agentMessage/delta`, tool progress, etc. These represent streaming model output plus any side effects (commands, tool calls, reasoning notes).
- Finish the turn: When the model is done (or the turn is interrupted via making the `turn/interrupt` call), the server sends `turn/completed` with the final turn state and token usage.
## Initialization
Clients must send a single `initialize` request before invoking any other method, then acknowledge with an `initialized` notification. The server returns the user agent string it will present to upstream services; subsequent requests issued before initialization receive a `"Not initialized"` error, and repeated `initialize` calls receive an `"Already initialized"` error.
Example:
```json
{ "method": "initialize", "id": 0, "params": {
"clientInfo": { "name": "codex-vscode", "title": "Codex VS Code Extension", "version": "0.1.0" }
} }
{ "id": 0, "result": { "userAgent": "codex-app-server/0.1.0 codex-vscode/0.1.0" } }
{ "method": "initialized" }
```
## Core primitives
We have 3 top level primitives:
- Thread - a conversation between the Codex agent and a user. Each thread contains multiple turns.
- Turn - one turn of the conversation, typically starting with a user message and finishing with an agent message. Each turn contains multiple items.
- Item - represents user inputs and agent outputs as part of the turn, persisted and used as the context for future conversations.
## Thread & turn endpoints
The JSON-RPC API exposes dedicated methods for managing Codex conversations. Threads store long-lived conversation metadata, and turns store the per-message exchange (input → Codex output, including streamed items). Use the thread APIs to create, list, or archive sessions, then drive the conversation with turn APIs and notifications.
### Quick reference
- `thread/start` — create a new thread; emits `thread/started` and auto-subscribes you to turn/item events for that thread.
- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it.
- `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders` filtering.
- `thread/archive` — move a threads rollout file into the archived directory; returns `{}` on success.
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications.
- `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`.
- `review/start` — kick off Codexs automated reviewer for a thread; responds like `turn/start` and emits `item/started`/`item/completed` notifications with `enteredReviewMode` and `exitedReviewMode` items, plus a final assistant `agentMessage` containing the review.
### 1) Start or resume a thread
Start a fresh thread when you need a new Codex conversation.
```json
{ "method": "thread/start", "id": 10, "params": {
// Optionally set config settings. If not specified, will use the user's
// current config settings.
"model": "gpt-5.1-codex",
"cwd": "/Users/me/project",
"approvalPolicy": "never",
"sandbox": "workspaceWrite",
} }
{ "id": 10, "result": {
"thread": {
"id": "thr_123",
"preview": "",
"modelProvider": "openai",
"createdAt": 1730910000
}
} }
{ "method": "thread/started", "params": { "thread": { } } }
```
To continue a stored session, call `thread/resume` with the `thread.id` you previously recorded. The response shape matches `thread/start`, and no additional notifications are emitted:
```json
{ "method": "thread/resume", "id": 11, "params": { "threadId": "thr_123" } }
{ "id": 11, "result": { "thread": { "id": "thr_123", } } }
```
### 2) List threads (pagination & filters)
`thread/list` lets you render a history UI. Pass any combination of:
- `cursor` — opaque string from a prior response; omit for the first page.
- `limit` — server defaults to a reasonable page size if unset.
- `modelProviders` — restrict results to specific providers; unset, null, or an empty array will include all providers.
Example:
```json
{ "method": "thread/list", "id": 20, "params": {
"cursor": null,
"limit": 25,
} }
{ "id": 20, "result": {
"data": [
{ "id": "thr_a", "preview": "Create a TUI", "modelProvider": "openai", "createdAt": 1730831111 },
{ "id": "thr_b", "preview": "Fix tests", "modelProvider": "openai", "createdAt": 1730750000 }
],
"nextCursor": "opaque-token-or-null"
} }
```
When `nextCursor` is `null`, youve reached the final page.
### 3) Archive a thread
Use `thread/archive` to move the persisted rollout (stored as a JSONL file on disk) into the archived sessions directory.
```json
{ "method": "thread/archive", "id": 21, "params": { "threadId": "thr_b" } }
{ "id": 21, "result": {} }
```
An archived thread will not appear in future calls to `thread/list`.
### 4) Start a turn (send user input)
Turns attach user input (text or images) to a thread and trigger Codex generation. The `input` field is a list of discriminated unions:
- `{"type":"text","text":"Explain this diff"}`
- `{"type":"image","url":"https://…png"}`
- `{"type":"localImage","path":"/tmp/screenshot.png"}`
You can optionally specify config overrides on the new turn. If specified, these settings become the default for subsequent turns on the same thread.
```json
{ "method": "turn/start", "id": 30, "params": {
"threadId": "thr_123",
"input": [ { "type": "text", "text": "Run tests" } ],
// Below are optional config overrides
"cwd": "/Users/me/project",
"approvalPolicy": "unlessTrusted",
"sandboxPolicy": {
"mode": "workspaceWrite",
"writableRoots": ["/Users/me/project"],
"networkAccess": true
},
"model": "gpt-5.1-codex",
"effort": "medium",
"summary": "concise"
} }
{ "id": 30, "result": { "turn": {
"id": "turn_456",
"status": "inProgress",
"items": [],
"error": null
} } }
```
### 5) Interrupt an active turn
You can cancel a running Turn with `turn/interrupt`.
```json
{ "method": "turn/interrupt", "id": 31, "params": {
"threadId": "thr_123",
"turnId": "turn_456"
} }
{ "id": 31, "result": {} }
```
The server requests cancellations for running subprocesses, then emits a `turn/completed` event with `status: "interrupted"`. Rely on the `turn/completed` to know when Codex-side cleanup is done.
### 6) Request a code review
Use `review/start` to run Codexs reviewer on the currently checked-out project. The request takes the thread id plus a `target` describing what should be reviewed:
- `{"type":"uncommittedChanges"}` — staged, unstaged, and untracked files.
- `{"type":"baseBranch","branch":"main"}` — diff against the provided branchs upstream (see prompt for the exact `git merge-base`/`git diff` instructions Codex will run).
- `{"type":"commit","sha":"abc1234","title":"Optional subject"}` — review a specific commit.
- `{"type":"custom","instructions":"Free-form reviewer instructions"}` — fallback prompt equivalent to the legacy manual review request.
- `delivery` (`"inline"` or `"detached"`, default `"inline"`) — where the review runs:
- `"inline"`: run the review as a new turn on the existing thread. The responses `reviewThreadId` equals the original `threadId`, and no new `thread/started` notification is emitted.
- `"detached"`: fork a new review thread from the parent conversation and run the review there. The responses `reviewThreadId` is the id of this new review thread, and the server emits a `thread/started` notification for it before streaming review items.
Example request/response:
```json
{ "method": "review/start", "id": 40, "params": {
"threadId": "thr_123",
"delivery": "inline",
"target": { "type": "commit", "sha": "1234567deadbeef", "title": "Polish tui colors" }
} }
{ "id": 40, "result": {
"turn": {
"id": "turn_900",
"status": "inProgress",
"items": [
{ "type": "userMessage", "id": "turn_900", "content": [ { "type": "text", "text": "Review commit 1234567: Polish tui colors" } ] }
],
"error": null
},
"reviewThreadId": "thr_123"
} }
```
For a detached review, use `"delivery": "detached"`. The response is the same shape, but `reviewThreadId` will be the id of the new review thread (different from the original `threadId`). The server also emits a `thread/started` notification for that new thread before streaming the review turn.
Codex streams the usual `turn/started` notification followed by an `item/started`
with an `enteredReviewMode` item so clients can show progress:
```json
{ "method": "item/started", "params": { "item": {
"type": "enteredReviewMode",
"id": "turn_900",
"review": "current changes"
} } }
```
When the reviewer finishes, the server emits `item/started` and `item/completed`
containing an `exitedReviewMode` item with the final review text:
```json
{ "method": "item/completed", "params": { "item": {
"type": "exitedReviewMode",
"id": "turn_900",
"review": "Looks solid overall...\n\n- Prefer Stylize helpers — app.rs:10-20\n ..."
} } }
```
The `review` string is plain text that already bundles the overall explanation plus a bullet list for each structured finding (matching `ThreadItem::ExitedReviewMode` in the generated schema). Use this notification to render the reviewer output in your client.
## Events (work-in-progress)
Event notifications are the server-initiated event stream for thread lifecycles, turn lifecycles, and the items within them. After you start or resume a thread, keep reading stdout for `thread/started`, `turn/*`, and `item/*` notifications.
### Turn events
The app-server streams JSON-RPC notifications while a turn is running. Each turn starts with `turn/started` (initial `turn`) and ends with `turn/completed` (final `turn` status). Token usage events stream separately via `thread/tokenUsage/updated`. Clients subscribe to the events they care about, rendering each item incrementally as updates arrive. The per-item lifecycle is always: `item/started` → zero or more item-specific deltas → `item/completed`.
- `turn/started``{ turn }` with the turn id, empty `items`, and `status: "inProgress"`.
- `turn/completed``{ turn }` where `turn.status` is `completed`, `interrupted`, or `failed`; failures carry `{ error: { message, codexErrorInfo? } }`.
- `turn/plan/updated``{ turnId, explanation?, plan }` whenever the agent shares or changes its plan; each `plan` entry is `{ step, status }` with `status` in `pending`, `inProgress`, or `completed`.
Today both notifications carry an empty `items` array even when item events were streamed; rely on `item/*` notifications for the canonical item list until this is fixed.
#### Thread items
`ThreadItem` is the tagged union carried in turn responses and `item/*` notifications. Currently we support events for the following items:
- `userMessage``{id, content}` where `content` is a list of user inputs (`text`, `image`, or `localImage`).
- `agentMessage``{id, text}` containing the accumulated agent reply.
- `reasoning``{id, summary, content}` where `summary` holds streamed reasoning summaries (applicable for most OpenAI models) and `content` holds raw reasoning blocks (applicable for e.g. open source models).
- `commandExecution``{id, command, cwd, status, commandActions, aggregatedOutput?, exitCode?, durationMs?}` for sandboxed commands; `status` is `inProgress`, `completed`, `failed`, or `declined`.
- `fileChange``{id, changes, status}` describing proposed edits; `changes` list `{path, kind, diff}` and `status` is `inProgress`, `completed`, `failed`, or `declined`.
- `mcpToolCall``{id, server, tool, status, arguments, result?, error?}` describing MCP calls; `status` is `inProgress`, `completed`, or `failed`.
- `webSearch``{id, query}` for a web search request issued by the agent.
- `compacted` - `{threadId, turnId}` when codex compacts the conversation history. This can happen automatically.
All items emit two shared lifecycle events:
- `item/started` — emits the full `item` when a new unit of work begins so the UI can render it immediately; the `item.id` in this payload matches the `itemId` used by deltas.
- `item/completed` — sends the final `item` once that work finishes (e.g., after a tool call or message completes); treat this as the authoritative state.
There are additional item-specific events:
#### agentMessage
- `item/agentMessage/delta` — appends streamed text for the agent message; concatenate `delta` values for the same `itemId` in order to reconstruct the full reply.
#### reasoning
- `item/reasoning/summaryTextDelta` — streams readable reasoning summaries; `summaryIndex` increments when a new summary section opens.
- `item/reasoning/summaryPartAdded` — marks the boundary between reasoning summary sections for an `itemId`; subsequent `summaryTextDelta` entries share the same `summaryIndex`.
- `item/reasoning/textDelta` — streams raw reasoning text (only applicable for e.g. open source models); use `contentIndex` to group deltas that belong together before showing them in the UI.
#### commandExecution
- `item/commandExecution/outputDelta` — streams stdout/stderr for the command; append deltas in order to render live output alongside `aggregatedOutput` in the final item.
Final `commandExecution` items include parsed `commandActions`, `status`, `exitCode`, and `durationMs` so the UI can summarize what ran and whether it succeeded.
#### fileChange
`fileChange` items contain a `changes` list with `{path, kind, diff}` entries (`kind` is `add`, `delete`, or `update` with an optional `movePath`). The `status` tracks whether apply succeeded (`completed`), failed, or was `declined`.
### Errors
`error` event is emitted whenever the server hits an error mid-turn (for example, upstream model errors or quota limits). Carries the same `{ error: { message, codexErrorInfo? } }` payload as `turn.status: "failed"` and may precede that terminal notification.
`codexErrorInfo` maps to the `CodexErrorInfo` enum. Common values:
- `ContextWindowExceeded`
- `UsageLimitExceeded`
- `HttpConnectionFailed { httpStatusCode? }`: upstream HTTP failures including 4xx/5xx
- `ResponseStreamConnectionFailed { httpStatusCode? }`: failure to connect to the response SSE stream
- `ResponseStreamDisconnected { httpStatusCode? }`: disconnect of the response SSE stream in the middle of a turn before completion
- `ResponseTooManyFailedAttempts { httpStatusCode? }`
- `BadRequest`
- `Unauthorized`
- `SandboxError`
- `InternalServerError`
- `Other`: all unclassified errors
When an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant.
## Approvals
Certain actions (shell commands or modifying files) may require explicit user approval depending on the user's config. When `turn/start` is used, the app-server drives an approval flow by sending a server-initiated JSON-RPC request to the client. The client must respond to tell Codex whether to proceed. UIs should present these requests inline with the active turn so users can review the proposed command or diff before choosing.
- Requests include `threadId` and `turnId`—use them to scope UI state to the active conversation.
- Respond with a single `{ "decision": "accept" | "decline" }` payload (plus optional `acceptSettings` on command executions). The server resumes or declines the work and ends the item with `item/completed`.
### Command execution approvals
Order of messages:
1. `item/started` — shows the pending `commandExecution` item with `command`, `cwd`, and other fields so you can render the proposed action.
2. `item/commandExecution/requestApproval` (request) — carries the same `itemId`, `threadId`, `turnId`, optionally `reason` or `risk`, plus `parsedCmd` for friendly display.
3. Client response — `{ "decision": "accept", "acceptSettings": { "forSession": false } }` or `{ "decision": "decline" }`.
4. `item/completed` — final `commandExecution` item with `status: "completed" | "failed" | "declined"` and execution output. Render this as the authoritative result.
### File change approvals
Order of messages:
1. `item/started` — emits a `fileChange` item with `changes` (diff chunk summaries) and `status: "inProgress"`. Show the proposed edits and paths to the user.
2. `item/fileChange/requestApproval` (request) — includes `itemId`, `threadId`, `turnId`, and an optional `reason`.
3. Client response — `{ "decision": "accept" }` or `{ "decision": "decline" }`.
4. `item/completed` — returns the same `fileChange` item with `status` updated to `completed`, `failed`, or `declined` after the patch attempt. Rely on this to show success/failure and finalize the diff state in your UI.
UI guidance for IDEs: surface an approval dialog as soon as the request arrives. The turn will proceed after the server receives a response to the approval request. The terminal `item/completed` notification will be sent with the appropriate status.
## Auth endpoints
The JSON-RPC auth/account surface exposes request/response methods plus server-initiated notifications (no `id`). Use these to determine auth state, start or cancel logins, logout, and inspect ChatGPT rate limits.
### Quick reference
- `account/read` — fetch current account info; optionally refresh tokens.
- `account/login/start` — begin login (`apiKey` or `chatgpt`).
- `account/login/completed` (notify) — emitted when a login attempt finishes (success or error).
- `account/login/cancel` — cancel a pending ChatGPT login by `loginId`.
- `account/logout` — sign out; triggers `account/updated`.
- `account/updated` (notify) — emitted whenever auth mode changes (`authMode`: `apikey`, `chatgpt`, or `null`).
- `account/rateLimits/read` — fetch ChatGPT rate limits; updates arrive via `account/rateLimits/updated` (notify).
### 1) Check auth state
Request:
```json
{ "method": "account/read", "id": 1, "params": { "refreshToken": false } }
```
Response examples:
```json
{ "id": 1, "result": { "account": null, "requiresOpenaiAuth": false } } // No OpenAI auth needed (e.g., OSS/local models)
{ "id": 1, "result": { "account": null, "requiresOpenaiAuth": true } } // OpenAI auth required (typical for OpenAI-hosted models)
{ "id": 1, "result": { "account": { "type": "apiKey" }, "requiresOpenaiAuth": true } }
{ "id": 1, "result": { "account": { "type": "chatgpt", "email": "user@example.com", "planType": "pro" }, "requiresOpenaiAuth": true } }
```
Field notes:
- `refreshToken` (bool): set `true` to force a token refresh.
- `requiresOpenaiAuth` reflects the active provider; when `false`, Codex can run without OpenAI credentials.
### 2) Log in with an API key
1. Send:
```json
{ "method": "account/login/start", "id": 2, "params": { "type": "apiKey", "apiKey": "sk-…" } }
```
2. Expect:
```json
{ "id": 2, "result": { "type": "apiKey" } }
```
3. Notifications:
```json
{ "method": "account/login/completed", "params": { "loginId": null, "success": true, "error": null } }
{ "method": "account/updated", "params": { "authMode": "apikey" } }
```
### 3) Log in with ChatGPT (browser flow)
1. Start:
```json
{ "method": "account/login/start", "id": 3, "params": { "type": "chatgpt" } }
{ "id": 3, "result": { "type": "chatgpt", "loginId": "<uuid>", "authUrl": "https://chatgpt.com/…&redirect_uri=http%3A%2F%2Flocalhost%3A<port>%2Fauth%2Fcallback" } }
```
2. Open `authUrl` in a browser; the app-server hosts the local callback.
3. Wait for notifications:
```json
{ "method": "account/login/completed", "params": { "loginId": "<uuid>", "success": true, "error": null } }
{ "method": "account/updated", "params": { "authMode": "chatgpt" } }
```
### 4) Cancel a ChatGPT login
```json
{ "method": "account/login/cancel", "id": 4, "params": { "loginId": "<uuid>" } }
{ "method": "account/login/completed", "params": { "loginId": "<uuid>", "success": false, "error": "…" } }
```
### 5) Logout
```json
{ "method": "account/logout", "id": 5 }
{ "id": 5, "result": {} }
{ "method": "account/updated", "params": { "authMode": null } }
```
### 6) Rate limits (ChatGPT)
```json
{ "method": "account/rateLimits/read", "id": 6 }
{ "id": 6, "result": { "rateLimits": { "primary": { "usedPercent": 25, "windowDurationMins": 15, "resetsAt": 1730947200 }, "secondary": null } } }
{ "method": "account/rateLimits/updated", "params": { "rateLimits": { … } } }
```
Field notes:
- `usedPercent` is current usage within the OpenAI quota window.
- `windowDurationMins` is the quota window length.
- `resetsAt` is a Unix timestamp (seconds) for the next reset.
### Dev notes
- `codex app-server generate-ts --out <dir>` emits v2 types under `v2/`.
- `codex app-server generate-json-schema --out <dir>` outputs `codex_app_server_protocol.schemas.json`.
- See [“Authentication and authorization” in the config docs](../../docs/config.md#authentication-and-authorization) for configuration knobs.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,974 @@
use crate::error_code::INTERNAL_ERROR_CODE;
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
use anyhow::anyhow;
use codex_app_server_protocol::ConfigBatchWriteParams;
use codex_app_server_protocol::ConfigLayer;
use codex_app_server_protocol::ConfigLayerMetadata;
use codex_app_server_protocol::ConfigLayerName;
use codex_app_server_protocol::ConfigReadParams;
use codex_app_server_protocol::ConfigReadResponse;
use codex_app_server_protocol::ConfigValueWriteParams;
use codex_app_server_protocol::ConfigWriteErrorCode;
use codex_app_server_protocol::ConfigWriteResponse;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::MergeStrategy;
use codex_app_server_protocol::OverriddenMetadata;
use codex_app_server_protocol::WriteStatus;
use codex_core::config::ConfigToml;
use codex_core::config_loader::LoadedConfigLayers;
use codex_core::config_loader::LoaderOverrides;
use codex_core::config_loader::load_config_layers_with_overrides;
use codex_core::config_loader::merge_toml_values;
use serde_json::Value as JsonValue;
use serde_json::json;
use sha2::Digest;
use sha2::Sha256;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use tempfile::NamedTempFile;
use tokio::task;
use toml::Value as TomlValue;
const SESSION_FLAGS_SOURCE: &str = "--config";
const MDM_SOURCE: &str = "com.openai.codex/config_toml_base64";
const CONFIG_FILE_NAME: &str = "config.toml";
#[derive(Clone)]
pub(crate) struct ConfigApi {
codex_home: PathBuf,
cli_overrides: Vec<(String, TomlValue)>,
loader_overrides: LoaderOverrides,
}
impl ConfigApi {
pub(crate) fn new(codex_home: PathBuf, cli_overrides: Vec<(String, TomlValue)>) -> Self {
Self {
codex_home,
cli_overrides,
loader_overrides: LoaderOverrides::default(),
}
}
#[cfg(test)]
fn with_overrides(
codex_home: PathBuf,
cli_overrides: Vec<(String, TomlValue)>,
loader_overrides: LoaderOverrides,
) -> Self {
Self {
codex_home,
cli_overrides,
loader_overrides,
}
}
pub(crate) async fn read(
&self,
params: ConfigReadParams,
) -> Result<ConfigReadResponse, JSONRPCErrorError> {
let layers = self
.load_layers_state()
.await
.map_err(|err| internal_error("failed to read configuration layers", err))?;
let effective = layers.effective_config();
validate_config(&effective).map_err(|err| internal_error("invalid configuration", err))?;
let response = ConfigReadResponse {
config: to_json_value(&effective),
origins: layers.origins(),
layers: params.include_layers.then(|| layers.layers_high_to_low()),
};
Ok(response)
}
pub(crate) async fn write_value(
&self,
params: ConfigValueWriteParams,
) -> Result<ConfigWriteResponse, JSONRPCErrorError> {
let edits = vec![(params.key_path, params.value, params.merge_strategy)];
self.apply_edits(params.file_path, params.expected_version, edits)
.await
}
pub(crate) async fn batch_write(
&self,
params: ConfigBatchWriteParams,
) -> Result<ConfigWriteResponse, JSONRPCErrorError> {
let edits = params
.edits
.into_iter()
.map(|edit| (edit.key_path, edit.value, edit.merge_strategy))
.collect();
self.apply_edits(params.file_path, params.expected_version, edits)
.await
}
async fn apply_edits(
&self,
file_path: String,
expected_version: Option<String>,
edits: Vec<(String, JsonValue, MergeStrategy)>,
) -> Result<ConfigWriteResponse, JSONRPCErrorError> {
let allowed_path = self.codex_home.join(CONFIG_FILE_NAME);
if !paths_match(&allowed_path, &file_path) {
return Err(config_write_error(
ConfigWriteErrorCode::ConfigLayerReadonly,
"Only writes to the user config are allowed",
));
}
let layers = self
.load_layers_state()
.await
.map_err(|err| internal_error("failed to load configuration", err))?;
if let Some(expected) = expected_version.as_deref()
&& expected != layers.user.version
{
return Err(config_write_error(
ConfigWriteErrorCode::ConfigVersionConflict,
"Configuration was modified since last read. Fetch latest version and retry.",
));
}
let mut user_config = layers.user.config.clone();
let mut mutated = false;
let mut parsed_segments = Vec::new();
for (key_path, value, strategy) in edits.into_iter() {
let segments = parse_key_path(&key_path).map_err(|message| {
config_write_error(ConfigWriteErrorCode::ConfigValidationError, message)
})?;
let parsed_value = parse_value(value).map_err(|message| {
config_write_error(ConfigWriteErrorCode::ConfigValidationError, message)
})?;
let changed = apply_merge(&mut user_config, &segments, parsed_value.as_ref(), strategy)
.map_err(|err| match err {
MergeError::PathNotFound => config_write_error(
ConfigWriteErrorCode::ConfigPathNotFound,
"Path not found",
),
MergeError::Validation(message) => {
config_write_error(ConfigWriteErrorCode::ConfigValidationError, message)
}
})?;
mutated |= changed;
parsed_segments.push(segments);
}
validate_config(&user_config).map_err(|err| {
config_write_error(
ConfigWriteErrorCode::ConfigValidationError,
format!("Invalid configuration: {err}"),
)
})?;
let updated_layers = layers.with_user_config(user_config.clone());
let effective = updated_layers.effective_config();
validate_config(&effective).map_err(|err| {
config_write_error(
ConfigWriteErrorCode::ConfigValidationError,
format!("Invalid configuration: {err}"),
)
})?;
if mutated {
self.persist_user_config(&user_config)
.await
.map_err(|err| internal_error("failed to persist config.toml", err))?;
}
let overridden = first_overridden_edit(&updated_layers, &effective, &parsed_segments);
let status = overridden
.as_ref()
.map(|_| WriteStatus::OkOverridden)
.unwrap_or(WriteStatus::Ok);
Ok(ConfigWriteResponse {
status,
version: updated_layers.user.version.clone(),
overridden_metadata: overridden,
})
}
async fn load_layers_state(&self) -> std::io::Result<LayersState> {
let LoadedConfigLayers {
base,
managed_config,
managed_preferences,
} = load_config_layers_with_overrides(&self.codex_home, self.loader_overrides.clone())
.await?;
let user = LayerState::new(
ConfigLayerName::User,
self.codex_home.join(CONFIG_FILE_NAME),
base,
);
let session_flags = LayerState::new(
ConfigLayerName::SessionFlags,
PathBuf::from(SESSION_FLAGS_SOURCE),
{
let mut root = TomlValue::Table(toml::map::Map::new());
for (path, value) in self.cli_overrides.iter() {
apply_override(&mut root, path, value.clone());
}
root
},
);
let system = managed_config.map(|cfg| {
LayerState::new(
ConfigLayerName::System,
system_config_path(&self.codex_home),
cfg,
)
});
let mdm = managed_preferences
.map(|cfg| LayerState::new(ConfigLayerName::Mdm, PathBuf::from(MDM_SOURCE), cfg));
Ok(LayersState {
user,
session_flags,
system,
mdm,
})
}
async fn persist_user_config(&self, user_config: &TomlValue) -> anyhow::Result<()> {
let codex_home = self.codex_home.clone();
let serialized = toml::to_string_pretty(user_config)?;
task::spawn_blocking(move || -> anyhow::Result<()> {
std::fs::create_dir_all(&codex_home)?;
let target = codex_home.join(CONFIG_FILE_NAME);
let tmp = NamedTempFile::new_in(&codex_home)?;
std::fs::write(tmp.path(), serialized.as_bytes())?;
tmp.persist(&target)?;
Ok(())
})
.await
.map_err(|err| anyhow!("config persistence task panicked: {err}"))??;
Ok(())
}
}
fn parse_value(value: JsonValue) -> Result<Option<TomlValue>, String> {
if value.is_null() {
return Ok(None);
}
serde_json::from_value::<TomlValue>(value)
.map(Some)
.map_err(|err| format!("invalid value: {err}"))
}
fn parse_key_path(path: &str) -> Result<Vec<String>, String> {
if path.trim().is_empty() {
return Err("keyPath must not be empty".to_string());
}
Ok(path
.split('.')
.map(std::string::ToString::to_string)
.collect())
}
fn apply_override(target: &mut TomlValue, path: &str, value: TomlValue) {
use toml::value::Table;
let segments: Vec<&str> = path.split('.').collect();
let mut current = target;
for (idx, segment) in segments.iter().enumerate() {
let is_last = idx == segments.len() - 1;
if is_last {
match current {
TomlValue::Table(table) => {
table.insert(segment.to_string(), value);
}
_ => {
let mut table = Table::new();
table.insert(segment.to_string(), value);
*current = TomlValue::Table(table);
}
}
return;
}
match current {
TomlValue::Table(table) => {
current = table
.entry((*segment).to_string())
.or_insert_with(|| TomlValue::Table(Table::new()));
}
_ => {
*current = TomlValue::Table(Table::new());
if let TomlValue::Table(tbl) = current {
current = tbl
.entry((*segment).to_string())
.or_insert_with(|| TomlValue::Table(Table::new()));
}
}
}
}
}
#[derive(Debug)]
enum MergeError {
PathNotFound,
Validation(String),
}
fn apply_merge(
root: &mut TomlValue,
segments: &[String],
value: Option<&TomlValue>,
strategy: MergeStrategy,
) -> Result<bool, MergeError> {
let Some(value) = value else {
return clear_path(root, segments);
};
let Some((last, parents)) = segments.split_last() else {
return Err(MergeError::Validation(
"keyPath must not be empty".to_string(),
));
};
let mut current = root;
for segment in parents {
match current {
TomlValue::Table(table) => {
current = table
.entry(segment.clone())
.or_insert_with(|| TomlValue::Table(toml::map::Map::new()));
}
_ => {
*current = TomlValue::Table(toml::map::Map::new());
if let TomlValue::Table(table) = current {
current = table
.entry(segment.clone())
.or_insert_with(|| TomlValue::Table(toml::map::Map::new()));
}
}
}
}
let table = current.as_table_mut().ok_or_else(|| {
MergeError::Validation("cannot set value on non-table parent".to_string())
})?;
if matches!(strategy, MergeStrategy::Upsert)
&& let Some(existing) = table.get_mut(last)
&& matches!(existing, TomlValue::Table(_))
&& matches!(value, TomlValue::Table(_))
{
merge_toml_values(existing, value);
return Ok(true);
}
let changed = table
.get(last)
.map(|existing| Some(existing) != Some(value))
.unwrap_or(true);
table.insert(last.clone(), value.clone());
Ok(changed)
}
fn clear_path(root: &mut TomlValue, segments: &[String]) -> Result<bool, MergeError> {
let Some((last, parents)) = segments.split_last() else {
return Err(MergeError::Validation(
"keyPath must not be empty".to_string(),
));
};
let mut current = root;
for segment in parents {
match current {
TomlValue::Table(table) => {
current = table.get_mut(segment).ok_or(MergeError::PathNotFound)?;
}
_ => return Err(MergeError::PathNotFound),
}
}
let Some(parent) = current.as_table_mut() else {
return Err(MergeError::PathNotFound);
};
Ok(parent.remove(last).is_some())
}
#[derive(Clone)]
struct LayerState {
name: ConfigLayerName,
source: PathBuf,
config: TomlValue,
version: String,
}
impl LayerState {
fn new(name: ConfigLayerName, source: PathBuf, config: TomlValue) -> Self {
let version = version_for_toml(&config);
Self {
name,
source,
config,
version,
}
}
fn metadata(&self) -> ConfigLayerMetadata {
ConfigLayerMetadata {
name: self.name.clone(),
source: self.source.display().to_string(),
version: self.version.clone(),
}
}
fn as_layer(&self) -> ConfigLayer {
ConfigLayer {
name: self.name.clone(),
source: self.source.display().to_string(),
version: self.version.clone(),
config: to_json_value(&self.config),
}
}
}
#[derive(Clone)]
struct LayersState {
user: LayerState,
session_flags: LayerState,
system: Option<LayerState>,
mdm: Option<LayerState>,
}
impl LayersState {
fn with_user_config(self, user_config: TomlValue) -> Self {
Self {
user: LayerState::new(self.user.name, self.user.source, user_config),
session_flags: self.session_flags,
system: self.system,
mdm: self.mdm,
}
}
fn effective_config(&self) -> TomlValue {
let mut merged = self.user.config.clone();
merge_toml_values(&mut merged, &self.session_flags.config);
if let Some(system) = &self.system {
merge_toml_values(&mut merged, &system.config);
}
if let Some(mdm) = &self.mdm {
merge_toml_values(&mut merged, &mdm.config);
}
merged
}
fn origins(&self) -> HashMap<String, ConfigLayerMetadata> {
let mut origins = HashMap::new();
let mut path = Vec::new();
record_origins(
&self.user.config,
&self.user.metadata(),
&mut path,
&mut origins,
);
record_origins(
&self.session_flags.config,
&self.session_flags.metadata(),
&mut path,
&mut origins,
);
if let Some(system) = &self.system {
record_origins(&system.config, &system.metadata(), &mut path, &mut origins);
}
if let Some(mdm) = &self.mdm {
record_origins(&mdm.config, &mdm.metadata(), &mut path, &mut origins);
}
origins
}
fn layers_high_to_low(&self) -> Vec<ConfigLayer> {
let mut layers = Vec::new();
if let Some(mdm) = &self.mdm {
layers.push(mdm.as_layer());
}
if let Some(system) = &self.system {
layers.push(system.as_layer());
}
layers.push(self.session_flags.as_layer());
layers.push(self.user.as_layer());
layers
}
}
fn record_origins(
value: &TomlValue,
meta: &ConfigLayerMetadata,
path: &mut Vec<String>,
origins: &mut HashMap<String, ConfigLayerMetadata>,
) {
match value {
TomlValue::Table(table) => {
for (key, val) in table {
path.push(key.clone());
record_origins(val, meta, path, origins);
path.pop();
}
}
TomlValue::Array(items) => {
for (idx, item) in items.iter().enumerate() {
path.push(idx.to_string());
record_origins(item, meta, path, origins);
path.pop();
}
}
_ => {
if !path.is_empty() {
origins.insert(path.join("."), meta.clone());
}
}
}
}
fn to_json_value(value: &TomlValue) -> JsonValue {
serde_json::to_value(value).unwrap_or(JsonValue::Null)
}
fn validate_config(value: &TomlValue) -> Result<(), toml::de::Error> {
let _: ConfigToml = value.clone().try_into()?;
Ok(())
}
fn version_for_toml(value: &TomlValue) -> String {
let json = to_json_value(value);
let canonical = canonical_json(&json);
let serialized = serde_json::to_vec(&canonical).unwrap_or_default();
let mut hasher = Sha256::new();
hasher.update(serialized);
let hash = hasher.finalize();
let hex = hash
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>();
format!("sha256:{hex}")
}
fn canonical_json(value: &JsonValue) -> JsonValue {
match value {
JsonValue::Object(map) => {
let mut sorted = serde_json::Map::new();
let mut keys = map.keys().cloned().collect::<Vec<_>>();
keys.sort();
for key in keys {
if let Some(val) = map.get(&key) {
sorted.insert(key, canonical_json(val));
}
}
JsonValue::Object(sorted)
}
JsonValue::Array(items) => JsonValue::Array(items.iter().map(canonical_json).collect()),
other => other.clone(),
}
}
fn paths_match(expected: &Path, provided: &str) -> bool {
let provided_path = PathBuf::from(provided);
if let (Ok(expanded_expected), Ok(expanded_provided)) =
(expected.canonicalize(), provided_path.canonicalize())
{
return expanded_expected == expanded_provided;
}
expected == provided_path
}
fn value_at_path<'a>(root: &'a TomlValue, segments: &[String]) -> Option<&'a TomlValue> {
let mut current = root;
for segment in segments {
match current {
TomlValue::Table(table) => {
current = table.get(segment)?;
}
TomlValue::Array(items) => {
let idx: usize = segment.parse().ok()?;
current = items.get(idx)?;
}
_ => return None,
}
}
Some(current)
}
fn override_message(layer: &ConfigLayerName) -> String {
match layer {
ConfigLayerName::Mdm => "Overridden by managed policy (mdm)".to_string(),
ConfigLayerName::System => "Overridden by managed config (system)".to_string(),
ConfigLayerName::SessionFlags => "Overridden by session flags".to_string(),
ConfigLayerName::User => "Overridden by user config".to_string(),
}
}
fn compute_override_metadata(
layers: &LayersState,
effective: &TomlValue,
segments: &[String],
) -> Option<OverriddenMetadata> {
let user_value = value_at_path(&layers.user.config, segments);
let effective_value = value_at_path(effective, segments);
if user_value.is_some() && user_value == effective_value {
return None;
}
if user_value.is_none() && effective_value.is_none() {
return None;
}
let effective_layer = find_effective_layer(layers, segments);
let overriding_layer = effective_layer.unwrap_or_else(|| layers.user.metadata());
let message = override_message(&overriding_layer.name);
Some(OverriddenMetadata {
message,
overriding_layer,
effective_value: effective_value
.map(to_json_value)
.unwrap_or(JsonValue::Null),
})
}
fn first_overridden_edit(
layers: &LayersState,
effective: &TomlValue,
edits: &[Vec<String>],
) -> Option<OverriddenMetadata> {
for segments in edits {
if let Some(meta) = compute_override_metadata(layers, effective, segments) {
return Some(meta);
}
}
None
}
fn find_effective_layer(layers: &LayersState, segments: &[String]) -> Option<ConfigLayerMetadata> {
let check =
|state: &LayerState| value_at_path(&state.config, segments).map(|_| state.metadata());
if let Some(mdm) = &layers.mdm
&& let Some(meta) = check(mdm)
{
return Some(meta);
}
if let Some(system) = &layers.system
&& let Some(meta) = check(system)
{
return Some(meta);
}
if let Some(meta) = check(&layers.session_flags) {
return Some(meta);
}
check(&layers.user)
}
fn system_config_path(codex_home: &Path) -> PathBuf {
if let Ok(path) = std::env::var("CODEX_MANAGED_CONFIG_PATH") {
return PathBuf::from(path);
}
#[cfg(unix)]
{
let _ = codex_home;
PathBuf::from("/etc/codex/managed_config.toml")
}
#[cfg(not(unix))]
{
codex_home.join("managed_config.toml")
}
}
fn internal_error<E: std::fmt::Display>(context: &str, err: E) -> JSONRPCErrorError {
JSONRPCErrorError {
code: INTERNAL_ERROR_CODE,
message: format!("{context}: {err}"),
data: None,
}
}
fn config_write_error(code: ConfigWriteErrorCode, message: impl Into<String>) -> JSONRPCErrorError {
JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
message: message.into(),
data: Some(json!({
"config_write_error_code": code,
})),
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use tempfile::tempdir;
#[tokio::test]
async fn read_includes_origins_and_layers() {
let tmp = tempdir().expect("tempdir");
std::fs::write(tmp.path().join(CONFIG_FILE_NAME), "model = \"user\"").unwrap();
let managed_path = tmp.path().join("managed_config.toml");
std::fs::write(&managed_path, "approval_policy = \"never\"").unwrap();
let api = ConfigApi::with_overrides(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides {
managed_config_path: Some(managed_path),
#[cfg(target_os = "macos")]
managed_preferences_base64: None,
},
);
let response = api
.read(ConfigReadParams {
include_layers: true,
})
.await
.expect("response");
assert_eq!(
response.config.get("approval_policy"),
Some(&json!("never"))
);
assert_eq!(
response
.origins
.get("approval_policy")
.expect("origin")
.name,
ConfigLayerName::System
);
let layers = response.layers.expect("layers present");
assert_eq!(layers.first().unwrap().name, ConfigLayerName::System);
assert_eq!(layers.get(1).unwrap().name, ConfigLayerName::SessionFlags);
assert_eq!(layers.last().unwrap().name, ConfigLayerName::User);
}
#[tokio::test]
async fn write_value_reports_override() {
let tmp = tempdir().expect("tempdir");
std::fs::write(
tmp.path().join(CONFIG_FILE_NAME),
"approval_policy = \"on-request\"",
)
.unwrap();
let managed_path = tmp.path().join("managed_config.toml");
std::fs::write(&managed_path, "approval_policy = \"never\"").unwrap();
let api = ConfigApi::with_overrides(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides {
managed_config_path: Some(managed_path),
#[cfg(target_os = "macos")]
managed_preferences_base64: None,
},
);
let result = api
.write_value(ConfigValueWriteParams {
file_path: tmp.path().join(CONFIG_FILE_NAME).display().to_string(),
key_path: "approval_policy".to_string(),
value: json!("never"),
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await
.expect("result");
let read_after = api
.read(ConfigReadParams {
include_layers: true,
})
.await
.expect("read");
let config_object = read_after.config.as_object().expect("object");
assert_eq!(config_object.get("approval_policy"), Some(&json!("never")));
assert_eq!(
read_after
.origins
.get("approval_policy")
.expect("origin")
.name,
ConfigLayerName::System
);
assert_eq!(result.status, WriteStatus::Ok);
assert!(result.overridden_metadata.is_none());
}
#[tokio::test]
async fn version_conflict_rejected() {
let tmp = tempdir().expect("tempdir");
std::fs::write(tmp.path().join(CONFIG_FILE_NAME), "model = \"user\"").unwrap();
let api = ConfigApi::new(tmp.path().to_path_buf(), vec![]);
let error = api
.write_value(ConfigValueWriteParams {
file_path: tmp.path().join(CONFIG_FILE_NAME).display().to_string(),
key_path: "model".to_string(),
value: json!("gpt-5"),
merge_strategy: MergeStrategy::Replace,
expected_version: Some("sha256:bogus".to_string()),
})
.await
.expect_err("should fail");
assert_eq!(error.code, INVALID_REQUEST_ERROR_CODE);
assert_eq!(
error
.data
.as_ref()
.and_then(|d| d.get("config_write_error_code"))
.and_then(serde_json::Value::as_str),
Some("configVersionConflict")
);
}
#[tokio::test]
async fn invalid_user_value_rejected_even_if_overridden_by_managed() {
let tmp = tempdir().expect("tempdir");
std::fs::write(tmp.path().join(CONFIG_FILE_NAME), "model = \"user\"").unwrap();
let managed_path = tmp.path().join("managed_config.toml");
std::fs::write(&managed_path, "approval_policy = \"never\"").unwrap();
let api = ConfigApi::with_overrides(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides {
managed_config_path: Some(managed_path),
#[cfg(target_os = "macos")]
managed_preferences_base64: None,
},
);
let error = api
.write_value(ConfigValueWriteParams {
file_path: tmp.path().join(CONFIG_FILE_NAME).display().to_string(),
key_path: "approval_policy".to_string(),
value: json!("bogus"),
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await
.expect_err("should fail validation");
assert_eq!(error.code, INVALID_REQUEST_ERROR_CODE);
assert_eq!(
error
.data
.as_ref()
.and_then(|d| d.get("config_write_error_code"))
.and_then(serde_json::Value::as_str),
Some("configValidationError")
);
let contents =
std::fs::read_to_string(tmp.path().join(CONFIG_FILE_NAME)).expect("read config");
assert_eq!(contents.trim(), "model = \"user\"");
}
#[tokio::test]
async fn read_reports_managed_overrides_user_and_session_flags() {
let tmp = tempdir().expect("tempdir");
std::fs::write(tmp.path().join(CONFIG_FILE_NAME), "model = \"user\"").unwrap();
let managed_path = tmp.path().join("managed_config.toml");
std::fs::write(&managed_path, "model = \"system\"").unwrap();
let cli_overrides = vec![(
"model".to_string(),
TomlValue::String("session".to_string()),
)];
let api = ConfigApi::with_overrides(
tmp.path().to_path_buf(),
cli_overrides,
LoaderOverrides {
managed_config_path: Some(managed_path),
#[cfg(target_os = "macos")]
managed_preferences_base64: None,
},
);
let response = api
.read(ConfigReadParams {
include_layers: true,
})
.await
.expect("response");
assert_eq!(response.config.get("model"), Some(&json!("system")));
assert_eq!(
response.origins.get("model").expect("origin").name,
ConfigLayerName::System
);
let layers = response.layers.expect("layers");
assert_eq!(layers.first().unwrap().name, ConfigLayerName::System);
assert_eq!(layers.get(1).unwrap().name, ConfigLayerName::SessionFlags);
assert_eq!(layers.get(2).unwrap().name, ConfigLayerName::User);
}
#[tokio::test]
async fn write_value_reports_managed_override() {
let tmp = tempdir().expect("tempdir");
std::fs::write(tmp.path().join(CONFIG_FILE_NAME), "").unwrap();
let managed_path = tmp.path().join("managed_config.toml");
std::fs::write(&managed_path, "approval_policy = \"never\"").unwrap();
let api = ConfigApi::with_overrides(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides {
managed_config_path: Some(managed_path),
#[cfg(target_os = "macos")]
managed_preferences_base64: None,
},
);
let result = api
.write_value(ConfigValueWriteParams {
file_path: tmp.path().join(CONFIG_FILE_NAME).display().to_string(),
key_path: "approval_policy".to_string(),
value: json!("on-request"),
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await
.expect("result");
assert_eq!(result.status, WriteStatus::OkOverridden);
let overridden = result.overridden_metadata.expect("overridden metadata");
assert_eq!(overridden.overriding_layer.name, ConfigLayerName::System);
assert_eq!(overridden.effective_value, json!("never"));
}
}

View File

@@ -19,6 +19,10 @@ pub(crate) async fn run_fuzzy_file_search(
roots: Vec<String>,
cancellation_flag: Arc<AtomicBool>,
) -> Vec<FuzzyFileSearchResult> {
if roots.is_empty() {
return Vec::new();
}
#[expect(clippy::expect_used)]
let limit_per_root =
NonZero::new(LIMIT_PER_ROOT).expect("LIMIT_PER_ROOT should be a valid non-zero usize");

View File

@@ -18,6 +18,7 @@ use tokio::io::AsyncWriteExt;
use tokio::io::BufReader;
use tokio::io::{self};
use tokio::sync::mpsc;
use toml::Value as TomlValue;
use tracing::Level;
use tracing::debug;
use tracing::error;
@@ -28,7 +29,9 @@ use tracing_subscriber::filter::Targets;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
mod bespoke_event_handling;
mod codex_message_processor;
mod config_api;
mod error_code;
mod fuzzy_file_search;
mod message_processor;
@@ -46,7 +49,7 @@ pub async fn run_main(
) -> IoResult<()> {
// Set up channels.
let (incoming_tx, mut incoming_rx) = mpsc::channel::<JSONRPCMessage>(CHANNEL_CAPACITY);
let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded_channel::<OutgoingMessage>();
let (outgoing_tx, mut outgoing_rx) = mpsc::channel::<OutgoingMessage>(CHANNEL_CAPACITY);
// Task: read from stdin, push to `incoming_tx`.
let stdin_reader_handle = tokio::spawn({
@@ -79,11 +82,12 @@ pub async fn run_main(
format!("error parsing -c overrides: {e}"),
)
})?;
let config = Config::load_with_cli_overrides(cli_kv_overrides, ConfigOverrides::default())
.await
.map_err(|e| {
std::io::Error::new(ErrorKind::InvalidData, format!("error loading config: {e}"))
})?;
let config =
Config::load_with_cli_overrides(cli_kv_overrides.clone(), ConfigOverrides::default())
.await
.map_err(|e| {
std::io::Error::new(ErrorKind::InvalidData, format!("error loading config: {e}"))
})?;
let feedback = CodexFeedback::new();
@@ -120,10 +124,12 @@ pub async fn run_main(
// Task: process incoming messages.
let processor_handle = tokio::spawn({
let outgoing_message_sender = OutgoingMessageSender::new(outgoing_tx);
let cli_overrides: Vec<(String, TomlValue)> = cli_kv_overrides.clone();
let mut processor = MessageProcessor::new(
outgoing_message_sender,
codex_linux_sandbox_exe,
std::sync::Arc::new(config),
cli_overrides,
feedback.clone(),
);
async move {

View File

@@ -1,17 +1,22 @@
use std::path::PathBuf;
use std::sync::Arc;
use crate::codex_message_processor::CodexMessageProcessor;
use crate::config_api::ConfigApi;
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
use crate::outgoing_message::OutgoingMessageSender;
use codex_app_server_protocol::ClientInfo;
use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::ConfigBatchWriteParams;
use codex_app_server_protocol::ConfigReadParams;
use codex_app_server_protocol::ConfigValueWriteParams;
use codex_app_server_protocol::InitializeResponse;
use codex_app_server_protocol::JSONRPCError;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::JSONRPCRequest;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_core::AuthManager;
use codex_core::ConversationManager;
use codex_core::config::Config;
@@ -19,11 +24,12 @@ use codex_core::default_client::USER_AGENT_SUFFIX;
use codex_core::default_client::get_codex_user_agent;
use codex_feedback::CodexFeedback;
use codex_protocol::protocol::SessionSource;
use std::sync::Arc;
use toml::Value as TomlValue;
pub(crate) struct MessageProcessor {
outgoing: Arc<OutgoingMessageSender>,
codex_message_processor: CodexMessageProcessor,
config_api: ConfigApi,
initialized: bool,
}
@@ -34,6 +40,7 @@ impl MessageProcessor {
outgoing: OutgoingMessageSender,
codex_linux_sandbox_exe: Option<PathBuf>,
config: Arc<Config>,
cli_overrides: Vec<(String, TomlValue)>,
feedback: CodexFeedback,
) -> Self {
let outgoing = Arc::new(outgoing);
@@ -51,13 +58,15 @@ impl MessageProcessor {
conversation_manager,
outgoing.clone(),
codex_linux_sandbox_exe,
config,
Arc::clone(&config),
feedback,
);
let config_api = ConfigApi::new(config.codex_home.clone(), cli_overrides);
Self {
outgoing,
codex_message_processor,
config_api,
initialized: false,
}
}
@@ -118,6 +127,7 @@ impl MessageProcessor {
self.outgoing.send_response(request_id, response).await;
self.initialized = true;
return;
}
}
@@ -134,9 +144,20 @@ impl MessageProcessor {
}
}
self.codex_message_processor
.process_request(codex_request)
.await;
match codex_request {
ClientRequest::ConfigRead { request_id, params } => {
self.handle_config_read(request_id, params).await;
}
ClientRequest::ConfigValueWrite { request_id, params } => {
self.handle_config_value_write(request_id, params).await;
}
ClientRequest::ConfigBatchWrite { request_id, params } => {
self.handle_config_batch_write(request_id, params).await;
}
other => {
self.codex_message_processor.process_request(other).await;
}
}
}
pub(crate) async fn process_notification(&self, notification: JSONRPCNotification) {
@@ -156,4 +177,33 @@ impl MessageProcessor {
pub(crate) fn process_error(&mut self, err: JSONRPCError) {
tracing::error!("<- error: {:?}", err);
}
async fn handle_config_read(&self, request_id: RequestId, params: ConfigReadParams) {
match self.config_api.read(params).await {
Ok(response) => self.outgoing.send_response(request_id, response).await,
Err(error) => self.outgoing.send_error(request_id, error).await,
}
}
async fn handle_config_value_write(
&self,
request_id: RequestId,
params: ConfigValueWriteParams,
) {
match self.config_api.write_value(params).await {
Ok(response) => self.outgoing.send_response(request_id, response).await,
Err(error) => self.outgoing.send_error(request_id, error).await,
}
}
async fn handle_config_batch_write(
&self,
request_id: RequestId,
params: ConfigBatchWriteParams,
) {
match self.config_api.batch_write(params).await {
Ok(response) => self.outgoing.send_response(request_id, response).await,
Err(error) => self.outgoing.send_error(request_id, error).await,
}
}
}

View File

@@ -1,11 +1,12 @@
use codex_app_server_protocol::AuthMode;
use codex_app_server_protocol::Model;
use codex_app_server_protocol::ReasoningEffortOption;
use codex_common::model_presets::ModelPreset;
use codex_common::model_presets::ReasoningEffortPreset;
use codex_common::model_presets::builtin_model_presets;
pub fn supported_models() -> Vec<Model> {
builtin_model_presets(None)
pub fn supported_models(auth_mode: Option<AuthMode>) -> Vec<Model> {
builtin_model_presets(auth_mode)
.into_iter()
.map(model_from_preset)
.collect()

View File

@@ -19,12 +19,12 @@ use crate::error_code::INTERNAL_ERROR_CODE;
/// Sends messages to the client and manages request callbacks.
pub(crate) struct OutgoingMessageSender {
next_request_id: AtomicI64,
sender: mpsc::UnboundedSender<OutgoingMessage>,
sender: mpsc::Sender<OutgoingMessage>,
request_id_to_callback: Mutex<HashMap<RequestId, oneshot::Sender<Result>>>,
}
impl OutgoingMessageSender {
pub(crate) fn new(sender: mpsc::UnboundedSender<OutgoingMessage>) -> Self {
pub(crate) fn new(sender: mpsc::Sender<OutgoingMessage>) -> Self {
Self {
next_request_id: AtomicI64::new(0),
sender,
@@ -45,8 +45,12 @@ impl OutgoingMessageSender {
}
let outgoing_message =
OutgoingMessage::Request(request.request_with_id(outgoing_message_id));
let _ = self.sender.send(outgoing_message);
OutgoingMessage::Request(request.request_with_id(outgoing_message_id.clone()));
if let Err(err) = self.sender.send(outgoing_message).await {
warn!("failed to send request {outgoing_message_id:?} to client: {err:?}");
let mut request_id_to_callback = self.request_id_to_callback.lock().await;
request_id_to_callback.remove(&outgoing_message_id);
}
rx_approve
}
@@ -72,7 +76,9 @@ impl OutgoingMessageSender {
match serde_json::to_value(response) {
Ok(result) => {
let outgoing_message = OutgoingMessage::Response(OutgoingResponse { id, result });
let _ = self.sender.send(outgoing_message);
if let Err(err) = self.sender.send(outgoing_message).await {
warn!("failed to send response to client: {err:?}");
}
}
Err(err) => {
self.send_error(
@@ -89,21 +95,29 @@ impl OutgoingMessageSender {
}
pub(crate) async fn send_server_notification(&self, notification: ServerNotification) {
let _ = self
if let Err(err) = self
.sender
.send(OutgoingMessage::AppServerNotification(notification));
.send(OutgoingMessage::AppServerNotification(notification))
.await
{
warn!("failed to send server notification to client: {err:?}");
}
}
/// All notifications should be migrated to [`ServerNotification`] and
/// [`OutgoingMessage::Notification`] should be removed.
pub(crate) async fn send_notification(&self, notification: OutgoingNotification) {
let outgoing_message = OutgoingMessage::Notification(notification);
let _ = self.sender.send(outgoing_message);
if let Err(err) = self.sender.send(outgoing_message).await {
warn!("failed to send notification to client: {err:?}");
}
}
pub(crate) async fn send_error(&self, id: RequestId, error: JSONRPCErrorError) {
let outgoing_message = OutgoingMessage::Error(OutgoingError { id, error });
let _ = self.sender.send(outgoing_message);
if let Err(err) = self.sender.send(outgoing_message).await {
warn!("failed to send error to client: {err:?}");
}
}
}
@@ -141,9 +155,13 @@ pub(crate) struct OutgoingError {
#[cfg(test)]
mod tests {
use codex_app_server_protocol::AccountLoginCompletedNotification;
use codex_app_server_protocol::AccountRateLimitsUpdatedNotification;
use codex_app_server_protocol::AccountUpdatedNotification;
use codex_app_server_protocol::AuthMode;
use codex_app_server_protocol::LoginChatGptCompleteNotification;
use codex_protocol::protocol::RateLimitSnapshot;
use codex_protocol::protocol::RateLimitWindow;
use codex_app_server_protocol::RateLimitSnapshot;
use codex_app_server_protocol::RateLimitWindow;
use pretty_assertions::assert_eq;
use serde_json::json;
use uuid::Uuid;
@@ -176,27 +194,79 @@ mod tests {
}
#[test]
fn verify_account_rate_limits_notification_serialization() {
let notification = ServerNotification::AccountRateLimitsUpdated(RateLimitSnapshot {
primary: Some(RateLimitWindow {
used_percent: 25.0,
window_minutes: Some(15),
resets_at: Some(123),
fn verify_account_login_completed_notification_serialization() {
let notification =
ServerNotification::AccountLoginCompleted(AccountLoginCompletedNotification {
login_id: Some(Uuid::nil().to_string()),
success: true,
error: None,
});
let jsonrpc_notification = OutgoingMessage::AppServerNotification(notification);
assert_eq!(
json!({
"method": "account/login/completed",
"params": {
"loginId": Uuid::nil().to_string(),
"success": true,
"error": null,
},
}),
secondary: None,
});
serde_json::to_value(jsonrpc_notification)
.expect("ensure the notification serializes correctly"),
"ensure the notification serializes correctly"
);
}
#[test]
fn verify_account_rate_limits_notification_serialization() {
let notification =
ServerNotification::AccountRateLimitsUpdated(AccountRateLimitsUpdatedNotification {
rate_limits: RateLimitSnapshot {
primary: Some(RateLimitWindow {
used_percent: 25,
window_duration_mins: Some(15),
resets_at: Some(123),
}),
secondary: None,
credits: None,
},
});
let jsonrpc_notification = OutgoingMessage::AppServerNotification(notification);
assert_eq!(
json!({
"method": "account/rateLimits/updated",
"params": {
"primary": {
"used_percent": 25.0,
"window_minutes": 15,
"resets_at": 123,
},
"secondary": null,
"rateLimits": {
"primary": {
"usedPercent": 25,
"windowDurationMins": 15,
"resetsAt": 123
},
"secondary": null,
"credits": null
}
},
}),
serde_json::to_value(jsonrpc_notification)
.expect("ensure the notification serializes correctly"),
"ensure the notification serializes correctly"
);
}
#[test]
fn verify_account_updated_notification_serialization() {
let notification = ServerNotification::AccountUpdated(AccountUpdatedNotification {
auth_mode: Some(AuthMode::ApiKey),
});
let jsonrpc_notification = OutgoingMessage::AppServerNotification(notification);
assert_eq!(
json!({
"method": "account/updated",
"params": {
"authMode": "apikey"
},
}),
serde_json::to_value(jsonrpc_notification)

View File

@@ -1,7 +1,8 @@
[package]
edition = "2024"
name = "app_test_support"
version = { workspace = true }
version.workspace = true
edition.workspace = true
license.workspace = true
[lib]
path = "lib.rs"
@@ -13,6 +14,7 @@ base64 = { workspace = true }
chrono = { workspace = true }
codex-app-server-protocol = { workspace = true }
codex-core = { workspace = true }
codex-protocol = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true, features = [
@@ -21,4 +23,7 @@ tokio = { workspace = true, features = [
"process",
"rt-multi-thread",
] }
uuid = { workspace = true }
wiremock = { workspace = true }
core_test_support = { path = "../../../core/tests/common" }
shlex = { workspace = true }

View File

@@ -2,17 +2,23 @@ mod auth_fixtures;
mod mcp_process;
mod mock_model_server;
mod responses;
mod rollout;
pub use auth_fixtures::ChatGptAuthFixture;
pub use auth_fixtures::ChatGptIdTokenClaims;
pub use auth_fixtures::encode_id_token;
pub use auth_fixtures::write_chatgpt_auth;
use codex_app_server_protocol::JSONRPCResponse;
pub use core_test_support::format_with_current_shell;
pub use core_test_support::format_with_current_shell_display;
pub use mcp_process::McpProcess;
pub use mock_model_server::create_mock_chat_completions_server;
pub use mock_model_server::create_mock_chat_completions_server_unchecked;
pub use responses::create_apply_patch_sse_response;
pub use responses::create_exec_command_sse_response;
pub use responses::create_final_assistant_message_sse_response;
pub use responses::create_shell_sse_response;
pub use responses::create_shell_command_sse_response;
pub use rollout::create_fake_rollout;
use serde::de::DeserializeOwned;
pub fn to_response<T: DeserializeOwned>(response: JSONRPCResponse) -> anyhow::Result<T> {

View File

@@ -14,30 +14,41 @@ use anyhow::Context;
use assert_cmd::prelude::*;
use codex_app_server_protocol::AddConversationListenerParams;
use codex_app_server_protocol::ArchiveConversationParams;
use codex_app_server_protocol::CancelLoginAccountParams;
use codex_app_server_protocol::CancelLoginChatGptParams;
use codex_app_server_protocol::ClientInfo;
use codex_app_server_protocol::ClientNotification;
use codex_app_server_protocol::ConfigBatchWriteParams;
use codex_app_server_protocol::ConfigReadParams;
use codex_app_server_protocol::ConfigValueWriteParams;
use codex_app_server_protocol::FeedbackUploadParams;
use codex_app_server_protocol::GetAccountParams;
use codex_app_server_protocol::GetAuthStatusParams;
use codex_app_server_protocol::InitializeParams;
use codex_app_server_protocol::InterruptConversationParams;
use codex_app_server_protocol::ListConversationsParams;
use codex_app_server_protocol::ListModelsParams;
use codex_app_server_protocol::LoginApiKeyParams;
use codex_app_server_protocol::NewConversationParams;
use codex_app_server_protocol::RemoveConversationListenerParams;
use codex_app_server_protocol::ResumeConversationParams;
use codex_app_server_protocol::SendUserMessageParams;
use codex_app_server_protocol::SendUserTurnParams;
use codex_app_server_protocol::ServerRequest;
use codex_app_server_protocol::SetDefaultModelParams;
use codex_app_server_protocol::UploadFeedbackParams;
use codex_app_server_protocol::JSONRPCError;
use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::JSONRPCRequest;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::ListConversationsParams;
use codex_app_server_protocol::LoginApiKeyParams;
use codex_app_server_protocol::ModelListParams;
use codex_app_server_protocol::NewConversationParams;
use codex_app_server_protocol::RemoveConversationListenerParams;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ResumeConversationParams;
use codex_app_server_protocol::ReviewStartParams;
use codex_app_server_protocol::SendUserMessageParams;
use codex_app_server_protocol::SendUserTurnParams;
use codex_app_server_protocol::ServerRequest;
use codex_app_server_protocol::SetDefaultModelParams;
use codex_app_server_protocol::ThreadArchiveParams;
use codex_app_server_protocol::ThreadListParams;
use codex_app_server_protocol::ThreadResumeParams;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::TurnInterruptParams;
use codex_app_server_protocol::TurnStartParams;
use std::process::Command as StdCommand;
use tokio::process::Command;
@@ -243,10 +254,19 @@ impl McpProcess {
self.send_request("account/rateLimits/read", None).await
}
/// Send a `feedback/upload` JSON-RPC request.
pub async fn send_upload_feedback_request(
/// Send an `account/read` JSON-RPC request.
pub async fn send_get_account_request(
&mut self,
params: UploadFeedbackParams,
params: GetAccountParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("account/read", params).await
}
/// Send a `feedback/upload` JSON-RPC request.
pub async fn send_feedback_upload_request(
&mut self,
params: FeedbackUploadParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("feedback/upload", params).await
@@ -275,10 +295,46 @@ impl McpProcess {
self.send_request("listConversations", params).await
}
/// Send a `thread/start` JSON-RPC request.
pub async fn send_thread_start_request(
&mut self,
params: ThreadStartParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("thread/start", params).await
}
/// Send a `thread/resume` JSON-RPC request.
pub async fn send_thread_resume_request(
&mut self,
params: ThreadResumeParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("thread/resume", params).await
}
/// Send a `thread/archive` JSON-RPC request.
pub async fn send_thread_archive_request(
&mut self,
params: ThreadArchiveParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("thread/archive", params).await
}
/// Send a `thread/list` JSON-RPC request.
pub async fn send_thread_list_request(
&mut self,
params: ThreadListParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("thread/list", params).await
}
/// Send a `model/list` JSON-RPC request.
pub async fn send_list_models_request(
&mut self,
params: ListModelsParams,
params: ModelListParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("model/list", params).await
@@ -307,6 +363,33 @@ impl McpProcess {
self.send_request("loginChatGpt", None).await
}
/// Send a `turn/start` JSON-RPC request (v2).
pub async fn send_turn_start_request(
&mut self,
params: TurnStartParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("turn/start", params).await
}
/// Send a `turn/interrupt` JSON-RPC request (v2).
pub async fn send_turn_interrupt_request(
&mut self,
params: TurnInterruptParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("turn/interrupt", params).await
}
/// Send a `review/start` JSON-RPC request (v2).
pub async fn send_review_start_request(
&mut self,
params: ReviewStartParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("review/start", params).await
}
/// Send a `cancelLoginChatGpt` JSON-RPC request.
pub async fn send_cancel_login_chat_gpt_request(
&mut self,
@@ -321,6 +404,64 @@ impl McpProcess {
self.send_request("logoutChatGpt", None).await
}
pub async fn send_config_read_request(
&mut self,
params: ConfigReadParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("config/read", params).await
}
pub async fn send_config_value_write_request(
&mut self,
params: ConfigValueWriteParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("config/value/write", params).await
}
pub async fn send_config_batch_write_request(
&mut self,
params: ConfigBatchWriteParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("config/batchWrite", params).await
}
/// Send an `account/logout` JSON-RPC request.
pub async fn send_logout_account_request(&mut self) -> anyhow::Result<i64> {
self.send_request("account/logout", None).await
}
/// Send an `account/login/start` JSON-RPC request for API key login.
pub async fn send_login_account_api_key_request(
&mut self,
api_key: &str,
) -> anyhow::Result<i64> {
let params = serde_json::json!({
"type": "apiKey",
"apiKey": api_key,
});
self.send_request("account/login/start", Some(params)).await
}
/// Send an `account/login/start` JSON-RPC request for ChatGPT login.
pub async fn send_login_account_chatgpt_request(&mut self) -> anyhow::Result<i64> {
let params = serde_json::json!({
"type": "chatgpt"
});
self.send_request("account/login/start", Some(params)).await
}
/// Send an `account/login/cancel` JSON-RPC request.
pub async fn send_cancel_login_account_request(
&mut self,
params: CancelLoginAccountParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("account/login/cancel", params).await
}
/// Send a `fuzzyFileSearch` JSON-RPC request.
pub async fn send_fuzzy_file_search_request(
&mut self,

View File

@@ -29,6 +29,25 @@ pub async fn create_mock_chat_completions_server(responses: Vec<String>) -> Mock
server
}
/// Same as `create_mock_chat_completions_server` but does not enforce an
/// expectation on the number of calls.
pub async fn create_mock_chat_completions_server_unchecked(responses: Vec<String>) -> MockServer {
let server = MockServer::start().await;
let seq_responder = SeqResponder {
num_calls: AtomicUsize::new(0),
responses,
};
Mock::given(method("POST"))
.and(path("/v1/chat/completions"))
.respond_with(seq_responder)
.mount(&server)
.await;
server
}
struct SeqResponder {
num_calls: AtomicUsize,
responses: Vec<String>,

View File

@@ -1,17 +1,18 @@
use serde_json::json;
use std::path::Path;
pub fn create_shell_sse_response(
pub fn create_shell_command_sse_response(
command: Vec<String>,
workdir: Option<&Path>,
timeout_ms: Option<u64>,
call_id: &str,
) -> anyhow::Result<String> {
// The `arguments`` for the `shell` tool is a serialized JSON object.
// The `arguments` for the `shell_command` tool is a serialized JSON object.
let command_str = shlex::try_join(command.iter().map(String::as_str))?;
let tool_call_arguments = serde_json::to_string(&json!({
"command": command,
"command": command_str,
"workdir": workdir.map(|w| w.to_string_lossy()),
"timeout": timeout_ms
"timeout_ms": timeout_ms
}))?;
let tool_call = json!({
"choices": [
@@ -21,7 +22,7 @@ pub fn create_shell_sse_response(
{
"id": call_id,
"function": {
"name": "shell",
"name": "shell_command",
"arguments": tool_call_arguments
}
}
@@ -62,10 +63,10 @@ pub fn create_apply_patch_sse_response(
patch_content: &str,
call_id: &str,
) -> anyhow::Result<String> {
// Use shell command to call apply_patch with heredoc format
let shell_command = format!("apply_patch <<'EOF'\n{patch_content}\nEOF");
// Use shell_command to call apply_patch with heredoc format
let command = format!("apply_patch <<'EOF'\n{patch_content}\nEOF");
let tool_call_arguments = serde_json::to_string(&json!({
"command": ["bash", "-lc", shell_command]
"command": command
}))?;
let tool_call = json!({
@@ -76,7 +77,46 @@ pub fn create_apply_patch_sse_response(
{
"id": call_id,
"function": {
"name": "shell",
"name": "shell_command",
"arguments": tool_call_arguments
}
}
]
},
"finish_reason": "tool_calls"
}
]
});
let sse = format!(
"data: {}\n\ndata: DONE\n\n",
serde_json::to_string(&tool_call)?
);
Ok(sse)
}
pub fn create_exec_command_sse_response(call_id: &str) -> anyhow::Result<String> {
let (cmd, args) = if cfg!(windows) {
("cmd.exe", vec!["/d", "/c", "echo hi"])
} else {
("/bin/sh", vec!["-c", "echo hi"])
};
let command = std::iter::once(cmd.to_string())
.chain(args.into_iter().map(str::to_string))
.collect::<Vec<_>>();
let tool_call_arguments = serde_json::to_string(&json!({
"cmd": command.join(" "),
"yield_time_ms": 500
}))?;
let tool_call = json!({
"choices": [
{
"delta": {
"tool_calls": [
{
"id": call_id,
"function": {
"name": "exec_command",
"arguments": tool_call_arguments
}
}

View File

@@ -0,0 +1,89 @@
use anyhow::Result;
use codex_protocol::ConversationId;
use codex_protocol::protocol::GitInfo;
use codex_protocol::protocol::SessionMeta;
use codex_protocol::protocol::SessionMetaLine;
use codex_protocol::protocol::SessionSource;
use serde_json::json;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use uuid::Uuid;
/// Create a minimal rollout file under `CODEX_HOME/sessions/YYYY/MM/DD/`.
///
/// - `filename_ts` is the filename timestamp component in `YYYY-MM-DDThh-mm-ss` format.
/// - `meta_rfc3339` is the envelope timestamp used in JSON lines.
/// - `preview` is the user message preview text.
/// - `model_provider` optionally sets the provider in the session meta payload.
///
/// Returns the generated conversation/session UUID as a string.
pub fn create_fake_rollout(
codex_home: &Path,
filename_ts: &str,
meta_rfc3339: &str,
preview: &str,
model_provider: Option<&str>,
git_info: Option<GitInfo>,
) -> Result<String> {
let uuid = Uuid::new_v4();
let uuid_str = uuid.to_string();
let conversation_id = ConversationId::from_string(&uuid_str)?;
// sessions/YYYY/MM/DD derived from filename_ts (YYYY-MM-DDThh-mm-ss)
let year = &filename_ts[0..4];
let month = &filename_ts[5..7];
let day = &filename_ts[8..10];
let dir = codex_home.join("sessions").join(year).join(month).join(day);
fs::create_dir_all(&dir)?;
let file_path = dir.join(format!("rollout-{filename_ts}-{uuid}.jsonl"));
// Build JSONL lines
let meta = SessionMeta {
id: conversation_id,
timestamp: meta_rfc3339.to_string(),
cwd: PathBuf::from("/"),
originator: "codex".to_string(),
cli_version: "0.0.0".to_string(),
instructions: None,
source: SessionSource::Cli,
model_provider: model_provider.map(str::to_string),
};
let payload = serde_json::to_value(SessionMetaLine {
meta,
git: git_info,
})?;
let lines = [
json!({
"timestamp": meta_rfc3339,
"type": "session_meta",
"payload": payload
})
.to_string(),
json!({
"timestamp": meta_rfc3339,
"type":"response_item",
"payload": {
"type":"message",
"role":"user",
"content":[{"type":"input_text","text": preview}]
}
})
.to_string(),
json!({
"timestamp": meta_rfc3339,
"type":"event_msg",
"payload": {
"type":"user_message",
"message": preview,
"kind": "plain"
}
})
.to_string(),
];
fs::write(file_path, lines.join("\n") + "\n")?;
Ok(uuid_str)
}

View File

@@ -12,7 +12,7 @@ use std::path::Path;
use tempfile::TempDir;
use tokio::time::timeout;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(20);
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn archive_conversation_moves_rollout_into_archived_directory() -> Result<()> {

View File

@@ -2,7 +2,8 @@ use anyhow::Result;
use app_test_support::McpProcess;
use app_test_support::create_final_assistant_message_sse_response;
use app_test_support::create_mock_chat_completions_server;
use app_test_support::create_shell_sse_response;
use app_test_support::create_shell_command_sse_response;
use app_test_support::format_with_current_shell;
use app_test_support::to_response;
use codex_app_server_protocol::AddConversationListenerParams;
use codex_app_server_protocol::AddConversationSubscriptionResponse;
@@ -56,7 +57,7 @@ async fn test_codex_jsonrpc_conversation_flow() -> Result<()> {
// Create a mock model server that immediately ends each turn.
// Two turns are expected: initial session configure + one user message.
let responses = vec![
create_shell_sse_response(
create_shell_command_sse_response(
vec!["ls".to_string()],
Some(&working_directory),
Some(5000),
@@ -175,7 +176,7 @@ async fn test_send_user_turn_changes_approval_policy_behavior() -> Result<()> {
// Mock server will request a python shell call for the first and second turn, then finish.
let responses = vec![
create_shell_sse_response(
create_shell_command_sse_response(
vec![
"python3".to_string(),
"-c".to_string(),
@@ -186,7 +187,7 @@ async fn test_send_user_turn_changes_approval_policy_behavior() -> Result<()> {
"call1",
)?,
create_final_assistant_message_sse_response("done 1")?,
create_shell_sse_response(
create_shell_command_sse_response(
vec![
"python3".to_string(),
"-c".to_string(),
@@ -267,11 +268,7 @@ async fn test_send_user_turn_changes_approval_policy_behavior() -> Result<()> {
ExecCommandApprovalParams {
conversation_id,
call_id: "call1".to_string(),
command: vec![
"python3".to_string(),
"-c".to_string(),
"print(42)".to_string(),
],
command: format_with_current_shell("python3 -c 'print(42)'"),
cwd: working_directory.clone(),
reason: None,
risk: None,
@@ -353,23 +350,15 @@ async fn test_send_user_turn_updates_sandbox_and_cwd_between_turns() -> Result<(
std::fs::create_dir(&second_cwd)?;
let responses = vec![
create_shell_sse_response(
vec![
"bash".to_string(),
"-lc".to_string(),
"echo first turn".to_string(),
],
create_shell_command_sse_response(
vec!["echo".to_string(), "first".to_string(), "turn".to_string()],
None,
Some(5000),
"call-first",
)?,
create_final_assistant_message_sse_response("done first")?,
create_shell_sse_response(
vec![
"bash".to_string(),
"-lc".to_string(),
"echo second turn".to_string(),
],
create_shell_command_sse_response(
vec!["echo".to_string(), "second".to_string(), "turn".to_string()],
None,
Some(5000),
"call-second",
@@ -481,13 +470,9 @@ async fn test_send_user_turn_updates_sandbox_and_cwd_between_turns() -> Result<(
exec_begin.cwd, second_cwd,
"exec turn should run from updated cwd"
);
let expected_command = format_with_current_shell("echo second turn");
assert_eq!(
exec_begin.command,
vec![
"bash".to_string(),
"-lc".to_string(),
"echo second turn".to_string()
],
exec_begin.command, expected_command,
"exec turn should run expected command"
);

View File

@@ -27,7 +27,7 @@ fn create_config_toml(codex_home: &Path) -> std::io::Result<()> {
std::fs::write(
config_toml,
r#"
model = "gpt-5-codex"
model = "gpt-5.1-codex-max"
approval_policy = "on-request"
sandbox_mode = "workspace-write"
model_reasoning_summary = "detailed"
@@ -87,7 +87,7 @@ async fn get_config_toml_parses_all_fields() -> Result<()> {
}),
forced_chatgpt_workspace_id: Some("12345678-0000-0000-0000-000000000000".into()),
forced_login_method: Some(ForcedLoginMethod::Chatgpt),
model: Some("gpt-5-codex".into()),
model: Some("gpt-5.1-codex-max".into()),
model_reasoning_effort: Some(ReasoningEffort::High),
model_reasoning_summary: Some(ReasoningSummary::Detailed),
model_verbosity: Some(Verbosity::Medium),

View File

@@ -19,7 +19,7 @@ use tokio::time::timeout;
use app_test_support::McpProcess;
use app_test_support::create_mock_chat_completions_server;
use app_test_support::create_shell_sse_response;
use app_test_support::create_shell_command_sse_response;
use app_test_support::to_response;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
@@ -56,7 +56,7 @@ async fn shell_command_interruption() -> anyhow::Result<()> {
std::fs::create_dir(&working_directory)?;
// Create mock server with a single SSE response: the long sleep command
let server = create_mock_chat_completions_server(vec![create_shell_sse_response(
let server = create_mock_chat_completions_server(vec![create_shell_command_sse_response(
shell_command.clone(),
Some(&working_directory),
Some(10_000), // 10 seconds timeout in ms
@@ -146,7 +146,7 @@ fn create_config_toml(codex_home: &Path, server_uri: String) -> std::io::Result<
r#"
model = "mock-model"
approval_policy = "never"
sandbox_mode = "danger-full-access"
sandbox_mode = "read-only"
model_provider = "mock_provider"

View File

@@ -1,5 +1,6 @@
use anyhow::Result;
use app_test_support::McpProcess;
use app_test_support::create_fake_rollout;
use app_test_support::to_response;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::JSONRPCResponse;
@@ -15,12 +16,8 @@ use codex_core::protocol::EventMsg;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::fs;
use std::path::Path;
use tempfile::TempDir;
use tokio::time::timeout;
use uuid::Uuid;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
@@ -34,6 +31,7 @@ async fn test_list_and_resume_conversations() -> Result<()> {
"2025-01-02T12:00:00Z",
"Hello A",
Some("openai"),
None,
)?;
create_fake_rollout(
codex_home.path(),
@@ -41,6 +39,7 @@ async fn test_list_and_resume_conversations() -> Result<()> {
"2025-01-01T13:00:00Z",
"Hello B",
Some("openai"),
None,
)?;
create_fake_rollout(
codex_home.path(),
@@ -48,6 +47,7 @@ async fn test_list_and_resume_conversations() -> Result<()> {
"2025-01-01T12:00:00Z",
"Hello C",
None,
None,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
@@ -108,6 +108,7 @@ async fn test_list_and_resume_conversations() -> Result<()> {
"2025-01-01T11:30:00Z",
"Hello TP",
Some("test-provider"),
None,
)?;
// Filtering by model provider should return only matching sessions.
@@ -357,70 +358,3 @@ async fn test_list_and_resume_conversations() -> Result<()> {
Ok(())
}
fn create_fake_rollout(
codex_home: &Path,
filename_ts: &str,
meta_rfc3339: &str,
preview: &str,
model_provider: Option<&str>,
) -> Result<()> {
let uuid = Uuid::new_v4();
// sessions/YYYY/MM/DD/ derived from filename_ts (YYYY-MM-DDThh-mm-ss)
let year = &filename_ts[0..4];
let month = &filename_ts[5..7];
let day = &filename_ts[8..10];
let dir = codex_home.join("sessions").join(year).join(month).join(day);
fs::create_dir_all(&dir)?;
let file_path = dir.join(format!("rollout-{filename_ts}-{uuid}.jsonl"));
let mut lines = Vec::new();
// Meta line with timestamp (flattened meta in payload for new schema)
let mut payload = json!({
"id": uuid,
"timestamp": meta_rfc3339,
"cwd": "/",
"originator": "codex",
"cli_version": "0.0.0",
"instructions": null,
});
if let Some(provider) = model_provider {
payload["model_provider"] = json!(provider);
}
lines.push(
json!({
"timestamp": meta_rfc3339,
"type": "session_meta",
"payload": payload
})
.to_string(),
);
// Minimal user message entry as a persisted response item (with envelope timestamp)
lines.push(
json!({
"timestamp": meta_rfc3339,
"type":"response_item",
"payload": {
"type":"message",
"role":"user",
"content":[{"type":"input_text","text": preview}]
}
})
.to_string(),
);
// Add a matching user message event line to satisfy filters
lines.push(
json!({
"timestamp": meta_rfc3339,
"type":"event_msg",
"payload": {
"type":"user_message",
"message": preview,
"kind": "plain"
}
})
.to_string(),
);
fs::write(file_path, lines.join("\n") + "\n")?;
Ok(())
}

View File

@@ -7,9 +7,8 @@ mod fuzzy_file_search;
mod interrupt;
mod list_resume;
mod login;
mod model_list;
mod rate_limits;
mod send_message;
mod set_default_model;
mod user_agent;
mod user_info;
mod v2;

View File

@@ -272,40 +272,45 @@ async fn read_raw_response_item(
mcp: &mut McpProcess,
conversation_id: ConversationId,
) -> ResponseItem {
let raw_notification: JSONRPCNotification = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("codex/event/raw_response_item"),
)
.await
.expect("codex/event/raw_response_item notification timeout")
.expect("codex/event/raw_response_item notification resp");
loop {
let raw_notification: JSONRPCNotification = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("codex/event/raw_response_item"),
)
.await
.expect("codex/event/raw_response_item notification timeout")
.expect("codex/event/raw_response_item notification resp");
let serde_json::Value::Object(params) = raw_notification
.params
.expect("codex/event/raw_response_item should have params")
else {
panic!("codex/event/raw_response_item should have params");
};
let serde_json::Value::Object(params) = raw_notification
.params
.expect("codex/event/raw_response_item should have params")
else {
panic!("codex/event/raw_response_item should have params");
};
let conversation_id_value = params
.get("conversationId")
.and_then(|value| value.as_str())
.expect("raw response item should include conversationId");
let conversation_id_value = params
.get("conversationId")
.and_then(|value| value.as_str())
.expect("raw response item should include conversationId");
assert_eq!(
conversation_id_value,
conversation_id.to_string(),
"raw response item conversation mismatch"
);
assert_eq!(
conversation_id_value,
conversation_id.to_string(),
"raw response item conversation mismatch"
);
let msg_value = params
.get("msg")
.cloned()
.expect("raw response item should include msg payload");
let msg_value = params
.get("msg")
.cloned()
.expect("raw response item should include msg payload");
let event: RawResponseItemEvent =
serde_json::from_value(msg_value).expect("deserialize raw response item");
event.item
// Ghost snapshots are produced concurrently and may arrive before the model reply.
let event: RawResponseItemEvent =
serde_json::from_value(msg_value).expect("deserialize raw response item");
if !matches!(event.item, ResponseItem::GhostSnapshot { .. }) {
return event.item;
}
}
}
fn assert_instructions_message(item: &ResponseItem) {

View File

@@ -57,7 +57,7 @@ fn create_config_toml(codex_home: &Path) -> std::io::Result<()> {
std::fs::write(
config_toml,
r#"
model = "gpt-5-codex"
model = "gpt-5.1-codex-max"
model_reasoning_effort = "medium"
"#,
)

View File

@@ -0,0 +1,492 @@
use anyhow::Result;
use anyhow::bail;
use app_test_support::McpProcess;
use app_test_support::to_response;
use app_test_support::ChatGptAuthFixture;
use app_test_support::write_chatgpt_auth;
use codex_app_server_protocol::Account;
use codex_app_server_protocol::AuthMode;
use codex_app_server_protocol::CancelLoginAccountParams;
use codex_app_server_protocol::CancelLoginAccountResponse;
use codex_app_server_protocol::GetAccountParams;
use codex_app_server_protocol::GetAccountResponse;
use codex_app_server_protocol::JSONRPCError;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::LoginAccountResponse;
use codex_app_server_protocol::LogoutAccountResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ServerNotification;
use codex_core::auth::AuthCredentialsStoreMode;
use codex_login::login_with_api_key;
use codex_protocol::account::PlanType as AccountPlanType;
use pretty_assertions::assert_eq;
use serial_test::serial;
use std::path::Path;
use std::time::Duration;
use tempfile::TempDir;
use tokio::time::timeout;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
// Helper to create a minimal config.toml for the app server
#[derive(Default)]
struct CreateConfigTomlParams {
forced_method: Option<String>,
forced_workspace_id: Option<String>,
requires_openai_auth: Option<bool>,
}
fn create_config_toml(codex_home: &Path, params: CreateConfigTomlParams) -> std::io::Result<()> {
let config_toml = codex_home.join("config.toml");
let forced_line = if let Some(method) = params.forced_method {
format!("forced_login_method = \"{method}\"\n")
} else {
String::new()
};
let forced_workspace_line = if let Some(ws) = params.forced_workspace_id {
format!("forced_chatgpt_workspace_id = \"{ws}\"\n")
} else {
String::new()
};
let requires_line = match params.requires_openai_auth {
Some(true) => "requires_openai_auth = true\n".to_string(),
Some(false) => String::new(),
None => String::new(),
};
let contents = format!(
r#"
model = "mock-model"
approval_policy = "never"
sandbox_mode = "danger-full-access"
{forced_line}
{forced_workspace_line}
model_provider = "mock_provider"
[model_providers.mock_provider]
name = "Mock provider for test"
base_url = "http://127.0.0.1:0/v1"
wire_api = "chat"
request_max_retries = 0
stream_max_retries = 0
{requires_line}
"#
);
std::fs::write(config_toml, contents)
}
#[tokio::test]
async fn logout_account_removes_auth_and_notifies() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?;
login_with_api_key(
codex_home.path(),
"sk-test-key",
AuthCredentialsStoreMode::File,
)?;
assert!(codex_home.path().join("auth.json").exists());
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let id = mcp.send_logout_account_request().await?;
let resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(id)),
)
.await??;
let _ok: LogoutAccountResponse = to_response(resp)?;
let note = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("account/updated"),
)
.await??;
let parsed: ServerNotification = note.try_into()?;
let ServerNotification::AccountUpdated(payload) = parsed else {
bail!("unexpected notification: {parsed:?}");
};
assert!(
payload.auth_mode.is_none(),
"auth_method should be None after logout"
);
assert!(
!codex_home.path().join("auth.json").exists(),
"auth.json should be deleted"
);
let get_id = mcp
.send_get_account_request(GetAccountParams {
refresh_token: false,
})
.await?;
let get_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(get_id)),
)
.await??;
let account: GetAccountResponse = to_response(get_resp)?;
assert_eq!(account.account, None);
Ok(())
}
#[tokio::test]
async fn login_account_api_key_succeeds_and_notifies() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let req_id = mcp
.send_login_account_api_key_request("sk-test-key")
.await?;
let resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(req_id)),
)
.await??;
let login: LoginAccountResponse = to_response(resp)?;
assert_eq!(login, LoginAccountResponse::ApiKey {});
let note = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("account/login/completed"),
)
.await??;
let parsed: ServerNotification = note.try_into()?;
let ServerNotification::AccountLoginCompleted(payload) = parsed else {
bail!("unexpected notification: {parsed:?}");
};
pretty_assertions::assert_eq!(payload.login_id, None);
pretty_assertions::assert_eq!(payload.success, true);
pretty_assertions::assert_eq!(payload.error, None);
let note = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("account/updated"),
)
.await??;
let parsed: ServerNotification = note.try_into()?;
let ServerNotification::AccountUpdated(payload) = parsed else {
bail!("unexpected notification: {parsed:?}");
};
pretty_assertions::assert_eq!(payload.auth_mode, Some(AuthMode::ApiKey));
assert!(codex_home.path().join("auth.json").exists());
Ok(())
}
#[tokio::test]
async fn login_account_api_key_rejected_when_forced_chatgpt() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(
codex_home.path(),
CreateConfigTomlParams {
forced_method: Some("chatgpt".to_string()),
..Default::default()
},
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_login_account_api_key_request("sk-test-key")
.await?;
let err: JSONRPCError = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert_eq!(
err.error.message,
"API key login is disabled. Use ChatGPT login instead."
);
Ok(())
}
#[tokio::test]
async fn login_account_chatgpt_rejected_when_forced_api() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(
codex_home.path(),
CreateConfigTomlParams {
forced_method: Some("api".to_string()),
..Default::default()
},
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp.send_login_account_chatgpt_request().await?;
let err: JSONRPCError = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert_eq!(
err.error.message,
"ChatGPT login is disabled. Use API key login instead."
);
Ok(())
}
#[tokio::test]
// Serialize tests that launch the login server since it binds to a fixed port.
#[serial(login_port)]
async fn login_account_chatgpt_start() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp.send_login_account_chatgpt_request().await?;
let resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let login: LoginAccountResponse = to_response(resp)?;
let LoginAccountResponse::Chatgpt { login_id, auth_url } = login else {
bail!("unexpected login response: {login:?}");
};
assert!(
auth_url.contains("redirect_uri=http%3A%2F%2Flocalhost"),
"auth_url should contain a redirect_uri to localhost"
);
let cancel_id = mcp
.send_cancel_login_account_request(CancelLoginAccountParams {
login_id: login_id.clone(),
})
.await?;
let cancel_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(cancel_id)),
)
.await??;
let _ok: CancelLoginAccountResponse = to_response(cancel_resp)?;
let note = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("account/login/completed"),
)
.await??;
let parsed: ServerNotification = note.try_into()?;
let ServerNotification::AccountLoginCompleted(payload) = parsed else {
bail!("unexpected notification: {parsed:?}");
};
pretty_assertions::assert_eq!(payload.login_id, Some(login_id));
pretty_assertions::assert_eq!(payload.success, false);
assert!(
payload.error.is_some(),
"expected a non-empty error on cancel"
);
let maybe_updated = timeout(
Duration::from_millis(500),
mcp.read_stream_until_notification_message("account/updated"),
)
.await;
assert!(
maybe_updated.is_err(),
"account/updated should not be emitted when login is cancelled"
);
Ok(())
}
#[tokio::test]
// Serialize tests that launch the login server since it binds to a fixed port.
#[serial(login_port)]
async fn login_account_chatgpt_includes_forced_workspace_query_param() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(
codex_home.path(),
CreateConfigTomlParams {
forced_workspace_id: Some("ws-forced".to_string()),
..Default::default()
},
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp.send_login_account_chatgpt_request().await?;
let resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let login: LoginAccountResponse = to_response(resp)?;
let LoginAccountResponse::Chatgpt { auth_url, .. } = login else {
bail!("unexpected login response: {login:?}");
};
assert!(
auth_url.contains("allowed_workspace_id=ws-forced"),
"auth URL should include forced workspace"
);
Ok(())
}
#[tokio::test]
async fn get_account_no_auth() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(
codex_home.path(),
CreateConfigTomlParams {
requires_openai_auth: Some(true),
..Default::default()
},
)?;
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let params = GetAccountParams {
refresh_token: false,
};
let request_id = mcp.send_get_account_request(params).await?;
let resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let account: GetAccountResponse = to_response(resp)?;
assert_eq!(account.account, None, "expected no account");
assert_eq!(account.requires_openai_auth, true);
Ok(())
}
#[tokio::test]
async fn get_account_with_api_key() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(
codex_home.path(),
CreateConfigTomlParams {
requires_openai_auth: Some(true),
..Default::default()
},
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let req_id = mcp
.send_login_account_api_key_request("sk-test-key")
.await?;
let resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(req_id)),
)
.await??;
let _login_ok = to_response::<LoginAccountResponse>(resp)?;
let params = GetAccountParams {
refresh_token: false,
};
let request_id = mcp.send_get_account_request(params).await?;
let resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let received: GetAccountResponse = to_response(resp)?;
let expected = GetAccountResponse {
account: Some(Account::ApiKey {}),
requires_openai_auth: true,
};
assert_eq!(received, expected);
Ok(())
}
#[tokio::test]
async fn get_account_when_auth_not_required() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(
codex_home.path(),
CreateConfigTomlParams {
requires_openai_auth: Some(false),
..Default::default()
},
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let params = GetAccountParams {
refresh_token: false,
};
let request_id = mcp.send_get_account_request(params).await?;
let resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let received: GetAccountResponse = to_response(resp)?;
let expected = GetAccountResponse {
account: None,
requires_openai_auth: false,
};
assert_eq!(received, expected);
Ok(())
}
#[tokio::test]
async fn get_account_with_chatgpt() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(
codex_home.path(),
CreateConfigTomlParams {
requires_openai_auth: Some(true),
..Default::default()
},
)?;
write_chatgpt_auth(
codex_home.path(),
ChatGptAuthFixture::new("access-chatgpt")
.email("user@example.com")
.plan_type("pro"),
AuthCredentialsStoreMode::File,
)?;
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let params = GetAccountParams {
refresh_token: false,
};
let request_id = mcp.send_get_account_request(params).await?;
let resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let received: GetAccountResponse = to_response(resp)?;
let expected = GetAccountResponse {
account: Some(Account::Chatgpt {
email: "user@example.com".to_string(),
plan_type: AccountPlanType::Pro,
}),
requires_openai_auth: true,
};
assert_eq!(received, expected);
Ok(())
}

View File

@@ -0,0 +1,347 @@
use anyhow::Result;
use app_test_support::McpProcess;
use app_test_support::to_response;
use codex_app_server_protocol::ConfigBatchWriteParams;
use codex_app_server_protocol::ConfigEdit;
use codex_app_server_protocol::ConfigLayerName;
use codex_app_server_protocol::ConfigReadParams;
use codex_app_server_protocol::ConfigReadResponse;
use codex_app_server_protocol::ConfigValueWriteParams;
use codex_app_server_protocol::ConfigWriteResponse;
use codex_app_server_protocol::JSONRPCError;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::MergeStrategy;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::WriteStatus;
use pretty_assertions::assert_eq;
use serde_json::json;
use tempfile::TempDir;
use tokio::time::timeout;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
fn write_config(codex_home: &TempDir, contents: &str) -> Result<()> {
Ok(std::fs::write(
codex_home.path().join("config.toml"),
contents,
)?)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_read_returns_effective_and_layers() -> Result<()> {
let codex_home = TempDir::new()?;
write_config(
&codex_home,
r#"
model = "gpt-user"
sandbox_mode = "workspace-write"
"#,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_config_read_request(ConfigReadParams {
include_layers: true,
})
.await?;
let resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let ConfigReadResponse {
config,
origins,
layers,
} = to_response(resp)?;
assert_eq!(config.get("model"), Some(&json!("gpt-user")));
assert_eq!(
origins.get("model").expect("origin").name,
ConfigLayerName::User
);
let layers = layers.expect("layers present");
assert_eq!(layers.len(), 2);
assert_eq!(layers[0].name, ConfigLayerName::SessionFlags);
assert_eq!(layers[1].name, ConfigLayerName::User);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_read_includes_system_layer_and_overrides() -> Result<()> {
let codex_home = TempDir::new()?;
write_config(
&codex_home,
r#"
model = "gpt-user"
approval_policy = "on-request"
sandbox_mode = "workspace-write"
[sandbox_workspace_write]
writable_roots = ["/user"]
network_access = true
"#,
)?;
let managed_path = codex_home.path().join("managed_config.toml");
std::fs::write(
&managed_path,
r#"
model = "gpt-system"
approval_policy = "never"
[sandbox_workspace_write]
writable_roots = ["/system"]
"#,
)?;
let managed_path_str = managed_path.display().to_string();
let mut mcp = McpProcess::new_with_env(
codex_home.path(),
&[("CODEX_MANAGED_CONFIG_PATH", Some(&managed_path_str))],
)
.await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_config_read_request(ConfigReadParams {
include_layers: true,
})
.await?;
let resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let ConfigReadResponse {
config,
origins,
layers,
} = to_response(resp)?;
assert_eq!(config.get("model"), Some(&json!("gpt-system")));
assert_eq!(
origins.get("model").expect("origin").name,
ConfigLayerName::System
);
assert_eq!(config.get("approval_policy"), Some(&json!("never")));
assert_eq!(
origins.get("approval_policy").expect("origin").name,
ConfigLayerName::System
);
assert_eq!(config.get("sandbox_mode"), Some(&json!("workspace-write")));
assert_eq!(
origins.get("sandbox_mode").expect("origin").name,
ConfigLayerName::User
);
assert_eq!(
config
.get("sandbox_workspace_write")
.and_then(|v| v.get("writable_roots")),
Some(&json!(["/system"]))
);
assert_eq!(
origins
.get("sandbox_workspace_write.writable_roots.0")
.expect("origin")
.name,
ConfigLayerName::System
);
assert_eq!(
config
.get("sandbox_workspace_write")
.and_then(|v| v.get("network_access")),
Some(&json!(true))
);
assert_eq!(
origins
.get("sandbox_workspace_write.network_access")
.expect("origin")
.name,
ConfigLayerName::User
);
let layers = layers.expect("layers present");
assert_eq!(layers.len(), 3);
assert_eq!(layers[0].name, ConfigLayerName::System);
assert_eq!(layers[1].name, ConfigLayerName::SessionFlags);
assert_eq!(layers[2].name, ConfigLayerName::User);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_value_write_replaces_value() -> Result<()> {
let codex_home = TempDir::new()?;
write_config(
&codex_home,
r#"
model = "gpt-old"
"#,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let read_id = mcp
.send_config_read_request(ConfigReadParams {
include_layers: false,
})
.await?;
let read_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(read_id)),
)
.await??;
let read: ConfigReadResponse = to_response(read_resp)?;
let expected_version = read.origins.get("model").map(|m| m.version.clone());
let write_id = mcp
.send_config_value_write_request(ConfigValueWriteParams {
file_path: codex_home.path().join("config.toml").display().to_string(),
key_path: "model".to_string(),
value: json!("gpt-new"),
merge_strategy: MergeStrategy::Replace,
expected_version,
})
.await?;
let write_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(write_id)),
)
.await??;
let write: ConfigWriteResponse = to_response(write_resp)?;
assert_eq!(write.status, WriteStatus::Ok);
assert!(write.overridden_metadata.is_none());
let verify_id = mcp
.send_config_read_request(ConfigReadParams {
include_layers: false,
})
.await?;
let verify_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(verify_id)),
)
.await??;
let verify: ConfigReadResponse = to_response(verify_resp)?;
assert_eq!(verify.config.get("model"), Some(&json!("gpt-new")));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_value_write_rejects_version_conflict() -> Result<()> {
let codex_home = TempDir::new()?;
write_config(
&codex_home,
r#"
model = "gpt-old"
"#,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let write_id = mcp
.send_config_value_write_request(ConfigValueWriteParams {
file_path: codex_home.path().join("config.toml").display().to_string(),
key_path: "model".to_string(),
value: json!("gpt-new"),
merge_strategy: MergeStrategy::Replace,
expected_version: Some("sha256:stale".to_string()),
})
.await?;
let err: JSONRPCError = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(write_id)),
)
.await??;
let code = err
.error
.data
.as_ref()
.and_then(|d| d.get("config_write_error_code"))
.and_then(|v| v.as_str());
assert_eq!(code, Some("configVersionConflict"));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_batch_write_applies_multiple_edits() -> Result<()> {
let codex_home = TempDir::new()?;
write_config(&codex_home, "")?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let batch_id = mcp
.send_config_batch_write_request(ConfigBatchWriteParams {
file_path: codex_home.path().join("config.toml").display().to_string(),
edits: vec![
ConfigEdit {
key_path: "sandbox_mode".to_string(),
value: json!("workspace-write"),
merge_strategy: MergeStrategy::Replace,
},
ConfigEdit {
key_path: "sandbox_workspace_write".to_string(),
value: json!({
"writable_roots": ["/tmp"],
"network_access": false
}),
merge_strategy: MergeStrategy::Replace,
},
],
expected_version: None,
})
.await?;
let batch_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(batch_id)),
)
.await??;
let batch_write: ConfigWriteResponse = to_response(batch_resp)?;
assert_eq!(batch_write.status, WriteStatus::Ok);
let read_id = mcp
.send_config_read_request(ConfigReadParams {
include_layers: false,
})
.await?;
let read_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(read_id)),
)
.await??;
let read: ConfigReadResponse = to_response(read_resp)?;
assert_eq!(
read.config.get("sandbox_mode"),
Some(&json!("workspace-write"))
);
assert_eq!(
read.config
.get("sandbox_workspace_write")
.and_then(|v| v.get("writable_roots")),
Some(&json!(["/tmp"]))
);
assert_eq!(
read.config
.get("sandbox_workspace_write")
.and_then(|v| v.get("network_access")),
Some(&json!(false))
);
Ok(())
}

View File

@@ -0,0 +1,11 @@
mod account;
mod config_rpc;
mod model_list;
mod rate_limits;
mod review;
mod thread_archive;
mod thread_list;
mod thread_resume;
mod thread_start;
mod turn_interrupt;
mod turn_start;

View File

@@ -6,9 +6,9 @@ use app_test_support::McpProcess;
use app_test_support::to_response;
use codex_app_server_protocol::JSONRPCError;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::ListModelsParams;
use codex_app_server_protocol::ListModelsResponse;
use codex_app_server_protocol::Model;
use codex_app_server_protocol::ModelListParams;
use codex_app_server_protocol::ModelListResponse;
use codex_app_server_protocol::ReasoningEffortOption;
use codex_app_server_protocol::RequestId;
use codex_protocol::config_types::ReasoningEffort;
@@ -19,7 +19,7 @@ use tokio::time::timeout;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[tokio::test]
async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
let codex_home = TempDir::new()?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
@@ -27,8 +27,8 @@ async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_list_models_request(ListModelsParams {
page_size: Some(100),
.send_list_models_request(ModelListParams {
limit: Some(100),
cursor: None,
})
.await?;
@@ -39,14 +39,44 @@ async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
)
.await??;
let ListModelsResponse { items, next_cursor } = to_response::<ListModelsResponse>(response)?;
let ModelListResponse {
data: items,
next_cursor,
} = to_response::<ModelListResponse>(response)?;
let expected_models = vec![
Model {
id: "gpt-5-codex".to_string(),
model: "gpt-5-codex".to_string(),
display_name: "gpt-5-codex".to_string(),
description: "Optimized for coding tasks with many tools.".to_string(),
id: "gpt-5.1-codex-max".to_string(),
model: "gpt-5.1-codex-max".to_string(),
display_name: "gpt-5.1-codex-max".to_string(),
description: "Latest Codex-optimized flagship for deep and fast reasoning.".to_string(),
supported_reasoning_efforts: vec![
ReasoningEffortOption {
reasoning_effort: ReasoningEffort::Low,
description: "Fast responses with lighter reasoning".to_string(),
},
ReasoningEffortOption {
reasoning_effort: ReasoningEffort::Medium,
description: "Balances speed and reasoning depth for everyday tasks"
.to_string(),
},
ReasoningEffortOption {
reasoning_effort: ReasoningEffort::High,
description: "Maximizes reasoning depth for complex problems".to_string(),
},
ReasoningEffortOption {
reasoning_effort: ReasoningEffort::XHigh,
description: "Extra high reasoning depth for complex problems".to_string(),
},
],
default_reasoning_effort: ReasoningEffort::Medium,
is_default: true,
},
Model {
id: "gpt-5.1-codex".to_string(),
model: "gpt-5.1-codex".to_string(),
display_name: "gpt-5.1-codex".to_string(),
description: "Optimized for codex.".to_string(),
supported_reasoning_efforts: vec![
ReasoningEffortOption {
reasoning_effort: ReasoningEffort::Low,
@@ -63,18 +93,33 @@ async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
},
],
default_reasoning_effort: ReasoningEffort::Medium,
is_default: true,
is_default: false,
},
Model {
id: "gpt-5".to_string(),
model: "gpt-5".to_string(),
display_name: "gpt-5".to_string(),
description: "Broad world knowledge with strong general reasoning.".to_string(),
id: "gpt-5.1-codex-mini".to_string(),
model: "gpt-5.1-codex-mini".to_string(),
display_name: "gpt-5.1-codex-mini".to_string(),
description: "Optimized for codex. Cheaper, faster, but less capable.".to_string(),
supported_reasoning_efforts: vec![
ReasoningEffortOption {
reasoning_effort: ReasoningEffort::Minimal,
description: "Fastest responses with little reasoning".to_string(),
reasoning_effort: ReasoningEffort::Medium,
description: "Dynamically adjusts reasoning based on the task".to_string(),
},
ReasoningEffortOption {
reasoning_effort: ReasoningEffort::High,
description: "Maximizes reasoning depth for complex or ambiguous problems"
.to_string(),
},
],
default_reasoning_effort: ReasoningEffort::Medium,
is_default: false,
},
Model {
id: "gpt-5.1".to_string(),
model: "gpt-5.1".to_string(),
display_name: "gpt-5.1".to_string(),
description: "Broad world knowledge with strong general reasoning.".to_string(),
supported_reasoning_efforts: vec![
ReasoningEffortOption {
reasoning_effort: ReasoningEffort::Low,
description: "Balances speed with some reasoning; useful for straightforward \
@@ -103,7 +148,7 @@ async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[tokio::test]
async fn list_models_pagination_works() -> Result<()> {
let codex_home = TempDir::new()?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
@@ -111,8 +156,8 @@ async fn list_models_pagination_works() -> Result<()> {
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let first_request = mcp
.send_list_models_request(ListModelsParams {
page_size: Some(1),
.send_list_models_request(ModelListParams {
limit: Some(1),
cursor: None,
})
.await?;
@@ -123,18 +168,18 @@ async fn list_models_pagination_works() -> Result<()> {
)
.await??;
let ListModelsResponse {
items: first_items,
let ModelListResponse {
data: first_items,
next_cursor: first_cursor,
} = to_response::<ListModelsResponse>(first_response)?;
} = to_response::<ModelListResponse>(first_response)?;
assert_eq!(first_items.len(), 1);
assert_eq!(first_items[0].id, "gpt-5-codex");
assert_eq!(first_items[0].id, "gpt-5.1-codex-max");
let next_cursor = first_cursor.ok_or_else(|| anyhow!("cursor for second page"))?;
let second_request = mcp
.send_list_models_request(ListModelsParams {
page_size: Some(1),
.send_list_models_request(ModelListParams {
limit: Some(1),
cursor: Some(next_cursor.clone()),
})
.await?;
@@ -145,18 +190,62 @@ async fn list_models_pagination_works() -> Result<()> {
)
.await??;
let ListModelsResponse {
items: second_items,
let ModelListResponse {
data: second_items,
next_cursor: second_cursor,
} = to_response::<ListModelsResponse>(second_response)?;
} = to_response::<ModelListResponse>(second_response)?;
assert_eq!(second_items.len(), 1);
assert_eq!(second_items[0].id, "gpt-5");
assert!(second_cursor.is_none());
assert_eq!(second_items[0].id, "gpt-5.1-codex");
let third_cursor = second_cursor.ok_or_else(|| anyhow!("cursor for third page"))?;
let third_request = mcp
.send_list_models_request(ModelListParams {
limit: Some(1),
cursor: Some(third_cursor.clone()),
})
.await?;
let third_response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(third_request)),
)
.await??;
let ModelListResponse {
data: third_items,
next_cursor: third_cursor,
} = to_response::<ModelListResponse>(third_response)?;
assert_eq!(third_items.len(), 1);
assert_eq!(third_items[0].id, "gpt-5.1-codex-mini");
let fourth_cursor = third_cursor.ok_or_else(|| anyhow!("cursor for fourth page"))?;
let fourth_request = mcp
.send_list_models_request(ModelListParams {
limit: Some(1),
cursor: Some(fourth_cursor.clone()),
})
.await?;
let fourth_response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(fourth_request)),
)
.await??;
let ModelListResponse {
data: fourth_items,
next_cursor: fourth_cursor,
} = to_response::<ModelListResponse>(fourth_response)?;
assert_eq!(fourth_items.len(), 1);
assert_eq!(fourth_items[0].id, "gpt-5.1");
assert!(fourth_cursor.is_none());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[tokio::test]
async fn list_models_rejects_invalid_cursor() -> Result<()> {
let codex_home = TempDir::new()?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
@@ -164,8 +253,8 @@ async fn list_models_rejects_invalid_cursor() -> Result<()> {
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_list_models_request(ListModelsParams {
page_size: None,
.send_list_models_request(ModelListParams {
limit: None,
cursor: Some("invalid".to_string()),
})
.await?;

View File

@@ -7,10 +7,10 @@ use codex_app_server_protocol::GetAccountRateLimitsResponse;
use codex_app_server_protocol::JSONRPCError;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::LoginApiKeyParams;
use codex_app_server_protocol::RateLimitSnapshot;
use codex_app_server_protocol::RateLimitWindow;
use codex_app_server_protocol::RequestId;
use codex_core::auth::AuthCredentialsStoreMode;
use codex_protocol::protocol::RateLimitSnapshot;
use codex_protocol::protocol::RateLimitWindow;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::path::Path;
@@ -26,7 +26,7 @@ use wiremock::matchers::path;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[tokio::test]
async fn get_account_rate_limits_requires_auth() -> Result<()> {
let codex_home = TempDir::new()?;
@@ -51,7 +51,7 @@ async fn get_account_rate_limits_requires_auth() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[tokio::test]
async fn get_account_rate_limits_requires_chatgpt_auth() -> Result<()> {
let codex_home = TempDir::new()?;
@@ -78,7 +78,7 @@ async fn get_account_rate_limits_requires_chatgpt_auth() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[tokio::test]
async fn get_account_rate_limits_returns_snapshot() -> Result<()> {
let codex_home = TempDir::new()?;
write_chatgpt_auth(
@@ -143,15 +143,16 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> {
let expected = GetAccountRateLimitsResponse {
rate_limits: RateLimitSnapshot {
primary: Some(RateLimitWindow {
used_percent: 42.0,
window_minutes: Some(60),
used_percent: 42,
window_duration_mins: Some(60),
resets_at: Some(primary_reset_timestamp),
}),
secondary: Some(RateLimitWindow {
used_percent: 5.0,
window_minutes: Some(1440),
used_percent: 5,
window_duration_mins: Some(1440),
resets_at: Some(secondary_reset_timestamp),
}),
credits: None,
},
};
assert_eq!(received, expected);

View File

@@ -0,0 +1,329 @@
use anyhow::Result;
use app_test_support::McpProcess;
use app_test_support::create_final_assistant_message_sse_response;
use app_test_support::create_mock_chat_completions_server_unchecked;
use app_test_support::to_response;
use codex_app_server_protocol::ItemCompletedNotification;
use codex_app_server_protocol::ItemStartedNotification;
use codex_app_server_protocol::JSONRPCError;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ReviewDelivery;
use codex_app_server_protocol::ReviewStartParams;
use codex_app_server_protocol::ReviewStartResponse;
use codex_app_server_protocol::ReviewTarget;
use codex_app_server_protocol::ThreadItem;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
use codex_app_server_protocol::TurnStatus;
use serde_json::json;
use tempfile::TempDir;
use tokio::time::timeout;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
#[tokio::test]
async fn review_start_runs_review_turn_and_emits_code_review_item() -> Result<()> {
let review_payload = json!({
"findings": [
{
"title": "Prefer Stylize helpers",
"body": "Use .dim()/.bold() chaining instead of manual Style.",
"confidence_score": 0.9,
"priority": 1,
"code_location": {
"absolute_file_path": "/tmp/file.rs",
"line_range": {"start": 10, "end": 20}
}
}
],
"overall_correctness": "good",
"overall_explanation": "Looks solid overall with minor polish suggested.",
"overall_confidence_score": 0.75
})
.to_string();
let responses = vec![create_final_assistant_message_sse_response(
&review_payload,
)?];
let server = create_mock_chat_completions_server_unchecked(responses).await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let thread_id = start_default_thread(&mut mcp).await?;
let review_req = mcp
.send_review_start_request(ReviewStartParams {
thread_id: thread_id.clone(),
delivery: Some(ReviewDelivery::Inline),
target: ReviewTarget::Commit {
sha: "1234567deadbeef".to_string(),
title: Some("Tidy UI colors".to_string()),
},
})
.await?;
let review_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(review_req)),
)
.await??;
let ReviewStartResponse {
turn,
review_thread_id,
} = to_response::<ReviewStartResponse>(review_resp)?;
assert_eq!(review_thread_id, thread_id.clone());
let turn_id = turn.id.clone();
assert_eq!(turn.status, TurnStatus::InProgress);
// Confirm we see the EnteredReviewMode marker on the main thread.
let mut saw_entered_review_mode = false;
for _ in 0..10 {
let item_started: JSONRPCNotification = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("item/started"),
)
.await??;
let started: ItemStartedNotification =
serde_json::from_value(item_started.params.expect("params must be present"))?;
match started.item {
ThreadItem::EnteredReviewMode { id, review } => {
assert_eq!(id, turn_id);
assert_eq!(review, "commit 1234567");
saw_entered_review_mode = true;
break;
}
_ => continue,
}
}
assert!(
saw_entered_review_mode,
"did not observe enteredReviewMode item"
);
// Confirm we see the ExitedReviewMode marker (with review text)
// on the same turn. Ignore any other items the stream surfaces.
let mut review_body: Option<String> = None;
for _ in 0..10 {
let review_notif: JSONRPCNotification = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("item/completed"),
)
.await??;
let completed: ItemCompletedNotification =
serde_json::from_value(review_notif.params.expect("params must be present"))?;
match completed.item {
ThreadItem::ExitedReviewMode { id, review } => {
assert_eq!(id, turn_id);
review_body = Some(review);
break;
}
_ => continue,
}
}
let review = review_body.expect("did not observe a code review item");
assert!(review.contains("Prefer Stylize helpers"));
assert!(review.contains("/tmp/file.rs:10-20"));
Ok(())
}
#[tokio::test]
async fn review_start_rejects_empty_base_branch() -> Result<()> {
let server = create_mock_chat_completions_server_unchecked(vec![]).await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let thread_id = start_default_thread(&mut mcp).await?;
let request_id = mcp
.send_review_start_request(ReviewStartParams {
thread_id,
delivery: Some(ReviewDelivery::Inline),
target: ReviewTarget::BaseBranch {
branch: " ".to_string(),
},
})
.await?;
let error: JSONRPCError = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert_eq!(error.error.code, INVALID_REQUEST_ERROR_CODE);
assert!(
error.error.message.contains("branch must not be empty"),
"unexpected message: {}",
error.error.message
);
Ok(())
}
#[tokio::test]
async fn review_start_with_detached_delivery_returns_new_thread_id() -> Result<()> {
let review_payload = json!({
"findings": [],
"overall_correctness": "ok",
"overall_explanation": "detached review",
"overall_confidence_score": 0.5
})
.to_string();
let responses = vec![create_final_assistant_message_sse_response(
&review_payload,
)?];
let server = create_mock_chat_completions_server_unchecked(responses).await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let thread_id = start_default_thread(&mut mcp).await?;
let review_req = mcp
.send_review_start_request(ReviewStartParams {
thread_id: thread_id.clone(),
delivery: Some(ReviewDelivery::Detached),
target: ReviewTarget::Custom {
instructions: "detached review".to_string(),
},
})
.await?;
let review_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(review_req)),
)
.await??;
let ReviewStartResponse {
turn,
review_thread_id,
} = to_response::<ReviewStartResponse>(review_resp)?;
assert_eq!(turn.status, TurnStatus::InProgress);
assert_ne!(
review_thread_id, thread_id,
"detached review should run on a different thread"
);
Ok(())
}
#[tokio::test]
async fn review_start_rejects_empty_commit_sha() -> Result<()> {
let server = create_mock_chat_completions_server_unchecked(vec![]).await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let thread_id = start_default_thread(&mut mcp).await?;
let request_id = mcp
.send_review_start_request(ReviewStartParams {
thread_id,
delivery: Some(ReviewDelivery::Inline),
target: ReviewTarget::Commit {
sha: "\t".to_string(),
title: None,
},
})
.await?;
let error: JSONRPCError = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert_eq!(error.error.code, INVALID_REQUEST_ERROR_CODE);
assert!(
error.error.message.contains("sha must not be empty"),
"unexpected message: {}",
error.error.message
);
Ok(())
}
#[tokio::test]
async fn review_start_rejects_empty_custom_instructions() -> Result<()> {
let server = create_mock_chat_completions_server_unchecked(vec![]).await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let thread_id = start_default_thread(&mut mcp).await?;
let request_id = mcp
.send_review_start_request(ReviewStartParams {
thread_id,
delivery: Some(ReviewDelivery::Inline),
target: ReviewTarget::Custom {
instructions: "\n\n".to_string(),
},
})
.await?;
let error: JSONRPCError = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert_eq!(error.error.code, INVALID_REQUEST_ERROR_CODE);
assert!(
error
.error
.message
.contains("instructions must not be empty"),
"unexpected message: {}",
error.error.message
);
Ok(())
}
async fn start_default_thread(mcp: &mut McpProcess) -> Result<String> {
let thread_req = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("mock-model".to_string()),
..Default::default()
})
.await?;
let thread_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
Ok(thread.id)
}
fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()> {
let config_toml = codex_home.join("config.toml");
std::fs::write(
config_toml,
format!(
r#"
model = "mock-model"
approval_policy = "never"
sandbox_mode = "read-only"
model_provider = "mock_provider"
[model_providers.mock_provider]
name = "Mock provider"
base_url = "{server_uri}/v1"
wire_api = "chat"
request_max_retries = 0
stream_max_retries = 0
"#
),
)
}

View File

@@ -0,0 +1,93 @@
use anyhow::Result;
use app_test_support::McpProcess;
use app_test_support::to_response;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ThreadArchiveParams;
use codex_app_server_protocol::ThreadArchiveResponse;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
use codex_core::ARCHIVED_SESSIONS_SUBDIR;
use codex_core::find_conversation_path_by_id_str;
use std::path::Path;
use tempfile::TempDir;
use tokio::time::timeout;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
#[tokio::test]
async fn thread_archive_moves_rollout_into_archived_directory() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path())?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
// Start a thread.
let start_id = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("mock-model".to_string()),
..Default::default()
})
.await?;
let start_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(start_id)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
assert!(!thread.id.is_empty());
// Locate the rollout path recorded for this thread id.
let rollout_path = find_conversation_path_by_id_str(codex_home.path(), &thread.id)
.await?
.expect("expected rollout path for thread id to exist");
assert!(
rollout_path.exists(),
"expected {} to exist",
rollout_path.display()
);
// Archive the thread.
let archive_id = mcp
.send_thread_archive_request(ThreadArchiveParams {
thread_id: thread.id.clone(),
})
.await?;
let archive_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(archive_id)),
)
.await??;
let _: ThreadArchiveResponse = to_response::<ThreadArchiveResponse>(archive_resp)?;
// Verify file moved.
let archived_directory = codex_home.path().join(ARCHIVED_SESSIONS_SUBDIR);
// The archived file keeps the original filename (rollout-...-<id>.jsonl).
let archived_rollout_path =
archived_directory.join(rollout_path.file_name().expect("rollout file name"));
assert!(
!rollout_path.exists(),
"expected rollout path {} to be moved",
rollout_path.display()
);
assert!(
archived_rollout_path.exists(),
"expected archived rollout path {} to exist",
archived_rollout_path.display()
);
Ok(())
}
fn create_config_toml(codex_home: &Path) -> std::io::Result<()> {
let config_toml = codex_home.join("config.toml");
std::fs::write(config_toml, config_contents())
}
fn config_contents() -> &'static str {
r#"model = "mock-model"
approval_policy = "never"
sandbox_mode = "read-only"
"#
}

View File

@@ -0,0 +1,259 @@
use anyhow::Result;
use app_test_support::McpProcess;
use app_test_support::create_fake_rollout;
use app_test_support::to_response;
use codex_app_server_protocol::GitInfo as ApiGitInfo;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::SessionSource;
use codex_app_server_protocol::ThreadListParams;
use codex_app_server_protocol::ThreadListResponse;
use codex_protocol::protocol::GitInfo as CoreGitInfo;
use std::path::PathBuf;
use tempfile::TempDir;
use tokio::time::timeout;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
#[tokio::test]
async fn thread_list_basic_empty() -> Result<()> {
let codex_home = TempDir::new()?;
create_minimal_config(codex_home.path())?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
// List threads in an empty CODEX_HOME; should return an empty page with nextCursor: null.
let list_id = mcp
.send_thread_list_request(ThreadListParams {
cursor: None,
limit: Some(10),
model_providers: Some(vec!["mock_provider".to_string()]),
})
.await?;
let list_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(list_id)),
)
.await??;
let ThreadListResponse { data, next_cursor } = to_response::<ThreadListResponse>(list_resp)?;
assert!(data.is_empty());
assert_eq!(next_cursor, None);
Ok(())
}
// Minimal config.toml for listing.
fn create_minimal_config(codex_home: &std::path::Path) -> std::io::Result<()> {
let config_toml = codex_home.join("config.toml");
std::fs::write(
config_toml,
r#"
model = "mock-model"
approval_policy = "never"
"#,
)
}
#[tokio::test]
async fn thread_list_pagination_next_cursor_none_on_last_page() -> Result<()> {
let codex_home = TempDir::new()?;
create_minimal_config(codex_home.path())?;
// Create three rollouts so we can paginate with limit=2.
let _a = create_fake_rollout(
codex_home.path(),
"2025-01-02T12-00-00",
"2025-01-02T12:00:00Z",
"Hello",
Some("mock_provider"),
None,
)?;
let _b = create_fake_rollout(
codex_home.path(),
"2025-01-01T13-00-00",
"2025-01-01T13:00:00Z",
"Hello",
Some("mock_provider"),
None,
)?;
let _c = create_fake_rollout(
codex_home.path(),
"2025-01-01T12-00-00",
"2025-01-01T12:00:00Z",
"Hello",
Some("mock_provider"),
None,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
// Page 1: limit 2 → expect next_cursor Some.
let page1_id = mcp
.send_thread_list_request(ThreadListParams {
cursor: None,
limit: Some(2),
model_providers: Some(vec!["mock_provider".to_string()]),
})
.await?;
let page1_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(page1_id)),
)
.await??;
let ThreadListResponse {
data: data1,
next_cursor: cursor1,
} = to_response::<ThreadListResponse>(page1_resp)?;
assert_eq!(data1.len(), 2);
for thread in &data1 {
assert_eq!(thread.preview, "Hello");
assert_eq!(thread.model_provider, "mock_provider");
assert!(thread.created_at > 0);
assert_eq!(thread.cwd, PathBuf::from("/"));
assert_eq!(thread.cli_version, "0.0.0");
assert_eq!(thread.source, SessionSource::Cli);
assert_eq!(thread.git_info, None);
}
let cursor1 = cursor1.expect("expected nextCursor on first page");
// Page 2: with cursor → expect next_cursor None when no more results.
let page2_id = mcp
.send_thread_list_request(ThreadListParams {
cursor: Some(cursor1),
limit: Some(2),
model_providers: Some(vec!["mock_provider".to_string()]),
})
.await?;
let page2_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(page2_id)),
)
.await??;
let ThreadListResponse {
data: data2,
next_cursor: cursor2,
} = to_response::<ThreadListResponse>(page2_resp)?;
assert!(data2.len() <= 2);
for thread in &data2 {
assert_eq!(thread.preview, "Hello");
assert_eq!(thread.model_provider, "mock_provider");
assert!(thread.created_at > 0);
assert_eq!(thread.cwd, PathBuf::from("/"));
assert_eq!(thread.cli_version, "0.0.0");
assert_eq!(thread.source, SessionSource::Cli);
assert_eq!(thread.git_info, None);
}
assert_eq!(cursor2, None, "expected nextCursor to be null on last page");
Ok(())
}
#[tokio::test]
async fn thread_list_respects_provider_filter() -> Result<()> {
let codex_home = TempDir::new()?;
create_minimal_config(codex_home.path())?;
// Create rollouts under two providers.
let _a = create_fake_rollout(
codex_home.path(),
"2025-01-02T10-00-00",
"2025-01-02T10:00:00Z",
"X",
Some("mock_provider"),
None,
)?; // mock_provider
let _b = create_fake_rollout(
codex_home.path(),
"2025-01-02T11-00-00",
"2025-01-02T11:00:00Z",
"X",
Some("other_provider"),
None,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
// Filter to only other_provider; expect 1 item, nextCursor None.
let list_id = mcp
.send_thread_list_request(ThreadListParams {
cursor: None,
limit: Some(10),
model_providers: Some(vec!["other_provider".to_string()]),
})
.await?;
let resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(list_id)),
)
.await??;
let ThreadListResponse { data, next_cursor } = to_response::<ThreadListResponse>(resp)?;
assert_eq!(data.len(), 1);
assert_eq!(next_cursor, None);
let thread = &data[0];
assert_eq!(thread.preview, "X");
assert_eq!(thread.model_provider, "other_provider");
let expected_ts = chrono::DateTime::parse_from_rfc3339("2025-01-02T11:00:00Z")?.timestamp();
assert_eq!(thread.created_at, expected_ts);
assert_eq!(thread.cwd, PathBuf::from("/"));
assert_eq!(thread.cli_version, "0.0.0");
assert_eq!(thread.source, SessionSource::Cli);
assert_eq!(thread.git_info, None);
Ok(())
}
#[tokio::test]
async fn thread_list_includes_git_info() -> Result<()> {
let codex_home = TempDir::new()?;
create_minimal_config(codex_home.path())?;
let git_info = CoreGitInfo {
commit_hash: Some("abc123".to_string()),
branch: Some("main".to_string()),
repository_url: Some("https://example.com/repo.git".to_string()),
};
let conversation_id = create_fake_rollout(
codex_home.path(),
"2025-02-01T09-00-00",
"2025-02-01T09:00:00Z",
"Git info preview",
Some("mock_provider"),
Some(git_info),
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let list_id = mcp
.send_thread_list_request(ThreadListParams {
cursor: None,
limit: Some(10),
model_providers: Some(vec!["mock_provider".to_string()]),
})
.await?;
let resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(list_id)),
)
.await??;
let ThreadListResponse { data, .. } = to_response::<ThreadListResponse>(resp)?;
let thread = data
.iter()
.find(|t| t.id == conversation_id)
.expect("expected thread for created rollout");
let expected_git = ApiGitInfo {
sha: Some("abc123".to_string()),
branch: Some("main".to_string()),
origin_url: Some("https://example.com/repo.git".to_string()),
};
assert_eq!(thread.git_info, Some(expected_git));
assert_eq!(thread.source, SessionSource::Cli);
assert_eq!(thread.cwd, PathBuf::from("/"));
assert_eq!(thread.cli_version, "0.0.0");
Ok(())
}

View File

@@ -0,0 +1,256 @@
use anyhow::Result;
use app_test_support::McpProcess;
use app_test_support::create_fake_rollout;
use app_test_support::create_mock_chat_completions_server;
use app_test_support::to_response;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::SessionSource;
use codex_app_server_protocol::ThreadItem;
use codex_app_server_protocol::ThreadResumeParams;
use codex_app_server_protocol::ThreadResumeResponse;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
use codex_app_server_protocol::TurnStatus;
use codex_app_server_protocol::UserInput;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use std::path::PathBuf;
use tempfile::TempDir;
use tokio::time::timeout;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
#[tokio::test]
async fn thread_resume_returns_original_thread() -> Result<()> {
let server = create_mock_chat_completions_server(vec![]).await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
// Start a thread.
let start_id = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.1-codex-max".to_string()),
..Default::default()
})
.await?;
let start_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(start_id)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
// Resume it via v2 API.
let resume_id = mcp
.send_thread_resume_request(ThreadResumeParams {
thread_id: thread.id.clone(),
..Default::default()
})
.await?;
let resume_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(resume_id)),
)
.await??;
let ThreadResumeResponse {
thread: resumed, ..
} = to_response::<ThreadResumeResponse>(resume_resp)?;
assert_eq!(resumed, thread);
Ok(())
}
#[tokio::test]
async fn thread_resume_returns_rollout_history() -> Result<()> {
let server = create_mock_chat_completions_server(vec![]).await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
let preview = "Saved user message";
let conversation_id = create_fake_rollout(
codex_home.path(),
"2025-01-05T12-00-00",
"2025-01-05T12:00:00Z",
preview,
Some("mock_provider"),
None,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let resume_id = mcp
.send_thread_resume_request(ThreadResumeParams {
thread_id: conversation_id.clone(),
..Default::default()
})
.await?;
let resume_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(resume_id)),
)
.await??;
let ThreadResumeResponse { thread, .. } = to_response::<ThreadResumeResponse>(resume_resp)?;
assert_eq!(thread.id, conversation_id);
assert_eq!(thread.preview, preview);
assert_eq!(thread.model_provider, "mock_provider");
assert!(thread.path.is_absolute());
assert_eq!(thread.cwd, PathBuf::from("/"));
assert_eq!(thread.cli_version, "0.0.0");
assert_eq!(thread.source, SessionSource::Cli);
assert_eq!(thread.git_info, None);
assert_eq!(
thread.turns.len(),
1,
"expected rollouts to include one turn"
);
let turn = &thread.turns[0];
assert_eq!(turn.status, TurnStatus::Completed);
assert_eq!(turn.items.len(), 1, "expected user message item");
match &turn.items[0] {
ThreadItem::UserMessage { content, .. } => {
assert_eq!(
content,
&vec![UserInput::Text {
text: preview.to_string()
}]
);
}
other => panic!("expected user message item, got {other:?}"),
}
Ok(())
}
#[tokio::test]
async fn thread_resume_prefers_path_over_thread_id() -> Result<()> {
let server = create_mock_chat_completions_server(vec![]).await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let start_id = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.1-codex-max".to_string()),
..Default::default()
})
.await?;
let start_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(start_id)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
let thread_path = thread.path.clone();
let resume_id = mcp
.send_thread_resume_request(ThreadResumeParams {
thread_id: "not-a-valid-thread-id".to_string(),
path: Some(thread_path),
..Default::default()
})
.await?;
let resume_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(resume_id)),
)
.await??;
let ThreadResumeResponse {
thread: resumed, ..
} = to_response::<ThreadResumeResponse>(resume_resp)?;
assert_eq!(resumed, thread);
Ok(())
}
#[tokio::test]
async fn thread_resume_supports_history_and_overrides() -> Result<()> {
let server = create_mock_chat_completions_server(vec![]).await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
// Start a thread.
let start_id = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.1-codex-max".to_string()),
..Default::default()
})
.await?;
let start_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(start_id)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
let history_text = "Hello from history";
let history = vec![ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: history_text.to_string(),
}],
}];
// Resume with explicit history and override the model.
let resume_id = mcp
.send_thread_resume_request(ThreadResumeParams {
thread_id: thread.id,
history: Some(history),
model: Some("mock-model".to_string()),
model_provider: Some("mock_provider".to_string()),
..Default::default()
})
.await?;
let resume_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(resume_id)),
)
.await??;
let ThreadResumeResponse {
thread: resumed,
model_provider,
..
} = to_response::<ThreadResumeResponse>(resume_resp)?;
assert!(!resumed.id.is_empty());
assert_eq!(model_provider, "mock_provider");
assert_eq!(resumed.preview, history_text);
Ok(())
}
// Helper to create a config.toml pointing at the mock model server.
fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()> {
let config_toml = codex_home.join("config.toml");
std::fs::write(
config_toml,
format!(
r#"
model = "mock-model"
approval_policy = "never"
sandbox_mode = "read-only"
model_provider = "mock_provider"
[model_providers.mock_provider]
name = "Mock provider for test"
base_url = "{server_uri}/v1"
wire_api = "chat"
request_max_retries = 0
stream_max_retries = 0
"#
),
)
}

View File

@@ -0,0 +1,94 @@
use anyhow::Result;
use app_test_support::McpProcess;
use app_test_support::create_mock_chat_completions_server;
use app_test_support::to_response;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
use codex_app_server_protocol::ThreadStartedNotification;
use std::path::Path;
use tempfile::TempDir;
use tokio::time::timeout;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
#[tokio::test]
async fn thread_start_creates_thread_and_emits_started() -> Result<()> {
// Provide a mock server and config so model wiring is valid.
let server = create_mock_chat_completions_server(vec![]).await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
// Start server and initialize.
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
// Start a v2 thread with an explicit model override.
let req_id = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.1".to_string()),
..Default::default()
})
.await?;
// Expect a proper JSON-RPC response with a thread id.
let resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(req_id)),
)
.await??;
let ThreadStartResponse {
thread,
model_provider,
..
} = to_response::<ThreadStartResponse>(resp)?;
assert!(!thread.id.is_empty(), "thread id should not be empty");
assert!(
thread.preview.is_empty(),
"new threads should start with an empty preview"
);
assert_eq!(model_provider, "mock_provider");
assert!(
thread.created_at > 0,
"created_at should be a positive UNIX timestamp"
);
// A corresponding thread/started notification should arrive.
let notif: JSONRPCNotification = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("thread/started"),
)
.await??;
let started: ThreadStartedNotification =
serde_json::from_value(notif.params.expect("params must be present"))?;
assert_eq!(started.thread, thread);
Ok(())
}
// Helper to create a config.toml pointing at the mock model server.
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> {
let config_toml = codex_home.join("config.toml");
std::fs::write(
config_toml,
format!(
r#"
model = "mock-model"
approval_policy = "never"
sandbox_mode = "read-only"
model_provider = "mock_provider"
[model_providers.mock_provider]
name = "Mock provider for test"
base_url = "{server_uri}/v1"
wire_api = "chat"
request_max_retries = 0
stream_max_retries = 0
"#
),
)
}

View File

@@ -0,0 +1,144 @@
#![cfg(unix)]
use anyhow::Result;
use app_test_support::McpProcess;
use app_test_support::create_mock_chat_completions_server;
use app_test_support::create_shell_command_sse_response;
use app_test_support::to_response;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
use codex_app_server_protocol::TurnCompletedNotification;
use codex_app_server_protocol::TurnInterruptParams;
use codex_app_server_protocol::TurnInterruptResponse;
use codex_app_server_protocol::TurnStartParams;
use codex_app_server_protocol::TurnStartResponse;
use codex_app_server_protocol::TurnStatus;
use codex_app_server_protocol::UserInput as V2UserInput;
use tempfile::TempDir;
use tokio::time::timeout;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
#[tokio::test]
async fn turn_interrupt_aborts_running_turn() -> Result<()> {
// Use a portable sleep command to keep the turn running.
#[cfg(target_os = "windows")]
let shell_command = vec![
"powershell".to_string(),
"-Command".to_string(),
"Start-Sleep -Seconds 10".to_string(),
];
#[cfg(not(target_os = "windows"))]
let shell_command = vec!["sleep".to_string(), "10".to_string()];
let tmp = TempDir::new()?;
let codex_home = tmp.path().join("codex_home");
std::fs::create_dir(&codex_home)?;
let working_directory = tmp.path().join("workdir");
std::fs::create_dir(&working_directory)?;
// Mock server: long-running shell command then (after abort) nothing else needed.
let server = create_mock_chat_completions_server(vec![create_shell_command_sse_response(
shell_command.clone(),
Some(&working_directory),
Some(10_000),
"call_sleep",
)?])
.await;
create_config_toml(&codex_home, &server.uri())?;
let mut mcp = McpProcess::new(&codex_home).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
// Start a v2 thread and capture its id.
let thread_req = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("mock-model".to_string()),
..Default::default()
})
.await?;
let thread_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
// Start a turn that triggers a long-running command.
let turn_req = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "run sleep".to_string(),
}],
cwd: Some(working_directory.clone()),
..Default::default()
})
.await?;
let turn_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
)
.await??;
let TurnStartResponse { turn } = to_response::<TurnStartResponse>(turn_resp)?;
// Give the command a brief moment to start.
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
let thread_id = thread.id.clone();
// Interrupt the in-progress turn by id (v2 API).
let interrupt_id = mcp
.send_turn_interrupt_request(TurnInterruptParams {
thread_id: thread_id.clone(),
turn_id: turn.id,
})
.await?;
let interrupt_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(interrupt_id)),
)
.await??;
let _resp: TurnInterruptResponse = to_response::<TurnInterruptResponse>(interrupt_resp)?;
let completed_notif: JSONRPCNotification = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
)
.await??;
let completed: TurnCompletedNotification = serde_json::from_value(
completed_notif
.params
.expect("turn/completed params must be present"),
)?;
assert_eq!(completed.thread_id, thread_id);
assert_eq!(completed.turn.status, TurnStatus::Interrupted);
Ok(())
}
// Helper to create a config.toml pointing at the mock model server.
fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()> {
let config_toml = codex_home.join("config.toml");
std::fs::write(
config_toml,
format!(
r#"
model = "mock-model"
approval_policy = "never"
sandbox_mode = "workspace-write"
model_provider = "mock_provider"
[model_providers.mock_provider]
name = "Mock provider for test"
base_url = "{server_uri}/v1"
wire_api = "chat"
request_max_retries = 0
stream_max_retries = 0
"#
),
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,8 @@
[package]
edition = "2024"
name = "codex-apply-patch"
version = { workspace = true }
version.workspace = true
edition.workspace = true
license.workspace = true
[lib]
name = "codex_apply_patch"

View File

@@ -31,6 +31,13 @@ pub const APPLY_PATCH_TOOL_INSTRUCTIONS: &str = include_str!("../apply_patch_too
const APPLY_PATCH_COMMANDS: [&str; 2] = ["apply_patch", "applypatch"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ApplyPatchShell {
Unix,
PowerShell,
Cmd,
}
#[derive(Debug, Error, PartialEq)]
pub enum ApplyPatchError {
#[error(transparent)]
@@ -96,6 +103,57 @@ pub struct ApplyPatchArgs {
pub workdir: Option<String>,
}
fn classify_shell_name(shell: &str) -> Option<String> {
std::path::Path::new(shell)
.file_stem()
.and_then(|name| name.to_str())
.map(str::to_ascii_lowercase)
}
fn classify_shell(shell: &str, flag: &str) -> Option<ApplyPatchShell> {
classify_shell_name(shell).and_then(|name| match name.as_str() {
"bash" | "zsh" | "sh" if flag == "-lc" => Some(ApplyPatchShell::Unix),
"pwsh" | "powershell" if flag.eq_ignore_ascii_case("-command") => {
Some(ApplyPatchShell::PowerShell)
}
"cmd" if flag.eq_ignore_ascii_case("/c") => Some(ApplyPatchShell::Cmd),
_ => None,
})
}
fn can_skip_flag(shell: &str, flag: &str) -> bool {
classify_shell_name(shell).is_some_and(|name| {
matches!(name.as_str(), "pwsh" | "powershell") && flag.eq_ignore_ascii_case("-noprofile")
})
}
fn parse_shell_script(argv: &[String]) -> Option<(ApplyPatchShell, &str)> {
match argv {
[shell, flag, script] => classify_shell(shell, flag).map(|shell_type| {
let script = script.as_str();
(shell_type, script)
}),
[shell, skip_flag, flag, script] if can_skip_flag(shell, skip_flag) => {
classify_shell(shell, flag).map(|shell_type| {
let script = script.as_str();
(shell_type, script)
})
}
_ => None,
}
}
fn extract_apply_patch_from_shell(
shell: ApplyPatchShell,
script: &str,
) -> std::result::Result<(String, Option<String>), ExtractHeredocError> {
match shell {
ApplyPatchShell::Unix | ApplyPatchShell::PowerShell | ApplyPatchShell::Cmd => {
extract_apply_patch_from_bash(script)
}
}
}
pub fn maybe_parse_apply_patch(argv: &[String]) -> MaybeApplyPatch {
match argv {
// Direct invocation: apply_patch <patch>
@@ -103,9 +161,9 @@ pub fn maybe_parse_apply_patch(argv: &[String]) -> MaybeApplyPatch {
Ok(source) => MaybeApplyPatch::Body(source),
Err(e) => MaybeApplyPatch::PatchParseError(e),
},
// Bash heredoc form: (optional `cd <path> &&`) apply_patch <<'EOF' ...
[bash, flag, script] if bash == "bash" && flag == "-lc" => {
match extract_apply_patch_from_bash(script) {
// Shell heredoc form: (optional `cd <path> &&`) apply_patch <<'EOF' ...
_ => match parse_shell_script(argv) {
Some((shell, script)) => match extract_apply_patch_from_shell(shell, script) {
Ok((body, workdir)) => match parse_patch(&body) {
Ok(mut source) => {
source.workdir = workdir;
@@ -117,9 +175,9 @@ pub fn maybe_parse_apply_patch(argv: &[String]) -> MaybeApplyPatch {
MaybeApplyPatch::NotApplyPatch
}
Err(e) => MaybeApplyPatch::ShellParseError(e),
}
}
_ => MaybeApplyPatch::NotApplyPatch,
},
None => MaybeApplyPatch::NotApplyPatch,
},
}
}
@@ -214,24 +272,17 @@ impl ApplyPatchAction {
/// cwd must be an absolute path so that we can resolve relative paths in the
/// patch.
pub fn maybe_parse_apply_patch_verified(argv: &[String], cwd: &Path) -> MaybeApplyPatchVerified {
// Detect a raw patch body passed directly as the command or as the body of a bash -lc
// Detect a raw patch body passed directly as the command or as the body of a shell
// script. In these cases, report an explicit error rather than applying the patch.
match argv {
[body] => {
if parse_patch(body).is_ok() {
return MaybeApplyPatchVerified::CorrectnessError(
ApplyPatchError::ImplicitInvocation,
);
}
}
[bash, flag, script] if bash == "bash" && flag == "-lc" => {
if parse_patch(script).is_ok() {
return MaybeApplyPatchVerified::CorrectnessError(
ApplyPatchError::ImplicitInvocation,
);
}
}
_ => {}
if let [body] = argv
&& parse_patch(body).is_ok()
{
return MaybeApplyPatchVerified::CorrectnessError(ApplyPatchError::ImplicitInvocation);
}
if let Some((_, script)) = parse_shell_script(argv)
&& parse_patch(script).is_ok()
{
return MaybeApplyPatchVerified::CorrectnessError(ApplyPatchError::ImplicitInvocation);
}
match maybe_parse_apply_patch(argv) {
@@ -288,7 +339,7 @@ pub fn maybe_parse_apply_patch_verified(argv: &[String], cwd: &Path) -> MaybeApp
path,
ApplyPatchFileChange::Update {
unified_diff,
move_path: move_path.map(|p| cwd.join(p)),
move_path: move_path.map(|p| effective_cwd.join(p)),
new_content: contents,
},
);
@@ -863,6 +914,22 @@ mod tests {
strs_to_strings(&["bash", "-lc", script])
}
fn args_powershell(script: &str) -> Vec<String> {
strs_to_strings(&["powershell.exe", "-Command", script])
}
fn args_powershell_no_profile(script: &str) -> Vec<String> {
strs_to_strings(&["powershell.exe", "-NoProfile", "-Command", script])
}
fn args_pwsh(script: &str) -> Vec<String> {
strs_to_strings(&["pwsh", "-NoProfile", "-Command", script])
}
fn args_cmd(script: &str) -> Vec<String> {
strs_to_strings(&["cmd.exe", "/c", script])
}
fn heredoc_script(prefix: &str) -> String {
format!(
"{prefix}apply_patch <<'PATCH'\n*** Begin Patch\n*** Add File: foo\n+hi\n*** End Patch\nPATCH"
@@ -882,8 +949,7 @@ mod tests {
}]
}
fn assert_match(script: &str, expected_workdir: Option<&str>) {
let args = args_bash(script);
fn assert_match_args(args: Vec<String>, expected_workdir: Option<&str>) {
match maybe_parse_apply_patch(&args) {
MaybeApplyPatch::Body(ApplyPatchArgs { hunks, workdir, .. }) => {
assert_eq!(workdir.as_deref(), expected_workdir);
@@ -893,6 +959,11 @@ mod tests {
}
}
fn assert_match(script: &str, expected_workdir: Option<&str>) {
let args = args_bash(script);
assert_match_args(args, expected_workdir);
}
fn assert_not_match(script: &str) {
let args = args_bash(script);
assert_matches!(
@@ -1006,6 +1077,28 @@ PATCH"#,
}
}
#[test]
fn test_powershell_heredoc() {
let script = heredoc_script("");
assert_match_args(args_powershell(&script), None);
}
#[test]
fn test_powershell_heredoc_no_profile() {
let script = heredoc_script("");
assert_match_args(args_powershell_no_profile(&script), None);
}
#[test]
fn test_pwsh_heredoc() {
let script = heredoc_script("");
assert_match_args(args_pwsh(&script), None);
}
#[test]
fn test_cmd_heredoc_with_cd() {
let script = heredoc_script("cd foo && ");
assert_match_args(args_cmd(&script), Some("foo"));
}
#[test]
fn test_heredoc_with_leading_cd() {
assert_match(&heredoc_script("cd foo && "), Some("foo"));
@@ -1603,6 +1696,53 @@ g
);
}
#[test]
fn test_apply_patch_resolves_move_path_with_effective_cwd() {
let session_dir = tempdir().unwrap();
let worktree_rel = "alt";
let worktree_dir = session_dir.path().join(worktree_rel);
fs::create_dir_all(&worktree_dir).unwrap();
let source_name = "old.txt";
let dest_name = "renamed.txt";
let source_path = worktree_dir.join(source_name);
fs::write(&source_path, "before\n").unwrap();
let patch = wrap_patch(&format!(
r#"*** Update File: {source_name}
*** Move to: {dest_name}
@@
-before
+after"#
));
let shell_script = format!("cd {worktree_rel} && apply_patch <<'PATCH'\n{patch}\nPATCH");
let argv = vec!["bash".into(), "-lc".into(), shell_script];
let result = maybe_parse_apply_patch_verified(&argv, session_dir.path());
let action = match result {
MaybeApplyPatchVerified::Body(action) => action,
other => panic!("expected verified body, got {other:?}"),
};
assert_eq!(action.cwd, worktree_dir);
let change = action
.changes()
.get(&worktree_dir.join(source_name))
.expect("source file change present");
match change {
ApplyPatchFileChange::Update { move_path, .. } => {
assert_eq!(
move_path.as_deref(),
Some(worktree_dir.join(dest_name).as_path())
);
}
other => panic!("expected update change, got {other:?}"),
}
}
#[test]
fn test_apply_patch_fails_on_write_error() {
let dir = tempdir().unwrap();

View File

@@ -1,7 +1,8 @@
[package]
edition = "2024"
name = "codex-arg0"
version = { workspace = true }
version.workspace = true
edition.workspace = true
license.workspace = true
[lib]
name = "codex_arg0"

View File

@@ -11,32 +11,7 @@ const LINUX_SANDBOX_ARG0: &str = "codex-linux-sandbox";
const APPLY_PATCH_ARG0: &str = "apply_patch";
const MISSPELLED_APPLY_PATCH_ARG0: &str = "applypatch";
/// While we want to deploy the Codex CLI as a single executable for simplicity,
/// we also want to expose some of its functionality as distinct CLIs, so we use
/// the "arg0 trick" to determine which CLI to dispatch. This effectively allows
/// us to simulate deploying multiple executables as a single binary on Mac and
/// Linux (but not Windows).
///
/// When the current executable is invoked through the hard-link or alias named
/// `codex-linux-sandbox` we *directly* execute
/// [`codex_linux_sandbox::run_main`] (which never returns). Otherwise we:
///
/// 1. Load `.env` values from `~/.codex/.env` before creating any threads.
/// 2. Construct a Tokio multi-thread runtime.
/// 3. Derive the path to the current executable (so children can re-invoke the
/// sandbox) when running on Linux.
/// 4. Execute the provided async `main_fn` inside that runtime, forwarding any
/// error. Note that `main_fn` receives `codex_linux_sandbox_exe:
/// Option<PathBuf>`, as an argument, which is generally needed as part of
/// constructing [`codex_core::config::Config`].
///
/// This function should be used to wrap any `main()` function in binary crates
/// in this workspace that depends on these helper CLIs.
pub fn arg0_dispatch_or_else<F, Fut>(main_fn: F) -> anyhow::Result<()>
where
F: FnOnce(Option<PathBuf>) -> Fut,
Fut: Future<Output = anyhow::Result<()>>,
{
pub fn arg0_dispatch() -> Option<TempDir> {
// Determine if we were invoked via the special alias.
let mut args = std::env::args_os();
let argv0 = args.next().unwrap_or_default();
@@ -76,10 +51,7 @@ where
// before creating any threads/the Tokio runtime.
load_dotenv();
// Retain the TempDir so it exists for the lifetime of the invocation of
// this executable. Admittedly, we could invoke `keep()` on it, but it
// would be nice to avoid leaving temporary directories behind, if possible.
let _path_entry = match prepend_path_entry_for_apply_patch() {
match prepend_path_entry_for_codex_aliases() {
Ok(path_entry) => Some(path_entry),
Err(err) => {
// It is possible that Codex will proceed successfully even if
@@ -87,7 +59,39 @@ where
eprintln!("WARNING: proceeding, even though we could not update PATH: {err}");
None
}
};
}
}
/// While we want to deploy the Codex CLI as a single executable for simplicity,
/// we also want to expose some of its functionality as distinct CLIs, so we use
/// the "arg0 trick" to determine which CLI to dispatch. This effectively allows
/// us to simulate deploying multiple executables as a single binary on Mac and
/// Linux (but not Windows).
///
/// When the current executable is invoked through the hard-link or alias named
/// `codex-linux-sandbox` we *directly* execute
/// [`codex_linux_sandbox::run_main`] (which never returns). Otherwise we:
///
/// 1. Load `.env` values from `~/.codex/.env` before creating any threads.
/// 2. Construct a Tokio multi-thread runtime.
/// 3. Derive the path to the current executable (so children can re-invoke the
/// sandbox) when running on Linux.
/// 4. Execute the provided async `main_fn` inside that runtime, forwarding any
/// error. Note that `main_fn` receives `codex_linux_sandbox_exe:
/// Option<PathBuf>`, as an argument, which is generally needed as part of
/// constructing [`codex_core::config::Config`].
///
/// This function should be used to wrap any `main()` function in binary crates
/// in this workspace that depends on these helper CLIs.
pub fn arg0_dispatch_or_else<F, Fut>(main_fn: F) -> anyhow::Result<()>
where
F: FnOnce(Option<PathBuf>) -> Fut,
Fut: Future<Output = anyhow::Result<()>>,
{
// Retain the TempDir so it exists for the lifetime of the invocation of
// this executable. Admittedly, we could invoke `keep()` on it, but it
// would be nice to avoid leaving temporary directories behind, if possible.
let _path_entry = arg0_dispatch();
// Regular invocation create a Tokio runtime and execute the provided
// async entry-point.
@@ -144,11 +148,16 @@ where
///
/// IMPORTANT: This function modifies the PATH environment variable, so it MUST
/// be called before multiple threads are spawned.
fn prepend_path_entry_for_apply_patch() -> std::io::Result<TempDir> {
pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result<TempDir> {
let temp_dir = TempDir::new()?;
let path = temp_dir.path();
for filename in &[APPLY_PATCH_ARG0, MISSPELLED_APPLY_PATCH_ARG0] {
for filename in &[
APPLY_PATCH_ARG0,
MISSPELLED_APPLY_PATCH_ARG0,
#[cfg(target_os = "linux")]
LINUX_SANDBOX_ARG0,
] {
let exe = std::env::current_exe()?;
#[cfg(unix)]

View File

@@ -1,7 +1,8 @@
[package]
edition.workspace = true
name = "codex-async-utils"
version.workspace = true
edition.workspace = true
license.workspace = true
[lints]
workspace = true

View File

@@ -1,7 +1,8 @@
[package]
name = "codex-backend-client"
version = "0.0.0"
edition = "2024"
version.workspace = true
edition.workspace = true
license.workspace = true
publish = false
[lib]

View File

@@ -1,4 +1,5 @@
use crate::types::CodeTaskDetailsResponse;
use crate::types::CreditStatusDetails;
use crate::types::PaginatedListTaskListItem;
use crate::types::RateLimitStatusPayload;
use crate::types::RateLimitWindowSnapshot;
@@ -6,6 +7,7 @@ use crate::types::TurnAttemptsSiblingTurnsResponse;
use anyhow::Result;
use codex_core::auth::CodexAuth;
use codex_core::default_client::get_codex_user_agent;
use codex_protocol::protocol::CreditsSnapshot;
use codex_protocol::protocol::RateLimitSnapshot;
use codex_protocol::protocol::RateLimitWindow;
use reqwest::header::AUTHORIZATION;
@@ -272,19 +274,23 @@ impl Client {
// rate limit helpers
fn rate_limit_snapshot_from_payload(payload: RateLimitStatusPayload) -> RateLimitSnapshot {
let Some(details) = payload
let rate_limit_details = payload
.rate_limit
.and_then(|inner| inner.map(|boxed| *boxed))
else {
return RateLimitSnapshot {
primary: None,
secondary: None,
};
.and_then(|inner| inner.map(|boxed| *boxed));
let (primary, secondary) = if let Some(details) = rate_limit_details {
(
Self::map_rate_limit_window(details.primary_window),
Self::map_rate_limit_window(details.secondary_window),
)
} else {
(None, None)
};
RateLimitSnapshot {
primary: Self::map_rate_limit_window(details.primary_window),
secondary: Self::map_rate_limit_window(details.secondary_window),
primary,
secondary,
credits: Self::map_credits(payload.credits),
}
}
@@ -306,6 +312,19 @@ impl Client {
})
}
fn map_credits(credits: Option<Option<Box<CreditStatusDetails>>>) -> Option<CreditsSnapshot> {
let details = match credits {
Some(Some(details)) => *details,
_ => return None,
};
Some(CreditsSnapshot {
has_credits: details.has_credits,
unlimited: details.unlimited,
balance: details.balance.and_then(|inner| inner),
})
}
fn window_minutes_from_seconds(seconds: i32) -> Option<i64> {
if seconds <= 0 {
return None;

View File

@@ -1,3 +1,4 @@
pub use codex_backend_openapi_models::models::CreditStatusDetails;
pub use codex_backend_openapi_models::models::PaginatedListTaskListItem;
pub use codex_backend_openapi_models::models::PlanType;
pub use codex_backend_openapi_models::models::RateLimitStatusDetails;

View File

@@ -1,7 +1,8 @@
[package]
edition = "2024"
name = "codex-chatgpt"
version = { workspace = true }
version.workspace = true
edition.workspace = true
license.workspace = true
[lints]
workspace = true

View File

@@ -1,7 +1,8 @@
[package]
edition = "2024"
name = "codex-cli"
version = { workspace = true }
version.workspace = true
edition.workspace = true
license.workspace = true
[[bin]]
name = "codex"
@@ -26,19 +27,22 @@ codex-cloud-tasks = { path = "../cloud-tasks" }
codex-common = { workspace = true, features = ["cli"] }
codex-core = { workspace = true }
codex-exec = { workspace = true }
codex-execpolicy = { workspace = true }
codex-login = { workspace = true }
codex-mcp-server = { workspace = true }
codex-process-hardening = { workspace = true }
codex-protocol = { workspace = true }
codex-protocol-ts = { workspace = true }
codex-responses-api-proxy = { workspace = true }
codex-rmcp-client = { workspace = true }
codex-stdio-to-uds = { workspace = true }
codex-tui = { workspace = true }
ctor = { workspace = true }
libc = { workspace = true }
owo-colors = { workspace = true }
regex-lite = { workspace = true}
serde_json = { workspace = true }
supports-color = { workspace = true }
toml = { workspace = true }
tokio = { workspace = true, features = [
"io-std",
"macros",
@@ -46,6 +50,7 @@ tokio = { workspace = true, features = [
"rt-multi-thread",
"signal",
] }
tracing = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies]
codex_windows_sandbox = { package = "codex-windows-sandbox", path = "../windows-sandbox-rs" }

View File

@@ -1,3 +1,8 @@
#[cfg(target_os = "macos")]
mod pid_tracker;
#[cfg(target_os = "macos")]
mod seatbelt;
use std::path::PathBuf;
use codex_common::CliConfigOverrides;
@@ -5,6 +10,7 @@ use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::exec_env::create_env;
use codex_core::landlock::spawn_command_under_linux_sandbox;
#[cfg(target_os = "macos")]
use codex_core::seatbelt::spawn_command_under_seatbelt;
use codex_core::spawn::StdioPolicy;
use codex_protocol::config_types::SandboxMode;
@@ -14,12 +20,17 @@ use crate::SeatbeltCommand;
use crate::WindowsCommand;
use crate::exit_status::handle_exit_status;
#[cfg(target_os = "macos")]
use seatbelt::DenialLogger;
#[cfg(target_os = "macos")]
pub async fn run_command_under_seatbelt(
command: SeatbeltCommand,
codex_linux_sandbox_exe: Option<PathBuf>,
) -> anyhow::Result<()> {
let SeatbeltCommand {
full_auto,
log_denials,
config_overrides,
command,
} = command;
@@ -29,10 +40,19 @@ pub async fn run_command_under_seatbelt(
config_overrides,
codex_linux_sandbox_exe,
SandboxType::Seatbelt,
log_denials,
)
.await
}
#[cfg(not(target_os = "macos"))]
pub async fn run_command_under_seatbelt(
_command: SeatbeltCommand,
_codex_linux_sandbox_exe: Option<PathBuf>,
) -> anyhow::Result<()> {
anyhow::bail!("Seatbelt sandbox is only available on macOS");
}
pub async fn run_command_under_landlock(
command: LandlockCommand,
codex_linux_sandbox_exe: Option<PathBuf>,
@@ -48,6 +68,7 @@ pub async fn run_command_under_landlock(
config_overrides,
codex_linux_sandbox_exe,
SandboxType::Landlock,
false,
)
.await
}
@@ -67,11 +88,13 @@ pub async fn run_command_under_windows(
config_overrides,
codex_linux_sandbox_exe,
SandboxType::Windows,
false,
)
.await
}
enum SandboxType {
#[cfg(target_os = "macos")]
Seatbelt,
Landlock,
Windows,
@@ -83,6 +106,7 @@ async fn run_command_under_sandbox(
config_overrides: CliConfigOverrides,
codex_linux_sandbox_exe: Option<PathBuf>,
sandbox_type: SandboxType,
log_denials: bool,
) -> anyhow::Result<()> {
let sandbox_mode = create_sandbox_mode(full_auto);
let config = Config::load_with_cli_overrides(
@@ -114,20 +138,20 @@ async fn run_command_under_sandbox(
{
use codex_windows_sandbox::run_windows_sandbox_capture;
let policy_str = match &config.sandbox_policy {
codex_core::protocol::SandboxPolicy::DangerFullAccess => "workspace-write",
codex_core::protocol::SandboxPolicy::ReadOnly => "read-only",
codex_core::protocol::SandboxPolicy::WorkspaceWrite { .. } => "workspace-write",
};
let policy_str = serde_json::to_string(&config.sandbox_policy)?;
let sandbox_cwd = sandbox_policy_cwd.clone();
let cwd_clone = cwd.clone();
let env_map = env.clone();
let command_vec = command.clone();
let base_dir = config.codex_home.clone();
// Preflight audit is invoked elsewhere at the appropriate times.
let res = tokio::task::spawn_blocking(move || {
run_windows_sandbox_capture(
policy_str,
policy_str.as_str(),
&sandbox_cwd,
base_dir.as_path(),
command_vec,
&cwd_clone,
env_map,
@@ -165,7 +189,13 @@ async fn run_command_under_sandbox(
}
}
#[cfg(target_os = "macos")]
let mut denial_logger = log_denials.then(DenialLogger::new).flatten();
#[cfg(not(target_os = "macos"))]
let _ = log_denials;
let mut child = match sandbox_type {
#[cfg(target_os = "macos")]
SandboxType::Seatbelt => {
spawn_command_under_seatbelt(
command,
@@ -197,8 +227,27 @@ async fn run_command_under_sandbox(
unreachable!("Windows sandbox should have been handled above");
}
};
#[cfg(target_os = "macos")]
if let Some(denial_logger) = &mut denial_logger {
denial_logger.on_child_spawn(&child);
}
let status = child.wait().await?;
#[cfg(target_os = "macos")]
if let Some(denial_logger) = denial_logger {
let denials = denial_logger.finish().await;
eprintln!("\n=== Sandbox denials ===");
if denials.is_empty() {
eprintln!("None found.");
} else {
for seatbelt::SandboxDenial { name, capability } in denials {
eprintln!("({name}) {capability}");
}
}
}
handle_exit_status(status);
}

View File

@@ -0,0 +1,372 @@
use std::collections::HashSet;
use tokio::task::JoinHandle;
use tracing::warn;
/// Tracks the (recursive) descendants of a process by using `kqueue` to watch for fork events, and
/// `proc_listchildpids` to list the children of a process.
pub(crate) struct PidTracker {
kq: libc::c_int,
handle: JoinHandle<HashSet<i32>>,
}
impl PidTracker {
pub(crate) fn new(root_pid: i32) -> Option<Self> {
if root_pid <= 0 {
return None;
}
let kq = unsafe { libc::kqueue() };
let handle = tokio::task::spawn_blocking(move || track_descendants(kq, root_pid));
Some(Self { kq, handle })
}
pub(crate) async fn stop(self) -> HashSet<i32> {
trigger_stop_event(self.kq);
self.handle.await.unwrap_or_default()
}
}
unsafe extern "C" {
fn proc_listchildpids(
ppid: libc::c_int,
buffer: *mut libc::c_void,
buffersize: libc::c_int,
) -> libc::c_int;
}
/// Wrap proc_listchildpids.
fn list_child_pids(parent: i32) -> Vec<i32> {
unsafe {
let mut capacity: usize = 16;
loop {
let mut buf: Vec<i32> = vec![0; capacity];
let count = proc_listchildpids(
parent as libc::c_int,
buf.as_mut_ptr() as *mut libc::c_void,
(buf.len() * std::mem::size_of::<i32>()) as libc::c_int,
);
if count <= 0 {
return Vec::new();
}
let returned = count as usize;
if returned < capacity {
buf.truncate(returned);
return buf;
}
capacity = capacity.saturating_mul(2).max(returned + 16);
}
}
}
fn pid_is_alive(pid: i32) -> bool {
if pid <= 0 {
return false;
}
let res = unsafe { libc::kill(pid as libc::pid_t, 0) };
if res == 0 {
true
} else {
matches!(
std::io::Error::last_os_error().raw_os_error(),
Some(libc::EPERM)
)
}
}
enum WatchPidError {
ProcessGone,
Other(std::io::Error),
}
/// Add `pid` to the watch list in `kq`.
fn watch_pid(kq: libc::c_int, pid: i32) -> Result<(), WatchPidError> {
if pid <= 0 {
return Err(WatchPidError::ProcessGone);
}
let kev = libc::kevent {
ident: pid as libc::uintptr_t,
filter: libc::EVFILT_PROC,
flags: libc::EV_ADD | libc::EV_CLEAR,
fflags: libc::NOTE_FORK | libc::NOTE_EXEC | libc::NOTE_EXIT,
data: 0,
udata: std::ptr::null_mut(),
};
let res = unsafe { libc::kevent(kq, &kev, 1, std::ptr::null_mut(), 0, std::ptr::null()) };
if res < 0 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::ESRCH) {
Err(WatchPidError::ProcessGone)
} else {
Err(WatchPidError::Other(err))
}
} else {
Ok(())
}
}
fn watch_children(
kq: libc::c_int,
parent: i32,
seen: &mut HashSet<i32>,
active: &mut HashSet<i32>,
) {
for child_pid in list_child_pids(parent) {
add_pid_watch(kq, child_pid, seen, active);
}
}
/// Watch `pid` and its children, updating `seen` and `active` sets.
fn add_pid_watch(kq: libc::c_int, pid: i32, seen: &mut HashSet<i32>, active: &mut HashSet<i32>) {
if pid <= 0 {
return;
}
let newly_seen = seen.insert(pid);
let mut should_recurse = newly_seen;
if active.insert(pid) {
match watch_pid(kq, pid) {
Ok(()) => {
should_recurse = true;
}
Err(WatchPidError::ProcessGone) => {
active.remove(&pid);
return;
}
Err(WatchPidError::Other(err)) => {
warn!("failed to watch pid {pid}: {err}");
active.remove(&pid);
return;
}
}
}
if should_recurse {
watch_children(kq, pid, seen, active);
}
}
const STOP_IDENT: libc::uintptr_t = 1;
fn register_stop_event(kq: libc::c_int) -> bool {
let kev = libc::kevent {
ident: STOP_IDENT,
filter: libc::EVFILT_USER,
flags: libc::EV_ADD | libc::EV_CLEAR,
fflags: 0,
data: 0,
udata: std::ptr::null_mut(),
};
let res = unsafe { libc::kevent(kq, &kev, 1, std::ptr::null_mut(), 0, std::ptr::null()) };
res >= 0
}
fn trigger_stop_event(kq: libc::c_int) {
if kq < 0 {
return;
}
let kev = libc::kevent {
ident: STOP_IDENT,
filter: libc::EVFILT_USER,
flags: 0,
fflags: libc::NOTE_TRIGGER,
data: 0,
udata: std::ptr::null_mut(),
};
let _ = unsafe { libc::kevent(kq, &kev, 1, std::ptr::null_mut(), 0, std::ptr::null()) };
}
/// Put all of the above together to track all the descendants of `root_pid`.
fn track_descendants(kq: libc::c_int, root_pid: i32) -> HashSet<i32> {
if kq < 0 {
let mut seen = HashSet::new();
seen.insert(root_pid);
return seen;
}
if !register_stop_event(kq) {
let mut seen = HashSet::new();
seen.insert(root_pid);
let _ = unsafe { libc::close(kq) };
return seen;
}
let mut seen: HashSet<i32> = HashSet::new();
let mut active: HashSet<i32> = HashSet::new();
add_pid_watch(kq, root_pid, &mut seen, &mut active);
const EVENTS_CAP: usize = 32;
let mut events: [libc::kevent; EVENTS_CAP] =
unsafe { std::mem::MaybeUninit::zeroed().assume_init() };
let mut stop_requested = false;
loop {
if active.is_empty() {
if !pid_is_alive(root_pid) {
break;
}
add_pid_watch(kq, root_pid, &mut seen, &mut active);
if active.is_empty() {
continue;
}
}
let nev = unsafe {
libc::kevent(
kq,
std::ptr::null::<libc::kevent>(),
0,
events.as_mut_ptr(),
EVENTS_CAP as libc::c_int,
std::ptr::null(),
)
};
if nev < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
break;
}
if nev == 0 {
continue;
}
for ev in events.iter().take(nev as usize) {
let pid = ev.ident as i32;
if ev.filter == libc::EVFILT_USER && ev.ident == STOP_IDENT {
stop_requested = true;
break;
}
if (ev.flags & libc::EV_ERROR) != 0 {
if ev.data == libc::ESRCH as isize {
active.remove(&pid);
}
continue;
}
if (ev.fflags & libc::NOTE_FORK) != 0 {
watch_children(kq, pid, &mut seen, &mut active);
}
if (ev.fflags & libc::NOTE_EXIT) != 0 {
active.remove(&pid);
}
}
if stop_requested {
break;
}
}
let _ = unsafe { libc::close(kq) };
seen
}
#[cfg(test)]
mod tests {
use super::*;
use std::process::Command;
use std::process::Stdio;
use std::time::Duration;
#[test]
fn pid_is_alive_detects_current_process() {
let pid = std::process::id() as i32;
assert!(pid_is_alive(pid));
}
#[cfg(target_os = "macos")]
#[test]
fn list_child_pids_includes_spawned_child() {
let mut child = Command::new("/bin/sleep")
.arg("5")
.stdin(Stdio::null())
.spawn()
.expect("failed to spawn child process");
let child_pid = child.id() as i32;
let parent_pid = std::process::id() as i32;
let mut found = false;
for _ in 0..100 {
if list_child_pids(parent_pid).contains(&child_pid) {
found = true;
break;
}
std::thread::sleep(Duration::from_millis(10));
}
let _ = child.kill();
let _ = child.wait();
assert!(found, "expected to find child pid {child_pid} in list");
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn pid_tracker_collects_spawned_children() {
let tracker = PidTracker::new(std::process::id() as i32).expect("failed to create tracker");
let mut child = Command::new("/bin/sleep")
.arg("0.1")
.stdin(Stdio::null())
.spawn()
.expect("failed to spawn child process");
let child_pid = child.id() as i32;
let parent_pid = std::process::id() as i32;
let _ = child.wait();
let seen = tracker.stop().await;
assert!(
seen.contains(&parent_pid),
"expected tracker to include parent pid {parent_pid}"
);
assert!(
seen.contains(&child_pid),
"expected tracker to include child pid {child_pid}"
);
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn pid_tracker_collects_bash_subshell_descendants() {
let tracker = PidTracker::new(std::process::id() as i32).expect("failed to create tracker");
let child = Command::new("/bin/bash")
.arg("-c")
.arg("(sleep 0.1 & echo $!; wait)")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("failed to spawn bash");
let output = child.wait_with_output().unwrap().stdout;
let subshell_pid = String::from_utf8_lossy(&output)
.trim()
.parse::<i32>()
.expect("failed to parse subshell pid");
let seen = tracker.stop().await;
assert!(
seen.contains(&subshell_pid),
"expected tracker to include subshell pid {subshell_pid}"
);
}
}

View File

@@ -0,0 +1,114 @@
use std::collections::HashSet;
use tokio::io::AsyncBufReadExt;
use tokio::process::Child;
use tokio::task::JoinHandle;
use super::pid_tracker::PidTracker;
pub struct SandboxDenial {
pub name: String,
pub capability: String,
}
pub struct DenialLogger {
log_stream: Child,
pid_tracker: Option<PidTracker>,
log_reader: Option<JoinHandle<Vec<u8>>>,
}
impl DenialLogger {
pub(crate) fn new() -> Option<Self> {
let mut log_stream = start_log_stream()?;
let stdout = log_stream.stdout.take()?;
let log_reader = tokio::spawn(async move {
let mut reader = tokio::io::BufReader::new(stdout);
let mut logs = Vec::new();
let mut chunk = Vec::new();
loop {
match reader.read_until(b'\n', &mut chunk).await {
Ok(0) | Err(_) => break,
Ok(_) => {
logs.extend_from_slice(&chunk);
chunk.clear();
}
}
}
logs
});
Some(Self {
log_stream,
pid_tracker: None,
log_reader: Some(log_reader),
})
}
pub(crate) fn on_child_spawn(&mut self, child: &Child) {
if let Some(root_pid) = child.id() {
self.pid_tracker = PidTracker::new(root_pid as i32);
}
}
pub(crate) async fn finish(mut self) -> Vec<SandboxDenial> {
let pid_set = match self.pid_tracker {
Some(tracker) => tracker.stop().await,
None => Default::default(),
};
if pid_set.is_empty() {
return Vec::new();
}
let _ = self.log_stream.kill().await;
let _ = self.log_stream.wait().await;
let logs_bytes = match self.log_reader.take() {
Some(handle) => handle.await.unwrap_or_default(),
None => Vec::new(),
};
let logs = String::from_utf8_lossy(&logs_bytes);
let mut seen: HashSet<(String, String)> = HashSet::new();
let mut denials: Vec<SandboxDenial> = Vec::new();
for line in logs.lines() {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(line)
&& let Some(msg) = json.get("eventMessage").and_then(|v| v.as_str())
&& let Some((pid, name, capability)) = parse_message(msg)
&& pid_set.contains(&pid)
&& seen.insert((name.clone(), capability.clone()))
{
denials.push(SandboxDenial { name, capability });
}
}
denials
}
}
fn start_log_stream() -> Option<Child> {
use std::process::Stdio;
const PREDICATE: &str = r#"(((processID == 0) AND (senderImagePath CONTAINS "/Sandbox")) OR (subsystem == "com.apple.sandbox.reporting"))"#;
tokio::process::Command::new("log")
.args(["stream", "--style", "ndjson", "--predicate", PREDICATE])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.kill_on_drop(true)
.spawn()
.ok()
}
fn parse_message(msg: &str) -> Option<(i32, String, String)> {
// Example message:
// Sandbox: processname(1234) deny(1) capability-name args...
static RE: std::sync::OnceLock<regex_lite::Regex> = std::sync::OnceLock::new();
let re = RE.get_or_init(|| {
#[expect(clippy::unwrap_used)]
regex_lite::Regex::new(r"^Sandbox:\s*(.+?)\((\d+)\)\s+deny\(.*?\)\s*(.+)$").unwrap()
});
let (_, [name, pid_str, capability]) = re.captures(msg)?.extract();
let pid = pid_str.trim().parse::<i32>().ok()?;
Some((pid, name.to_string(), capability.to_string()))
}

View File

@@ -11,6 +11,10 @@ pub struct SeatbeltCommand {
#[arg(long = "full-auto", default_value_t = false)]
pub full_auto: bool,
/// While the command runs, capture macOS sandbox denials via `log stream` and print them after exit
#[arg(long = "log-denials", default_value_t = false)]
pub log_denials: bool,
#[clap(skip)]
pub config_overrides: CliConfigOverrides,

View File

@@ -1,3 +1,4 @@
use clap::Args;
use clap::CommandFactory;
use clap::Parser;
use clap_complete::Shell;
@@ -17,17 +18,21 @@ use codex_cli::login::run_logout;
use codex_cloud_tasks::Cli as CloudTasksCli;
use codex_common::CliConfigOverrides;
use codex_exec::Cli as ExecCli;
use codex_execpolicy::ExecPolicyCheckCommand;
use codex_responses_api_proxy::Args as ResponsesApiProxyArgs;
use codex_tui::AppExitInfo;
use codex_tui::Cli as TuiCli;
use codex_tui::updates::UpdateAction;
use codex_tui::update_action::UpdateAction;
use owo_colors::OwoColorize;
use std::path::PathBuf;
use supports_color::Stream;
mod mcp_cmd;
#[cfg(not(windows))]
mod wsl_paths;
use crate::mcp_cmd::McpCli;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::features::is_known_feature_key;
@@ -79,8 +84,8 @@ enum Subcommand {
/// [experimental] Run the Codex MCP server (stdio transport).
McpServer,
/// [experimental] Run the app server.
AppServer,
/// [experimental] Run the app server or related tooling.
AppServer(AppServerCommand),
/// Generate shell completion scripts.
Completion(CompletionCommand),
@@ -89,6 +94,10 @@ enum Subcommand {
#[clap(visible_alias = "debug")]
Sandbox(SandboxArgs),
/// Execpolicy tooling.
#[clap(hide = true)]
Execpolicy(ExecpolicyCommand),
/// Apply the latest diff produced by Codex agent as a `git apply` to your local working tree.
#[clap(visible_alias = "a")]
Apply(ApplyCommand),
@@ -96,9 +105,6 @@ enum Subcommand {
/// Resume a previous interactive session (picker by default; use --last to continue the most recent).
Resume(ResumeCommand),
/// Internal: generate TypeScript protocol bindings.
#[clap(hide = true)]
GenerateTs(GenerateTsCommand),
/// [EXPERIMENTAL] Browse tasks from Codex Cloud and apply changes locally.
#[clap(name = "cloud", alias = "cloud-tasks")]
Cloud(CloudTasksCli),
@@ -133,6 +139,10 @@ struct ResumeCommand {
#[arg(long = "last", default_value_t = false, conflicts_with = "session_id")]
last: bool,
/// Show all sessions (disables cwd filtering and shows CWD column).
#[arg(long = "all", default_value_t = false)]
all: bool,
#[clap(flatten)]
config_overrides: TuiCli,
}
@@ -157,6 +167,19 @@ enum SandboxCommand {
Windows(WindowsCommand),
}
#[derive(Debug, Parser)]
struct ExecpolicyCommand {
#[command(subcommand)]
sub: ExecpolicySubcommand,
}
#[derive(Debug, clap::Subcommand)]
enum ExecpolicySubcommand {
/// Check execpolicy files against a command.
#[clap(name = "check")]
Check(ExecPolicyCheckCommand),
}
#[derive(Debug, Parser)]
struct LoginCommand {
#[clap(skip)]
@@ -205,6 +228,22 @@ struct LogoutCommand {
}
#[derive(Debug, Parser)]
struct AppServerCommand {
/// Omit to run the app server; specify a subcommand for tooling.
#[command(subcommand)]
subcommand: Option<AppServerSubcommand>,
}
#[derive(Debug, clap::Subcommand)]
enum AppServerSubcommand {
/// [experimental] Generate TypeScript bindings for the app server protocol.
GenerateTs(GenerateTsCommand),
/// [experimental] Generate JSON Schema for the app server protocol.
GenerateJsonSchema(GenerateJsonSchemaCommand),
}
#[derive(Debug, Args)]
struct GenerateTsCommand {
/// Output directory where .ts files will be written
#[arg(short = 'o', long = "out", value_name = "DIR")]
@@ -215,6 +254,13 @@ struct GenerateTsCommand {
prettier: Option<PathBuf>,
}
#[derive(Debug, Args)]
struct GenerateJsonSchemaCommand {
/// Output directory where the schema bundle will be written
#[arg(short = 'o', long = "out", value_name = "DIR")]
out_dir: PathBuf,
}
#[derive(Debug, Parser)]
struct StdioToUdsCommand {
/// Path to the Unix domain socket to connect to.
@@ -267,10 +313,30 @@ fn handle_app_exit(exit_info: AppExitInfo) -> anyhow::Result<()> {
/// Run the update action and print the result.
fn run_update_action(action: UpdateAction) -> anyhow::Result<()> {
println!();
let (cmd, args) = action.command_args();
let cmd_str = action.command_str();
println!("Updating Codex via `{cmd_str}`...");
let status = std::process::Command::new(cmd).args(args).status()?;
let status = {
#[cfg(windows)]
{
// On Windows, run via cmd.exe so .CMD/.BAT are correctly resolved (PATHEXT semantics).
std::process::Command::new("cmd")
.args(["/C", &cmd_str])
.status()?
}
#[cfg(not(windows))]
{
let (cmd, args) = action.command_args();
let command_path = crate::wsl_paths::normalize_for_wsl(cmd);
let normalized_args: Vec<String> = args
.iter()
.map(crate::wsl_paths::normalize_for_wsl)
.collect();
std::process::Command::new(&command_path)
.args(&normalized_args)
.status()?
}
};
if !status.success() {
anyhow::bail!("`{cmd_str}` failed with status {status}");
}
@@ -279,6 +345,10 @@ fn run_update_action(action: UpdateAction) -> anyhow::Result<()> {
Ok(())
}
fn run_execpolicycheck(cmd: ExecPolicyCheckCommand) -> anyhow::Result<()> {
cmd.run()
}
#[derive(Debug, Default, Parser, Clone)]
struct FeatureToggles {
/// Enable a feature (repeatable). Equivalent to `-c features.<name>=true`.
@@ -387,12 +457,24 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
prepend_config_flags(&mut mcp_cli.config_overrides, root_config_overrides.clone());
mcp_cli.run().await?;
}
Some(Subcommand::AppServer) => {
codex_app_server::run_main(codex_linux_sandbox_exe, root_config_overrides).await?;
}
Some(Subcommand::AppServer(app_server_cli)) => match app_server_cli.subcommand {
None => {
codex_app_server::run_main(codex_linux_sandbox_exe, root_config_overrides).await?;
}
Some(AppServerSubcommand::GenerateTs(gen_cli)) => {
codex_app_server_protocol::generate_ts(
&gen_cli.out_dir,
gen_cli.prettier.as_deref(),
)?;
}
Some(AppServerSubcommand::GenerateJsonSchema(gen_cli)) => {
codex_app_server_protocol::generate_json(&gen_cli.out_dir)?;
}
},
Some(Subcommand::Resume(ResumeCommand {
session_id,
last,
all,
config_overrides,
})) => {
interactive = finalize_resume_interactive(
@@ -400,6 +482,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
root_config_overrides.clone(),
session_id,
last,
all,
config_overrides,
);
let exit_info = codex_tui::run_main(interactive, codex_linux_sandbox_exe).await?;
@@ -488,6 +571,9 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
.await?;
}
},
Some(Subcommand::Execpolicy(ExecpolicyCommand { sub })) => match sub {
ExecpolicySubcommand::Check(cmd) => run_execpolicycheck(cmd)?,
},
Some(Subcommand::Apply(mut apply_cli)) => {
prepend_config_flags(
&mut apply_cli.config_overrides,
@@ -504,21 +590,24 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
tokio::task::spawn_blocking(move || codex_stdio_to_uds::run(socket_path.as_path()))
.await??;
}
Some(Subcommand::GenerateTs(gen_cli)) => {
codex_protocol_ts::generate_ts(&gen_cli.out_dir, gen_cli.prettier.as_deref())?;
}
Some(Subcommand::Features(FeaturesCli { sub })) => match sub {
FeaturesSubcommand::List => {
// Respect root-level `-c` overrides plus top-level flags like `--profile`.
let cli_kv_overrides = root_config_overrides
let mut cli_kv_overrides = root_config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?;
// Honor `--search` via the new feature toggle.
if interactive.web_search {
cli_kv_overrides.push((
"features.web_search_request".to_string(),
toml::Value::Boolean(true),
));
}
// Thread through relevant top-level flags (at minimum, `--profile`).
// Also honor `--search` since it maps to a feature toggle.
let overrides = ConfigOverrides {
config_profile: interactive.config_profile.clone(),
tools_web_search_request: interactive.web_search.then_some(true),
..Default::default()
};
@@ -553,6 +642,7 @@ fn finalize_resume_interactive(
root_config_overrides: CliConfigOverrides,
session_id: Option<String>,
last: bool,
show_all: bool,
resume_cli: TuiCli,
) -> TuiCli {
// Start with the parsed interactive CLI so resume shares the same
@@ -561,6 +651,7 @@ fn finalize_resume_interactive(
interactive.resume_picker = resume_session_id.is_none() && !last;
interactive.resume_last = last;
interactive.resume_session_id = resume_session_id;
interactive.resume_show_all = show_all;
// Merge resume-scoped flags and overrides with highest precedence.
merge_resume_cli_flags(&mut interactive, resume_cli);
@@ -644,13 +735,21 @@ mod tests {
let Subcommand::Resume(ResumeCommand {
session_id,
last,
all,
config_overrides: resume_cli,
}) = subcommand.expect("resume present")
else {
unreachable!()
};
finalize_resume_interactive(interactive, root_overrides, session_id, last, resume_cli)
finalize_resume_interactive(
interactive,
root_overrides,
session_id,
last,
all,
resume_cli,
)
}
fn sample_exit_info(conversation: Option<&str>) -> AppExitInfo {
@@ -703,9 +802,9 @@ mod tests {
#[test]
fn resume_model_flag_applies_when_no_root_flags() {
let interactive = finalize_from_args(["codex", "resume", "-m", "gpt-5-test"].as_ref());
let interactive = finalize_from_args(["codex", "resume", "-m", "gpt-5.1-test"].as_ref());
assert_eq!(interactive.model.as_deref(), Some("gpt-5-test"));
assert_eq!(interactive.model.as_deref(), Some("gpt-5.1-test"));
assert!(interactive.resume_picker);
assert!(!interactive.resume_last);
assert_eq!(interactive.resume_session_id, None);
@@ -717,6 +816,7 @@ mod tests {
assert!(interactive.resume_picker);
assert!(!interactive.resume_last);
assert_eq!(interactive.resume_session_id, None);
assert!(!interactive.resume_show_all);
}
#[test]
@@ -725,6 +825,7 @@ mod tests {
assert!(!interactive.resume_picker);
assert!(interactive.resume_last);
assert_eq!(interactive.resume_session_id, None);
assert!(!interactive.resume_show_all);
}
#[test]
@@ -733,6 +834,14 @@ mod tests {
assert!(!interactive.resume_picker);
assert!(!interactive.resume_last);
assert_eq!(interactive.resume_session_id.as_deref(), Some("1234"));
assert!(!interactive.resume_show_all);
}
#[test]
fn resume_all_flag_sets_show_all() {
let interactive = finalize_from_args(["codex", "resume", "--all"].as_ref());
assert!(interactive.resume_picker);
assert!(interactive.resume_show_all);
}
#[test]
@@ -750,7 +859,7 @@ mod tests {
"--ask-for-approval",
"on-request",
"-m",
"gpt-5-test",
"gpt-5.1-test",
"-p",
"my-profile",
"-C",
@@ -761,7 +870,7 @@ mod tests {
.as_ref(),
);
assert_eq!(interactive.model.as_deref(), Some("gpt-5-test"));
assert_eq!(interactive.model.as_deref(), Some("gpt-5.1-test"));
assert!(interactive.oss);
assert_eq!(interactive.config_profile.as_deref(), Some("my-profile"));
assert_matches!(

View File

@@ -79,6 +79,7 @@ pub struct GetArgs {
}
#[derive(Debug, clap::Parser)]
#[command(override_usage = "codex mcp add [OPTIONS] <NAME> (--url <URL> | -- <COMMAND>...)")]
pub struct AddArgs {
/// Name for the MCP server configuration.
pub name: String,
@@ -353,7 +354,9 @@ async fn run_login(config_overrides: &CliConfigOverrides, login_args: LoginArgs)
.context("failed to load configuration")?;
if !config.features.enabled(Feature::RmcpClient) {
bail!("OAuth login is only supported when [feature].rmcp_client is true in config.toml.");
bail!(
"OAuth login is only supported when [features].rmcp_client is true in config.toml. See https://github.com/openai/codex/blob/main/docs/config.md#feature-flags for details."
);
}
let LoginArgs { name, scopes } = login_args;

View File

@@ -0,0 +1,76 @@
use std::ffi::OsStr;
/// WSL-specific path helpers used by the updater logic.
///
/// See https://github.com/openai/codex/issues/6086.
pub fn is_wsl() -> bool {
#[cfg(target_os = "linux")]
{
if std::env::var_os("WSL_DISTRO_NAME").is_some() {
return true;
}
match std::fs::read_to_string("/proc/version") {
Ok(version) => version.to_lowercase().contains("microsoft"),
Err(_) => false,
}
}
#[cfg(not(target_os = "linux"))]
{
false
}
}
/// Convert a Windows absolute path (`C:\foo\bar` or `C:/foo/bar`) to a WSL mount path (`/mnt/c/foo/bar`).
/// Returns `None` if the input does not look like a Windows drive path.
pub fn win_path_to_wsl(path: &str) -> Option<String> {
let bytes = path.as_bytes();
if bytes.len() < 3
|| bytes[1] != b':'
|| !(bytes[2] == b'\\' || bytes[2] == b'/')
|| !bytes[0].is_ascii_alphabetic()
{
return None;
}
let drive = (bytes[0] as char).to_ascii_lowercase();
let tail = path[3..].replace('\\', "/");
if tail.is_empty() {
return Some(format!("/mnt/{drive}"));
}
Some(format!("/mnt/{drive}/{tail}"))
}
/// If under WSL and given a Windows-style path, return the equivalent `/mnt/<drive>/…` path.
/// Otherwise returns the input unchanged.
pub fn normalize_for_wsl<P: AsRef<OsStr>>(path: P) -> String {
let value = path.as_ref().to_string_lossy().to_string();
if !is_wsl() {
return value;
}
if let Some(mapped) = win_path_to_wsl(&value) {
return mapped;
}
value
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn win_to_wsl_basic() {
assert_eq!(
win_path_to_wsl(r"C:\Temp\codex.zip").as_deref(),
Some("/mnt/c/Temp/codex.zip")
);
assert_eq!(
win_path_to_wsl("D:/Work/codex.tgz").as_deref(),
Some("/mnt/d/Work/codex.tgz")
);
assert!(win_path_to_wsl("/home/user/codex").is_none());
}
#[test]
fn normalize_is_noop_on_unix_paths() {
assert_eq!(normalize_for_wsl("/home/u/x"), "/home/u/x");
}
}

View File

@@ -0,0 +1,58 @@
use std::fs;
use assert_cmd::Command;
use pretty_assertions::assert_eq;
use serde_json::json;
use tempfile::TempDir;
#[test]
fn execpolicy_check_matches_expected_json() -> Result<(), Box<dyn std::error::Error>> {
let codex_home = TempDir::new()?;
let policy_path = codex_home.path().join("policy.codexpolicy");
fs::write(
&policy_path,
r#"
prefix_rule(
pattern = ["git", "push"],
decision = "forbidden",
)
"#,
)?;
let output = Command::cargo_bin("codex")?
.env("CODEX_HOME", codex_home.path())
.args([
"execpolicy",
"check",
"--policy",
policy_path
.to_str()
.expect("policy path should be valid UTF-8"),
"git",
"push",
"origin",
"main",
])
.output()?;
assert!(output.status.success());
let result: serde_json::Value = serde_json::from_slice(&output.stdout)?;
assert_eq!(
result,
json!({
"match": {
"decision": "forbidden",
"matchedRules": [
{
"prefixRuleMatch": {
"matchedPrefix": ["git", "push"],
"decision": "forbidden"
}
}
]
}
})
);
Ok(())
}

View File

@@ -1,7 +1,8 @@
[package]
name = "codex-cloud-tasks-client"
version = { workspace = true }
edition = "2024"
version.workspace = true
edition.workspace = true
license.workspace = true
[lib]
name = "codex_cloud_tasks_client"

View File

@@ -1,7 +1,8 @@
[package]
edition = "2024"
name = "codex-cloud-tasks"
version = { workspace = true }
version.workspace = true
edition.workspace = true
license.workspace = true
[lib]
name = "codex_cloud_tasks"

View File

@@ -8,6 +8,7 @@ pub mod util;
pub use cli::Cli;
use anyhow::anyhow;
use codex_login::AuthManager;
use std::io::IsTerminal;
use std::io::Read;
use std::path::PathBuf;
@@ -56,20 +57,8 @@ async fn init_backend(user_agent_suffix: &str) -> anyhow::Result<BackendContext>
};
append_error_log(format!("startup: base_url={base_url} path_style={style}"));
let auth = match codex_core::config::find_codex_home()
.ok()
.map(|home| {
let store_mode = codex_core::config::Config::load_from_base_config_with_overrides(
codex_core::config::ConfigToml::default(),
codex_core::config::ConfigOverrides::default(),
home.clone(),
)
.map(|cfg| cfg.cli_auth_credentials_store_mode)
.unwrap_or_default();
codex_login::AuthManager::new(home, false, store_mode)
})
.and_then(|am| am.auth())
{
let auth_manager = util::load_auth_manager().await;
let auth = match auth_manager.as_ref().and_then(AuthManager::auth) {
Some(auth) => auth,
None => {
eprintln!(
@@ -1044,7 +1033,7 @@ pub async fn run_main(cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> an
// Close task modal/pending apply if present before opening env modal
app.diff_overlay = None;
app.env_modal = Some(app::EnvModalState { query: String::new(), selected: 0 });
// Cache environments until user explicitly refreshes with 'r' inside the modal.
// Cache environments while the modal is open to avoid repeated fetches.
let should_fetch = app.environments.is_empty();
if should_fetch {
app.env_loading = true;
@@ -1115,7 +1104,7 @@ pub async fn run_main(cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> an
let _ = tx.send(evt);
});
} else {
app.status = "No environment selected (press 'e' to choose)".to_string();
app.status = "No environment selected".to_string();
}
}
needs_redraw = true;
@@ -1313,18 +1302,6 @@ pub async fn run_main(cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> an
// Environment modal key handling
match key.code {
KeyCode::Esc => { app.env_modal = None; needs_redraw = true; }
KeyCode::Char('r') | KeyCode::Char('R') => {
// Trigger refresh of environments
app.env_loading = true; app.env_error = None; needs_redraw = true;
let _ = frame_tx.send(Instant::now() + Duration::from_millis(100));
let tx = tx.clone();
tokio::spawn(async move {
let base_url = crate::util::normalize_base_url(&std::env::var("CODEX_CLOUD_TASKS_BASE_URL").unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()));
let headers = crate::util::build_chatgpt_headers().await;
let res = crate::env_detect::list_environments(&base_url, &headers).await;
let _ = tx.send(app::AppEvent::EnvironmentsLoaded(res));
});
}
KeyCode::Char(ch) if !key.modifiers.contains(KeyModifiers::CONTROL) && !key.modifiers.contains(KeyModifiers::ALT) => {
if let Some(m) = app.env_modal.as_mut() { m.query.push(ch); }
needs_redraw = true;
@@ -1431,7 +1408,7 @@ pub async fn run_main(cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> an
}
KeyCode::Char('o') | KeyCode::Char('O') => {
app.env_modal = Some(app::EnvModalState { query: String::new(), selected: 0 });
// Cache environments until user explicitly refreshes with 'r' inside the modal.
// Cache environments while the modal is open to avoid repeated fetches.
let should_fetch = app.environments.is_empty();
if should_fetch { app.env_loading = true; app.env_error = None; }
needs_redraw = true;
@@ -1744,6 +1721,7 @@ mod tests {
use ratatui::layout::Rect;
#[test]
#[ignore = "very slow"]
fn composer_input_renders_typed_characters() {
let mut composer = ComposerInput::new();
let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);

View File

@@ -945,9 +945,7 @@ pub fn draw_env_modal(frame: &mut Frame, area: Rect, app: &mut App) {
// Subheader with usage hints (dim cyan)
let subheader = Paragraph::new(Line::from(
"Type to search, Enter select, Esc cancel; r refresh"
.cyan()
.dim(),
"Type to search, Enter select, Esc cancel".cyan().dim(),
))
.wrap(Wrap { trim: true });
frame.render_widget(subheader, rows[0]);

View File

@@ -2,6 +2,10 @@ use base64::Engine as _;
use chrono::Utc;
use reqwest::header::HeaderMap;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_login::AuthManager;
pub fn set_user_agent_suffix(suffix: &str) {
if let Ok(mut guard) = codex_core::default_client::USER_AGENT_SUFFIX.lock() {
guard.replace(suffix.to_string());
@@ -54,6 +58,18 @@ pub fn extract_chatgpt_account_id(token: &str) -> Option<String> {
.map(str::to_string)
}
pub async fn load_auth_manager() -> Option<AuthManager> {
// TODO: pass in cli overrides once cloud tasks properly support them.
let config = Config::load_with_cli_overrides(Vec::new(), ConfigOverrides::default())
.await
.ok()?;
Some(AuthManager::new(
config.codex_home,
false,
config.cli_auth_credentials_store_mode,
))
}
/// Build headers for ChatGPT-backed requests: `User-Agent`, optional `Authorization`,
/// and optional `ChatGPT-Account-Id`.
pub async fn build_chatgpt_headers() -> HeaderMap {
@@ -69,31 +85,22 @@ pub async fn build_chatgpt_headers() -> HeaderMap {
USER_AGENT,
HeaderValue::from_str(&ua).unwrap_or(HeaderValue::from_static("codex-cli")),
);
if let Ok(home) = codex_core::config::find_codex_home() {
let store_mode = codex_core::config::Config::load_from_base_config_with_overrides(
codex_core::config::ConfigToml::default(),
codex_core::config::ConfigOverrides::default(),
home.clone(),
)
.map(|cfg| cfg.cli_auth_credentials_store_mode)
.unwrap_or_default();
let am = codex_login::AuthManager::new(home, false, store_mode);
if let Some(auth) = am.auth()
&& let Ok(tok) = auth.get_token().await
&& !tok.is_empty()
if let Some(am) = load_auth_manager().await
&& let Some(auth) = am.auth()
&& let Ok(tok) = auth.get_token().await
&& !tok.is_empty()
{
let v = format!("Bearer {tok}");
if let Ok(hv) = HeaderValue::from_str(&v) {
headers.insert(AUTHORIZATION, hv);
}
if let Some(acc) = auth
.get_account_id()
.or_else(|| extract_chatgpt_account_id(&tok))
&& let Ok(name) = HeaderName::from_bytes(b"ChatGPT-Account-Id")
&& let Ok(hv) = HeaderValue::from_str(&acc)
{
let v = format!("Bearer {tok}");
if let Ok(hv) = HeaderValue::from_str(&v) {
headers.insert(AUTHORIZATION, hv);
}
if let Some(acc) = auth
.get_account_id()
.or_else(|| extract_chatgpt_account_id(&tok))
&& let Ok(name) = HeaderName::from_bytes(b"ChatGPT-Account-Id")
&& let Ok(hv) = HeaderValue::from_str(&acc)
{
headers.insert(name, hv);
}
headers.insert(name, hv);
}
}
headers

View File

@@ -0,0 +1,30 @@
[package]
name = "codex-api"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
async-trait = { workspace = true }
bytes = { workspace = true }
codex-client = { workspace = true }
codex-protocol = { workspace = true }
futures = { workspace = true }
http = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt", "sync", "time"] }
tracing = { workspace = true }
eventsource-stream = { workspace = true }
regex-lite = { workspace = true }
tokio-util = { workspace = true, features = ["codec"] }
[dev-dependencies]
anyhow = { workspace = true }
assert_matches = { workspace = true }
pretty_assertions = { workspace = true }
tokio-test = { workspace = true }
[lints]
workspace = true

View File

@@ -0,0 +1,32 @@
# codex-api
Typed clients for Codex/OpenAI APIs built on top of the generic transport in `codex-client`.
- Hosts the request/response models and prompt helpers for Responses, Chat Completions, and Compact APIs.
- Owns provider configuration (base URLs, headers, query params), auth header injection, retry tuning, and stream idle settings.
- Parses SSE streams into `ResponseEvent`/`ResponseStream`, including rate-limit snapshots and API-specific error mapping.
- Serves as the wire-level layer consumed by `codex-core`; higher layers handle auth refresh and business logic.
## Core interface
The public interface of this crate is intentionally small and uniform:
- **Prompted endpoints (Chat + Responses)**
- Input: a single `Prompt` plus endpoint-specific options.
- `Prompt` (re-exported as `codex_api::Prompt`) carries:
- `instructions: String` the fully-resolved system prompt for this turn.
- `input: Vec<ResponseItem>` conversation history and user/tool messages.
- `tools: Vec<serde_json::Value>` JSON tools compatible with the target API.
- `parallel_tool_calls: bool`.
- `output_schema: Option<Value>` used to build `text.format` when present.
- Output: a `ResponseStream` of `ResponseEvent` (both re-exported from `common`).
- **Compaction endpoint**
- Input: `CompactionInput<'a>` (re-exported as `codex_api::CompactionInput`):
- `model: &str`.
- `input: &[ResponseItem]` history to compact.
- `instructions: &str` fully-resolved compaction instructions.
- Output: `Vec<ResponseItem>`.
- `CompactClient::compact_input(&CompactionInput, extra_headers)` wraps the JSON encoding and retry/telemetry wiring.
All HTTP details (URLs, headers, retry/backoff policies, SSE framing) are encapsulated in `codex-api` and `codex-client`. Callers construct prompts/inputs using protocol types and work with typed streams of `ResponseEvent` or compacted `ResponseItem` values.

View File

@@ -0,0 +1,27 @@
use codex_client::Request;
/// Provides bearer and account identity information for API requests.
///
/// Implementations should be cheap and non-blocking; any asynchronous
/// refresh or I/O should be handled by higher layers before requests
/// reach this interface.
pub trait AuthProvider: Send + Sync {
fn bearer_token(&self) -> Option<String>;
fn account_id(&self) -> Option<String> {
None
}
}
pub(crate) fn add_auth_headers<A: AuthProvider>(auth: &A, mut req: Request) -> Request {
if let Some(token) = auth.bearer_token()
&& let Ok(header) = format!("Bearer {token}").parse()
{
let _ = req.headers.insert(http::header::AUTHORIZATION, header);
}
if let Some(account_id) = auth.account_id()
&& let Ok(header) = account_id.parse()
{
let _ = req.headers.insert("ChatGPT-Account-ID", header);
}
req
}

View File

@@ -0,0 +1,167 @@
use crate::error::ApiError;
use codex_protocol::config_types::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
use codex_protocol::config_types::Verbosity as VerbosityConfig;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::RateLimitSnapshot;
use codex_protocol::protocol::TokenUsage;
use futures::Stream;
use serde::Serialize;
use serde_json::Value;
use std::pin::Pin;
use std::task::Context;
use std::task::Poll;
use tokio::sync::mpsc;
/// Canonical prompt input for Chat and Responses endpoints.
#[derive(Debug, Clone)]
pub struct Prompt {
/// Fully-resolved system instructions for this turn.
pub instructions: String,
/// Conversation history and user/tool messages.
pub input: Vec<ResponseItem>,
/// JSON-encoded tool definitions compatible with the target API.
// TODO(jif) have a proper type here
pub tools: Vec<Value>,
/// Whether parallel tool calls are permitted.
pub parallel_tool_calls: bool,
/// Optional output schema used to build the `text.format` controls.
pub output_schema: Option<Value>,
}
/// Canonical input payload for the compaction endpoint.
#[derive(Debug, Clone, Serialize)]
pub struct CompactionInput<'a> {
pub model: &'a str,
pub input: &'a [ResponseItem],
pub instructions: &'a str,
}
#[derive(Debug)]
pub enum ResponseEvent {
Created,
OutputItemDone(ResponseItem),
OutputItemAdded(ResponseItem),
Completed {
response_id: String,
token_usage: Option<TokenUsage>,
},
OutputTextDelta(String),
ReasoningSummaryDelta {
delta: String,
summary_index: i64,
},
ReasoningContentDelta {
delta: String,
content_index: i64,
},
ReasoningSummaryPartAdded {
summary_index: i64,
},
RateLimits(RateLimitSnapshot),
}
#[derive(Debug, Serialize, Clone)]
pub struct Reasoning {
#[serde(skip_serializing_if = "Option::is_none")]
pub effort: Option<ReasoningEffortConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<ReasoningSummaryConfig>,
}
#[derive(Debug, Serialize, Default, Clone)]
#[serde(rename_all = "snake_case")]
pub enum TextFormatType {
#[default]
JsonSchema,
}
#[derive(Debug, Serialize, Default, Clone)]
pub struct TextFormat {
/// Format type used by the OpenAI text controls.
pub r#type: TextFormatType,
/// When true, the server is expected to strictly validate responses.
pub strict: bool,
/// JSON schema for the desired output.
pub schema: Value,
/// Friendly name for the format, used in telemetry/debugging.
pub name: String,
}
/// Controls the `text` field for the Responses API, combining verbosity and
/// optional JSON schema output formatting.
#[derive(Debug, Serialize, Default, Clone)]
pub struct TextControls {
#[serde(skip_serializing_if = "Option::is_none")]
pub verbosity: Option<OpenAiVerbosity>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<TextFormat>,
}
#[derive(Debug, Serialize, Default, Clone)]
#[serde(rename_all = "lowercase")]
pub enum OpenAiVerbosity {
Low,
#[default]
Medium,
High,
}
impl From<VerbosityConfig> for OpenAiVerbosity {
fn from(v: VerbosityConfig) -> Self {
match v {
VerbosityConfig::Low => OpenAiVerbosity::Low,
VerbosityConfig::Medium => OpenAiVerbosity::Medium,
VerbosityConfig::High => OpenAiVerbosity::High,
}
}
}
#[derive(Debug, Serialize)]
pub struct ResponsesApiRequest<'a> {
pub model: &'a str,
pub instructions: &'a str,
pub input: &'a [ResponseItem],
pub tools: &'a [serde_json::Value],
pub tool_choice: &'static str,
pub parallel_tool_calls: bool,
pub reasoning: Option<Reasoning>,
pub store: bool,
pub stream: bool,
pub include: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_cache_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<TextControls>,
}
pub fn create_text_param_for_request(
verbosity: Option<VerbosityConfig>,
output_schema: &Option<Value>,
) -> Option<TextControls> {
if verbosity.is_none() && output_schema.is_none() {
return None;
}
Some(TextControls {
verbosity: verbosity.map(std::convert::Into::into),
format: output_schema.as_ref().map(|schema| TextFormat {
r#type: TextFormatType::JsonSchema,
strict: true,
schema: schema.clone(),
name: "codex_output_schema".to_string(),
}),
})
}
pub struct ResponseStream {
pub rx_event: mpsc::Receiver<Result<ResponseEvent, ApiError>>,
}
impl Stream for ResponseStream {
type Item = Result<ResponseEvent, ApiError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
self.rx_event.poll_recv(cx)
}
}

View File

@@ -0,0 +1,266 @@
use crate::ChatRequest;
use crate::auth::AuthProvider;
use crate::common::Prompt as ApiPrompt;
use crate::common::ResponseEvent;
use crate::common::ResponseStream;
use crate::endpoint::streaming::StreamingClient;
use crate::error::ApiError;
use crate::provider::Provider;
use crate::provider::WireApi;
use crate::sse::chat::spawn_chat_stream;
use crate::telemetry::SseTelemetry;
use codex_client::HttpTransport;
use codex_client::RequestTelemetry;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ReasoningItemContent;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::SessionSource;
use futures::Stream;
use http::HeaderMap;
use serde_json::Value;
use std::collections::VecDeque;
use std::pin::Pin;
use std::sync::Arc;
use std::task::Context;
use std::task::Poll;
pub struct ChatClient<T: HttpTransport, A: AuthProvider> {
streaming: StreamingClient<T, A>,
}
impl<T: HttpTransport, A: AuthProvider> ChatClient<T, A> {
pub fn new(transport: T, provider: Provider, auth: A) -> Self {
Self {
streaming: StreamingClient::new(transport, provider, auth),
}
}
pub fn with_telemetry(
self,
request: Option<Arc<dyn RequestTelemetry>>,
sse: Option<Arc<dyn SseTelemetry>>,
) -> Self {
Self {
streaming: self.streaming.with_telemetry(request, sse),
}
}
pub async fn stream_request(&self, request: ChatRequest) -> Result<ResponseStream, ApiError> {
self.stream(request.body, request.headers).await
}
pub async fn stream_prompt(
&self,
model: &str,
prompt: &ApiPrompt,
conversation_id: Option<String>,
session_source: Option<SessionSource>,
) -> Result<ResponseStream, ApiError> {
use crate::requests::ChatRequestBuilder;
let request =
ChatRequestBuilder::new(model, &prompt.instructions, &prompt.input, &prompt.tools)
.conversation_id(conversation_id)
.session_source(session_source)
.build(self.streaming.provider())?;
self.stream_request(request).await
}
fn path(&self) -> &'static str {
match self.streaming.provider().wire {
WireApi::Chat => "chat/completions",
_ => "responses",
}
}
pub async fn stream(
&self,
body: Value,
extra_headers: HeaderMap,
) -> Result<ResponseStream, ApiError> {
self.streaming
.stream(self.path(), body, extra_headers, spawn_chat_stream)
.await
}
}
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum AggregateMode {
AggregatedOnly,
Streaming,
}
/// Stream adapter that merges token deltas into a single assistant message per turn.
pub struct AggregatedStream {
inner: ResponseStream,
cumulative: String,
cumulative_reasoning: String,
pending: VecDeque<ResponseEvent>,
mode: AggregateMode,
}
impl Stream for AggregatedStream {
type Item = Result<ResponseEvent, ApiError>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
if let Some(ev) = this.pending.pop_front() {
return Poll::Ready(Some(Ok(ev)));
}
loop {
match Pin::new(&mut this.inner).poll_next(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(None) => return Poll::Ready(None),
Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))),
Poll::Ready(Some(Ok(ResponseEvent::OutputItemDone(item)))) => {
let is_assistant_message = matches!(
&item,
ResponseItem::Message { role, .. } if role == "assistant"
);
if is_assistant_message {
match this.mode {
AggregateMode::AggregatedOnly => {
if this.cumulative.is_empty()
&& let ResponseItem::Message { content, .. } = &item
&& let Some(text) = content.iter().find_map(|c| match c {
ContentItem::OutputText { text } => Some(text),
_ => None,
})
{
this.cumulative.push_str(text);
}
continue;
}
AggregateMode::Streaming => {
if this.cumulative.is_empty() {
return Poll::Ready(Some(Ok(ResponseEvent::OutputItemDone(
item,
))));
} else {
continue;
}
}
}
}
return Poll::Ready(Some(Ok(ResponseEvent::OutputItemDone(item))));
}
Poll::Ready(Some(Ok(ResponseEvent::RateLimits(snapshot)))) => {
return Poll::Ready(Some(Ok(ResponseEvent::RateLimits(snapshot))));
}
Poll::Ready(Some(Ok(ResponseEvent::Completed {
response_id,
token_usage,
}))) => {
let mut emitted_any = false;
if !this.cumulative_reasoning.is_empty() {
let aggregated_reasoning = ResponseItem::Reasoning {
id: String::new(),
summary: Vec::new(),
content: Some(vec![ReasoningItemContent::ReasoningText {
text: std::mem::take(&mut this.cumulative_reasoning),
}]),
encrypted_content: None,
};
this.pending
.push_back(ResponseEvent::OutputItemDone(aggregated_reasoning));
emitted_any = true;
}
if !this.cumulative.is_empty() {
let aggregated_message = ResponseItem::Message {
id: None,
role: "assistant".to_string(),
content: vec![ContentItem::OutputText {
text: std::mem::take(&mut this.cumulative),
}],
};
this.pending
.push_back(ResponseEvent::OutputItemDone(aggregated_message));
emitted_any = true;
}
if emitted_any {
this.pending.push_back(ResponseEvent::Completed {
response_id: response_id.clone(),
token_usage: token_usage.clone(),
});
if let Some(ev) = this.pending.pop_front() {
return Poll::Ready(Some(Ok(ev)));
}
}
return Poll::Ready(Some(Ok(ResponseEvent::Completed {
response_id,
token_usage,
})));
}
Poll::Ready(Some(Ok(ResponseEvent::Created))) => {
continue;
}
Poll::Ready(Some(Ok(ResponseEvent::OutputTextDelta(delta)))) => {
this.cumulative.push_str(&delta);
if matches!(this.mode, AggregateMode::Streaming) {
return Poll::Ready(Some(Ok(ResponseEvent::OutputTextDelta(delta))));
} else {
continue;
}
}
Poll::Ready(Some(Ok(ResponseEvent::ReasoningContentDelta {
delta,
content_index,
}))) => {
this.cumulative_reasoning.push_str(&delta);
if matches!(this.mode, AggregateMode::Streaming) {
return Poll::Ready(Some(Ok(ResponseEvent::ReasoningContentDelta {
delta,
content_index,
})));
} else {
continue;
}
}
Poll::Ready(Some(Ok(ResponseEvent::ReasoningSummaryDelta { .. }))) => continue,
Poll::Ready(Some(Ok(ResponseEvent::ReasoningSummaryPartAdded { .. }))) => {
continue;
}
Poll::Ready(Some(Ok(ResponseEvent::OutputItemAdded(item)))) => {
return Poll::Ready(Some(Ok(ResponseEvent::OutputItemAdded(item))));
}
}
}
}
}
pub trait AggregateStreamExt {
fn aggregate(self) -> AggregatedStream;
fn streaming_mode(self) -> ResponseStream;
}
impl AggregateStreamExt for ResponseStream {
fn aggregate(self) -> AggregatedStream {
AggregatedStream::new(self, AggregateMode::AggregatedOnly)
}
fn streaming_mode(self) -> ResponseStream {
self
}
}
impl AggregatedStream {
fn new(inner: ResponseStream, mode: AggregateMode) -> Self {
AggregatedStream {
inner,
cumulative: String::new(),
cumulative_reasoning: String::new(),
pending: VecDeque::new(),
mode,
}
}
}

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