Compare commits

...

84 Commits

Author SHA1 Message Date
Edward Frazer
925bd22dbc fix: publish windows direct install docs 2026-02-26 21:31:52 -08:00
Edward Frazer
f26a3c8e74 fix: clarify Windows direct installer status 2026-02-25 16:43:21 -08:00
Edward Frazer
68ec43d213 fix: scope direct install docs to macOS and Linux 2026-02-25 16:41:52 -08:00
Edward Frazer
5dabc52e73 Document direct install commands 2026-02-25 09:35:25 -08:00
Edward Frazer
02525c193a Add Windows install script 2026-02-25 09:35:25 -08:00
Edward Frazer
0f570e20d5 Add macOS and Linux install script 2026-02-25 09:35:25 -08:00
mcgrew-oai
9a393c9b6f feat(network-proxy): add embedded OTEL policy audit logging (#12046)
**PR Summary**

This PR adds embedded-only OTEL policy audit logging for
`codex-network-proxy` and threads audit metadata from `codex-core` into
managed proxy startup.

### What changed
- Added structured audit event emission in `network_policy.rs` with
target `codex_otel.network_proxy`.
- Emitted:
- `codex.network_proxy.domain_policy_decision` once per domain-policy
evaluation.
  - `codex.network_proxy.block_decision` for non-domain denies.
- Added required policy/network fields, RFC3339 UTC millisecond
`event.timestamp`, and fallback defaults (`http.request.method="none"`,
`client.address="unknown"`).
- Added non-domain deny audit emission in HTTP/SOCKS handlers for
mode-guard and proxy-state denies, including unix-socket deny paths.
- Added `REASON_UNIX_SOCKET_UNSUPPORTED` and used it for unsupported
unix-socket auditing.
- Added `NetworkProxyAuditMetadata` to runtime/state, re-exported from
`lib.rs` and `state.rs`.
- Added `start_proxy_with_audit_metadata(...)` in core config, with
`start_proxy()` delegating to default metadata.
- Wired metadata construction in `codex.rs` from session/auth context,
including originator sanitization for OTEL-safe tagging.
- Updated `network-proxy/README.md` with embedded-mode audit schema and
behavior notes.
- Refactored HTTP block-audit emission to a small local helper to reduce
duplication.
- Preserved existing unix-socket proxy-disabled host/path behavior for
responses and blocked history while using an audit-only endpoint
override (`server.address="unix-socket"`, `server.port=0`).

### Explicit exclusions
- No standalone proxy OTEL startup work.
- No `main.rs` binary wiring.
- No `standalone_otel.rs`.
- No standalone docs/tests.

### Tests
- Extended `network_policy.rs` tests for event mapping, metadata
propagation, fallbacks, timestamp format, and target prefix.
- Extended HTTP tests to assert unix-socket deny block audit events.
- Extended SOCKS tests to cover deny emission from handler deny
branches.
- Added/updated core tests to verify audit metadata threading into
managed proxy state.

### Validation run
- `just fmt`
- `cargo test -p codex-network-proxy` 
- `cargo test -p codex-core` ran with one unrelated flaky timeout
(`shell_snapshot::tests::snapshot_shell_does_not_inherit_stdin`), and
the test passed when rerun directly 

---------

Co-authored-by: viyatb-oai <viyatb@openai.com>
2026-02-25 11:46:37 -05:00
jif-oai
8362b79cb4 feat: fix sqlite home (#12787) 2026-02-25 15:52:55 +00:00
jif-oai
01f25a7b96 chore: unify max depth parameter (#12770)
Users were confused
2026-02-25 15:20:24 +00:00
mcgrew-oai
bccce0d75f otel: add host.name resource attribute to logs/traces via gethostname (#12352)
**PR Summary**

This PR adds the OpenTelemetry `host.name` resource attribute to Codex
OTEL exports so every OTEL log (and trace, via the shared resource)
carries the machine hostname.

**What changed**

- Added `host.name` to the shared OTEL `Resource` in
`/Users/michael.mcgrew/code/codex/codex-rs/otel/src/otel_provider.rs`
  - This applies to both:
    - OTEL logs (`SdkLoggerProvider`)
    - OTEL traces (`SdkTracerProvider`)
- Hostname is now resolved via `gethostname::gethostname()`
(best-effort)
  - Value is trimmed
  - Empty values are omitted (non-fatal)
- Added focused unit tests for:
  - including `host.name` when present
  - omitting `host.name` when missing/empty

**Why**

- `host.name` is host/process metadata and belongs on the OTEL
`resource`, not per-event attributes.
- Attaching it in the shared resource is the smallest change that
guarantees coverage across all exported OTEL logs/traces.

**Scope / Non-goals**

- No public API changes
- No changes to metrics behavior (this PR only updates log/trace
resource metadata)

**Dependency updates**

- Added `gethostname` as a workspace dependency and `codex-otel`
dependency
- `Cargo.lock` updated accordingly
- `MODULE.bazel.lock` unchanged after refresh/check

**Validation**

- `just fmt`
- `cargo test -p codex-otel`
- `just bazel-lock-update`
- `just bazel-lock-check`
2026-02-25 09:54:45 -05:00
jif-oai
8d49e0d0c4 nit: migration (#12772) 2026-02-25 13:56:52 +00:00
jif-oai
e4bfa763f6 feat: record memory usage (#12761) 2026-02-25 13:48:40 +00:00
jif-oai
5441130e0a feat: adding stream parser (#12666)
Add a stream parser to extract citations (and others) from a stream.
This support cases where markers are split in differen tokens.

Codex never manage to make this code work so everything was done
manually. Please review correctly and do not touch this part of the code
without a very clear understanding of it
2026-02-25 13:27:58 +00:00
jif-oai
5a9a5b51b2 feat: add large stack test macro (#12768)
This PR adds the macro `#[large_stack_test]`

This spawns the tests in a dedicated tokio runtime with a larger stack.
It is useful for tests that needs the full recursion on the harness
(which is now too deep for windows for example)
2026-02-25 13:19:21 +00:00
jif-oai
bcd6e68054 Display pending child-thread approvals in TUI (#12767)
Summary
- propagate approval policy from parent to spawned agents and drop the
Never override so sub-agents respect the caller’s request
- refresh the pending-approval list whenever events arrive or the active
thread changes and surface the list above the composer for inactive
threads
- add widgets, helpers, and tests covering the new pending-thread
approval UI state

![Uploading Screenshot 2026-02-25 at 11.02.18.png…]()
2026-02-25 11:40:11 +00:00
Michael Bolin
93efcfd50d feat: record whether a skill script is approved for the session (#12756)
## Why

`unix_escalation.rs` checks a session-scoped approval cache before
prompting again for an execve-intercepted skill script. Without also
recording `ReviewDecision::ApprovedForSession`, that cache never gets
populated, so the same skill script can still trigger repeated approval
prompts within one session.

## What Changed

- Add `execve_session_approvals` to `SessionServices` so the session can
track approved skill script paths.
- Record the script path when a skill-script prompt returns
`ReviewDecision::ApprovedForSession`, but only for the skill-script path
rather than broader prefix-rule approvals.
- Reuse the cached approval on later execve callbacks by treating an
already-approved skill script as `Decision::Allow`.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/12756).
* #12758
* __->__ #12756
2026-02-25 10:17:22 +00:00
alexsong-oai
6d6570d89d Support external agent config detect and import (#12660)
Migration Behavior

* Config
  *  Migrates settings.json into config.toml
* Only adds fields when config.toml is missing, or when those fields are
missing from the existing file
  *  Supported mappings:
    env -> shell_environment_policy
     sandbox.enabled = true -> sandbox_mode = "workspace-write"

* Skills
  *  Copies home and repo .claude/skills into .agents/skills
  *  Existing skill directories are not overwritten
  *  SKILL.md content is rewritten from Claude-related terms to Codex

* AgentsMd
  *  Repo only
  *  Migrates CLAUDE.md into AGENTS.md
* Detect/import only proceed when AGENTS.md is missing or present but
empty
  *  Content is rewritten from Claude-related terms to Codex
2026-02-25 02:11:51 -08:00
jif-oai
f46b767b7e feat: add search term to thread list (#12578)
Add `searchTerm` to `thread/list` that will search for a match in the
titles (the condition being `searchTerm` $$\in$$ `title`)
2026-02-25 09:59:41 +00:00
jif-oai
a046849438 fix: flaky test due to second-resolution for thread ordering (#12692) 2026-02-25 09:59:25 +00:00
jif-oai
10c04e11b8 feat: add service name to app-server (#12319)
Add service name to the app-server so that the app can use it's own
service name

This is on thread level because later we might plan the app-server to
become a singleton on the computer
2026-02-25 09:51:42 +00:00
Celia Chen
6a3233da64 Surface skill permission profiles in zsh-fork exec approvals (#12753)
## Summary

- Preserve each skill’s raw permissions block as a permission_profile on
SkillMetadata during skill loading.
- Keep compiling that same metadata into the existing runtime
Permissions object, so current enforcement
    behavior stays intact.
- When zsh-fork intercepts execution of a script that belongs to a
skill, include the skill’s
    permission_profile in the exec approval request.
- This lets approval UIs show the extra filesystem access the skill
declared when prompting for approval.
2026-02-25 01:23:10 -08:00
Michael Bolin
c4ec6be4ab fix: keep shell escalation exec paths absolute (#12750)
## Why

In the `shell_zsh_fork` flow, `codex-shell-escalation` receives the
executable path exactly as the shell passed it to `execve()`. That path
is not guaranteed to be absolute.

For commands such as `./scripts/hello-mbolin.sh`, if the shell was
launched with a different `workdir`, resolving the intercepted `file`
against the server process working directory makes policy checks and
skill matching inspect the wrong executable. This change pushes that fix
a step further by keeping the normalized path typed as `AbsolutePathBuf`
throughout the rest of the escalation pipeline.

That makes the absolute-path invariant explicit, so later code cannot
accidentally treat the resolved executable path as an arbitrary
`PathBuf`.

## What Changed

- record the wrapper process working directory as an `AbsolutePathBuf`
- update the escalation protocol so `workdir` is explicitly absolute
while `file` remains the raw intercepted exec path
- resolve a relative intercepted `file` against the request `workdir` as
soon as the server receives the request
- thread `AbsolutePathBuf` through `EscalationPolicy`,
`CoreShellActionProvider`, and command normalization helpers so the
resolved executable path stays type-checked as absolute
- replace the `path-absolutize` dependency in `codex-shell-escalation`
with `codex-utils-absolute-path`
- add a regression test that covers a relative `file` with a distinct
`workdir`

## Verification

- `cargo test -p codex-shell-escalation`
2026-02-24 23:52:36 -08:00
Michael Bolin
59398125f6 feat: zsh-fork forces scripts/**/* for skills to trigger a prompt (#12730)
Direct skill-script matches force `Decision::Prompt`, so skill-backed
scripts require explicit approval before they run. (Note "allow for
session" is not supported in this PR, but will be done in a follow-up.)

In the process of implementing this, I fixed an important bug:
`ShellZshFork` is supposed to keep ordinary allowed execs on the
client-side `Run` path so later `execve()` calls are still intercepted
and reviewed. After the shell-escalation port, `Decision::Allow` still
mapped to `Escalate`, which moved `zsh` to server-side execution too
early. That broke the intended flow for skill-backed scripts and made
the approval prompt depend on the wrong execution path.

## What changed
- In `codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs`,
`Decision::Allow` now returns `Run` unless escalation is actually
required.
- Removed the zsh-specific `argv[0]` fallback. With the `Allow -> Run`
fix in place, zsh's later `execve()` of the script is intercepted
normally, so the skill match happens on the script path itself.
- Kept the skill-path handling in `determine_action()` focused on the
direct `program` match path.

## Verification
- Updated `shell_zsh_fork_prompts_for_skill_script_execution` in
`codex-rs/core/tests/suite/skill_approval.rs` (gated behind `cfg(unix)`)
to:
- run under `SandboxPolicy::new_workspace_write_policy()` instead of
`DangerFullAccess`
  - assert the approval command contains only the script path
- assert the approved run returns both stdout and stderr markers in the
shell output
- Ran `cargo test -p codex-core
shell_zsh_fork_prompts_for_skill_script_execution -- --nocapture`

## Manual Testing

Run the dev build:

```
just codex --config zsh_path=/Users/mbolin/code/codex2/codex-rs/app-server/tests/suite/zsh --enable shell_zsh_fork
```

I have created `/Users/mbolin/.agents/skills/mbolin-test-skill` with:

```
├── scripts
│   └── hello-mbolin.sh
└── SKILL.md
```

The skill:

```
---
name: mbolin-test-skill
description: Used to exercise various features of skills.
---

When this skill is invoked, run the `hello-mbolin.sh` script and report the output.
```

The script:

```
set -e

# Note this script will fail if run with network disabled.
curl --location openai.com
```

Use `$mbolin-test-skill` to invoke the skill manually and verify that I
get prompted to run `hello-mbolin.sh`.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/12730).
* #12750
* __->__ #12730
2026-02-24 23:51:26 -08:00
viyatb-oai
c086b36b58 feat(ui): add network approval persistence plumbing (#12358)
## Summary
- add TUI approval options for persistent network host rules
- add app-server v2 approval payload plumbing for network approval
context + proposed network policy amendments
- add app-server handling to translate `applyNetworkPolicyAmendment`
decisions back into core review decisions
- update docs/test client output and generated app-server schemas/types
2026-02-25 07:06:19 +00:00
Curtis 'Fjord' Hawthorne
9501669a24 tests(js_repl): remove node-related skip paths from js_repl tests (#12185)
## Summary
Remove js_repl/node test-skip paths and make Node setup explicit in CI
so js_repl tests always run instead of silently skipping.

## Why
We had multiple “expediency” skip paths that let js_repl tests pass
without actually exercising Node-backed behavior. This reduced CI signal
and hid runtime/environment regressions.

## What changed

### CI
- Added Node setup using `codex-rs/node-version.txt` in:
  - `.github/workflows/rust-ci.yml`
  - `.github/workflows/bazel.yml`
- Added a Unix PATH copy step in Bazel workflow to expose the setup-node
binary in common paths.

### js_repl test harness
- Added explicit js_repl sandbox test configuration helpers in:
  - `codex-rs/core/src/tools/js_repl/mod.rs`
  - `codex-rs/core/src/tools/handlers/js_repl.rs`
- Added Linux arg0 dispatch glue for js_repl tests so sandbox subprocess
entrypoint behavior is correct under Linux test execution.

### Removed skip behavior
- Deleted runtime guard function and early-return skips in js_repl tests
(`can_run_js_repl_runtime_tests` and related per-test short-circuits).
- Removed view_image integration test skip behavior:
  - dropped `skip_if_no_network!(Ok(()))`
- removed “skip on Node missing/too old” branch after js_repl output
inspection.

## Impact
- js_repl/node tests now consistently execute and fail loudly when the
environment is not correctly provisioned.
- CI has stronger signal for js_repl regressions instead of false green
from conditional skips.

## Testing
- `cargo test -p codex-core` (locally) to validate js_repl
unit/integration behavior with skips removed.
- CI expected to surface any remaining environment/runtime gaps directly
(rather than masking them).


#### [git stack](https://github.com/magus/git-stack-cli)
-  `1` https://github.com/openai/codex/pull/12300
-  `2` https://github.com/openai/codex/pull/12275
-  `3` https://github.com/openai/codex/pull/12205
-  `4` https://github.com/openai/codex/pull/12407
-  `5` https://github.com/openai/codex/pull/12372
- 👉 `6` https://github.com/openai/codex/pull/12185
-  `7` https://github.com/openai/codex/pull/10673
2026-02-24 22:52:14 -08:00
Michael Bolin
ddfa032eb8 fix: chatwidget was not honoring approval_id for an ExecApprovalRequestEvent (#12746)
## Why

`ExecApprovalRequestEvent` can carry a distinct `approval_id` for
subcommand approvals, including the `execve`-intercepted zsh-fork path.

The session registers the pending approval callback under `approval_id`
when one is present, but `ChatWidget` was stashing `call_id` in the
approval modal state. When the user approved the command in the TUI, the
response was sent back with the wrong identifier, so the pending
approval could not be matched and the approval callback would not
resolve.

Note `approval_id` was introduced in
https://github.com/openai/codex/pull/12051.

## What changed

- In `tui/src/chatwidget.rs`, `ChatWidget` now uses
`ExecApprovalRequestEvent::effective_approval_id()` when constructing
`ApprovalRequest::Exec`.
- That preserves the existing behavior for normal shell and
`unified_exec` approvals, where `approval_id` is absent and the
effective id still falls back to `call_id`.
- For subcommand approvals that provide a distinct `approval_id`, the
TUI now sends back the same key that
`Session::request_command_approval()` registered.

## Verification

- Traced the approval flow end to end to confirm the same effective
approval id is now used on both sides of the round trip:
- `Session::request_command_approval()` registers the pending callback
under `approval_id.unwrap_or(call_id)`.
- `ChatWidget` now emits `Op::ExecApproval` with that same effective id.
2026-02-24 22:27:05 -08:00
Curtis 'Fjord' Hawthorne
6cb2f02ef8 feat: update Docker image digest to reflect #12205 (#12372)
This is a clone of #12371 for easier rebasing/testing.

#### [git stack](https://github.com/magus/git-stack-cli)
-  `1` https://github.com/openai/codex/pull/12407
- 👉 `2` https://github.com/openai/codex/pull/12372
-  `3` https://github.com/openai/codex/pull/12185
-  `4` https://github.com/openai/codex/pull/10673

Co-authored-by: Michael Bolin <mbolin@openai.com>
2026-02-24 22:19:46 -08:00
Celia Chen
1151972fb2 feat: add experimental additionalPermissions to v2 command execution approval requests (#12737)
This adds additionalPermissions to the app-server v2
item/commandExecution/requestApproval payload as an experimental field.

The field is now exposed on CommandExecutionRequestApprovalParams and is
populated from the existing core approval event when a command requests
additional sandbox permissions.

This PR also contains changes to make server requests to support
experiment API.

A real app server test client test:

sample payload with experimental flag off:
```
 {
<   "id": 0,
<   "method": "item/commandExecution/requestApproval",
<   "params": {
<     "command": "/bin/zsh -lc 'mkdir -p ~/some/test && touch ~/some/test/file'",
<     "commandActions": [
<       {
<         "command": "mkdir -p '~/some/test'",
<         "type": "unknown"
<       },
<       {
<         "command": "touch '~/some/test/file'",
<         "type": "unknown"
<       }
<     ],
<     "cwd": "/Users/celia/code/codex/codex-rs",
<     "itemId": "call_QLp0LWkQ1XkU6VW9T2vUZFWB",
<     "proposedExecpolicyAmendment": [
<       "mkdir",
<       "-p",
<       "~/some/test"
<     ],
<     "reason": "Do you want to allow creating ~/some/test/file outside the workspace?",
<     "threadId": "019c9309-e209-7d82-a01b-dcf9556a354d",
<     "turnId": "019c9309-e27a-7f33-834f-6011e795c2d6"
<   }
< }
```
with experimental flag on: 
```
< {
<   "id": 0,
<   "method": "item/commandExecution/requestApproval",
<   "params": {
<     "additionalPermissions": {
<       "fileSystem": null,
<       "macos": null,
<       "network": true
<     },
<     "command": "/bin/zsh -lc 'install -D /dev/null ~/some/test/file'",
<     "commandActions": [
<       {
<         "command": "install -D /dev/null '~/some/test/file'",
<         "type": "unknown"
<       }
<     ],
<     "cwd": "/Users/celia/code/codex/codex-rs",
<     "itemId": "call_K3U4b3dRbj3eMCqslmncbGsq",
<     "proposedExecpolicyAmendment": [
<       "install",
<       "-D"
<     ],
<     "reason": "Do you want to allow creating the file at ~/some/test/file outside the workspace sandbox?",
<     "threadId": "019c9303-3a8e-76e1-81bf-d67ac446d892",
<     "turnId": "019c9303-3af1-7143-88a1-73132f771234"
<   }
< }
```
2026-02-25 05:16:35 +00:00
Curtis 'Fjord' Hawthorne
8f3f2c3c02 tests(js_repl): stabilize CI runtime test execution (#12407)
## Summary

Stabilize `js_repl` runtime test setup in CI and move tool-facing
`js_repl` behavior coverage into integration tests.

This is a test/CI change only. No production `js_repl` behavior change
is intended.

## Why

- Bazel test sandboxes (especially on macOS) could resolve a different
`node` than the one installed by `actions/setup-node`, which caused
`js_repl` runtime/version failures.
- `js_repl` runtime tests depend on platform-specific
sandbox/test-harness behavior, so they need explicit gating in a
base-stability commit.
- Several tests in the `js_repl` unit test module were actually
black-box/tool-level behavior tests and fit better in the integration
suite.

## Changes

- Add `actions/setup-node` to the Bazel and Rust `Tests` workflows,
using the exact version pinned in the repo’s Node version file.
- In Bazel (non-Windows), pass `CODEX_JS_REPL_NODE_PATH=$(which node)`
into test env so `js_repl` uses the `actions/setup-node` runtime inside
Bazel tests.
- Add a new integration test suite for `js_repl` tool behavior and
register it in the core integration test suite module.
- Move black-box `js_repl` behavior tests into the integration suite
(persistence/TLA, builtin tool invocation, recursive self-call
rejection, `process` isolation, blocked builtin imports).
- Keep white-box manager/kernel tests in the `js_repl` unit test module.
- Gate `js_repl` runtime tests to run only on macOS and only when a
usable Node runtime is available (skip on other platforms / missing Node
in this commit).

## Impact

- Reduces `js_repl` CI failures caused by Node resolution drift in
Bazel.
- Improves test organization by separating tool-facing behavior tests
from white-box manager/kernel tests.
- Keeps the base commit stable while expanding `js_repl` runtime
coverage.


#### [git stack](https://github.com/magus/git-stack-cli)
-  `1` https://github.com/openai/codex/pull/12372
- 👉 `2` https://github.com/openai/codex/pull/12407
-  `3` https://github.com/openai/codex/pull/12185
-  `4` https://github.com/openai/codex/pull/10673
2026-02-24 21:04:34 -08:00
Celia Chen
16ca527c80 chore: migrate additional permissions to PermissionProfile (#12731)
This PR replaces the old `additional_permissions.fs_read/fs_write` shape
with a shared `PermissionProfile`
model and wires it through the command approval, sandboxing, protocol,
and TUI layers. The schema is adopted from the
`SkillManifestPermissions`, which is also refactored to use this unified
struct. This helps us easily expose permission profiles in app
server/core as a follow-up.
2026-02-25 03:35:28 +00:00
sayan-oai
e6bb5d8553 chore: change catalog mode to enum (#12656)
make presence of custom catalog more clear by changing to enum instead
of bool.
2026-02-24 19:33:32 -08:00
Curtis 'Fjord' Hawthorne
125fbec317 Fix js_repl view_image attachments in nested tool calls (#12725)
## Summary

- Fix `js_repl` so `await codex.tool("view_image", { path })` actually
attaches the image to the active turn when called from inside the JS
REPL.
- Restore the behavior expected by the existing `js_repl`
image-attachment test.
- This is a follow-up to
[#12553](https://github.com/openai/codex/pull/12553), which changed
`view_image` to return structured image content.

## Root Cause

- [#12553](https://github.com/openai/codex/pull/12553) changed
`view_image` from directly injecting a pending user image message to
returning structured `function_call_output` content items.
- The nested tool-call bridge inside `js_repl` serialized that tool
response back to the JS runtime, but it did not mirror returned image
content into the active turn.
- As a result, `view_image` appeared to succeed inside `js_repl`, but no
`input_image` was actually attached for the outer turn.

## What Changed

- Updated the nested tool-call path in `js_repl` to inspect function
tool responses for structured content items.
- When a nested tool response includes `input_image` content, `js_repl`
now injects a corresponding user `Message` into the active turn before
returning the raw tool result back to the JS runtime.
- Kept the normal JSON result flow intact, so `codex.tool(...)` still
returns the original tool output object to JavaScript.

## Why

- `js_repl` documentation and tests already assume that `view_image` can
be used from inside the REPL to attach generated images to the model.
- Without this fix, the nested call path silently dropped that
attachment behavior.
2026-02-24 18:23:53 -08:00
sayan-oai
74e112ea09 add AWS_LC_SYS_NO_JITTER_ENTROPY=1 to release musl build step to unblock releases (#12720)
linux musl build steps in `rust-release.yml` are [currently
broken](https://github.com/openai/codex/actions/runs/22367312571)
because of linking issues due to ubsan-calling types (`jitterentropy`)
leaking into the build.

add `AWS_LC_SYS_NO_JITTER_ENTROPY=1` to the musl build step to avoid
linking those ubsan-calling types. this is a more temporary fix, we need
to clean up ubsan usage upstream so they dont leak into release-build
steps anyways.

codex's more thorough explanation below:

[pr 9859](https://github.com/openai/codex/pull/9859) added [MITM
init](https://github.com/openai/codex/pull/9859/changes#diff-db782967007060c5520651633e1ea21681d64be21f2b791d3d84519860245b97R62-R68)
in network-proxy, which wires in cert generation code (rcgen/rustls).
this didnt bump/change dep versions, but it changed symbol reachability
at link time.

for musl builds, that made aws-lc-sys’s jitterentropy objects get pulled
into the final link. those objects contain UBSan calls
(__ubsan_handle_*). musl release linking is static (*-linux-musl-gcc,
-nodefaultlibs) and does not link a musl UBSan runtime, so link fails
with undefined __ubsan_*.

before, our custom musl CI UBSan steps (install libubsan1, RUSTC_WRAPPER
+ LD_PRELOAD, partial flag scrubbing) masked some sanitizer issues.
after this pr, more aws-lc code became link-reachable, and that band-aid
wasn't enough.
2026-02-24 18:11:04 -08:00
Michael Bolin
e88f74d140 feat: pass helper executable paths via Arg0DispatchPaths (#12719)
## Why

`codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs` previously
located `codex-execve-wrapper` by scanning `PATH` and sibling
directories. That lookup is brittle and can select the wrong binary when
the runtime environment differs from startup assumptions.

We already pass `codex-linux-sandbox` from `codex-arg0`;
`codex-execve-wrapper` should use the same startup-driven path plumbing.

## What changed

- Introduced `Arg0DispatchPaths` in `codex-arg0` to carry both helper
executable paths:
  - `codex_linux_sandbox_exe`
  - `main_execve_wrapper_exe`
- Updated `arg0_dispatch_or_else()` to pass `Arg0DispatchPaths` to
top-level binaries and preserve helper paths created in
`prepend_path_entry_for_codex_aliases()`.
- Threaded `Arg0DispatchPaths` through entrypoints in `cli`, `exec`,
`tui`, `app-server`, and `mcp-server`.
- Added `main_execve_wrapper_exe` to core configuration plumbing
(`Config`, `ConfigOverrides`, and `SessionServices`).
- Updated zsh-fork shell escalation to consume the configured
`main_execve_wrapper_exe` and removed path-sniffing fallback logic.
- Updated app-server config reload paths so reloaded configs keep the
same startup-provided helper executable paths.

## References

- [`Arg0DispatchPaths`
definition](e355b43d5c/codex-rs/arg0/src/lib.rs (L20-L24))
- [`arg0_dispatch_or_else()` forwarding both
paths](e355b43d5c/codex-rs/arg0/src/lib.rs (L145-L176))
- [zsh-fork escalation using configured wrapper
path](e355b43d5c/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs (L109-L150))

## Testing

- `cargo check -p codex-arg0 -p codex-core -p codex-exec -p codex-tui -p
codex-mcp-server -p codex-app-server`
- `cargo test -p codex-arg0`
- `cargo test -p codex-core tools::runtimes::shell::unix_escalation:: --
--nocapture`
2026-02-24 17:44:38 -08:00
Michael Bolin
448fb6ac22 fix: clarify the value of SkillMetadata.path (#12729)
Rename `SkillMetadata.path` to `SkillMetadata.path_to_skills_md` for
clarity.

Would ideally change the type to `AbsolutePathBuf`, but that can be done
later.
2026-02-24 17:15:54 -08:00
Curtis 'Fjord' Hawthorne
63c2ac96cd fix(js_repl): surface uncaught kernel errors and reset cleanly (#12636)
## Summary

Improve `js_repl` behavior when the Node kernel hits a process-level
failure (for example, an uncaught exception or unhandled Promise
rejection).

Instead of only surfacing a generic `js_repl kernel exited unexpectedly`
after stdout EOF, `js_repl` now returns a clearer exec error for the
active request, then resets the kernel cleanly.

## Why

Some sandbox-denied operations can trigger Node errors that become
process-level failures (for example, an unhandled EventEmitter `'error'`
event). In that case:

- the kernel process exits,
- the host sees stdout EOF,
- the user gets a generic kernel-exit error,
- and the next request can briefly race with stale kernel state.

This change improves that failure mode without monkeypatching Node APIs.

## Changes

### Kernel-side (`js_repl` Node process)
- Add process-level handlers for:
  - `uncaughtException`
  - `unhandledRejection`
- When one of these fires:
  - best-effort emit a normal `exec_result` error for the active exec
- include actionable guidance to catch/handle async errors (including
Promise rejections and EventEmitter `'error'` events)
  - exit intentionally so the host can reset/restart the kernel

### Host-side (`JsReplManager`)
- Clear dead kernel state as soon as the stdout reader observes
unexpected kernel exit/EOF.
- This lets the next `js_repl` exec start a fresh kernel instead of
hitting a stale broken-pipe path.

### Tests
- Add regression coverage for:
- uncaught async exception -> exec error + kernel recovery on next exec
- Update forced-kernel-exit test to validate recovery behavior (next
exec restarts cleanly)

## Impact

- Better user-facing error for kernel crashes caused by
uncaught/unhandled async failures.
- Cleaner recovery behavior after kernel exit.

## Validation

- `cargo test -p codex-core --lib
tools::js_repl::tests::js_repl_uncaught_exception_returns_exec_error_and_recovers
-- --exact`
- `cargo test -p codex-core --lib
tools::js_repl::tests::js_repl_forced_kernel_exit_recovers_on_next_exec
-- --exact`
- `just fmt`
2026-02-24 17:12:02 -08:00
Max Johnson
5163850025 codex-rs/app-server: graceful websocket restart on Ctrl-C (#12517)
## Summary
- add graceful websocket app-server restart on Ctrl-C by draining until
no assistant turns are running
- stop the websocket acceptor and disconnect existing connections once
the drain condition is met
- add a websocket integration test that verifies Ctrl-C waits for an
in-flight turn before exit

## Verification
- `cargo check -p codex-app-server --quiet`
- `cargo test -p codex-app-server --test all
suite::v2::connection_handling_websocket`
- I (maxj) tested remote and local Codex.app

---------

Co-authored-by: Codex <noreply@openai.com>
2026-02-24 16:27:59 -08:00
Michael Bolin
3d356723c4 fix: make EscalateServer public and remove shell escalation wrappers (#12724)
## Why

`codex-shell-escalation` exposed a `codex-core`-specific adapter layer
(`ShellActionProvider`, `ShellPolicyFactory`, and `run_escalate_server`)
that existed only to bridge `codex-core` to `EscalateServer`. That
indirection increased API surface and obscured crate ownership without
adding behavior.

This change moves orchestration into `codex-core` so boundaries are
clearer: `codex-shell-escalation` provides reusable escalation
primitives, and `codex-core` provides shell-tool policy decisions.

Admittedly, @pakrym rightfully requested this sort of cleanup as part of
https://github.com/openai/codex/pull/12649, though this avoids moving
all of `codex-shell-escalation` into `codex-core`.

## What changed

- Made `EscalateServer` public and exported it from `shell-escalation`.
- Removed the adapter layer from `shell-escalation`:
  - deleted `shell-escalation/src/unix/core_shell_escalation.rs`
- removed exports for `ShellActionProvider`, `ShellPolicyFactory`,
`EscalationPolicyFactory`, and `run_escalate_server`
- Updated `core/src/tools/runtimes/shell/unix_escalation.rs` to:
  - create `Stopwatch`/cancellation in `codex-core`
  - instantiate `EscalateServer` directly
  - implement `EscalationPolicy` directly on `CoreShellActionProvider`

Net effect: same escalation flow with fewer wrappers and a smaller
public API.

## Verification

- Manually reviewed the old vs. new escalation call flow to confirm
timeout/cancellation behavior and approval policy decisions are
preserved while removing wrapper types.
2026-02-24 16:20:08 -08:00
Eric Traut
8da40c9251 Raise image byte estimate for compaction token accounting (#12717)
Increase `IMAGE_BYTES_ESTIMATE` from 340 bytes to 7,373 bytes so the
existing 4-bytes/token heuristic yields an image estimate of ~1,844
tokens instead of ~85. This makes auto-compaction more conservative for
image-heavy transcripts and avoids underestimating context usage, which
can otherwise cause compaction to fail when there is not enough free
context remaining. The new value was chosen because that's the image
resolution cap used for our latest models.

Follow-up to [#12419](https://github.com/openai/codex/pull/12419).
Refs [#11845](https://github.com/openai/codex/issues/11845).
2026-02-24 16:11:38 -08:00
pakrym-oai
5571a022eb Add app-server event tracing (#12695)
To help with debugging
2026-02-24 14:45:50 -08:00
Won Park
ee1520e79e feat(tui) - /copy (#12613)
# /copy!

/copy allows you to copy the latest **complete** message from Codex on
the TUI.
2026-02-24 14:17:01 -08:00
zuxin-oai
61cd3a9700 fix: temp remove citation (#12711)
- **temp remove citation**
2026-02-24 22:07:30 +00:00
Jeremy Rose
fefdc03b25 revert audio scope (#12700) 2026-02-24 13:38:28 -08:00
daveaitel-openai
dcab40123f Agent jobs (spawn_agents_on_csv) + progress UI (#10935)
## Summary
- Add agent job support: spawn a batch of sub-agents from CSV, auto-run,
auto-export, and store results in SQLite.
- Simplify workflow: remove run/resume/get-status/export tools; spawn is
deterministic and completes in one call.
- Improve exec UX: stable, single-line progress bar with ETA; suppress
sub-agent chatter in exec.

## Why
Enables map-reduce style workflows over arbitrarily large repos using
the existing Codex orchestrator. This addresses review feedback about
overly complex job controls and non-deterministic monitoring.

## Demo (progress bar)
```
./codex-rs/target/debug/codex exec \
  --enable collab \
  --enable sqlite \
  --full-auto \
  --progress-cursor \
  -c agents.max_threads=16 \
  -C /Users/daveaitel/code/codex \
  - <<'PROMPT'
Create /tmp/agent_job_progress_demo.csv with columns: path,area and 30 rows:
path = item-01..item-30, area = test.

Then call spawn_agents_on_csv with:
- csv_path: /tmp/agent_job_progress_demo.csv
- instruction: "Run `python - <<'PY'` to sleep a random 0.3–1.2s, then output JSON with keys: path, score (int). Set score = 1."
- output_csv_path: /tmp/agent_job_progress_demo_out.csv
PROMPT
```

## Review feedback addressed
- Auto-start jobs on spawn; removed run/resume/status/export tools.
- Auto-export on success.
- More descriptive tool spec + clearer prompts.
- Avoid deadlocks on spawn failure; pending/running handled safely.
- Progress bar no longer scrolls; stable single-line redraw.

## Tests
- `cd codex-rs && cargo test -p codex-exec`
- `cd codex-rs && cargo build -p codex-cli`
2026-02-24 21:00:19 +00:00
Eric Traut
bd192b54cd Honor project_root_markers when discovering AGENTS.md (#12639)
Fixes #12128

The docs indicates that `project_root_markers` are used to discover the
project root for local config as well as `AGENTS.md`. It looks like it
was never wired up to support the latter.

Summary
- resolve project docs by walking to the configured
`project_root_markers` (or defaults) instead of assuming the Git root,
while honoring CLI overrides and handling malformed configs
- fall back to the project’s canonical path chain and add a test that
makes sure custom markers upstream of `.git` are respected
2026-02-24 12:55:48 -08:00
Ahmed Ibrahim
b6ab2214e3 Add TUI realtime conversation mode (#12687)
- Add a hidden `realtime_conversation` feature flag and `/realtime`
slash command for start/stop live voice sessions.
- Reuse transcription composer/footer UI for live metering, stream mic
audio, play assistant audio, render realtime user text events, and
force-close on feature disable.

---------

Co-authored-by: Codex <noreply@openai.com>
2026-02-24 12:54:30 -08:00
Michael Bolin
3b5fc7547e refactor: remove unused seatbelt unix socket arg (#12707)
https://github.com/openai/codex/pull/12052 introduced an
`allowed_unix_socket_paths` parameter to
`create_seatbelt_command_args()`, but
https://github.com/openai/codex/pull/12649 removed the abstraction that
#12052 introduced, so this parameter is no longer necessary as it is
always an empty slice.
2026-02-24 12:30:26 -08:00
pakrym-oai
daf0f03ac8 Ensure shell command skills trigger approval (#12697)
Summary
- detect skill-invoking shell commands based on the original command
string, request approvals when needed, and cache positive decisions per
session
- keep implicit skill invocation emitted after approval and keep skill
approval decline messaging centralized to the shell handler
- expand and adjust skill approval tests to cover shell-based skill
scripts while matching the new detection expectations

Testing
- Not run (not requested)
2026-02-24 12:13:20 -08:00
Felipe Coury
061d1d3b5e feat(tui): add theme-aware diff backgrounds with capability-graded palettes (#12581)
## Problem

Diff lines used only foreground colors (green/red) with no background
tinting, making them hard to scan. The gutter (line numbers) also had no
theme awareness — dimmed text was fine on dark terminals but unreadable
on light ones.

## Mental model

Each diff line now has four styled layers: **gutter** (line number),
**sign** (`+`/`-`), **content** (text), and **line background** (full
terminal width). A `DiffTheme` enum (`Dark` / `Light`) is selected once
per render by probing the terminal's queried background via
`default_bg()`. A companion `DiffColorLevel` enum (`TrueColor` /
`Ansi256` / `Ansi16`) is derived from `stdout_color_level()` and gates
which palette is used. All style helpers dispatch on `(theme,
DiffLineType, color_level)` to pick the right colors.

| Theme Picker Wide | Theme Picker Narrow |
|---|---|
| <img width="1552" height="1012" alt="image"
src="https://github.com/user-attachments/assets/231b21b7-32d4-4727-80ed-7d01924954be"
/> | <img width="795" height="1012" alt="image"
src="https://github.com/user-attachments/assets/549cacdf-daec-43c9-ad64-2a28d16d140e"
/> |

| Dark BG - 16 colors | Dark BG - 256 colors | Dark BG - True Colors |
|---|---|---|
| <img width="1552" height="1012" alt="dark-16colors"
src="https://github.com/user-attachments/assets/fba36de3-c101-47d4-9e63-88cdd00410d0"
/> | <img width="1552" height="1012" alt="dark-256colors"
src="https://github.com/user-attachments/assets/f39e4307-c6b0-49c4-b4fe-bd26d3d8e41c"
/> | <img width="1552" height="1012" alt="dark-truecolor"
src="https://github.com/user-attachments/assets/1af4ec57-04bf-4dfb-8a44-0ab5e5aaaf18"
/> |

| Light BG - 16 colors | Light BG - 256 colors | Light BG - True Colors
|
|---|---|---|
| <img width="1552" height="1012" alt="light-16colors"
src="https://github.com/user-attachments/assets/2b5423d1-74b4-4b1e-8123-7c2488ff436b"
/> | <img width="1552" height="1012" alt="light-256colors"
src="https://github.com/user-attachments/assets/c94cff9a-8d3e-42c9-bbe7-079da39953a8"
/> | <img width="1552" height="1012" alt="light-truecolor"
src="https://github.com/user-attachments/assets/f73da626-725f-4452-99ee-69ef706df2c6"
/> |

## Non-goals

- No runtime theme switching beyond what `default_bg()` already
provides.
- No change to syntax highlighting theme selection or the highlight
module.

## Tradeoffs

- Three fixed palettes (truecolor RGB, 256-color indexed, 16-color
named) are maintained rather than using `best_color` nearest-match. This
is deliberate: `supports_color::on_cached(Stream::Stdout)` can misreport
capabilities once crossterm enters the alternate screen, so hand-picked
palette entries give better visual results than automatic quantization.
- Delete lines in the syntax-highlighted path get `Modifier::DIM` to
visually recede compared to insert lines. This trades some readability
of deleted code for scan-ability of additions.
- The theme picker's diff preview sets `preserve_side_content_bg: true`
on `ListSelectionView` so diff background tints survive into the side
panel. Other popups keep the default (`false`) to preserve their
reset-background look.

## Architecture

- **Color constants** are module-level `const` items grouped by palette
tier: `DARK_TC_*` / `LIGHT_TC_*` (truecolor RGB tuples), `DARK_256_*` /
`LIGHT_256_*` (xterm indexed), with named `Color` variants used for the
16-color tier.
- **`DiffTheme`** is a private enum; `diff_theme()` probes the terminal
and `diff_theme_for_bg()` is the testable pure-function version.
- **`DiffColorLevel`** is a private enum derived from `StdoutColorLevel`
via `diff_color_level()`.
- **Palette helpers** (`add_line_bg`, `del_line_bg`, `light_gutter_fg`,
`light_add_num_bg`, `light_del_num_bg`) each take `(DiffTheme,
DiffColorLevel)` or just `DiffColorLevel` and return a `Color`.
- **Style helpers** (`style_line_bg_for`, `style_gutter_for`,
`style_sign_add`, `style_sign_del`, `style_add`, `style_del`) each take
`(DiffLineType, DiffTheme, DiffColorLevel)` or `(DiffTheme,
DiffColorLevel)` and return a `Style`.
- **`push_wrapped_diff_line_inner_with_theme_and_color_level`** is the
innermost renderer, accepting both theme and color level so tests can
exercise any combination without depending on the terminal.
- **Line-level background** is applied via
`RtLine::from(...).style(line_bg)` so the tint extends across the full
terminal width, not just the text content.
- **Theme picker integration**: `ListSelectionView` gained a
`preserve_side_content_bg` flag. When `true`, the side panel skips
`force_bg_to_terminal_bg`, letting diff preview backgrounds render
faithfully.

## Observability

No new logging. Theme selection is deterministic from `default_bg()`,
which is already queried and cached at TUI startup.

## Tests

1. **`DiffTheme` is determined per `render_change` call** — if
`default_bg()` changes mid-render (e.g. `requery_default_colors()`
fires), different file chunks could render with different themes. Low
risk in practice since re-query only happens on explicit user action.
2. **16-color tier uses named `Color` variants** (`Color::Green`,
`Color::Red`, etc.) which the terminal maps to its own palette. On
unusual terminal themes these could clash with the background.
Acceptable since 16-color terminals already have unpredictable color
rendering.
3. **Light-theme `style_add` / `style_del` set bg but no fg** — on light
terminals, non-syntax-highlighted content uses the terminal's default
foreground against a pastel background. If the terminal's default fg
happens to be very light, contrast could suffer. This is an edge case
since light-terminal users typically have dark default fg.
4. **`preserve_side_content_bg` is a general-purpose flag but only used
by the theme picker** — if other popups start using side content with
intentional backgrounds they'll need to opt in explicitly. Not a real
risk today, just a note for future callers.
2026-02-24 11:55:01 -08:00
Yaroslav Volovich
67d9261e2c feat(sleep-inhibitor): add Linux and Windows idle-sleep prevention (#11766)
## Background
- follow-up to previous macOS-only PR:
https://github.com/openai/codex/pull/11711
- follow-up macOS refactor PR (current structural approach used here):
https://github.com/openai/codex/pull/12340

## Summary
- extend `codex-utils-sleep-inhibitor` with Linux and Windows backends
while preserving existing macOS behavior
- Linux backend:
  - use `systemd-inhibit` (`--what=idle --mode=block`) when available
- fall back to `gnome-session-inhibit` (`--inhibit idle`) when available
  - keep no-op behavior if neither backend exists on host
- Windows backend:
- use Win32 power request handles (`PowerCreateRequest` +
`PowerSetRequest` / `PowerClearRequest`) with
`PowerRequestSystemRequired`
- make `prevent_idle_sleep` Experimental on macOS/Linux/Windows; keep
under development on other targets

## Testing
- `just fmt`
- `cargo test -p codex-utils-sleep-inhibitor`
- `cargo test -p codex-core features::tests::`
- `cargo test -p codex-tui chatwidget::tests::`
- `just fix -p codex-utils-sleep-inhibitor`
- `just fix -p codex-core`

## Semantics and API references
- Goal remains: prevent idle system sleep while a turn is running.
- Linux:
  - `systemd-inhibit` / login1 inhibitor model:
-
https://www.freedesktop.org/software/systemd/man/latest/systemd-inhibit.html
-
https://www.freedesktop.org/software/systemd/man/org.freedesktop.login1.html
    - https://systemd.io/INHIBITOR_LOCKS/
  - xdg-desktop-portal Inhibit (relevant for sandboxed apps):
-
https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Inhibit.html
- Windows:
  - `PowerCreateRequest`:
-
https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-powercreaterequest
  - `PowerSetRequest`:
-
https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-powersetrequest
  - `PowerClearRequest`:
-
https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-powerclearrequest
  - `SetThreadExecutionState` (alternative baseline API):
-
https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setthreadexecutionstate

## Chromium vs this PR
- Chromium Linux backend:
-
https://github.com/chromium/chromium/blob/main/services/device/wake_lock/power_save_blocker/power_save_blocker_linux.cc
- Chromium Windows backend:
-
https://github.com/chromium/chromium/blob/main/services/device/wake_lock/power_save_blocker/power_save_blocker_win.cc
- Electron powerSaveBlocker entry point:
-
https://github.com/electron/electron/blob/main/shell/browser/api/electron_api_power_save_blocker.cc

## Why we differ from Chromium
- Linux implementation mechanism:
- Chromium uses in-process D-Bus APIs plus UI-integrated screen-saver
suspension.
- This PR uses command-based inhibitor backends (`systemd-inhibit`,
`gnome-session-inhibit`) instead of linking a Linux D-Bus client in this
crate.
- Reason: keep `codex-utils-sleep-inhibitor` dependency-light and avoid
Linux CI/toolchain fragility from new native D-Bus linkage, while
preserving the same runtime intent (hold an inhibitor while a turn
runs).
- Linux UI integration scope:
- Chromium also uses `display::Screen::SuspendScreenSaver()` in its UI
stack.
- Codex `codex-rs` does not have that display abstraction in this crate,
so this PR scopes Linux behavior to process-level sleep inhibition only.
- Windows wake-lock type breadth:
- Chromium supports both display/system wake-lock types and extra
display-specific handling for some pre-Win11 scenarios.
- Codex’s feature is scoped to turn execution continuity (not forcing
display on), so this PR uses `PowerRequestSystemRequired` only.
2026-02-24 11:51:44 -08:00
sayan-oai
0b6c2e5652 fix: also try matching namespaced prefix for modelinfo candidate (#12658)
#### What
Try matching `\w+`-namespaced model after `longest prefix` as heuristic
to match `ModelInfo` from list of candidates.

This shouldn't regress existing behavior:
- `gpt-5.2-codex` -> `gpt-5.2` if `gpt-5.2-codex` not present
- `gpt-5.3` -> `gpt-5` if `gpt-5.3` not present
- `gpt-9` still doesn't match anything

while being more forgiving for custom prefixes:
- `oai/gpt-5.3-codex` -> `gpt-5.3-codex`

#### Tests
Added unit test.
2026-02-24 10:57:26 -08:00
Eric Traut
74cebceed7 Fix @mention token parsing in chat composer (#12643)
Fixes #12175

If a user types an npm package name with multiple `@` symbols like `npx
-y @foo/bar@latest`, the TUI currently treats this as though it's
attempting to invoke the file picker.

### What changed

- **Generalized `@` token parsing**
- `current_prefixed_token(...)` now treats `@` as a token start **only
at a whitespace boundary** (or start-of-line).
- If the cursor is on a nested `@` inside an existing
whitespace-delimited token (for example `@scope/pkg@latest`), it keeps
the surrounding token active instead of starting a new token at the
second `@`.
- It also avoids misclassifying mid-word usages like `foo@bar` as an `@`
file token.

- **Enter behavior with file popup**
- If the file-search popup is open but has **no selected match**,
pressing `Enter` now closes the popup and falls through to normal submit
behavior.
- This prevents pasted strings containing `@...` from blocking
submission just because file-search was active with no actionable
selection.

### Testing
I manually built and tested the scenarios involved with the bug report
and related use of `@` mentions to verify no regressions
2026-02-24 10:50:00 -08:00
Michael Bolin
3ca0e7673b feat: run zsh fork shell tool via shell-escalation (#12649)
## Why

This PR switches the `shell_command` zsh-fork path over to
`codex-shell-escalation` so the new shell tool can use the shared
exec-wrapper/escalation protocol instead of the `zsh_exec_bridge`
implementation that was introduced in
https://github.com/openai/codex/pull/12052. `zsh_exec_bridge` relied on
UNIX domain sockets, which is not as tamper-proof as the FD-based
approach in `codex-shell-escalation`.

## What Changed

- Added a Unix zsh-fork runtime adapter in `core`
(`core/src/tools/runtimes/shell/unix_escalation.rs`) that:
- runs zsh-fork commands through
`codex_shell_escalation::run_escalate_server`
  - bridges exec-policy / approval decisions into `ShellActionProvider`
- executes escalated commands via a `ShellCommandExecutor` that calls
`process_exec_tool_call`
- Updated `ShellRuntime` / `ShellCommandHandler` / tool spec wiring to
select a `shell_command` backend (`classic` vs `zsh-fork`) while leaving
the generic `shell` tool path unchanged.
- Removed the `zsh_exec_bridge`-based session service and deleted
`core/src/zsh_exec_bridge/mod.rs`.
- Moved exec-wrapper entrypoint dispatch to `arg0` by handling the
`codex-execve-wrapper` arg0 alias there, and removed the old
`codex_core::maybe_run_zsh_exec_wrapper_mode()` hooks from `cli` and
`app-server` mains.
- Added the needed `codex-shell-escalation` dependencies for `core` and
`arg0`.

## Tests

- `cargo test -p codex-core
shell_zsh_fork_prefers_shell_command_over_unified_exec`
- `cargo test -p codex-app-server turn_start_shell_zsh_fork --
--nocapture`
- verifies zsh-fork command execution and approval flows through the new
backend
- includes subcommand approve/decline coverage using the shared zsh
DotSlash fixture in `app-server/tests/suite/zsh`
- To test manually, I added the following to `~/.codex/config.toml`:

```toml
zsh_path = "/Users/mbolin/code/codex3/codex-rs/app-server/tests/suite/zsh"

[features]
shell_zsh_fork = true
```

Then I ran `just c` to run the dev build of Codex with these changes and
sent it the message:

```
run `echo $0`
```

And it replied with:

```
  echo $0 printed:

  /Users/mbolin/code/codex3/codex-rs/app-server/tests/suite/zsh

  In this tool context, $0 reflects the script path used to invoke the shell, not just zsh.
```

so the tool appears to be wired up correctly.

## Notes

- The zsh subcommand-decline integration test now uses `rm` under a
`WorkspaceWrite` sandbox. The previous `/usr/bin/true` scenario is
auto-allowed by the new `shell-escalation` policy path, which no longer
produces subcommand approval prompts.
2026-02-24 10:31:08 -08:00
viyatb-oai
8d3d58f992 feat(network-proxy): add MITM support and gate limited-mode CONNECT (#9859)
## Description
- Adds MITM support (CA load/issue, TLS termination, optional body
inspection).
- Adds `codex-network-proxy init` to create
`CODEX_HOME/network_proxy/mitm`.
- Enforces limited-mode HTTPS correctly: `CONNECT` requires MITM,
otherwise blocked with `mitm_required`.
- Keeps `origin/main` layering/reload semantics (managed layers included
in reload checks).
- Centralizes block reasons (`REASON_MITM_REQUIRED`) and removes
`println!`.
- Scope is MITM-only (no SOCKS changes).

gated by `mitm=false` (default)
2026-02-24 18:15:15 +00:00
Won Park
ca556fa313 ctrl-L (clears terminal but does not start a new chat) (#12628)
# ctrl-L

- Clears your terminal window
- Does not start a new chat
2026-02-24 10:03:42 -08:00
Dylan Hurd
f6053fdfb3 feat(core) Introduce Feature::RequestPermissions (#11871)
## Summary
Introduces the initial implementation of Feature::RequestPermissions.
RequestPermissions allows the model to request that a command be run
inside the sandbox, with additional permissions, like writing to a
specific folder. Eventually this will include other rules as well, and
the ability to persist these permissions, but this PR is already quite
large - let's get the core flow working and go from there!

<img width="1279" height="541" alt="Screenshot 2026-02-15 at 2 26 22 PM"
src="https://github.com/user-attachments/assets/0ee3ec0f-02ec-4509-91a2-809ac80be368"
/>

## Testing
- [x] Added tests
- [x] Tested locally
- [x] Feature
2026-02-24 09:48:57 -08:00
jif-oai
9a8adbf6e5 feat: use process group to kill the PTY (#12688)
Use the process group kill logic to kill the PTY
2026-02-24 16:55:23 +00:00
pakrym-oai
97d0068658 Send warmup request (#11258)
Send a request with `generate: falls` but a full set of tools and
instructions to pre-warm inference.

---------

Co-authored-by: Codex <noreply@openai.com>
2026-02-24 08:15:47 -08:00
jif-oai
0679e70bfc fix: replay after /agent (#12663)
Filter the events after a`/agent` replay to prevent replaying decision
events
2026-02-24 12:08:38 +00:00
zuxin-oai
3fe365ad8a memories: tighten memory lookup guidance and citation requirements (#12635)
## Summary
- tighten the memory-use decision boundary so agents skip memory only
for clearly self-contained asks
- make the quick memory pass more explicit and bounded (including a
lightweight search budget)
- add structured `<memory_citation>` requirements and examples for final
replies
- clarify memory update guidance and end-state wording for memory lookup

## Why
The previous template was directionally correct, but still left room for
inconsistent memory lookup behavior and citation formatting. This change
makes the default behavior, quick-pass scope, and citation output
contract much more explicit.

## Testing
- not run (prompt/template text change only)

Co-authored-by: jif-oai <jif@openai.com>
2026-02-24 11:46:28 +00:00
jif-oai
8758db5d5b feat: mutli agents persist config overrides (#12667)
Fix propagation of runtime config changes and `--yolo`
2026-02-24 11:33:00 +00:00
zuxin-oai
15f6cfb047 memories: tighten consolidation prompt schema and indexing guidance (#12653)
## Summary
- tighten the Phase 2 consolidation prompt for task-oriented `MEMORY.md`
generation
- address Phase 2 under-coverage / "laziness" with stronger workflow +
final-pass checks
- improve recency/ordering behavior for `MEMORY.md` and
`memory_summary.md`
- rewrite `## What's in Memory` as a clearer routing index with explicit
recent-3-day structure

## Key Changes
- `MEMORY.md` schema cleanup:
- align on `## Task <n>` task sections (remove stale `task:`
rule/example references)
  - include `thread_id` in rollout provenance examples
  - compact comma-separated `### keywords` format
- Phase 2 completeness guardrails:
  - chunked INIT coverage pass over `raw_memories.md`
  - incremental net-new indexing / routing steps
- stronger final checks (day ordering, topic coverage, keyword
searchability, accidental duplication)
- Recency / ordering rules:
- clearer scan-order guidance for raw memories (newest-first bias in
incremental mode)
- utility+recency ordering guidance for `MEMORY.md` task groups and
summary topics
  - rebuild recent active window from current `updated_at` coverage
- `## What's in Memory` rewrite:
  - index/routing-layer framing (not a mini-handbook)
  - explicit recent 3 distinct memory-day layout
  - richer recent-topic entries + compact lower-priority routing entries
- clearer `desc` / `learnings` expectations and separation from `##
General Tips`
- Explicitly allow rollout-summary reuse across multiple tasks/blocks
when it supports distinct task angles (with distinct task-local value)

## Notes
- Prompt-template only:
`codex-rs/core/templates/memories/consolidation.md`
- No runtime/code changes

## Validation
- Manual diff review only
2026-02-24 09:41:20 +00:00
pakrym-oai
68a7d98363 Simplify skill tracking (#12652)
Remove a few layers of structs and store SkillMetadata.

---------

Co-authored-by: alexsong-oai <alexsong@openai.com>
2026-02-23 22:47:39 -08:00
sayan-oai
7e46e5b9c2 chore: rm hardcoded PRESETS list (#12650)
rm `PRESETS` list harcoded in `model_presets` as we now have bundled
`models.json` with equivalent info.

update logic to rely on bundled models instead, update tests.
2026-02-23 22:35:51 -08:00
pakrym-oai
58763afa0f Add skill approval event/response (#12633)
Set the stage for skill-level permission approval in addition to
command-level.

Behind a feature flag.
2026-02-23 22:28:58 -08:00
Eric Traut
a4076ab4b1 Avoid AbsolutePathBuf::parent() panic under EMFILE by skipping re-absolutization (#12647)
Fixes #12216

Fixes a panic in `AbsolutePathBuf::parent()` when the process hits file
descriptor exhaustion (`EMFILE` / "Too many open files").

### Root cause

`AbsolutePathBuf::parent()` was re-validating the parent path via
`from_absolute_path(...).expect(...)`.

`from_absolute_path()` calls `path_absolutize::absolutize()`, which can
depend on `std::env::current_dir()`. Under `EMFILE`, that can fail,
causing `parent()` to panic even though the parent of an absolute path
is already known.

### Change

- Stop re-absolutizing the result of `self.0.parent()`
- Construct `AbsolutePathBuf` directly from the known parent path
- Keep an invariant check with `debug_assert!(p.is_absolute())`

### Why this is safe

`self` is already an `AbsolutePathBuf`, so `self.0` is
absolute/normalized. The parent of an absolute path is expected to be
absolute, so re-running fallible normalization here is unnecessary and
can introduce unrelated panics.
2026-02-23 21:59:33 -08:00
alexsong-oai
09a82f364f Support implicit skill invocation analytics events (#12049)
- use `skills_for_cwd` lookup to scope allowed skills and build
invocation context for downstream processing
- add detection in `stream_events_utils` to classify tool calls as
implicit skill invocations per the proposal (script runners, extensions,
`scripts` dirs, and SKILL.md reads)
- deduplicate invocations per turn and emit analytics/OTEL events on the
same background queue as explicit invokes
2026-02-23 21:55:49 -08:00
Dylan Hurd
fbeda61cc3 fix(exec) Patch resume test race condition (#12648)
## Summary
The test exec_resume_last_respects_cwd_filter_and_all_flag makes one
session “newest” by resuming it, but rollout updated_at is stored/sorted
at second precision. On fast CI (especially Windows), the touch could
land in the same second as initial session creation, making ordering
nondeterministic.

This change adds a short sleep before the recency-touch step so the
resumed session is guaranteed to have a later updated_at, preserving the
intended assertion without changing product behavior.
2026-02-23 21:54:25 -08:00
viyatb-oai
c3048ff90a feat(core): persist network approvals in execpolicy (#12357)
## Summary
Persist network approval allow/deny decisions as `network_rule(...)`
entries in execpolicy (not proxy config)

It adds `network_rule` parsing + append support in `codex-execpolicy`,
including `decision="prompt"` (parse-only; not compiled into proxy
allow/deny lists)
- compile execpolicy network rules into proxy allow/deny lists and
update the live proxy state on approval
- preserve requirements execpolicy `network_rule(...)` entries when
merging with file-based execpolicy
- reject broad wildcard hosts (for example `*`) for persisted
`network_rule(...)`
2026-02-23 21:37:46 -08:00
Michael Bolin
af215eb390 refactor: decouple shell-escalation from codex-core (#12638)
## Why

After removing `exec-server`, the next step is to wire a new shell tool
to `codex-rs/shell-escalation` directly.

That is blocked while `codex-shell-escalation` depends on `codex-core`,
because the new integration would require `codex-core` to depend on
`codex-shell-escalation` and create a dependency cycle.

This change ports the reusable pieces from the earlier prep work, but
drops the old compatibility shim because `exec-server`/MCP support is
already gone.

## What Changed

### Decouple `shell-escalation` from `codex-core`

- Introduce a crate-local `SandboxState` in `shell-escalation`
- Introduce a `ShellCommandExecutor` trait so callers provide process
execution/sandbox integration
- Update `EscalateServer::exec(...)` and `run_escalate_server(...)` to
use the injected executor
- Remove the direct `codex_core::exec::process_exec_tool_call(...)` call
from `shell-escalation`
- Remove the `codex-core` dependency from `codex-shell-escalation`

### Restore reusable policy adapter exports

- Re-enable `unix::core_shell_escalation`
- Export `ShellActionProvider` and `ShellPolicyFactory` from
`shell-escalation`
- Keep the crate root API simple (no `legacy_api` compatibility layer)

### Port socket fixes from the earlier prep commit

- Use `socket2::Socket::pair_raw(...)` for AF_UNIX socketpairs and
restore `CLOEXEC` explicitly on both endpoints
- Keep `CLOEXEC` cleared only on the single datagram client FD that is
intentionally passed across `exec`
- Clean up `tokio::AsyncFd::try_io(...)` error handling in the socket
helpers

## Verification

- `cargo shear`
- `cargo clippy -p codex-shell-escalation --tests`
- `cargo test -p codex-shell-escalation`
2026-02-23 20:58:24 -08:00
Michael Bolin
38f84b6b29 refactor: delete exec-server and move execve wrapper into shell-escalation (#12632)
## Why

We already plan to remove the shell-tool MCP path, and doing that
cleanup first makes the follow-on `shell-escalation` work much simpler.

This change removes the last remaining reason to keep
`codex-rs/exec-server` around by moving the `codex-execve-wrapper`
binary and shared shell test fixtures to the crates/tests that now own
that functionality.

## What Changed

### Delete `codex-rs/exec-server`

- Remove the `exec-server` crate, including the MCP server binary,
MCP-specific modules, and its test support/test suite
- Remove `exec-server` from the `codex-rs` workspace and update
`Cargo.lock`

### Move `codex-execve-wrapper` into `codex-rs/shell-escalation`

- Move the wrapper implementation into `shell-escalation`
(`src/unix/execve_wrapper.rs`)
- Add the `codex-execve-wrapper` binary entrypoint under
`shell-escalation/src/bin/`
- Update `shell-escalation` exports/module layout so the wrapper
entrypoint is hosted there
- Move the wrapper README content from `exec-server` to
`shell-escalation/README.md`

### Move shared shell test fixtures to `app-server`

- Move the DotSlash `bash`/`zsh` test fixtures from
`exec-server/tests/suite/` to `app-server/tests/suite/`
- Update `app-server` zsh-fork tests to reference the new fixture paths

### Keep `shell-tool-mcp` as a shell-assets package

- Update `.github/workflows/shell-tool-mcp.yml` packaging so the npm
artifact contains only patched Bash/Zsh payloads (no Rust binaries)
- Update `shell-tool-mcp/package.json`, `shell-tool-mcp/src/index.ts`,
and docs to reflect the shell-assets-only package shape
- `shell-tool-mcp-ci.yml` does not need changes because it is already
JS-only

## Verification

- `cargo shear`
- `cargo clippy -p codex-shell-escalation --tests`
- `just clippy`
2026-02-23 20:10:22 -08:00
Javi
5a3bdcb27b app-server: fix connecting via websockets with Sec-WebSocket-Extensions: permessage-deflate (#12629)
# 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.
2026-02-24 02:41:03 +00:00
github-actions[bot]
d580995957 Update models.json (#11408)
Automated update of models.json.

---------

Co-authored-by: sayan-oai <244841968+sayan-oai@users.noreply.github.com>
Co-authored-by: sayan-oai <sayan@openai.com>
2026-02-23 18:37:31 -08:00
Ahmed Ibrahim
10a3adad8e Handle realtime spawn_transcript delegation (#12619) 2026-02-23 14:39:07 -08:00
Jeremy Rose
855e275591 voice transcription (#3381)
Adds voice transcription on press-and-hold of spacebar.


https://github.com/user-attachments/assets/85039314-26f3-46d1-a83b-8c4a4a1ecc21

---------

Co-authored-by: Codex <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
Co-authored-by: David Zbarsky <zbarsky@openai.com>
2026-02-23 22:15:18 +00:00
sayan-oai
50953ea39a fix: show command running in background terminal in details under status indicator (#12549)
#### What
Display in-progress background terminal command in `status.details`
(right under header) rather than inline, as it gets cut off currently.

###### Before
<img width="993" height="395" alt="image"
src="https://github.com/user-attachments/assets/6792b666-8184-40f7-bf29-409bb06c21d5"
/>

###### After
<img width="469" height="137" alt="image"
src="https://github.com/user-attachments/assets/4d6a2481-bd19-4333-8c1a-92f521b09b3d"
/>

#### Tests
Added/updated tests
2026-02-23 21:04:24 +00:00
dependabot[bot]
cd5acf6af7 chore(deps): bump owo-colors from 4.2.3 to 4.3.0 in /codex-rs (#12530)
Bumps [owo-colors](https://github.com/owo-colors/owo-colors) from 4.2.3
to 4.3.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/owo-colors/owo-colors/releases">owo-colors's
releases</a>.</em></p>
<blockquote>
<h2>owo-colors 4.3.0</h2>
<h3>Fixed</h3>
<ul>
<li>Scripts in the <code>scripts/</code> directory are no longer
published in the crate package. Thanks <a
href="https://redirect.github.com/owo-colors/owo-colors/pull/152">weiznich</a>
for your first contribution!</li>
</ul>
<h3>Changed</h3>
<ul>
<li>
<p>Mark methods with
<code>#[rust_analyzer::completions(ignore_flyimport)]</code> and the
<code>OwoColorize</code> trait with
<code>#[rust_analyzer::completions(ignore_flyimport_methods)]</code>.
This prevents owo-colors methods from being completed with rust-analyzer
unless the <code>OwoColorize</code> trait is included.</p>
<p>Unfortunately, this also breaks explicit autocomplete commands such
as Ctrl-Space in many editors. (The language server protocol doesn't
appear to have a way to differentiate between implicit and explicit
autocomplete commands.) On balance we believe this is the right
approach, but please do provide feedback on [PR <a
href="https://redirect.github.com/owo-colors/owo-colors/issues/141">#141</a>](<a
href="https://redirect.github.com/owo-colors/owo-colors/pull/141">owo-colors/owo-colors#141</a>)
if it negatively affects you.</p>
</li>
<li>
<p>Updated MSRV to Rust 1.81.</p>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/owo-colors/owo-colors/blob/main/CHANGELOG.md">owo-colors's
changelog</a>.</em></p>
<blockquote>
<h2>[4.3.0] - 2026-02-22</h2>
<h3>Fixed</h3>
<ul>
<li>Scripts in the <code>scripts/</code> directory are no longer
published in the crate package. Thanks <a
href="https://redirect.github.com/owo-colors/owo-colors/pull/152">weiznich</a>
for your first contribution!</li>
</ul>
<h3>Changed</h3>
<ul>
<li>
<p>Mark methods with
<code>#[rust_analyzer::completions(ignore_flyimport)]</code> and the
<code>OwoColorize</code> trait with
<code>#[rust_analyzer::completions(ignore_flyimport_methods)]</code>.
This prevents owo-colors methods from being completed with rust-analyzer
unless the <code>OwoColorize</code> trait is included.</p>
<p>Unfortunately, this also breaks explicit autocomplete commands such
as Ctrl-Space in many editors. (The language server protocol doesn't
appear to have a way to differentiate between implicit and explicit
autocomplete commands.) On balance we believe this is the right
approach, but please do provide feedback on [PR <a
href="https://redirect.github.com/owo-colors/owo-colors/issues/141">#141</a>](<a
href="https://redirect.github.com/owo-colors/owo-colors/pull/141">owo-colors/owo-colors#141</a>)
if it negatively affects you.</p>
</li>
<li>
<p>Updated MSRV to Rust 1.81.</p>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="baf10f9a74"><code>baf10f9</code></a>
[owo-colors] version 4.3.0</li>
<li><a
href="6abe2026c5"><code>6abe202</code></a>
[meta] prepare changelog</li>
<li><a
href="ca81447041"><code>ca81447</code></a>
[RFC] add ignore_flyimport and ignore_flyimport_methods (<a
href="https://redirect.github.com/owo-colors/owo-colors/issues/141">#141</a>)</li>
<li><a
href="61de72e7f9"><code>61de72e</code></a>
Exclude development script from published package (<a
href="https://redirect.github.com/owo-colors/owo-colors/issues/152">#152</a>)</li>
<li><a
href="b2ad6bcd41"><code>b2ad6bc</code></a>
update MSRV to Rust 1.81 (<a
href="https://redirect.github.com/owo-colors/owo-colors/issues/156">#156</a>)</li>
<li>See full diff in <a
href="https://github.com/owo-colors/owo-colors/compare/v4.2.3...v4.3.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=owo-colors&package-manager=cargo&previous-version=4.2.3&new-version=4.3.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 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>
2026-02-23 13:01:15 -08:00
Beehive Innovations
be4203023d fix(tui): queue steer Enter while final answer is still streaming to prevent dead state (#12569)
## Summary
This fixes a TUI race (https://github.com/openai/codex/issues/11008)
where pressing Enter with Steer enabled while the assistant is still
streaming the final answer could put Codex into a non-recoverable
“running” state (no further prompts handled until exiting and resuming).

## Root Cause
In steer mode, `InputResult::Submitted` could submit immediately even
while a final-answer stream was active. That immediate submission races
with turn completion and can strand turn state.

## Fix
When handling `InputResult::Submitted`, we now queue instead of
immediate-submit if a final-answer stream is active
(`stream_controller.is_some()`).

This keeps behavior deterministic:
- Prompt is preserved in the queue.
- `on_task_complete()` drains queued input through
`maybe_send_next_queued_input()`.
- Follow-up prompts continue in FIFO order after completion.

## Why this resolves the “dead mode”
The problematic timing window is now converted into queueing, so prompts
entered during final streaming are not lost and are processed after the
current output ends. The model continues handling prompts normally
without requiring `/quit` + `resume`.

## Tests
Added regression coverage in `tui/src/chatwidget/tests.rs`:

- `steer_enter_queues_while_final_answer_stream_is_active`
- `steer_enter_during_final_stream_preserves_follow_up_prompts_in_order`

Both fail on old behavior and pass with this fix.
2026-02-23 12:58:40 -08:00
Felipe Coury
48e08a1561 fix(tui): recover on owned wrap mapping mismatch (#12609)
## Summary

- Replace the `panic!` in `map_owned_wrapped_line_to_range` with a
recoverable flow that skips synthetic leading characters, logs a warning
on mid-line mismatch, and returns the mapped prefix range instead of
crashing
- Fixes a crash when `textwrap` produces owned lines with synthetic
indent prefixes (e.g. non-space indents via
`initial_indent`/`subsequent_indent`)

## Test plan

- [x] Added unit test for direct mismatch recovery
(`map_owned_wrapped_line_to_range_recovers_on_non_prefix_mismatch`)
- [x] Added end-to-end `wrap_ranges` test with non-space indents that
forces owned wrapped lines and validates full source reconstruction
- [x] Verify no regressions in existing `wrapping.rs` tests (`cargo test
-p codex-tui`)
2026-02-23 20:14:50 +00:00
sayan-oai
bfe622f495 fix: add ellipsis for truncated status indicator (#12540)
#### What

- Add ellipsis truncation of the status indicator, similar to equivalent
truncation done in the footer.
- Extract truncation helpers into separate file



https://github.com/user-attachments/assets/a2d5f22f-8adc-456e-8059-97359194c25c


#### Tests
Updated relevant snapshot tests
2026-02-23 11:45:46 -08:00
Michael Bolin
7f75e74201 Use Arc-based ToolCtx in tool runtimes (#12583)
## Why
Tool handlers and runtimes needed to pass the same turn/session context
for shell and non-shell workflows without duplicative ownership churn.
Using shared pointers avoids temporary lifetimes and keeps existing
behavior unchanged while simplifying call sites.

## What changed
- Converted `ToolCtx` to store shared context handles (`Arc`-based),
including updates across shell, apply-patch, and unified-exec paths.
- Updated orchestrator/runtime call sites to consume the shared context
consistently and remove brittle move/borrow patterns.
- Kept behavior unchanged while preparing the type surface for the new
shell escalation integration in the next stack commit.

## Verification
- Validated this commit stack point with `just clippy` and confirmed
workspace compiles cleanly in this stack state.

[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/12583).
* #12584
* __->__ #12583
* #12556
2026-02-23 18:29:26 +00:00
dependabot[bot]
fec517cd38 chore(deps): bump syn from 2.0.114 to 2.0.117 in /codex-rs (#12529)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.114 to 2.0.117.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/dtolnay/syn/releases">syn's
releases</a>.</em></p>
<blockquote>
<h2>2.0.117</h2>
<ul>
<li>Fix parsing of <code>self::</code> pattern in first function
argument (<a
href="https://redirect.github.com/dtolnay/syn/issues/1970">#1970</a>)</li>
</ul>
<h2>2.0.116</h2>
<ul>
<li>Optimize parse_fn_arg_or_variadic for less lookahead on erroneous
receiver (<a
href="https://redirect.github.com/dtolnay/syn/issues/1968">#1968</a>)</li>
</ul>
<h2>2.0.115</h2>
<ul>
<li>Enable GenericArgument::Constraint parsing in non-full mode (<a
href="https://redirect.github.com/dtolnay/syn/issues/1966">#1966</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="7bcb37cdb3"><code>7bcb37c</code></a>
Release 2.0.117</li>
<li><a
href="9c6e7d3b8d"><code>9c6e7d3</code></a>
Merge pull request <a
href="https://redirect.github.com/dtolnay/syn/issues/1970">#1970</a>
from dtolnay/receiver</li>
<li><a
href="019a84847e"><code>019a848</code></a>
Fix self:: pattern in first function argument</li>
<li><a
href="23f54f3cf6"><code>23f54f3</code></a>
Update test suite to nightly-2026-02-18</li>
<li><a
href="b99b9a627c"><code>b99b9a6</code></a>
Unpin CI miri toolchain</li>
<li><a
href="a62e54a48b"><code>a62e54a</code></a>
Release 2.0.116</li>
<li><a
href="5a8ed9f32e"><code>5a8ed9f</code></a>
Merge pull request <a
href="https://redirect.github.com/dtolnay/syn/issues/1968">#1968</a>
from dtolnay/receiver</li>
<li><a
href="813afcc773"><code>813afcc</code></a>
Optimize parse_fn_arg_or_variadic for less lookahead on erroneous
receiver</li>
<li><a
href="c172150113"><code>c172150</code></a>
Add regression test for issue 1718</li>
<li><a
href="0071ab367c"><code>0071ab3</code></a>
Ignore type_complexity clippy lint</li>
<li>Additional commits viewable in <a
href="https://github.com/dtolnay/syn/compare/2.0.114...2.0.117">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=syn&package-manager=cargo&previous-version=2.0.114&new-version=2.0.117)](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 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>
2026-02-23 10:25:05 -08:00
dependabot[bot]
5c52ef8e60 chore(deps): bump libc from 0.2.180 to 0.2.182 in /codex-rs (#12528)
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.180 to 0.2.182.
<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.182</h2>
<h3>Added</h3>
<ul>
<li>Android, Linux: Add <code>tgkill</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4970">#4970</a>)</li>
<li>Redox: Add <code>RENAME_NOREPLACE</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4968">#4968</a>)</li>
<li>Redox: Add <code>renameat2</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4968">#4968</a>)</li>
</ul>
<h2>0.2.181</h2>
<h3>Added</h3>
<ul>
<li>Apple: Add <code>MADV_ZERO</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4924">#4924</a>)</li>
<li>Redox: Add <code>makedev</code>, <code>major</code>, and
<code>minor</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4928">#4928</a>)</li>
<li>GLibc: Add <code>PTRACE_SET_SYSCALL_INFO</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4933">#4933</a>)</li>
<li>OpenBSD: Add more kqueue related constants for (<a
href="https://redirect.github.com/rust-lang/libc/pull/4945">#4945</a>)</li>
<li>Linux: add CAN error types (<a
href="https://redirect.github.com/rust-lang/libc/pull/4944">#4944</a>)</li>
<li>OpenBSD: Add siginfo_t::si_status (<a
href="https://redirect.github.com/rust-lang/libc/pull/4946">#4946</a>)</li>
<li>QNX NTO: Add <code>max_align_t</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4927">#4927</a>)</li>
<li>Illumos: Add <code>_CS_PATH</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4956">#4956</a>)</li>
<li>OpenBSD: add <code>ppoll</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4957">#4957</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li><strong>Breaking</strong>: Redox: Fix the type of <code>dev_t</code>
(<a
href="https://redirect.github.com/rust-lang/libc/pull/4928">#4928</a>)</li>
<li>AIX: Change 'tv_nsec' of 'struct timespec' to type 'c_long' (<a
href="https://redirect.github.com/rust-lang/libc/pull/4931">#4931</a>)</li>
<li>AIX: Use 'struct st_timespec' in 'struct stat{,64}' (<a
href="https://redirect.github.com/rust-lang/libc/pull/4931">#4931</a>)</li>
<li>Glibc: Link old version of <code>tc{g,s}etattr</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4938">#4938</a>)</li>
<li>Glibc: Link the correct version of <code>cf{g,s}et{i,o}speed</code>
on mips{32,64}r6 (<a
href="https://redirect.github.com/rust-lang/libc/pull/4938">#4938</a>)</li>
<li>OpenBSD: Fix constness of tm.tm_zone (<a
href="https://redirect.github.com/rust-lang/libc/pull/4948">#4948</a>)</li>
<li>OpenBSD: Fix the definition of <code>ptrace_thread_state</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4947">#4947</a>)</li>
<li>QuRT: Fix type visibility and defs (<a
href="https://redirect.github.com/rust-lang/libc/pull/4932">#4932</a>)</li>
<li>Redox: Fix values for <code>PTHREAD_MUTEX_{NORMAL, RECURSIVE}</code>
(<a
href="https://redirect.github.com/rust-lang/libc/pull/4943">#4943</a>)</li>
<li>Various: Mark additional fields as private padding (<a
href="https://redirect.github.com/rust-lang/libc/pull/4922">#4922</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Fuchsia: Update <code>SO_*</code> constants (<a
href="https://redirect.github.com/rust-lang/libc/pull/4937">#4937</a>)</li>
<li>Revert &quot;musl: convert inline timespecs to timespec&quot;
(resolves build issues on targets only supported by Musl 1.2.3+ ) (<a
href="https://redirect.github.com/rust-lang/libc/pull/4958">#4958</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/rust-lang/libc/blob/0.2.182/CHANGELOG.md">libc's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/rust-lang/libc/compare/0.2.181...0.2.182">0.2.182</a>
- 2026-02-13</h2>
<h3>Added</h3>
<ul>
<li>Android, Linux: Add <code>tgkill</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4970">#4970</a>)</li>
<li>Redox: Add <code>RENAME_NOREPLACE</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4968">#4968</a>)</li>
<li>Redox: Add <code>renameat2</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4968">#4968</a>)</li>
</ul>
<h2><a
href="https://github.com/rust-lang/libc/compare/0.2.180...0.2.181">0.2.181</a>
- 2026-02-09</h2>
<h3>Added</h3>
<ul>
<li>Apple: Add <code>MADV_ZERO</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4924">#4924</a>)</li>
<li>Redox: Add <code>makedev</code>, <code>major</code>, and
<code>minor</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4928">#4928</a>)</li>
<li>GLibc: Add <code>PTRACE_SET_SYSCALL_INFO</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4933">#4933</a>)</li>
<li>OpenBSD: Add more kqueue related constants for (<a
href="https://redirect.github.com/rust-lang/libc/pull/4945">#4945</a>)</li>
<li>Linux: add CAN error types (<a
href="https://redirect.github.com/rust-lang/libc/pull/4944">#4944</a>)</li>
<li>OpenBSD: Add siginfo_t::si_status (<a
href="https://redirect.github.com/rust-lang/libc/pull/4946">#4946</a>)</li>
<li>QNX NTO: Add <code>max_align_t</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4927">#4927</a>)</li>
<li>Illumos: Add <code>_CS_PATH</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4956">#4956</a>)</li>
<li>OpenBSD: add <code>ppoll</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4957">#4957</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li><strong>breaking</strong>: Redox: Fix the type of dev_t (<a
href="https://redirect.github.com/rust-lang/libc/pull/4928">#4928</a>)</li>
<li>AIX: Change 'tv_nsec' of 'struct timespec' to type 'c_long' (<a
href="https://redirect.github.com/rust-lang/libc/pull/4931">#4931</a>)</li>
<li>AIX: Use 'struct st_timespec' in 'struct stat{,64}' (<a
href="https://redirect.github.com/rust-lang/libc/pull/4931">#4931</a>)</li>
<li>Glibc: Link old version of <code>tc{g,s}etattr</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4938">#4938</a>)</li>
<li>Glibc: Link the correct version of <code>cf{g,s}et{i,o}speed</code>
on mips{32,64}r6 (<a
href="https://redirect.github.com/rust-lang/libc/pull/4938">#4938</a>)</li>
<li>OpenBSD: Fix constness of tm.tm_zone (<a
href="https://redirect.github.com/rust-lang/libc/pull/4948">#4948</a>)</li>
<li>OpenBSD: Fix the definition of <code>ptrace_thread_state</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/4947">#4947</a>)</li>
<li>QuRT: Fix type visibility and defs (<a
href="https://redirect.github.com/rust-lang/libc/pull/4932">#4932</a>)</li>
<li>Redox: Fix values for <code>PTHREAD_MUTEX_{NORMAL, RECURSIVE}</code>
(<a
href="https://redirect.github.com/rust-lang/libc/pull/4943">#4943</a>)</li>
<li>Various: Mark additional fields as private padding (<a
href="https://redirect.github.com/rust-lang/libc/pull/4922">#4922</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Fuchsia: Update <code>SO_*</code> constants (<a
href="https://redirect.github.com/rust-lang/libc/pull/4937">#4937</a>)</li>
<li>Revert &quot;musl: convert inline timespecs to timespec&quot;
(resolves build issues on targets only supported by Musl 1.2.3+ ) (<a
href="https://redirect.github.com/rust-lang/libc/pull/4958">#4958</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="e879ee90b6"><code>e879ee9</code></a>
chore: Release libc 0.2.182</li>
<li><a
href="2efe72f4da"><code>2efe72f</code></a>
remove copyright year in LICENSE-MIT</li>
<li><a
href="634bc4e66e"><code>634bc4e</code></a>
ci: Update the list of tested and documented targets</li>
<li><a
href="d7aa109ab5"><code>d7aa109</code></a>
Revert &quot;Disable hexagon-unknown-linux-musl testing for
now&quot;</li>
<li><a
href="14e2f5641e"><code>14e2f56</code></a>
Revert &quot;ci: Skip hexagon-unknown-linux-musl&quot;</li>
<li><a
href="b7807c369b"><code>b7807c3</code></a>
Revert &quot;aix: Temporarily skip checking powerpc64-ibm-aix
builds&quot;</li>
<li><a
href="abe93a0bfe"><code>abe93a0</code></a>
feat(linux): add <code>tgkill</code> for Linux and Android</li>
<li><a
href="25f7dde943"><code>25f7dde</code></a>
feat(redox): add <code>RENAME_NOREPLACE</code></li>
<li><a
href="4b4ce4f220"><code>4b4ce4f</code></a>
feat(redox): add <code>renameat2</code></li>
<li><a
href="ab8c36c493"><code>ab8c36c</code></a>
build(deps): bump vmactions/solaris-vm from 1.2.8 to 1.3.0</li>
<li>Additional commits viewable in <a
href="https://github.com/rust-lang/libc/compare/0.2.180...0.2.182">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.180&new-version=0.2.182)](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 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>
2026-02-23 10:24:35 -08:00
Charley Cunningham
3cea3e665e app-server: box request dispatch future to reduce stack pressure (#12421) 2026-02-23 10:05:41 -08:00
346 changed files with 27661 additions and 5392 deletions

View File

@@ -47,6 +47,11 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Set up Node.js for js_repl tests
uses: actions/setup-node@v6
with:
node-version-file: codex-rs/node-version.txt
# Some integration tests rely on DotSlash being installed.
# See https://github.com/openai/codex/pull/7617.
- name: Install DotSlash
@@ -156,6 +161,13 @@ jobs:
--build_metadata=VISIBILITY=PUBLIC
)
if [[ "${RUNNER_OS:-}" != "Windows" ]]; then
# Bazel test sandboxes on macOS may resolve an older Homebrew `node`
# before the `actions/setup-node` runtime on PATH.
node_bin="$(which node)"
bazel_args+=("--test_env=CODEX_JS_REPL_NODE_PATH=${node_bin}")
fi
if [[ -n "${BUILDBUDDY_API_KEY:-}" ]]; then
echo "BuildBuddy API key is available; using remote Bazel configuration."
# Work around Bazel 9 remote repo contents cache / overlay materialization failures

View File

@@ -499,6 +499,10 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Set up Node.js for js_repl tests
uses: actions/setup-node@v6
with:
node-version-file: codex-rs/node-version.txt
- name: Install Linux build dependencies
if: ${{ runner.os == 'Linux' }}
shell: bash

View File

@@ -178,6 +178,12 @@ jobs:
shell: bash
run: |
set -euo pipefail
# Avoid problematic aws-lc jitter entropy code path on musl builders.
echo "AWS_LC_SYS_NO_JITTER_ENTROPY=1" >> "$GITHUB_ENV"
target_no_jitter="AWS_LC_SYS_NO_JITTER_ENTROPY_${{ matrix.target }}"
target_no_jitter="${target_no_jitter//-/_}"
echo "${target_no_jitter}=1" >> "$GITHUB_ENV"
# Clear global Rust flags so host/proc-macro builds don't pull in UBSan.
echo "RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV"
@@ -488,6 +494,11 @@ jobs:
--package codex-responses-api-proxy \
--package codex-sdk
- name: Stage installer scripts
run: |
cp scripts/install/install.sh dist/install.sh
cp scripts/install/install.ps1 dist/install.ps1
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:

View File

@@ -67,128 +67,6 @@ jobs:
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
env:
CARGO_PROFILE_RELEASE_LTO: ${{ contains(needs.metadata.outputs.version, '-alpha') && 'thin' || 'fat' }}
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
- name: Install UBSan runtime (musl)
if: ${{ matrix.install_musl }}
shell: bash
run: |
set -euo pipefail
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update -y
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1
fi
- uses: dtolnay/rust-toolchain@1.93.0
with:
targets: ${{ matrix.target }}
- if: ${{ matrix.install_musl }}
name: Install Zig
uses: mlugg/setup-zig@v2
with:
version: 0.14.0
- if: ${{ matrix.install_musl }}
name: Install musl build dependencies
env:
TARGET: ${{ matrix.target }}
run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh"
- if: ${{ matrix.install_musl }}
name: Configure rustc UBSan wrapper (musl host)
shell: bash
run: |
set -euo pipefail
ubsan=""
if command -v ldconfig >/dev/null 2>&1; then
ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')"
fi
wrapper_root="${RUNNER_TEMP:-/tmp}"
wrapper="${wrapper_root}/rustc-ubsan-wrapper"
cat > "${wrapper}" <<EOF
#!/usr/bin/env bash
set -euo pipefail
if [[ -n "${ubsan}" ]]; then
export LD_PRELOAD="${ubsan}\${LD_PRELOAD:+:\${LD_PRELOAD}}"
fi
exec "\$1" "\${@:2}"
EOF
chmod +x "${wrapper}"
echo "RUSTC_WRAPPER=${wrapper}" >> "$GITHUB_ENV"
echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV"
- if: ${{ matrix.install_musl }}
name: Clear sanitizer flags (musl)
shell: bash
run: |
set -euo pipefail
# Clear global Rust flags so host/proc-macro builds don't pull in UBSan.
echo "RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "RUSTDOCFLAGS=" >> "$GITHUB_ENV"
# Override any runner-level Cargo config rustflags as well.
echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV"
sanitize_flags() {
local input="$1"
input="${input//-fsanitize=undefined/}"
input="${input//-fno-sanitize-recover=undefined/}"
input="${input//-fno-sanitize-trap=undefined/}"
echo "$input"
}
cflags="$(sanitize_flags "${CFLAGS-}")"
cxxflags="$(sanitize_flags "${CXXFLAGS-}")"
echo "CFLAGS=${cflags}" >> "$GITHUB_ENV"
echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV"
- 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@v6
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
@@ -537,7 +415,6 @@ jobs:
name: Package npm module
needs:
- metadata
- rust-binaries
- bash-linux
- bash-darwin
- zsh-linux
@@ -579,7 +456,6 @@ jobs:
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
@@ -613,8 +489,6 @@ jobs:
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 \
"$staging"/vendor/*/zsh/*/zsh

View File

@@ -1,4 +1,11 @@
load("@apple_support//xcode:xcode_config.bzl", "xcode_config")
load("@rules_cc//cc:defs.bzl", "cc_shared_library")
cc_shared_library(
name = "clang",
deps = ["@llvm-project//clang:libclang"],
visibility = ["//visibility:public"],
)
xcode_config(name = "disable_xcode")

View File

@@ -1,5 +1,7 @@
module(name = "codex")
bazel_dep(name = "platforms", version = "1.0.0")
bazel_dep(name = "toolchains_llvm_bootstrapped", version = "0.5.3")
bazel_dep(name = "toolchains_llvm_bootstrapped", version = "0.5.6")
single_version_override(
module_name = "toolchains_llvm_bootstrapped",
patch_strip = 1,
@@ -8,6 +10,8 @@ single_version_override(
],
)
register_toolchains("@toolchains_llvm_bootstrapped//toolchain:all")
osx = use_extension("@toolchains_llvm_bootstrapped//extensions:osx.bzl", "osx")
osx.framework(name = "ApplicationServices")
osx.framework(name = "AppKit")
@@ -16,8 +20,12 @@ osx.framework(name = "CoreFoundation")
osx.framework(name = "CoreGraphics")
osx.framework(name = "CoreServices")
osx.framework(name = "CoreText")
osx.framework(name = "AudioToolbox")
osx.framework(name = "CFNetwork")
osx.framework(name = "FontServices")
osx.framework(name = "AudioUnit")
osx.framework(name = "CoreAudio")
osx.framework(name = "CoreAudioTypes")
osx.framework(name = "Foundation")
osx.framework(name = "ImageIO")
osx.framework(name = "IOKit")
@@ -25,10 +33,7 @@ osx.framework(name = "Kernel")
osx.framework(name = "OSLog")
osx.framework(name = "Security")
osx.framework(name = "SystemConfiguration")
register_toolchains(
"@toolchains_llvm_bootstrapped//toolchain:all",
)
use_repo(osx, "macosx15.4.sdk")
# Needed to disable xcode...
bazel_dep(name = "apple_support", version = "2.1.0")
@@ -39,9 +44,9 @@ bazel_dep(name = "rules_rs", version = "0.0.23")
# Special toolchains branch
archive_override(
module_name = "rules_rs",
integrity = "sha256-YbDRjZos4UmfIPY98znK1BgBWRQ1/ui3CtL6RqxE30I=",
strip_prefix = "rules_rs-6cf3d940fdc48baf3ebd6c37daf8e0be8fc73ecb",
url = "https://github.com/dzbarsky/rules_rs/archive/6cf3d940fdc48baf3ebd6c37daf8e0be8fc73ecb.tar.gz",
integrity = "sha256-O34UF4H7b1Qacu3vlu2Od4ILGVApzg5j1zl952SFL3w=",
strip_prefix = "rules_rs-097123c2aa72672e371e69e7035869f5a45c7b2b",
url = "https://github.com/dzbarsky/rules_rs/archive/097123c2aa72672e371e69e7035869f5a45c7b2b.tar.gz",
)
rules_rust = use_extension("@rules_rs//rs/experimental:rules_rust.bzl", "rules_rust")
@@ -134,6 +139,9 @@ crate.annotation(
"OPENSSL_NO_VENDOR": "1",
"OPENSSL_STATIC": "1",
},
crate_features = [
"dep:openssl-src",
],
crate = "openssl-sys",
data = ["@openssl//:gen_dir"],
)
@@ -145,6 +153,28 @@ crate.annotation(
workspace_cargo_toml = "rust/runfiles/Cargo.toml",
)
llvm = use_extension("@toolchains_llvm_bootstrapped//extensions:llvm.bzl", "llvm")
use_repo(llvm, "llvm-project")
crate.annotation(
# Provide the hermetic SDK path so the build script doesn't try to invoke an unhermetic `xcrun --show-sdk-path`.
build_script_data = [
"@macosx15.4.sdk//sysroot",
],
build_script_env = {
"BINDGEN_EXTRA_CLANG_ARGS": "-isystem $(location @toolchains_llvm_bootstrapped//:builtin_headers)",
"COREAUDIO_SDK_PATH": "$(location @macosx15.4.sdk//sysroot)",
"LIBCLANG_PATH": "$(location @codex//:clang)",
},
build_script_tools = [
"@codex//:clang",
"@toolchains_llvm_bootstrapped//:builtin_headers",
],
crate = "coreaudio-sys",
)
inject_repo(crate, "codex", "toolchains_llvm_bootstrapped", "macosx15.4.sdk")
# Fix readme inclusions
crate.annotation(
crate = "windows-link",
@@ -175,6 +205,17 @@ crate.annotation(
gen_build_script = "off",
deps = [":windows_import_lib"],
)
bazel_dep(name = "alsa_lib", version = "1.2.9.bcr.4")
crate.annotation(
crate = "alsa-sys",
gen_build_script = "off",
deps = ["@alsa_lib"],
)
inject_repo(crate, "alsa_lib")
use_repo(crate, "crates")
rbe_platform_repository = use_repo_rule("//:rbe.bzl", "rbe_platform_repository")

52
MODULE.bazel.lock generated

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
<p align="center"><code>npm i -g @openai/codex</code><br />or <code>brew install --cask codex</code></p>
<p align="center"><code>curl -fsSL https://chatgpt.com/codex/install.sh | sh</code><br />or <code>npm i -g @openai/codex</code><br />or <code>brew install --cask codex</code></p>
<p align="center"><strong>Codex CLI</strong> is a coding agent from OpenAI that runs locally on your computer.
<p align="center">
<img src="https://github.com/openai/codex/blob/main/.github/codex-cli-splash.png" alt="Codex CLI splash" width="80%" />
@@ -14,7 +14,19 @@ If you want Codex in your code editor (VS Code, Cursor, Windsurf), <a href="http
### Installing and running Codex CLI
Install globally with your preferred package manager:
Install the latest Codex release directly:
```shell
# Install on macOS, Linux, or WSL
curl -fsSL https://chatgpt.com/codex/install.sh | sh
```
```powershell
# Install on Windows
powershell -c "irm https://chatgpt.com/codex/install.ps1|iex"
```
You can also install with your preferred package manager:
```shell
# Install using npm

335
codex-rs/Cargo.lock generated
View File

@@ -291,6 +291,28 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "alsa"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43"
dependencies = [
"alsa-sys",
"bitflags 2.10.0",
"cfg-if",
"libc",
]
[[package]]
name = "alsa-sys"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
dependencies = [
"libc",
"pkg-config",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -861,6 +883,24 @@ dependencies = [
"serde",
]
[[package]]
name = "bindgen"
version = "0.72.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [
"bitflags 2.10.0",
"cexpr",
"clang-sys",
"itertools 0.13.0",
"proc-macro2",
"quote",
"regex",
"rustc-hash 2.1.1",
"shlex",
"syn 2.0.114",
]
[[package]]
name = "bit-set"
version = "0.5.3"
@@ -1098,6 +1138,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom 7.1.3",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
@@ -1182,6 +1231,17 @@ dependencies = [
"zeroize",
]
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "clap"
version = "4.5.58"
@@ -1319,6 +1379,7 @@ dependencies = [
"codex-protocol",
"codex-rmcp-client",
"codex-shell-command",
"codex-state",
"codex-utils-absolute-path",
"codex-utils-cargo-bin",
"codex-utils-cli",
@@ -1408,6 +1469,7 @@ dependencies = [
"anyhow",
"codex-apply-patch",
"codex-linux-sandbox",
"codex-shell-escalation",
"codex-utils-home-dir",
"dotenvy",
"tempfile",
@@ -1653,17 +1715,21 @@ dependencies = [
"codex-rmcp-client",
"codex-secrets",
"codex-shell-command",
"codex-shell-escalation",
"codex-skills",
"codex-state",
"codex-test-macros",
"codex-utils-absolute-path",
"codex-utils-cargo-bin",
"codex-utils-home-dir",
"codex-utils-pty",
"codex-utils-readiness",
"codex-utils-stream-parser",
"codex-utils-string",
"codex-windows-sandbox",
"core-foundation 0.9.4",
"core_test_support",
"csv",
"ctor 0.6.3",
"dirs",
"dunce",
@@ -1674,7 +1740,6 @@ dependencies = [
"http 1.4.0",
"image",
"indexmap 2.13.0",
"indoc",
"insta",
"keyring",
"landlock",
@@ -1775,34 +1840,6 @@ dependencies = [
"wiremock",
]
[[package]]
name = "codex-exec-server"
version = "0.0.0"
dependencies = [
"anyhow",
"async-trait",
"clap",
"codex-core",
"codex-execpolicy",
"codex-protocol",
"codex-shell-command",
"codex-shell-escalation",
"codex-utils-cargo-bin",
"core_test_support",
"exec_server_test_support",
"maplit",
"pretty_assertions",
"rmcp",
"schemars 1.2.1",
"serde",
"serde_json",
"shlex",
"tempfile",
"tokio",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "codex-execpolicy"
version = "0.0.0"
@@ -2006,8 +2043,10 @@ version = "0.0.0"
dependencies = [
"anyhow",
"async-trait",
"chrono",
"clap",
"codex-utils-absolute-path",
"codex-utils-home-dir",
"codex-utils-rustls-provider",
"globset",
"pretty_assertions",
@@ -2057,6 +2096,7 @@ dependencies = [
"codex-utils-absolute-path",
"codex-utils-string",
"eventsource-stream",
"gethostname",
"http 1.4.0",
"opentelemetry",
"opentelemetry-appender-tracing",
@@ -2205,11 +2245,9 @@ version = "0.0.0"
dependencies = [
"anyhow",
"async-trait",
"codex-core",
"codex-execpolicy",
"codex-protocol",
"clap",
"codex-utils-absolute-path",
"libc",
"path-absolutize",
"pretty_assertions",
"serde",
"serde_json",
@@ -2218,6 +2256,7 @@ dependencies = [
"tokio",
"tokio-util",
"tracing",
"tracing-subscriber",
]
[[package]]
@@ -2263,6 +2302,15 @@ dependencies = [
"uds_windows",
]
[[package]]
name = "codex-test-macros"
version = "0.0.0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "codex-tui"
version = "0.0.0"
@@ -2300,11 +2348,13 @@ dependencies = [
"codex-utils-sleep-inhibitor",
"codex-windows-sandbox",
"color-eyre",
"cpal",
"crossterm",
"derive_more 2.1.1",
"diffy",
"dirs",
"dunce",
"hound",
"image",
"insta",
"itertools 0.14.0",
@@ -2495,7 +2545,16 @@ name = "codex-utils-sleep-inhibitor"
version = "0.0.0"
dependencies = [
"core-foundation 0.9.4",
"libc",
"tracing",
"windows-sys 0.61.2",
]
[[package]]
name = "codex-utils-stream-parser"
version = "0.0.0"
dependencies = [
"pretty_assertions",
]
[[package]]
@@ -2736,6 +2795,49 @@ dependencies = [
"zstd",
]
[[package]]
name = "coreaudio-rs"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace"
dependencies = [
"bitflags 1.3.2",
"core-foundation-sys",
"coreaudio-sys",
]
[[package]]
name = "coreaudio-sys"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6"
dependencies = [
"bindgen",
]
[[package]]
name = "cpal"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
dependencies = [
"alsa",
"core-foundation-sys",
"coreaudio-rs",
"dasp_sample",
"jni",
"js-sys",
"libc",
"mach2",
"ndk",
"ndk-context",
"oboe",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows 0.54.0",
]
[[package]]
name = "cpufeatures"
version = "0.2.17"
@@ -3036,6 +3138,12 @@ dependencies = [
"syn 2.0.114",
]
[[package]]
name = "dasp_sample"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
[[package]]
name = "data-encoding"
version = "2.10.0"
@@ -3582,19 +3690,6 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "exec_server_test_support"
version = "0.0.0"
dependencies = [
"anyhow",
"codex-core",
"codex-protocol",
"codex-utils-cargo-bin",
"rmcp",
"serde_json",
"tokio",
]
[[package]]
name = "eyre"
version = "0.6.12"
@@ -4074,6 +4169,12 @@ version = "0.32.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "globset"
version = "0.4.18"
@@ -4290,6 +4391,12 @@ dependencies = [
"windows-link",
]
[[package]]
name = "hound"
version = "3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f"
[[package]]
name = "http"
version = "0.2.12"
@@ -5135,9 +5242,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.180"
version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
[[package]]
name = "libdbus-sys"
@@ -5148,6 +5255,16 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "libloading"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
dependencies = [
"cfg-if",
"windows-link",
]
[[package]]
name = "libm"
version = "0.2.16"
@@ -5339,6 +5456,15 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "mach2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
dependencies = [
"libc",
]
[[package]]
name = "maplit"
version = "1.0.2"
@@ -5522,12 +5648,35 @@ dependencies = [
"tempfile",
]
[[package]]
name = "ndk"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7"
dependencies = [
"bitflags 2.10.0",
"jni-sys",
"log",
"ndk-sys",
"num_enum",
"thiserror 1.0.69",
]
[[package]]
name = "ndk-context"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
[[package]]
name = "ndk-sys"
version = "0.5.0+25.2.9519653"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691"
dependencies = [
"jni-sys",
]
[[package]]
name = "new_debug_unreachable"
version = "1.0.6"
@@ -5715,6 +5864,17 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
[[package]]
name = "num-derive"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "num-integer"
version = "0.1.46"
@@ -5766,6 +5926,28 @@ dependencies = [
"libc",
]
[[package]]
name = "num_enum"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c"
dependencies = [
"num_enum_derive",
"rustversion",
]
[[package]]
name = "num_enum_derive"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "num_threads"
version = "0.1.7"
@@ -5975,6 +6157,29 @@ dependencies = [
"memchr",
]
[[package]]
name = "oboe"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb"
dependencies = [
"jni",
"ndk",
"ndk-context",
"num-derive",
"num-traits",
"oboe-sys",
]
[[package]]
name = "oboe-sys"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d"
dependencies = [
"cc",
]
[[package]]
name = "oid-registry"
version = "0.8.1"
@@ -6231,9 +6436,9 @@ dependencies = [
[[package]]
name = "owo-colors"
version = "4.2.3"
version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52"
checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d"
dependencies = [
"supports-color 2.1.0",
"supports-color 3.0.2",
@@ -7403,6 +7608,7 @@ dependencies = [
"js-sys",
"log",
"mime",
"mime_guess",
"native-tls",
"percent-encoding",
"pin-project-lite",
@@ -10405,6 +10611,16 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.54.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49"
dependencies = [
"windows-core 0.54.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.58.0"
@@ -10436,6 +10652,16 @@ dependencies = [
"windows-core 0.62.2",
]
[[package]]
name = "windows-core"
version = "0.54.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65"
dependencies = [
"windows-result 0.1.2",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.58.0"
@@ -10544,6 +10770,15 @@ dependencies = [
"windows-strings 0.5.1",
]
[[package]]
name = "windows-result"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.2.0"

View File

@@ -23,7 +23,6 @@ members = [
"hooks",
"secrets",
"exec",
"exec-server",
"execpolicy",
"execpolicy-legacy",
"keyring-store",
@@ -59,10 +58,12 @@ members = [
"utils/approval-presets",
"utils/oss",
"utils/fuzzy-match",
"utils/stream-parser",
"codex-client",
"codex-api",
"state",
"codex-experimental-api-macros",
"test-macros",
]
resolver = "2"
@@ -117,6 +118,7 @@ codex-shell-command = { path = "shell-command" }
codex-shell-escalation = { path = "shell-escalation" }
codex-skills = { path = "skills" }
codex-state = { path = "state" }
codex-test-macros = { path = "test-macros" }
codex-stdio-to-uds = { path = "stdio-to-uds" }
codex-tui = { path = "tui" }
codex-utils-absolute-path = { path = "utils/absolute-path" }
@@ -136,9 +138,9 @@ codex-utils-rustls-provider = { path = "utils/rustls-provider" }
codex-utils-sandbox-summary = { path = "utils/sandbox-summary" }
codex-utils-sleep-inhibitor = { path = "utils/sleep-inhibitor" }
codex-utils-string = { path = "utils/string" }
codex-utils-stream-parser = { path = "utils/stream-parser" }
codex-windows-sandbox = { path = "windows-sandbox-rs" }
core_test_support = { path = "core/tests/common" }
exec_server_test_support = { path = "exec-server/tests/common" }
mcp_test_support = { path = "mcp-server/tests/common" }
# External
@@ -162,6 +164,7 @@ clap = "4"
clap_complete = "4"
color-eyre = "0.6.3"
crossbeam-channel = "0.5.15"
csv = "1.3.1"
crossterm = "0.28.1"
ctor = "0.6.3"
derive_more = "2"
@@ -175,6 +178,7 @@ env_logger = "0.11.9"
eventsource-stream = "0.2.3"
futures = { version = "0.3", default-features = false }
globset = "0.4"
gethostname = "1.1.0"
http = "1.3.1"
icu_decimal = "2.1"
icu_locale_core = "2.1"
@@ -183,14 +187,13 @@ ignore = "0.4.23"
image = { version = "^0.25.9", default-features = false }
include_dir = "0.7.4"
indexmap = "2.12.0"
indoc = "2.0"
insta = "1.46.3"
inventory = "0.3.19"
itertools = "0.14.0"
keyring = { version = "3.6", default-features = false }
landlock = "0.4.4"
lazy_static = "1"
libc = "0.2.177"
libc = "0.2.182"
log = "0.4"
lru = "0.16.3"
maplit = "1.0.2"
@@ -206,7 +209,7 @@ opentelemetry-otlp = "0.31.0"
opentelemetry-semantic-conventions = "0.31.0"
opentelemetry_sdk = "0.31.0"
os_info = "3.12.0"
owo-colors = "4.2.0"
owo-colors = "4.3.0"
path-absolutize = "3.1.1"
pathdiff = "0.2"
portable-pty = "0.9.0"

View File

@@ -4,14 +4,21 @@ We provide Codex CLI as a standalone, native executable to ensure a zero-depende
## Installing Codex
Today, the easiest way to install Codex is via `npm`:
Install the latest Codex release directly:
```shell
npm i -g @openai/codex
# macOS, Linux, or WSL
curl -fsSL https://chatgpt.com/codex/install.sh | sh
codex
```
You can also install via Homebrew (`brew install --cask codex`) or download a platform-specific release directly from our [GitHub Releases](https://github.com/openai/codex/releases).
```powershell
# Windows
powershell -c "irm https://chatgpt.com/codex/install.ps1|iex"
codex
```
You can also install via npm (`npm i -g @openai/codex`), Homebrew (`brew install --cask codex`), or download a platform-specific release directly from our [GitHub Releases](https://github.com/openai/codex/releases).
## Documentation quickstart

View File

@@ -1,6 +1,28 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"NetworkPolicyAmendment": {
"properties": {
"action": {
"$ref": "#/definitions/NetworkPolicyRuleAction"
},
"host": {
"type": "string"
}
},
"required": [
"action",
"host"
],
"type": "object"
},
"NetworkPolicyRuleAction": {
"enum": [
"allow",
"deny"
],
"type": "string"
},
"ReviewDecision": {
"description": "User's decision in response to an ExecApprovalRequest.",
"oneOf": [
@@ -43,6 +65,28 @@
],
"type": "string"
},
{
"additionalProperties": false,
"description": "User chose to persist a network policy rule (allow/deny) for future requests to the same host.",
"properties": {
"network_policy_amendment": {
"properties": {
"network_policy_amendment": {
"$ref": "#/definitions/NetworkPolicyAmendment"
}
},
"required": [
"network_policy_amendment"
],
"type": "object"
}
},
"required": [
"network_policy_amendment"
],
"title": "NetworkPolicyAmendmentReviewDecision",
"type": "object"
},
{
"description": "User has denied this command and the agent should not execute it, but it should continue the session and try something else.",
"enum": [

View File

@@ -376,6 +376,70 @@
},
"type": "object"
},
"ExternalAgentConfigDetectParams": {
"properties": {
"cwds": {
"description": "Zero or more working directories to include for repo-scoped detection.",
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"includeHome": {
"description": "If true, include detection under the user's home (~/.claude, ~/.codex, etc.).",
"type": "boolean"
}
},
"type": "object"
},
"ExternalAgentConfigImportParams": {
"properties": {
"migrationItems": {
"items": {
"$ref": "#/definitions/ExternalAgentConfigMigrationItem"
},
"type": "array"
}
},
"required": [
"migrationItems"
],
"type": "object"
},
"ExternalAgentConfigMigrationItem": {
"properties": {
"cwd": {
"description": "Null or empty means home-scoped migration; non-empty means repo-scoped migration.",
"type": [
"string",
"null"
]
},
"description": {
"type": "string"
},
"itemType": {
"$ref": "#/definitions/ExternalAgentConfigMigrationItemType"
}
},
"required": [
"description",
"itemType"
],
"type": "object"
},
"ExternalAgentConfigMigrationItemType": {
"enum": [
"AGENTS_MD",
"CONFIG",
"SKILLS",
"MCP_SERVER_CONFIG"
],
"type": "string"
},
"FeedbackUploadParams": {
"properties": {
"classification": {
@@ -1920,6 +1984,13 @@
"null"
]
},
"searchTerm": {
"description": "Optional substring filter for the extracted thread title.",
"type": [
"string",
"null"
]
},
"sortKey": {
"anyOf": [
{
@@ -2190,6 +2261,12 @@
"type": "null"
}
]
},
"serviceName": {
"type": [
"string",
"null"
]
}
},
"type": "object"
@@ -3390,6 +3467,54 @@
"title": "Config/readRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"externalAgentConfig/detect"
],
"title": "ExternalAgentConfig/detectRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ExternalAgentConfigDetectParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "ExternalAgentConfig/detectRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"externalAgentConfig/import"
],
"title": "ExternalAgentConfig/importRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ExternalAgentConfigImportParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "ExternalAgentConfig/importRequest",
"type": "object"
},
{
"properties": {
"id": {

View File

@@ -1,6 +1,97 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"AdditionalFileSystemPermissions": {
"properties": {
"read": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"write": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
}
},
"type": "object"
},
"AdditionalMacOsPermissions": {
"properties": {
"accessibility": {
"type": [
"boolean",
"null"
]
},
"automations": {
"anyOf": [
{
"$ref": "#/definitions/MacOsAutomationValue"
},
{
"type": "null"
}
]
},
"calendar": {
"type": [
"boolean",
"null"
]
},
"preferences": {
"anyOf": [
{
"$ref": "#/definitions/MacOsPreferencesValue"
},
{
"type": "null"
}
]
}
},
"type": "object"
},
"AdditionalPermissionProfile": {
"properties": {
"fileSystem": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalFileSystemPermissions"
},
{
"type": "null"
}
]
},
"macos": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalMacOsPermissions"
},
{
"type": "null"
}
]
},
"network": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"CommandAction": {
"oneOf": [
{
@@ -111,6 +202,29 @@
}
]
},
"MacOsAutomationValue": {
"anyOf": [
{
"type": "boolean"
},
{
"items": {
"type": "string"
},
"type": "array"
}
]
},
"MacOsPreferencesValue": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "string"
}
]
},
"NetworkApprovalContext": {
"properties": {
"host": {
@@ -134,6 +248,28 @@
"socks5Udp"
],
"type": "string"
},
"NetworkPolicyAmendment": {
"properties": {
"action": {
"$ref": "#/definitions/NetworkPolicyRuleAction"
},
"host": {
"type": "string"
}
},
"required": [
"action",
"host"
],
"type": "object"
},
"NetworkPolicyRuleAction": {
"enum": [
"allow",
"deny"
],
"type": "string"
}
},
"properties": {
@@ -180,7 +316,7 @@
"type": "null"
}
],
"description": "Optional context for managed-network approval prompts."
"description": "Optional context for a managed-network approval prompt."
},
"proposedExecpolicyAmendment": {
"description": "Optional proposed execpolicy amendment to allow similar commands without prompting.",
@@ -192,6 +328,16 @@
"null"
]
},
"proposedNetworkPolicyAmendments": {
"description": "Optional proposed network policy amendments (allow/deny host) for future requests.",
"items": {
"$ref": "#/definitions/NetworkPolicyAmendment"
},
"type": [
"array",
"null"
]
},
"reason": {
"description": "Optional explanatory reason (e.g. request for network access).",
"type": [

View File

@@ -42,6 +42,28 @@
"title": "AcceptWithExecpolicyAmendmentCommandExecutionApprovalDecision",
"type": "object"
},
{
"additionalProperties": false,
"description": "User chose a persistent network policy rule (allow/deny) for this host.",
"properties": {
"applyNetworkPolicyAmendment": {
"properties": {
"network_policy_amendment": {
"$ref": "#/definitions/NetworkPolicyAmendment"
}
},
"required": [
"network_policy_amendment"
],
"type": "object"
}
},
"required": [
"applyNetworkPolicyAmendment"
],
"title": "ApplyNetworkPolicyAmendmentCommandExecutionApprovalDecision",
"type": "object"
},
{
"description": "User denied the command. The agent will continue the turn.",
"enum": [
@@ -57,6 +79,28 @@
"type": "string"
}
]
},
"NetworkPolicyAmendment": {
"properties": {
"action": {
"$ref": "#/definitions/NetworkPolicyRuleAction"
},
"host": {
"type": "string"
}
},
"required": [
"action",
"host"
],
"type": "object"
},
"NetworkPolicyRuleAction": {
"enum": [
"allow",
"deny"
],
"type": "string"
}
},
"properties": {

View File

@@ -1613,6 +1613,17 @@
},
{
"properties": {
"additional_permissions": {
"anyOf": [
{
"$ref": "#/definitions/PermissionProfile"
},
{
"type": "null"
}
],
"description": "Optional additional filesystem permissions requested for this command."
},
"approval_id": {
"description": "Identifier for this specific approval callback.\n\nWhen absent, the approval is for the command item itself (`call_id`). This is present for subcommand approvals (via execve intercept).",
"type": [
@@ -1662,6 +1673,16 @@
"null"
]
},
"proposed_network_policy_amendments": {
"description": "Proposed network policy amendments (for example allow/deny this host in future).",
"items": {
"$ref": "#/definitions/NetworkPolicyAmendment"
},
"type": [
"array",
"null"
]
},
"reason": {
"description": "Optional human-readable reason for the approval (e.g. retry without sandbox).",
"type": [
@@ -1755,6 +1776,30 @@
"title": "DynamicToolCallRequestEventMsg",
"type": "object"
},
{
"properties": {
"item_id": {
"type": "string"
},
"skill_name": {
"type": "string"
},
"type": {
"enum": [
"skill_request_approval"
],
"title": "SkillRequestApprovalEventMsgType",
"type": "string"
}
},
"required": [
"item_id",
"skill_name",
"type"
],
"title": "SkillRequestApprovalEventMsg",
"type": "object"
},
{
"properties": {
"id": {
@@ -3268,6 +3313,29 @@
}
]
},
"FileSystemPermissions": {
"properties": {
"read": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"write": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
}
},
"type": "object"
},
"FunctionCallOutputBody": {
"anyOf": [
{
@@ -3461,6 +3529,66 @@
],
"type": "string"
},
"MacOsAutomationValue": {
"anyOf": [
{
"type": "boolean"
},
{
"items": {
"type": "string"
},
"type": "array"
}
]
},
"MacOsPermissions": {
"properties": {
"accessibility": {
"type": [
"boolean",
"null"
]
},
"automations": {
"anyOf": [
{
"$ref": "#/definitions/MacOsAutomationValue"
},
{
"type": "null"
}
]
},
"calendar": {
"type": [
"boolean",
"null"
]
},
"preferences": {
"anyOf": [
{
"$ref": "#/definitions/MacOsPreferencesValue"
},
{
"type": "null"
}
]
}
},
"type": "object"
},
"MacOsPreferencesValue": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "string"
}
]
},
"McpAuthStatus": {
"enum": [
"unsupported",
@@ -3637,6 +3765,28 @@
],
"type": "string"
},
"NetworkPolicyAmendment": {
"properties": {
"action": {
"$ref": "#/definitions/NetworkPolicyRuleAction"
},
"host": {
"type": "string"
}
},
"required": [
"action",
"host"
],
"type": "object"
},
"NetworkPolicyRuleAction": {
"enum": [
"allow",
"deny"
],
"type": "string"
},
"ParsedCommand": {
"oneOf": [
{
@@ -3756,6 +3906,37 @@
],
"type": "string"
},
"PermissionProfile": {
"properties": {
"file_system": {
"anyOf": [
{
"$ref": "#/definitions/FileSystemPermissions"
},
{
"type": "null"
}
]
},
"macos": {
"anyOf": [
{
"$ref": "#/definitions/MacOsPermissions"
},
{
"type": "null"
}
]
},
"network": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"PlanItemArg": {
"additionalProperties": false,
"properties": {
@@ -6858,6 +7039,17 @@
},
{
"properties": {
"additional_permissions": {
"anyOf": [
{
"$ref": "#/definitions/PermissionProfile"
},
{
"type": "null"
}
],
"description": "Optional additional filesystem permissions requested for this command."
},
"approval_id": {
"description": "Identifier for this specific approval callback.\n\nWhen absent, the approval is for the command item itself (`call_id`). This is present for subcommand approvals (via execve intercept).",
"type": [
@@ -6907,6 +7099,16 @@
"null"
]
},
"proposed_network_policy_amendments": {
"description": "Proposed network policy amendments (for example allow/deny this host in future).",
"items": {
"$ref": "#/definitions/NetworkPolicyAmendment"
},
"type": [
"array",
"null"
]
},
"reason": {
"description": "Optional human-readable reason for the approval (e.g. retry without sandbox).",
"type": [
@@ -7000,6 +7202,30 @@
"title": "DynamicToolCallRequestEventMsg",
"type": "object"
},
{
"properties": {
"item_id": {
"type": "string"
},
"skill_name": {
"type": "string"
},
"type": {
"enum": [
"skill_request_approval"
],
"title": "SkillRequestApprovalEventMsgType",
"type": "string"
}
},
"required": [
"item_id",
"skill_name",
"type"
],
"title": "SkillRequestApprovalEventMsg",
"type": "object"
},
{
"properties": {
"id": {

View File

@@ -1,6 +1,28 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"NetworkPolicyAmendment": {
"properties": {
"action": {
"$ref": "#/definitions/NetworkPolicyRuleAction"
},
"host": {
"type": "string"
}
},
"required": [
"action",
"host"
],
"type": "object"
},
"NetworkPolicyRuleAction": {
"enum": [
"allow",
"deny"
],
"type": "string"
},
"ReviewDecision": {
"description": "User's decision in response to an ExecApprovalRequest.",
"oneOf": [
@@ -43,6 +65,28 @@
],
"type": "string"
},
{
"additionalProperties": false,
"description": "User chose to persist a network policy rule (allow/deny) for future requests to the same host.",
"properties": {
"network_policy_amendment": {
"properties": {
"network_policy_amendment": {
"$ref": "#/definitions/NetworkPolicyAmendment"
}
},
"required": [
"network_policy_amendment"
],
"type": "object"
}
},
"required": [
"network_policy_amendment"
],
"title": "NetworkPolicyAmendmentReviewDecision",
"type": "object"
},
{
"description": "User has denied this command and the agent should not execute it, but it should continue the session and try something else.",
"enum": [

View File

@@ -1,6 +1,97 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"AdditionalFileSystemPermissions": {
"properties": {
"read": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"write": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
}
},
"type": "object"
},
"AdditionalMacOsPermissions": {
"properties": {
"accessibility": {
"type": [
"boolean",
"null"
]
},
"automations": {
"anyOf": [
{
"$ref": "#/definitions/MacOsAutomationValue"
},
{
"type": "null"
}
]
},
"calendar": {
"type": [
"boolean",
"null"
]
},
"preferences": {
"anyOf": [
{
"$ref": "#/definitions/MacOsPreferencesValue"
},
{
"type": "null"
}
]
}
},
"type": "object"
},
"AdditionalPermissionProfile": {
"properties": {
"fileSystem": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalFileSystemPermissions"
},
{
"type": "null"
}
]
},
"macos": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalMacOsPermissions"
},
{
"type": "null"
}
]
},
"network": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"ApplyPatchApprovalParams": {
"properties": {
"callId": {
@@ -222,7 +313,7 @@
"type": "null"
}
],
"description": "Optional context for managed-network approval prompts."
"description": "Optional context for a managed-network approval prompt."
},
"proposedExecpolicyAmendment": {
"description": "Optional proposed execpolicy amendment to allow similar commands without prompting.",
@@ -234,6 +325,16 @@
"null"
]
},
"proposedNetworkPolicyAmendments": {
"description": "Optional proposed network policy amendments (allow/deny host) for future requests.",
"items": {
"$ref": "#/definitions/NetworkPolicyAmendment"
},
"type": [
"array",
"null"
]
},
"reason": {
"description": "Optional explanatory reason (e.g. request for network access).",
"type": [
@@ -430,6 +531,29 @@
],
"type": "object"
},
"MacOsAutomationValue": {
"anyOf": [
{
"type": "boolean"
},
{
"items": {
"type": "string"
},
"type": "array"
}
]
},
"MacOsPreferencesValue": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "string"
}
]
},
"NetworkApprovalContext": {
"properties": {
"host": {
@@ -454,6 +578,28 @@
],
"type": "string"
},
"NetworkPolicyAmendment": {
"properties": {
"action": {
"$ref": "#/definitions/NetworkPolicyRuleAction"
},
"host": {
"type": "string"
}
},
"required": [
"action",
"host"
],
"type": "object"
},
"NetworkPolicyRuleAction": {
"enum": [
"allow",
"deny"
],
"type": "string"
},
"ParsedCommand": {
"oneOf": [
{
@@ -576,6 +722,21 @@
}
]
},
"SkillRequestApprovalParams": {
"properties": {
"itemId": {
"type": "string"
},
"skillName": {
"type": "string"
}
},
"required": [
"itemId",
"skillName"
],
"type": "object"
},
"ThreadId": {
"type": "string"
},
@@ -737,6 +898,30 @@
"title": "Item/tool/requestUserInputRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"skill/requestApproval"
],
"title": "Skill/requestApprovalRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/SkillRequestApprovalParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Skill/requestApprovalRequest",
"type": "object"
},
{
"description": "Execute a dynamic tool call on the client.",
"properties": {

View File

@@ -0,0 +1,17 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"itemId": {
"type": "string"
},
"skillName": {
"type": "string"
}
},
"required": [
"itemId",
"skillName"
],
"title": "SkillRequestApprovalParams",
"type": "object"
}

View File

@@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"SkillApprovalDecision": {
"enum": [
"approve",
"decline"
],
"type": "string"
}
},
"properties": {
"decision": {
"$ref": "#/definitions/SkillApprovalDecision"
}
},
"required": [
"decision"
],
"title": "SkillRequestApprovalResponse",
"type": "object"
}

View File

@@ -1,6 +1,97 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"AdditionalFileSystemPermissions": {
"properties": {
"read": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"write": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
}
},
"type": "object"
},
"AdditionalMacOsPermissions": {
"properties": {
"accessibility": {
"type": [
"boolean",
"null"
]
},
"automations": {
"anyOf": [
{
"$ref": "#/definitions/MacOsAutomationValue"
},
{
"type": "null"
}
]
},
"calendar": {
"type": [
"boolean",
"null"
]
},
"preferences": {
"anyOf": [
{
"$ref": "#/definitions/MacOsPreferencesValue"
},
{
"type": "null"
}
]
}
},
"type": "object"
},
"AdditionalPermissionProfile": {
"properties": {
"fileSystem": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalFileSystemPermissions"
},
{
"type": "null"
}
]
},
"macos": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalMacOsPermissions"
},
{
"type": "null"
}
]
},
"network": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"AgentMessageContent": {
"oneOf": [
{
@@ -1078,6 +1169,54 @@
"title": "Config/readRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"externalAgentConfig/detect"
],
"title": "ExternalAgentConfig/detectRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/ExternalAgentConfigDetectParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "ExternalAgentConfig/detectRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"externalAgentConfig/import"
],
"title": "ExternalAgentConfig/importRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/ExternalAgentConfigImportParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "ExternalAgentConfig/importRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -1310,6 +1449,28 @@
"title": "AcceptWithExecpolicyAmendmentCommandExecutionApprovalDecision",
"type": "object"
},
{
"additionalProperties": false,
"description": "User chose a persistent network policy rule (allow/deny) for this host.",
"properties": {
"applyNetworkPolicyAmendment": {
"properties": {
"network_policy_amendment": {
"$ref": "#/definitions/NetworkPolicyAmendment"
}
},
"required": [
"network_policy_amendment"
],
"type": "object"
}
},
"required": [
"applyNetworkPolicyAmendment"
],
"title": "ApplyNetworkPolicyAmendmentCommandExecutionApprovalDecision",
"type": "object"
},
{
"description": "User denied the command. The agent will continue the turn.",
"enum": [
@@ -1372,7 +1533,7 @@
"type": "null"
}
],
"description": "Optional context for managed-network approval prompts."
"description": "Optional context for a managed-network approval prompt."
},
"proposedExecpolicyAmendment": {
"description": "Optional proposed execpolicy amendment to allow similar commands without prompting.",
@@ -1384,6 +1545,16 @@
"null"
]
},
"proposedNetworkPolicyAmendments": {
"description": "Optional proposed network policy amendments (allow/deny host) for future requests.",
"items": {
"$ref": "#/definitions/NetworkPolicyAmendment"
},
"type": [
"array",
"null"
]
},
"reason": {
"description": "Optional explanatory reason (e.g. request for network access).",
"type": [
@@ -2674,6 +2845,17 @@
},
{
"properties": {
"additional_permissions": {
"anyOf": [
{
"$ref": "#/definitions/PermissionProfile"
},
{
"type": "null"
}
],
"description": "Optional additional filesystem permissions requested for this command."
},
"approval_id": {
"description": "Identifier for this specific approval callback.\n\nWhen absent, the approval is for the command item itself (`call_id`). This is present for subcommand approvals (via execve intercept).",
"type": [
@@ -2723,6 +2905,16 @@
"null"
]
},
"proposed_network_policy_amendments": {
"description": "Proposed network policy amendments (for example allow/deny this host in future).",
"items": {
"$ref": "#/definitions/NetworkPolicyAmendment"
},
"type": [
"array",
"null"
]
},
"reason": {
"description": "Optional human-readable reason for the approval (e.g. retry without sandbox).",
"type": [
@@ -2816,6 +3008,30 @@
"title": "DynamicToolCallRequestEventMsg",
"type": "object"
},
{
"properties": {
"item_id": {
"type": "string"
},
"skill_name": {
"type": "string"
},
"type": {
"enum": [
"skill_request_approval"
],
"title": "SkillRequestApprovalEventMsgType",
"type": "string"
}
},
"required": [
"item_id",
"skill_name",
"type"
],
"title": "SkillRequestApprovalEventMsg",
"type": "object"
},
{
"properties": {
"id": {
@@ -4472,6 +4688,29 @@
"title": "FileChangeRequestApprovalResponse",
"type": "object"
},
"FileSystemPermissions": {
"properties": {
"read": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"write": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
}
},
"type": "object"
},
"FuzzyFileSearchParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -4772,6 +5011,66 @@
"title": "JSONRPCResponse",
"type": "object"
},
"MacOsAutomationValue": {
"anyOf": [
{
"type": "boolean"
},
{
"items": {
"type": "string"
},
"type": "array"
}
]
},
"MacOsPermissions": {
"properties": {
"accessibility": {
"type": [
"boolean",
"null"
]
},
"automations": {
"anyOf": [
{
"$ref": "#/definitions/MacOsAutomationValue"
},
{
"type": "null"
}
]
},
"calendar": {
"type": [
"boolean",
"null"
]
},
"preferences": {
"anyOf": [
{
"$ref": "#/definitions/MacOsPreferencesValue"
},
{
"type": "null"
}
]
}
},
"type": "object"
},
"MacOsPreferencesValue": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "string"
}
]
},
"McpInvocation": {
"properties": {
"arguments": {
@@ -4898,6 +5197,28 @@
],
"type": "string"
},
"NetworkPolicyAmendment": {
"properties": {
"action": {
"$ref": "#/definitions/NetworkPolicyRuleAction"
},
"host": {
"type": "string"
}
},
"required": [
"action",
"host"
],
"type": "object"
},
"NetworkPolicyRuleAction": {
"enum": [
"allow",
"deny"
],
"type": "string"
},
"ParsedCommand": {
"oneOf": [
{
@@ -5009,6 +5330,37 @@
}
]
},
"PermissionProfile": {
"properties": {
"file_system": {
"anyOf": [
{
"$ref": "#/definitions/FileSystemPermissions"
},
{
"type": "null"
}
]
},
"macos": {
"anyOf": [
{
"$ref": "#/definitions/MacOsPermissions"
},
{
"type": "null"
}
]
},
"network": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"PlanItemArg": {
"additionalProperties": false,
"properties": {
@@ -5310,6 +5662,28 @@
],
"type": "string"
},
{
"additionalProperties": false,
"description": "User chose to persist a network policy rule (allow/deny) for future requests to the same host.",
"properties": {
"network_policy_amendment": {
"properties": {
"network_policy_amendment": {
"$ref": "#/definitions/NetworkPolicyAmendment"
}
},
"required": [
"network_policy_amendment"
],
"type": "object"
}
},
"required": [
"network_policy_amendment"
],
"title": "NetworkPolicyAmendmentReviewDecision",
"type": "object"
},
{
"description": "User has denied this command and the agent should not execute it, but it should continue the session and try something else.",
"enum": [
@@ -6194,6 +6568,30 @@
"title": "Item/tool/requestUserInputRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"skill/requestApproval"
],
"title": "Skill/requestApprovalRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/SkillRequestApprovalParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Skill/requestApprovalRequest",
"type": "object"
},
{
"description": "Execute a dynamic tool call on the client.",
"properties": {
@@ -6315,6 +6713,43 @@
],
"type": "object"
},
"SkillApprovalDecision": {
"enum": [
"approve",
"decline"
],
"type": "string"
},
"SkillRequestApprovalParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"itemId": {
"type": "string"
},
"skillName": {
"type": "string"
}
},
"required": [
"itemId",
"skillName"
],
"title": "SkillRequestApprovalParams",
"type": "object"
},
"SkillRequestApprovalResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"decision": {
"$ref": "#/definitions/SkillApprovalDecision"
}
},
"required": [
"decision"
],
"title": "SkillRequestApprovalResponse",
"type": "object"
},
"StepStatus": {
"enum": [
"pending",
@@ -8738,6 +9173,95 @@
}
]
},
"ExternalAgentConfigDetectParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"cwds": {
"description": "Zero or more working directories to include for repo-scoped detection.",
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"includeHome": {
"description": "If true, include detection under the user's home (~/.claude, ~/.codex, etc.).",
"type": "boolean"
}
},
"title": "ExternalAgentConfigDetectParams",
"type": "object"
},
"ExternalAgentConfigDetectResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"items": {
"items": {
"$ref": "#/definitions/v2/ExternalAgentConfigMigrationItem"
},
"type": "array"
}
},
"required": [
"items"
],
"title": "ExternalAgentConfigDetectResponse",
"type": "object"
},
"ExternalAgentConfigImportParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"migrationItems": {
"items": {
"$ref": "#/definitions/v2/ExternalAgentConfigMigrationItem"
},
"type": "array"
}
},
"required": [
"migrationItems"
],
"title": "ExternalAgentConfigImportParams",
"type": "object"
},
"ExternalAgentConfigImportResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ExternalAgentConfigImportResponse",
"type": "object"
},
"ExternalAgentConfigMigrationItem": {
"properties": {
"cwd": {
"description": "Null or empty means home-scoped migration; non-empty means repo-scoped migration.",
"type": [
"string",
"null"
]
},
"description": {
"type": "string"
},
"itemType": {
"$ref": "#/definitions/v2/ExternalAgentConfigMigrationItemType"
}
},
"required": [
"description",
"itemType"
],
"type": "object"
},
"ExternalAgentConfigMigrationItemType": {
"enum": [
"AGENTS_MD",
"CONFIG",
"SKILLS",
"MCP_SERVER_CONFIG"
],
"type": "string"
},
"FeedbackUploadParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -12509,6 +13033,13 @@
"null"
]
},
"searchTerm": {
"description": "Optional substring filter for the extracted thread title.",
"type": [
"string",
"null"
]
},
"sortKey": {
"anyOf": [
{
@@ -12936,6 +13467,12 @@
"type": "null"
}
]
},
"serviceName": {
"type": [
"string",
"null"
]
}
},
"title": "ThreadStartParams",

View File

@@ -0,0 +1,21 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"cwds": {
"description": "Zero or more working directories to include for repo-scoped detection.",
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"includeHome": {
"description": "If true, include detection under the user's home (~/.claude, ~/.codex, etc.).",
"type": "boolean"
}
},
"title": "ExternalAgentConfigDetectParams",
"type": "object"
}

View File

@@ -0,0 +1,49 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ExternalAgentConfigMigrationItem": {
"properties": {
"cwd": {
"description": "Null or empty means home-scoped migration; non-empty means repo-scoped migration.",
"type": [
"string",
"null"
]
},
"description": {
"type": "string"
},
"itemType": {
"$ref": "#/definitions/ExternalAgentConfigMigrationItemType"
}
},
"required": [
"description",
"itemType"
],
"type": "object"
},
"ExternalAgentConfigMigrationItemType": {
"enum": [
"AGENTS_MD",
"CONFIG",
"SKILLS",
"MCP_SERVER_CONFIG"
],
"type": "string"
}
},
"properties": {
"items": {
"items": {
"$ref": "#/definitions/ExternalAgentConfigMigrationItem"
},
"type": "array"
}
},
"required": [
"items"
],
"title": "ExternalAgentConfigDetectResponse",
"type": "object"
}

View File

@@ -0,0 +1,49 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ExternalAgentConfigMigrationItem": {
"properties": {
"cwd": {
"description": "Null or empty means home-scoped migration; non-empty means repo-scoped migration.",
"type": [
"string",
"null"
]
},
"description": {
"type": "string"
},
"itemType": {
"$ref": "#/definitions/ExternalAgentConfigMigrationItemType"
}
},
"required": [
"description",
"itemType"
],
"type": "object"
},
"ExternalAgentConfigMigrationItemType": {
"enum": [
"AGENTS_MD",
"CONFIG",
"SKILLS",
"MCP_SERVER_CONFIG"
],
"type": "string"
}
},
"properties": {
"migrationItems": {
"items": {
"$ref": "#/definitions/ExternalAgentConfigMigrationItem"
},
"type": "array"
}
},
"required": [
"migrationItems"
],
"title": "ExternalAgentConfigImportParams",
"type": "object"
}

View File

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

View File

@@ -65,6 +65,13 @@
"null"
]
},
"searchTerm": {
"description": "Optional substring filter for the extracted thread title.",
"type": [
"string",
"null"
]
},
"sortKey": {
"anyOf": [
{

View File

@@ -150,6 +150,12 @@
"type": "null"
}
]
},
"serviceName": {
"type": [
"string",
"null"
]
}
},
"title": "ThreadStartParams",

File diff suppressed because one or more lines are too long

View File

@@ -56,6 +56,7 @@ import type { RemoteSkillDownloadedEvent } from "./RemoteSkillDownloadedEvent";
import type { RequestUserInputEvent } from "./RequestUserInputEvent";
import type { ReviewRequest } from "./ReviewRequest";
import type { SessionConfiguredEvent } from "./SessionConfiguredEvent";
import type { SkillRequestApprovalEvent } from "./SkillRequestApprovalEvent";
import type { StreamErrorEvent } from "./StreamErrorEvent";
import type { TerminalInteractionEvent } from "./TerminalInteractionEvent";
import type { ThreadNameUpdatedEvent } from "./ThreadNameUpdatedEvent";
@@ -78,4 +79,4 @@ import type { WebSearchEndEvent } from "./WebSearchEndEvent";
* Response event from the agent
* NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen.
*/
export type EventMsg = { "type": "error" } & ErrorEvent | { "type": "warning" } & WarningEvent | { "type": "realtime_conversation_started" } & RealtimeConversationStartedEvent | { "type": "realtime_conversation_realtime" } & RealtimeConversationRealtimeEvent | { "type": "realtime_conversation_closed" } & RealtimeConversationClosedEvent | { "type": "model_reroute" } & ModelRerouteEvent | { "type": "context_compacted" } & ContextCompactedEvent | { "type": "thread_rolled_back" } & ThreadRolledBackEvent | { "type": "task_started" } & TurnStartedEvent | { "type": "task_complete" } & TurnCompleteEvent | { "type": "token_count" } & TokenCountEvent | { "type": "agent_message" } & AgentMessageEvent | { "type": "user_message" } & UserMessageEvent | { "type": "agent_message_delta" } & AgentMessageDeltaEvent | { "type": "agent_reasoning" } & AgentReasoningEvent | { "type": "agent_reasoning_delta" } & AgentReasoningDeltaEvent | { "type": "agent_reasoning_raw_content" } & AgentReasoningRawContentEvent | { "type": "agent_reasoning_raw_content_delta" } & AgentReasoningRawContentDeltaEvent | { "type": "agent_reasoning_section_break" } & AgentReasoningSectionBreakEvent | { "type": "session_configured" } & SessionConfiguredEvent | { "type": "thread_name_updated" } & ThreadNameUpdatedEvent | { "type": "mcp_startup_update" } & McpStartupUpdateEvent | { "type": "mcp_startup_complete" } & McpStartupCompleteEvent | { "type": "mcp_tool_call_begin" } & McpToolCallBeginEvent | { "type": "mcp_tool_call_end" } & McpToolCallEndEvent | { "type": "web_search_begin" } & WebSearchBeginEvent | { "type": "web_search_end" } & WebSearchEndEvent | { "type": "exec_command_begin" } & ExecCommandBeginEvent | { "type": "exec_command_output_delta" } & ExecCommandOutputDeltaEvent | { "type": "terminal_interaction" } & TerminalInteractionEvent | { "type": "exec_command_end" } & ExecCommandEndEvent | { "type": "view_image_tool_call" } & ViewImageToolCallEvent | { "type": "exec_approval_request" } & ExecApprovalRequestEvent | { "type": "request_user_input" } & RequestUserInputEvent | { "type": "dynamic_tool_call_request" } & DynamicToolCallRequest | { "type": "elicitation_request" } & ElicitationRequestEvent | { "type": "apply_patch_approval_request" } & ApplyPatchApprovalRequestEvent | { "type": "deprecation_notice" } & DeprecationNoticeEvent | { "type": "background_event" } & BackgroundEventEvent | { "type": "undo_started" } & UndoStartedEvent | { "type": "undo_completed" } & UndoCompletedEvent | { "type": "stream_error" } & StreamErrorEvent | { "type": "patch_apply_begin" } & PatchApplyBeginEvent | { "type": "patch_apply_end" } & PatchApplyEndEvent | { "type": "turn_diff" } & TurnDiffEvent | { "type": "get_history_entry_response" } & GetHistoryEntryResponseEvent | { "type": "mcp_list_tools_response" } & McpListToolsResponseEvent | { "type": "list_custom_prompts_response" } & ListCustomPromptsResponseEvent | { "type": "list_skills_response" } & ListSkillsResponseEvent | { "type": "list_remote_skills_response" } & ListRemoteSkillsResponseEvent | { "type": "remote_skill_downloaded" } & RemoteSkillDownloadedEvent | { "type": "skills_update_available" } | { "type": "plan_update" } & UpdatePlanArgs | { "type": "turn_aborted" } & TurnAbortedEvent | { "type": "shutdown_complete" } | { "type": "entered_review_mode" } & ReviewRequest | { "type": "exited_review_mode" } & ExitedReviewModeEvent | { "type": "raw_response_item" } & RawResponseItemEvent | { "type": "item_started" } & ItemStartedEvent | { "type": "item_completed" } & ItemCompletedEvent | { "type": "agent_message_content_delta" } & AgentMessageContentDeltaEvent | { "type": "plan_delta" } & PlanDeltaEvent | { "type": "reasoning_content_delta" } & ReasoningContentDeltaEvent | { "type": "reasoning_raw_content_delta" } & ReasoningRawContentDeltaEvent | { "type": "collab_agent_spawn_begin" } & CollabAgentSpawnBeginEvent | { "type": "collab_agent_spawn_end" } & CollabAgentSpawnEndEvent | { "type": "collab_agent_interaction_begin" } & CollabAgentInteractionBeginEvent | { "type": "collab_agent_interaction_end" } & CollabAgentInteractionEndEvent | { "type": "collab_waiting_begin" } & CollabWaitingBeginEvent | { "type": "collab_waiting_end" } & CollabWaitingEndEvent | { "type": "collab_close_begin" } & CollabCloseBeginEvent | { "type": "collab_close_end" } & CollabCloseEndEvent | { "type": "collab_resume_begin" } & CollabResumeBeginEvent | { "type": "collab_resume_end" } & CollabResumeEndEvent;
export type EventMsg = { "type": "error" } & ErrorEvent | { "type": "warning" } & WarningEvent | { "type": "realtime_conversation_started" } & RealtimeConversationStartedEvent | { "type": "realtime_conversation_realtime" } & RealtimeConversationRealtimeEvent | { "type": "realtime_conversation_closed" } & RealtimeConversationClosedEvent | { "type": "model_reroute" } & ModelRerouteEvent | { "type": "context_compacted" } & ContextCompactedEvent | { "type": "thread_rolled_back" } & ThreadRolledBackEvent | { "type": "task_started" } & TurnStartedEvent | { "type": "task_complete" } & TurnCompleteEvent | { "type": "token_count" } & TokenCountEvent | { "type": "agent_message" } & AgentMessageEvent | { "type": "user_message" } & UserMessageEvent | { "type": "agent_message_delta" } & AgentMessageDeltaEvent | { "type": "agent_reasoning" } & AgentReasoningEvent | { "type": "agent_reasoning_delta" } & AgentReasoningDeltaEvent | { "type": "agent_reasoning_raw_content" } & AgentReasoningRawContentEvent | { "type": "agent_reasoning_raw_content_delta" } & AgentReasoningRawContentDeltaEvent | { "type": "agent_reasoning_section_break" } & AgentReasoningSectionBreakEvent | { "type": "session_configured" } & SessionConfiguredEvent | { "type": "thread_name_updated" } & ThreadNameUpdatedEvent | { "type": "mcp_startup_update" } & McpStartupUpdateEvent | { "type": "mcp_startup_complete" } & McpStartupCompleteEvent | { "type": "mcp_tool_call_begin" } & McpToolCallBeginEvent | { "type": "mcp_tool_call_end" } & McpToolCallEndEvent | { "type": "web_search_begin" } & WebSearchBeginEvent | { "type": "web_search_end" } & WebSearchEndEvent | { "type": "exec_command_begin" } & ExecCommandBeginEvent | { "type": "exec_command_output_delta" } & ExecCommandOutputDeltaEvent | { "type": "terminal_interaction" } & TerminalInteractionEvent | { "type": "exec_command_end" } & ExecCommandEndEvent | { "type": "view_image_tool_call" } & ViewImageToolCallEvent | { "type": "exec_approval_request" } & ExecApprovalRequestEvent | { "type": "request_user_input" } & RequestUserInputEvent | { "type": "dynamic_tool_call_request" } & DynamicToolCallRequest | { "type": "skill_request_approval" } & SkillRequestApprovalEvent | { "type": "elicitation_request" } & ElicitationRequestEvent | { "type": "apply_patch_approval_request" } & ApplyPatchApprovalRequestEvent | { "type": "deprecation_notice" } & DeprecationNoticeEvent | { "type": "background_event" } & BackgroundEventEvent | { "type": "undo_started" } & UndoStartedEvent | { "type": "undo_completed" } & UndoCompletedEvent | { "type": "stream_error" } & StreamErrorEvent | { "type": "patch_apply_begin" } & PatchApplyBeginEvent | { "type": "patch_apply_end" } & PatchApplyEndEvent | { "type": "turn_diff" } & TurnDiffEvent | { "type": "get_history_entry_response" } & GetHistoryEntryResponseEvent | { "type": "mcp_list_tools_response" } & McpListToolsResponseEvent | { "type": "list_custom_prompts_response" } & ListCustomPromptsResponseEvent | { "type": "list_skills_response" } & ListSkillsResponseEvent | { "type": "list_remote_skills_response" } & ListRemoteSkillsResponseEvent | { "type": "remote_skill_downloaded" } & RemoteSkillDownloadedEvent | { "type": "skills_update_available" } | { "type": "plan_update" } & UpdatePlanArgs | { "type": "turn_aborted" } & TurnAbortedEvent | { "type": "shutdown_complete" } | { "type": "entered_review_mode" } & ReviewRequest | { "type": "exited_review_mode" } & ExitedReviewModeEvent | { "type": "raw_response_item" } & RawResponseItemEvent | { "type": "item_started" } & ItemStartedEvent | { "type": "item_completed" } & ItemCompletedEvent | { "type": "agent_message_content_delta" } & AgentMessageContentDeltaEvent | { "type": "plan_delta" } & PlanDeltaEvent | { "type": "reasoning_content_delta" } & ReasoningContentDeltaEvent | { "type": "reasoning_raw_content_delta" } & ReasoningRawContentDeltaEvent | { "type": "collab_agent_spawn_begin" } & CollabAgentSpawnBeginEvent | { "type": "collab_agent_spawn_end" } & CollabAgentSpawnEndEvent | { "type": "collab_agent_interaction_begin" } & CollabAgentInteractionBeginEvent | { "type": "collab_agent_interaction_end" } & CollabAgentInteractionEndEvent | { "type": "collab_waiting_begin" } & CollabWaitingBeginEvent | { "type": "collab_waiting_end" } & CollabWaitingEndEvent | { "type": "collab_close_begin" } & CollabCloseBeginEvent | { "type": "collab_close_end" } & CollabCloseEndEvent | { "type": "collab_resume_begin" } & CollabResumeBeginEvent | { "type": "collab_resume_end" } & CollabResumeEndEvent;

View File

@@ -3,7 +3,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ExecPolicyAmendment } from "./ExecPolicyAmendment";
import type { NetworkApprovalContext } from "./NetworkApprovalContext";
import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment";
import type { ParsedCommand } from "./ParsedCommand";
import type { PermissionProfile } from "./PermissionProfile";
export type ExecApprovalRequestEvent = {
/**
@@ -41,4 +43,12 @@ network_approval_context?: NetworkApprovalContext,
/**
* Proposed execpolicy amendment that can be applied to allow future runs.
*/
proposed_execpolicy_amendment?: ExecPolicyAmendment, parsed_cmd: Array<ParsedCommand>, };
proposed_execpolicy_amendment?: ExecPolicyAmendment,
/**
* Proposed network policy amendments (for example allow/deny this host in future).
*/
proposed_network_policy_amendments?: Array<NetworkPolicyAmendment>,
/**
* Optional additional filesystem permissions requested for this command.
*/
additional_permissions?: PermissionProfile, parsed_cmd: Array<ParsedCommand>, };

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { MacOsAutomationValue } from "./MacOsAutomationValue";
import type { MacOsPreferencesValue } from "./MacOsPreferencesValue";
export type MacOsPermissions = { preferences: MacOsPreferencesValue | null, automations: MacOsAutomationValue | null, accessibility: boolean | null, calendar: boolean | null, };

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { FileSystemPermissions } from "./FileSystemPermissions";
import type { MacOsPermissions } from "./MacOsPermissions";
export type PermissionProfile = { network: boolean | null, file_system: FileSystemPermissions | null, macos: MacOsPermissions | null, };

View File

@@ -2,8 +2,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ExecPolicyAmendment } from "./ExecPolicyAmendment";
import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment";
/**
* User's decision in response to an ExecApprovalRequest.
*/
export type ReviewDecision = "approved" | { "approved_execpolicy_amendment": { proposed_execpolicy_amendment: ExecPolicyAmendment, } } | "approved_for_session" | "denied" | "abort";
export type ReviewDecision = "approved" | { "approved_execpolicy_amendment": { proposed_execpolicy_amendment: ExecPolicyAmendment, } } | "approved_for_session" | { "network_policy_amendment": { network_policy_amendment: NetworkPolicyAmendment, } } | "denied" | "abort";

View File

@@ -8,9 +8,10 @@ import type { ChatgptAuthTokensRefreshParams } from "./v2/ChatgptAuthTokensRefre
import type { CommandExecutionRequestApprovalParams } from "./v2/CommandExecutionRequestApprovalParams";
import type { DynamicToolCallParams } from "./v2/DynamicToolCallParams";
import type { FileChangeRequestApprovalParams } from "./v2/FileChangeRequestApprovalParams";
import type { SkillRequestApprovalParams } from "./v2/SkillRequestApprovalParams";
import type { ToolRequestUserInputParams } from "./v2/ToolRequestUserInputParams";
/**
* Request initiated from the server and sent to the client.
*/
export type ServerRequest = { "method": "item/commandExecution/requestApproval", id: RequestId, params: CommandExecutionRequestApprovalParams, } | { "method": "item/fileChange/requestApproval", id: RequestId, params: FileChangeRequestApprovalParams, } | { "method": "item/tool/requestUserInput", id: RequestId, params: ToolRequestUserInputParams, } | { "method": "item/tool/call", id: RequestId, params: DynamicToolCallParams, } | { "method": "account/chatgptAuthTokens/refresh", id: RequestId, params: ChatgptAuthTokensRefreshParams, } | { "method": "applyPatchApproval", id: RequestId, params: ApplyPatchApprovalParams, } | { "method": "execCommandApproval", id: RequestId, params: ExecCommandApprovalParams, };
export type ServerRequest = { "method": "item/commandExecution/requestApproval", id: RequestId, params: CommandExecutionRequestApprovalParams, } | { "method": "item/fileChange/requestApproval", id: RequestId, params: FileChangeRequestApprovalParams, } | { "method": "item/tool/requestUserInput", id: RequestId, params: ToolRequestUserInputParams, } | { "method": "skill/requestApproval", id: RequestId, params: SkillRequestApprovalParams, } | { "method": "item/tool/call", id: RequestId, params: DynamicToolCallParams, } | { "method": "account/chatgptAuthTokens/refresh", id: RequestId, params: ChatgptAuthTokensRefreshParams, } | { "method": "applyPatchApproval", id: RequestId, params: ApplyPatchApprovalParams, } | { "method": "execCommandApproval", id: RequestId, params: ExecCommandApprovalParams, };

View File

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

View File

@@ -71,6 +71,7 @@ export type { ExecOutputStream } from "./ExecOutputStream";
export type { ExecPolicyAmendment } from "./ExecPolicyAmendment";
export type { ExitedReviewModeEvent } from "./ExitedReviewModeEvent";
export type { FileChange } from "./FileChange";
export type { FileSystemPermissions } from "./FileSystemPermissions";
export type { ForcedLoginMethod } from "./ForcedLoginMethod";
export type { ForkConversationParams } from "./ForkConversationParams";
export type { ForkConversationResponse } from "./ForkConversationResponse";
@@ -116,6 +117,9 @@ export type { LoginApiKeyResponse } from "./LoginApiKeyResponse";
export type { LoginChatGptCompleteNotification } from "./LoginChatGptCompleteNotification";
export type { LoginChatGptResponse } from "./LoginChatGptResponse";
export type { LogoutChatGptResponse } from "./LogoutChatGptResponse";
export type { MacOsAutomationValue } from "./MacOsAutomationValue";
export type { MacOsPermissions } from "./MacOsPermissions";
export type { MacOsPreferencesValue } from "./MacOsPreferencesValue";
export type { McpAuthStatus } from "./McpAuthStatus";
export type { McpInvocation } from "./McpInvocation";
export type { McpListToolsResponseEvent } from "./McpListToolsResponseEvent";
@@ -132,12 +136,15 @@ export type { ModelRerouteReason } from "./ModelRerouteReason";
export type { NetworkAccess } from "./NetworkAccess";
export type { NetworkApprovalContext } from "./NetworkApprovalContext";
export type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol";
export type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment";
export type { NetworkPolicyRuleAction } from "./NetworkPolicyRuleAction";
export type { NewConversationParams } from "./NewConversationParams";
export type { NewConversationResponse } from "./NewConversationResponse";
export type { ParsedCommand } from "./ParsedCommand";
export type { PatchApplyBeginEvent } from "./PatchApplyBeginEvent";
export type { PatchApplyEndEvent } from "./PatchApplyEndEvent";
export type { PatchApplyStatus } from "./PatchApplyStatus";
export type { PermissionProfile } from "./PermissionProfile";
export type { Personality } from "./Personality";
export type { PlanDeltaEvent } from "./PlanDeltaEvent";
export type { PlanItem } from "./PlanItem";
@@ -201,6 +208,7 @@ export type { SkillDependencies } from "./SkillDependencies";
export type { SkillErrorInfo } from "./SkillErrorInfo";
export type { SkillInterface } from "./SkillInterface";
export type { SkillMetadata } from "./SkillMetadata";
export type { SkillRequestApprovalEvent } from "./SkillRequestApprovalEvent";
export type { SkillScope } from "./SkillScope";
export type { SkillToolDependency } from "./SkillToolDependency";
export type { SkillsListEntry } from "./SkillsListEntry";

View File

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

View File

@@ -0,0 +1,7 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { MacOsAutomationValue } from "../MacOsAutomationValue";
import type { MacOsPreferencesValue } from "../MacOsPreferencesValue";
export type AdditionalMacOsPermissions = { preferences: MacOsPreferencesValue | null, automations: MacOsAutomationValue | null, accessibility: boolean | null, calendar: boolean | null, };

View File

@@ -0,0 +1,7 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions";
import type { AdditionalMacOsPermissions } from "./AdditionalMacOsPermissions";
export type AdditionalPermissionProfile = { network: boolean | null, fileSystem: AdditionalFileSystemPermissions | null, macos: AdditionalMacOsPermissions | null, };

View File

@@ -2,5 +2,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ExecPolicyAmendment } from "./ExecPolicyAmendment";
import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment";
export type CommandExecutionApprovalDecision = "accept" | "acceptForSession" | { "acceptWithExecpolicyAmendment": { execpolicy_amendment: ExecPolicyAmendment, } } | "decline" | "cancel";
export type CommandExecutionApprovalDecision = "accept" | "acceptForSession" | { "acceptWithExecpolicyAmendment": { execpolicy_amendment: ExecPolicyAmendment, } } | { "applyNetworkPolicyAmendment": { network_policy_amendment: NetworkPolicyAmendment, } } | "decline" | "cancel";

View File

@@ -1,9 +1,11 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile";
import type { CommandAction } from "./CommandAction";
import type { ExecPolicyAmendment } from "./ExecPolicyAmendment";
import type { NetworkApprovalContext } from "./NetworkApprovalContext";
import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment";
export type CommandExecutionRequestApprovalParams = { threadId: string, turnId: string, itemId: string,
/**
@@ -21,7 +23,7 @@ approvalId?: string | null,
*/
reason?: string | null,
/**
* Optional context for managed-network approval prompts.
* Optional context for a managed-network approval prompt.
*/
networkApprovalContext?: NetworkApprovalContext | null,
/**
@@ -36,7 +38,15 @@ cwd?: string | null,
* Best-effort parsed command actions for friendly display.
*/
commandActions?: Array<CommandAction> | null,
/**
* Optional additional permissions requested for this command.
*/
additionalPermissions?: AdditionalPermissionProfile | null,
/**
* Optional proposed execpolicy amendment to allow similar commands without prompting.
*/
proposedExecpolicyAmendment?: ExecPolicyAmendment | null, };
proposedExecpolicyAmendment?: ExecPolicyAmendment | null,
/**
* Optional proposed network policy amendments (allow/deny host) for future requests.
*/
proposedNetworkPolicyAmendments?: Array<NetworkPolicyAmendment> | null, };

View File

@@ -0,0 +1,13 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ExternalAgentConfigDetectParams = {
/**
* If true, include detection under the user's home (~/.claude, ~/.codex, etc.).
*/
includeHome?: boolean,
/**
* Zero or more working directories to include for repo-scoped detection.
*/
cwds?: Array<string> | null, };

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,10 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ExternalAgentConfigMigrationItemType } from "./ExternalAgentConfigMigrationItemType";
export type ExternalAgentConfigMigrationItem = { itemType: ExternalAgentConfigMigrationItemType, description: string,
/**
* Null or empty means home-scoped migration; non-empty means repo-scoped migration.
*/
cwd: string | null, };

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ExternalAgentConfigMigrationItemType = "AGENTS_MD" | "CONFIG" | "SKILLS" | "MCP_SERVER_CONFIG";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,4 +36,8 @@ archived?: boolean | null,
* Optional cwd filter; when set, only threads whose session cwd exactly
* matches this path are returned.
*/
cwd?: string | null, };
cwd?: string | null,
/**
* Optional substring filter for the extracted thread title.
*/
searchTerm?: string | null, };

View File

@@ -6,7 +6,7 @@ import type { JsonValue } from "../serde_json/JsonValue";
import type { AskForApproval } from "./AskForApproval";
import type { SandboxMode } from "./SandboxMode";
export type ThreadStartParams = {model?: string | null, modelProvider?: string | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, /**
export type ThreadStartParams = {model?: string | null, modelProvider?: string | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, /**
* If true, opt into emitting raw Responses API items on the event stream.
* This is for internal use only (e.g. Codex Cloud).
*/

View File

@@ -4,6 +4,9 @@ export type { Account } from "./Account";
export type { AccountLoginCompletedNotification } from "./AccountLoginCompletedNotification";
export type { AccountRateLimitsUpdatedNotification } from "./AccountRateLimitsUpdatedNotification";
export type { AccountUpdatedNotification } from "./AccountUpdatedNotification";
export type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions";
export type { AdditionalMacOsPermissions } from "./AdditionalMacOsPermissions";
export type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile";
export type { AgentMessageDeltaNotification } from "./AgentMessageDeltaNotification";
export type { AnalyticsConfig } from "./AnalyticsConfig";
export type { AppBranding } from "./AppBranding";
@@ -65,6 +68,12 @@ export type { ExperimentalFeature } from "./ExperimentalFeature";
export type { ExperimentalFeatureListParams } from "./ExperimentalFeatureListParams";
export type { ExperimentalFeatureListResponse } from "./ExperimentalFeatureListResponse";
export type { ExperimentalFeatureStage } from "./ExperimentalFeatureStage";
export type { ExternalAgentConfigDetectParams } from "./ExternalAgentConfigDetectParams";
export type { ExternalAgentConfigDetectResponse } from "./ExternalAgentConfigDetectResponse";
export type { ExternalAgentConfigImportParams } from "./ExternalAgentConfigImportParams";
export type { ExternalAgentConfigImportResponse } from "./ExternalAgentConfigImportResponse";
export type { ExternalAgentConfigMigrationItem } from "./ExternalAgentConfigMigrationItem";
export type { ExternalAgentConfigMigrationItemType } from "./ExternalAgentConfigMigrationItemType";
export type { FeedbackUploadParams } from "./FeedbackUploadParams";
export type { FeedbackUploadResponse } from "./FeedbackUploadResponse";
export type { FileChangeApprovalDecision } from "./FileChangeApprovalDecision";
@@ -103,6 +112,8 @@ export type { ModelReroutedNotification } from "./ModelReroutedNotification";
export type { NetworkAccess } from "./NetworkAccess";
export type { NetworkApprovalContext } from "./NetworkApprovalContext";
export type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol";
export type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment";
export type { NetworkPolicyRuleAction } from "./NetworkPolicyRuleAction";
export type { NetworkRequirements } from "./NetworkRequirements";
export type { OverriddenMetadata } from "./OverriddenMetadata";
export type { PatchApplyStatus } from "./PatchApplyStatus";
@@ -128,10 +139,13 @@ export type { SandboxMode } from "./SandboxMode";
export type { SandboxPolicy } from "./SandboxPolicy";
export type { SandboxWorkspaceWrite } from "./SandboxWorkspaceWrite";
export type { SessionSource } from "./SessionSource";
export type { SkillApprovalDecision } from "./SkillApprovalDecision";
export type { SkillDependencies } from "./SkillDependencies";
export type { SkillErrorInfo } from "./SkillErrorInfo";
export type { SkillInterface } from "./SkillInterface";
export type { SkillMetadata } from "./SkillMetadata";
export type { SkillRequestApprovalParams } from "./SkillRequestApprovalParams";
export type { SkillRequestApprovalResponse } from "./SkillRequestApprovalResponse";
export type { SkillScope } from "./SkillScope";
export type { SkillToolDependency } from "./SkillToolDependency";
export type { SkillsConfigWriteParams } from "./SkillsConfigWriteParams";

View File

@@ -1947,6 +1947,15 @@ mod tests {
let thread_start_ts =
fs::read_to_string(output_dir.join("v2").join("ThreadStartParams.ts"))?;
assert_eq!(thread_start_ts.contains("mockExperimentalField"), true);
let command_execution_request_approval_ts = fs::read_to_string(
output_dir
.join("v2")
.join("CommandExecutionRequestApprovalParams.ts"),
)?;
assert_eq!(
command_execution_request_approval_ts.contains("additionalPermissions"),
true
);
Ok(())
}
@@ -2083,6 +2092,12 @@ export type Config = { stableField: Keep, unstableField: string | null } & ({ [k
let thread_start_json =
fs::read_to_string(output_dir.join("v2").join("ThreadStartParams.json"))?;
assert_eq!(thread_start_json.contains("mockExperimentalField"), false);
let command_execution_request_approval_json =
fs::read_to_string(output_dir.join("CommandExecutionRequestApprovalParams.json"))?;
assert_eq!(
command_execution_request_approval_json.contains("additionalPermissions"),
false
);
let client_request_json = fs::read_to_string(output_dir.join("ClientRequest.json"))?;
assert_eq!(
@@ -2093,6 +2108,7 @@ export type Config = { stableField: Keep, unstableField: string | null } & ({ [k
let bundle_json =
fs::read_to_string(output_dir.join("codex_app_server_protocol.schemas.json"))?;
assert_eq!(bundle_json.contains("mockExperimentalField"), false);
assert_eq!(bundle_json.contains("additionalPermissions"), false);
assert_eq!(bundle_json.contains("MockExperimentalMethodParams"), false);
assert_eq!(
bundle_json.contains("MockExperimentalMethodResponse"),

View File

@@ -350,6 +350,14 @@ client_request_definitions! {
params: v2::ConfigReadParams,
response: v2::ConfigReadResponse,
},
ExternalAgentConfigDetect => "externalAgentConfig/detect" {
params: v2::ExternalAgentConfigDetectParams,
response: v2::ExternalAgentConfigDetectResponse,
},
ExternalAgentConfigImport => "externalAgentConfig/import" {
params: v2::ExternalAgentConfigImportParams,
response: v2::ExternalAgentConfigImportResponse,
},
ConfigValueWrite => "config/value/write" {
params: v2::ConfigValueWriteParams,
response: v2::ConfigWriteResponse,
@@ -501,6 +509,7 @@ macro_rules! server_request_definitions {
) => {
/// Request initiated from the server and sent to the client.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[allow(clippy::large_enum_variant)]
#[serde(tag = "method", rename_all = "camelCase")]
pub enum ServerRequest {
$(
@@ -515,6 +524,7 @@ macro_rules! server_request_definitions {
}
#[derive(Debug, Clone, PartialEq, JsonSchema)]
#[allow(clippy::large_enum_variant)]
pub enum ServerRequestPayload {
$( $variant($params), )*
}
@@ -671,6 +681,11 @@ server_request_definitions! {
response: v2::ToolRequestUserInputResponse,
},
SkillRequestApproval => "skill/requestApproval" {
params: v2::SkillRequestApprovalParams,
response: v2::SkillRequestApprovalResponse,
},
/// Execute a dynamic tool call on the client.
DynamicToolCall => "item/tool/call" {
params: v2::DynamicToolCallParams,
@@ -1366,4 +1381,34 @@ mod tests {
let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&request);
assert_eq!(reason, Some("mock/experimentalMethod"));
}
#[test]
fn command_execution_request_approval_additional_permissions_is_marked_experimental() {
let params = v2::CommandExecutionRequestApprovalParams {
thread_id: "thr_123".to_string(),
turn_id: "turn_123".to_string(),
item_id: "call_123".to_string(),
approval_id: None,
reason: None,
network_approval_context: None,
command: Some("cat file".to_string()),
cwd: None,
command_actions: None,
additional_permissions: Some(v2::AdditionalPermissionProfile {
network: None,
file_system: Some(v2::AdditionalFileSystemPermissions {
read: Some(vec![std::path::PathBuf::from("/tmp/allowed")]),
write: None,
}),
macos: None,
}),
proposed_execpolicy_amendment: None,
proposed_network_policy_amendments: None,
};
let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&params);
assert_eq!(
reason,
Some("item/commandExecution/requestApproval.additionalPermissions")
);
}
}

View File

@@ -7,6 +7,8 @@ use codex_protocol::account::PlanType;
use codex_protocol::approvals::ExecPolicyAmendment as CoreExecPolicyAmendment;
use codex_protocol::approvals::NetworkApprovalContext as CoreNetworkApprovalContext;
use codex_protocol::approvals::NetworkApprovalProtocol as CoreNetworkApprovalProtocol;
use codex_protocol::approvals::NetworkPolicyAmendment as CoreNetworkPolicyAmendment;
use codex_protocol::approvals::NetworkPolicyRuleAction as CoreNetworkPolicyRuleAction;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::CollaborationModeMask;
use codex_protocol::config_types::ForcedLoginMethod;
@@ -20,7 +22,12 @@ use codex_protocol::items::TurnItem as CoreTurnItem;
use codex_protocol::mcp::Resource as McpResource;
use codex_protocol::mcp::ResourceTemplate as McpResourceTemplate;
use codex_protocol::mcp::Tool as McpTool;
use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions;
use codex_protocol::models::MacOsAutomationValue as CoreMacOsAutomationValue;
use codex_protocol::models::MacOsPermissions as CoreMacOsPermissions;
use codex_protocol::models::MacOsPreferencesValue as CoreMacOsPreferencesValue;
use codex_protocol::models::MessagePhase;
use codex_protocol::models::PermissionProfile as CorePermissionProfile;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::InputModality;
use codex_protocol::openai_models::ReasoningEffort;
@@ -633,6 +640,64 @@ pub struct ConfigRequirementsReadResponse {
pub requirements: Option<ConfigRequirements>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, JsonSchema, TS)]
#[ts(export_to = "v2/")]
pub enum ExternalAgentConfigMigrationItemType {
#[serde(rename = "AGENTS_MD")]
#[ts(rename = "AGENTS_MD")]
AgentsMd,
#[serde(rename = "CONFIG")]
#[ts(rename = "CONFIG")]
Config,
#[serde(rename = "SKILLS")]
#[ts(rename = "SKILLS")]
Skills,
#[serde(rename = "MCP_SERVER_CONFIG")]
#[ts(rename = "MCP_SERVER_CONFIG")]
McpServerConfig,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ExternalAgentConfigMigrationItem {
pub item_type: ExternalAgentConfigMigrationItemType,
pub description: String,
/// Null or empty means home-scoped migration; non-empty means repo-scoped migration.
pub cwd: Option<PathBuf>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ExternalAgentConfigDetectResponse {
pub items: Vec<ExternalAgentConfigMigrationItem>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ExternalAgentConfigDetectParams {
/// If true, include detection under the user's home (~/.claude, ~/.codex, etc.).
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub include_home: bool,
/// Zero or more working directories to include for repo-scoped detection.
#[ts(optional = nullable)]
pub cwds: Option<Vec<PathBuf>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ExternalAgentConfigImportParams {
pub migration_items: Vec<ExternalAgentConfigMigrationItem>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ExternalAgentConfigImportResponse {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -681,6 +746,10 @@ pub enum CommandExecutionApprovalDecision {
AcceptWithExecpolicyAmendment {
execpolicy_amendment: ExecPolicyAmendment,
},
/// User chose a persistent network policy rule (allow/deny) for this host.
ApplyNetworkPolicyAmendment {
network_policy_amendment: NetworkPolicyAmendment,
},
/// User denied the command. The agent will continue the turn.
Decline,
/// User denied the command. The turn will also be immediately interrupted.
@@ -713,6 +782,63 @@ impl From<CoreNetworkApprovalContext> for NetworkApprovalContext {
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AdditionalFileSystemPermissions {
pub read: Option<Vec<PathBuf>>,
pub write: Option<Vec<PathBuf>>,
}
impl From<CoreFileSystemPermissions> for AdditionalFileSystemPermissions {
fn from(value: CoreFileSystemPermissions) -> Self {
Self {
read: value.read,
write: value.write,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AdditionalMacOsPermissions {
pub preferences: Option<CoreMacOsPreferencesValue>,
pub automations: Option<CoreMacOsAutomationValue>,
pub accessibility: Option<bool>,
pub calendar: Option<bool>,
}
impl From<CoreMacOsPermissions> for AdditionalMacOsPermissions {
fn from(value: CoreMacOsPermissions) -> Self {
Self {
preferences: value.preferences,
automations: value.automations,
accessibility: value.accessibility,
calendar: value.calendar,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AdditionalPermissionProfile {
pub network: Option<bool>,
pub file_system: Option<AdditionalFileSystemPermissions>,
pub macos: Option<AdditionalMacOsPermissions>,
}
impl From<CorePermissionProfile> for AdditionalPermissionProfile {
fn from(value: CorePermissionProfile) -> Self {
Self {
network: value.network,
file_system: value.file_system.map(AdditionalFileSystemPermissions::from),
macos: value.macos.map(AdditionalMacOsPermissions::from),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -910,6 +1036,38 @@ impl From<CoreExecPolicyAmendment> for ExecPolicyAmendment {
}
}
v2_enum_from_core!(
pub enum NetworkPolicyRuleAction from CoreNetworkPolicyRuleAction {
Allow, Deny
}
);
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct NetworkPolicyAmendment {
pub host: String,
pub action: NetworkPolicyRuleAction,
}
impl NetworkPolicyAmendment {
pub fn into_core(self) -> CoreNetworkPolicyAmendment {
CoreNetworkPolicyAmendment {
host: self.host,
action: self.action.to_core(),
}
}
}
impl From<CoreNetworkPolicyAmendment> for NetworkPolicyAmendment {
fn from(value: CoreNetworkPolicyAmendment) -> Self {
Self {
host: value.host,
action: NetworkPolicyRuleAction::from(value.action),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "camelCase")]
#[ts(tag = "type")]
@@ -1561,6 +1719,8 @@ pub struct ThreadStartParams {
#[ts(optional = nullable)]
pub config: Option<HashMap<String, JsonValue>>,
#[ts(optional = nullable)]
pub service_name: Option<String>,
#[ts(optional = nullable)]
pub base_instructions: Option<String>,
#[ts(optional = nullable)]
pub developer_instructions: Option<String>,
@@ -1861,6 +2021,9 @@ pub struct ThreadListParams {
/// matches this path are returned.
#[ts(optional = nullable)]
pub cwd: Option<String>,
/// Optional substring filter for the extracted thread title.
#[ts(optional = nullable)]
pub search_term: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
@@ -3359,7 +3522,7 @@ pub struct ContextCompactedNotification {
pub turn_id: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct CommandExecutionRequestApprovalParams {
@@ -3380,7 +3543,7 @@ pub struct CommandExecutionRequestApprovalParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub reason: Option<String>,
/// Optional context for managed-network approval prompts.
/// Optional context for a managed-network approval prompt.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub network_approval_context: Option<NetworkApprovalContext>,
@@ -3396,10 +3559,28 @@ pub struct CommandExecutionRequestApprovalParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub command_actions: Option<Vec<CommandAction>>,
/// Optional additional permissions requested for this command.
#[experimental("item/commandExecution/requestApproval.additionalPermissions")]
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub additional_permissions: Option<AdditionalPermissionProfile>,
/// Optional proposed execpolicy amendment to allow similar commands without prompting.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
/// Optional proposed network policy amendments (allow/deny host) for future requests.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub proposed_network_policy_amendments: Option<Vec<NetworkPolicyAmendment>>,
}
impl CommandExecutionRequestApprovalParams {
pub fn strip_experimental_fields(&mut self) {
// TODO: Avoid hardcoding individual experimental fields here.
// We need a generic outbound compatibility design for stripping or
// otherwise handling experimental server->client payloads.
self.additional_permissions = None;
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -3431,6 +3612,29 @@ pub struct FileChangeRequestApprovalResponse {
pub decision: FileChangeApprovalDecision,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct SkillRequestApprovalParams {
pub item_id: String,
pub skill_name: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub enum SkillApprovalDecision {
Approve,
Decline,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct SkillRequestApprovalResponse {
pub decision: SkillApprovalDecision,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]

View File

@@ -57,6 +57,9 @@ 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::SkillApprovalDecision;
use codex_app_server_protocol::SkillRequestApprovalParams;
use codex_app_server_protocol::SkillRequestApprovalResponse;
use codex_app_server_protocol::ThreadItem;
use codex_app_server_protocol::ThreadListParams;
use codex_app_server_protocol::ThreadListResponse;
@@ -168,6 +171,9 @@ enum CliCommand {
},
/// Send a user message through the app-server V2 thread/turn APIs.
SendMessageV2 {
/// Opt into experimental app-server methods and fields.
#[arg(long)]
experimental_api: bool,
/// User message to send to Codex.
user_message: String,
},
@@ -257,9 +263,18 @@ pub fn run() -> Result<()> {
let endpoint = resolve_endpoint(codex_bin, url)?;
send_message(&endpoint, &config_overrides, user_message)
}
CliCommand::SendMessageV2 { user_message } => {
CliCommand::SendMessageV2 {
experimental_api,
user_message,
} => {
let endpoint = resolve_endpoint(codex_bin, url)?;
send_message_v2_endpoint(&endpoint, &config_overrides, user_message, &dynamic_tools)
send_message_v2_endpoint(
&endpoint,
&config_overrides,
user_message,
experimental_api,
&dynamic_tools,
)
}
CliCommand::ResumeMessageV2 {
thread_id,
@@ -505,19 +520,31 @@ pub fn send_message_v2(
dynamic_tools: &Option<Vec<DynamicToolSpec>>,
) -> Result<()> {
let endpoint = Endpoint::SpawnCodex(codex_bin.to_path_buf());
send_message_v2_endpoint(&endpoint, config_overrides, user_message, dynamic_tools)
send_message_v2_endpoint(
&endpoint,
config_overrides,
user_message,
true,
dynamic_tools,
)
}
fn send_message_v2_endpoint(
endpoint: &Endpoint,
config_overrides: &[String],
user_message: String,
experimental_api: bool,
dynamic_tools: &Option<Vec<DynamicToolSpec>>,
) -> Result<()> {
if dynamic_tools.is_some() && !experimental_api {
bail!("--dynamic-tools requires --experimental-api for send-message-v2");
}
send_message_v2_with_policies(
endpoint,
config_overrides,
user_message,
experimental_api,
None,
None,
dynamic_tools,
@@ -687,6 +714,7 @@ fn trigger_cmd_approval(
endpoint,
config_overrides,
message,
true,
Some(AskForApproval::OnRequest),
Some(SandboxPolicy::ReadOnly {
access: ReadOnlyAccess::FullAccess,
@@ -708,6 +736,7 @@ fn trigger_patch_approval(
endpoint,
config_overrides,
message,
true,
Some(AskForApproval::OnRequest),
Some(SandboxPolicy::ReadOnly {
access: ReadOnlyAccess::FullAccess,
@@ -726,6 +755,7 @@ fn no_trigger_cmd_approval(
endpoint,
config_overrides,
prompt.to_string(),
true,
None,
None,
dynamic_tools,
@@ -736,13 +766,14 @@ fn send_message_v2_with_policies(
endpoint: &Endpoint,
config_overrides: &[String],
user_message: String,
experimental_api: bool,
approval_policy: Option<AskForApproval>,
sandbox_policy: Option<SandboxPolicy>,
dynamic_tools: &Option<Vec<DynamicToolSpec>>,
) -> Result<()> {
let mut client = CodexClient::connect(endpoint, config_overrides)?;
let initialize = client.initialize()?;
let initialize = client.initialize_with_experimental_api(experimental_api)?;
println!("< initialize response: {initialize:?}");
let thread_response = client.thread_start(ThreadStartParams {
@@ -885,6 +916,7 @@ fn thread_list(endpoint: &Endpoint, config_overrides: &[String], limit: u32) ->
source_kinds: None,
archived: None,
cwd: None,
search_term: None,
})?;
println!("< thread/list response: {response:?}");
@@ -1029,6 +1061,13 @@ impl CodexClient {
}
fn initialize(&mut self) -> Result<InitializeResponse> {
self.initialize_with_experimental_api(true)
}
fn initialize_with_experimental_api(
&mut self,
experimental_api: bool,
) -> Result<InitializeResponse> {
let request_id = self.request_id();
let request = ClientRequest::Initialize {
request_id: request_id.clone(),
@@ -1039,7 +1078,7 @@ impl CodexClient {
version: env!("CARGO_PKG_VERSION").to_string(),
},
capabilities: Some(InitializeCapabilities {
experimental_api: true,
experimental_api,
opt_out_notification_methods: Some(
NOTIFICATIONS_TO_OPT_OUT
.iter()
@@ -1472,6 +1511,9 @@ impl CodexClient {
ServerRequest::FileChangeRequestApproval { request_id, params } => {
self.approve_file_change_request(request_id, params)?;
}
ServerRequest::SkillRequestApproval { request_id, params } => {
self.approve_skill_request(request_id, params)?;
}
other => {
bail!("received unsupported server request: {other:?}");
}
@@ -1495,7 +1537,9 @@ impl CodexClient {
command,
cwd,
command_actions,
additional_permissions,
proposed_execpolicy_amendment,
proposed_network_policy_amendments,
} = params;
println!(
@@ -1521,9 +1565,15 @@ impl CodexClient {
{
println!("< command actions: {command_actions:?}");
}
if let Some(additional_permissions) = additional_permissions.as_ref() {
println!("< additional permissions: {additional_permissions:?}");
}
if let Some(execpolicy_amendment) = proposed_execpolicy_amendment.as_ref() {
println!("< proposed execpolicy amendment: {execpolicy_amendment:?}");
}
if let Some(network_policy_amendments) = proposed_network_policy_amendments.as_ref() {
println!("< proposed network policy amendments: {network_policy_amendments:?}");
}
let decision = match self.command_approval_behavior {
CommandApprovalBehavior::AlwaysAccept => CommandExecutionApprovalDecision::Accept,
@@ -1543,6 +1593,22 @@ impl CodexClient {
Ok(())
}
fn approve_skill_request(
&mut self,
request_id: RequestId,
params: SkillRequestApprovalParams,
) -> Result<()> {
println!(
"\n< skill approval requested for item {}, skill {}",
params.item_id, params.skill_name
);
let response = SkillRequestApprovalResponse {
decision: SkillApprovalDecision::Approve,
};
self.send_server_request_response(request_id, &response)?;
Ok(())
}
fn approve_file_change_request(
&mut self,
request_id: RequestId,

View File

@@ -65,6 +65,7 @@ axum = { workspace = true, default-features = false, features = [
base64 = { workspace = true }
codex-execpolicy = { workspace = true }
core_test_support = { workspace = true }
codex-state = { workspace = true }
codex-utils-cargo-bin = { workspace = true }
os_info = { workspace = true }
pretty_assertions = { workspace = true }

View File

@@ -122,7 +122,7 @@ Example with notification opt-out:
- `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/fork` — fork an existing thread into a new thread id by copying the stored history; emits `thread/started` and auto-subscribes you to turn/item events for the new thread.
- `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, and `cwd` filters. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
- `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
- `thread/loaded/list` — list the thread ids currently loaded in memory.
- `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
- `thread/status/changed` — notification emitted when a loaded threads status changes (`threadId` + new `status`).
@@ -153,6 +153,8 @@ Example with notification opt-out:
- `feedback/upload` — submit a feedback report (classification + optional reason/logs, conversation_id, and optional `extraLogFiles` attachments array); returns the tracking thread id.
- `command/exec` — run a single command under the server sandbox without starting a thread/turn (handy for utilities and validation).
- `config/read` — fetch the effective config on disk after resolving config layering.
- `externalAgentConfig/detect` — detect migratable external-agent artifacts with `includeHome` and optional `cwds`; each detected item includes `cwd` (`null` for home).
- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home).
- `config/value/write` — write a single config key/value to the user's config.toml on disk.
- `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk.
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), `enforceResidency`, and `network` constraints.
@@ -170,6 +172,7 @@ Start a fresh thread when you need a new Codex conversation.
"approvalPolicy": "never",
"sandbox": "workspaceWrite",
"personality": "friendly",
"serviceName": "my_app_server_client", // optional metrics tag (`service_name`)
// Experimental: requires opt-in
"dynamicTools": [
{
@@ -229,6 +232,7 @@ Experimental API: `thread/start`, `thread/resume`, and `thread/fork` accept `per
- `sourceKinds` — restrict results to specific sources; omit or pass `[]` for interactive sessions only (`cli`, `vscode`).
- `archived` — when `true`, list archived threads only. When `false` or `null`, list non-archived threads (default).
- `cwd` — restrict results to threads whose session cwd exactly matches this path.
- `searchTerm` — restrict results to threads whose extracted title contains this substring (case-sensitive).
- Responses include `agentNickname` and `agentRole` for AgentControl-spawned thread sub-agents when available.
Example:
@@ -660,15 +664,15 @@ When an upstream HTTP status is available (for example, from the Responses API o
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`.
- Respond with a single `{ "decision": ... }` payload. Command approvals support `accept`, `acceptForSession`, `acceptWithExecpolicyAmendment`, `applyNetworkPolicyAmendment`, `decline`, or `cancel`. 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 `approvalId` (for subcommand callbacks), and `reason`. For normal command approvals, it also includes `command`, `cwd`, and `commandActions` for friendly display. For network-only approvals, those command fields may be omitted and `networkApprovalContext` is provided instead.
3. Client response — `{ "decision": "accept", "acceptSettings": { "forSession": false } }` or `{ "decision": "decline" }`.
2. `item/commandExecution/requestApproval` (request) — carries the same `itemId`, `threadId`, `turnId`, optionally `approvalId` (for subcommand callbacks), and `reason`. For normal command approvals, it also includes `command`, `cwd`, and `commandActions` for friendly display. When `initialize.params.capabilities.experimentalApi = true`, it may also include experimental `additionalPermissions` describing requested per-command sandbox access. For network-only approvals, those command fields may be omitted and `networkApprovalContext` is provided instead. Optional persistence hints may also be included via `proposedExecpolicyAmendment` and `proposedNetworkPolicyAmendments`.
3. Client response — for example `{ "decision": "accept" }`, `{ "decision": "acceptForSession" }`, `{ "decision": { "acceptWithExecpolicyAmendment": { "execpolicy_amendment": [...] } } }`, `{ "decision": { "applyNetworkPolicyAmendment": { "network_policy_amendment": { "host": "example.com", "action": "allow" } } } }`, `{ "decision": "decline" }`, or `{ "decision": "cancel" }`.
4. `item/completed` — final `commandExecution` item with `status: "completed" | "failed" | "declined"` and execution output. Render this as the authoritative result.
### File change approvals
@@ -1072,6 +1076,8 @@ At runtime, clients must send `initialize` with `capabilities.experimentalApi =
3. In `app-server-protocol/src/protocol/common.rs`, keep the method stable and use `inspect_params: true` when only some fields are experimental (like `thread/start`). If the entire method is experimental, annotate the method variant with `#[experimental("method/name")]`.
For server-initiated request payloads, annotate the field the same way so schema generation treats it as experimental, and make sure app-server omits that field when the client did not opt into `experimentalApi`.
4. Regenerate protocol fixtures:
```bash

View File

@@ -11,6 +11,7 @@ use crate::thread_state::TurnSummary;
use crate::thread_status::ThreadWatchActiveGuard;
use crate::thread_status::ThreadWatchManager;
use codex_app_server_protocol::AccountRateLimitsUpdatedNotification;
use codex_app_server_protocol::AdditionalPermissionProfile as V2AdditionalPermissionProfile;
use codex_app_server_protocol::AgentMessageDeltaNotification;
use codex_app_server_protocol::ApplyPatchApprovalParams;
use codex_app_server_protocol::ApplyPatchApprovalResponse;
@@ -45,6 +46,8 @@ use codex_app_server_protocol::McpToolCallResult;
use codex_app_server_protocol::McpToolCallStatus;
use codex_app_server_protocol::ModelReroutedNotification;
use codex_app_server_protocol::NetworkApprovalContext as V2NetworkApprovalContext;
use codex_app_server_protocol::NetworkPolicyAmendment as V2NetworkPolicyAmendment;
use codex_app_server_protocol::NetworkPolicyRuleAction as V2NetworkPolicyRuleAction;
use codex_app_server_protocol::PatchApplyStatus;
use codex_app_server_protocol::PlanDeltaNotification;
use codex_app_server_protocol::RawResponseItemCompletedNotification;
@@ -53,6 +56,9 @@ use codex_app_server_protocol::ReasoningSummaryTextDeltaNotification;
use codex_app_server_protocol::ReasoningTextDeltaNotification;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerRequestPayload;
use codex_app_server_protocol::SkillApprovalDecision as V2SkillApprovalDecision;
use codex_app_server_protocol::SkillRequestApprovalParams;
use codex_app_server_protocol::SkillRequestApprovalResponse;
use codex_app_server_protocol::TerminalInteractionNotification;
use codex_app_server_protocol::ThreadItem;
use codex_app_server_protocol::ThreadNameUpdatedNotification;
@@ -97,6 +103,7 @@ use codex_protocol::protocol::TokenCountEvent;
use codex_protocol::protocol::TurnDiffEvent;
use codex_protocol::request_user_input::RequestUserInputAnswer as CoreRequestUserInputAnswer;
use codex_protocol::request_user_input::RequestUserInputResponse as CoreRequestUserInputResponse;
use codex_protocol::skill_approval::SkillApprovalResponse as CoreSkillApprovalResponse;
use codex_shell_command::parse_command::shlex_join;
use std::collections::HashMap;
use std::convert::TryFrom;
@@ -263,6 +270,8 @@ pub(crate) async fn apply_bespoke_event_handling(
reason,
network_approval_context,
proposed_execpolicy_amendment,
proposed_network_policy_amendments,
additional_permissions,
parsed_cmd,
..
} = ev;
@@ -325,6 +334,15 @@ pub(crate) async fn apply_bespoke_event_handling(
};
let proposed_execpolicy_amendment_v2 =
proposed_execpolicy_amendment.map(V2ExecPolicyAmendment::from);
let proposed_network_policy_amendments_v2 = proposed_network_policy_amendments
.map(|amendments| {
amendments
.into_iter()
.map(V2NetworkPolicyAmendment::from)
.collect()
});
let additional_permissions =
additional_permissions.map(V2AdditionalPermissionProfile::from);
let params = CommandExecutionRequestApprovalParams {
thread_id: conversation_id.to_string(),
@@ -336,7 +354,9 @@ pub(crate) async fn apply_bespoke_event_handling(
command,
cwd,
command_actions,
additional_permissions,
proposed_execpolicy_amendment: proposed_execpolicy_amendment_v2,
proposed_network_policy_amendments: proposed_network_policy_amendments_v2,
};
let rx = outgoing
.send_request(ServerRequestPayload::CommandExecutionRequestApproval(
@@ -423,6 +443,37 @@ pub(crate) async fn apply_bespoke_event_handling(
}
}
}
EventMsg::SkillRequestApproval(request) => {
if matches!(api_version, ApiVersion::V2) {
let item_id = request.item_id;
let skill_name = request.skill_name;
let params = SkillRequestApprovalParams {
item_id: item_id.clone(),
skill_name,
};
let rx = outgoing
.send_request(ServerRequestPayload::SkillRequestApproval(params))
.await;
tokio::spawn(async move {
let approved = match rx.await {
Ok(Ok(value)) => {
serde_json::from_value::<SkillRequestApprovalResponse>(value)
.map(|response| {
matches!(response.decision, V2SkillApprovalDecision::Approve)
})
.unwrap_or(false)
}
_ => false,
};
let _ = conversation
.submit(Op::SkillApproval {
id: item_id,
response: CoreSkillApprovalResponse { approved },
})
.await;
});
}
}
EventMsg::DynamicToolCallRequest(request) => {
if matches!(api_version, ApiVersion::V2) {
let call_id = request.call_id;
@@ -1875,6 +1926,20 @@ async fn on_command_execution_request_approval_response(
},
None,
),
CommandExecutionApprovalDecision::ApplyNetworkPolicyAmendment {
network_policy_amendment,
} => {
let completion_status = match network_policy_amendment.action {
V2NetworkPolicyRuleAction::Allow => None,
V2NetworkPolicyRuleAction::Deny => Some(CommandExecutionStatus::Declined),
};
(
ReviewDecision::NetworkPolicyAmendment {
network_policy_amendment: network_policy_amendment.into_core(),
},
completion_status,
)
}
CommandExecutionApprovalDecision::Decline => (
ReviewDecision::Denied,
Some(CommandExecutionStatus::Declined),

View File

@@ -168,6 +168,7 @@ use codex_app_server_protocol::WindowsSandboxSetupMode;
use codex_app_server_protocol::WindowsSandboxSetupStartParams;
use codex_app_server_protocol::WindowsSandboxSetupStartResponse;
use codex_app_server_protocol::build_turns_from_rollout_items;
use codex_arg0::Arg0DispatchPaths;
use codex_backend_client::Client as BackendClient;
use codex_chatgpt::connectors;
use codex_cloud_requirements::cloud_requirements_loader;
@@ -189,6 +190,7 @@ use codex_core::auth::login_with_chatgpt_auth_tokens;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::config::ConfigService;
use codex_core::config::NetworkProxyAuditMetadata;
use codex_core::config::edit::ConfigEdit;
use codex_core::config::edit::ConfigEditsBuilder;
use codex_core::config::types::McpServerTransportConfig;
@@ -269,6 +271,7 @@ use std::time::SystemTime;
use tokio::sync::Mutex;
use tokio::sync::broadcast;
use tokio::sync::oneshot;
use tokio::sync::watch;
use toml::Value as TomlValue;
use tracing::error;
use tracing::info;
@@ -288,6 +291,7 @@ struct ThreadListFilters {
source_kinds: Option<Vec<ThreadSourceKind>>,
archived: bool,
cwd: Option<PathBuf>,
search_term: Option<String>,
}
// Duration before a ChatGPT login attempt is abandoned.
@@ -337,7 +341,7 @@ pub(crate) struct CodexMessageProcessor {
auth_manager: Arc<AuthManager>,
thread_manager: Arc<ThreadManager>,
outgoing: Arc<OutgoingMessageSender>,
codex_linux_sandbox_exe: Option<PathBuf>,
arg0_paths: Arg0DispatchPaths,
config: Arc<Config>,
single_client_mode: bool,
cli_overrides: Vec<(String, TomlValue)>,
@@ -361,7 +365,7 @@ pub(crate) struct CodexMessageProcessorArgs {
pub(crate) auth_manager: Arc<AuthManager>,
pub(crate) thread_manager: Arc<ThreadManager>,
pub(crate) outgoing: Arc<OutgoingMessageSender>,
pub(crate) codex_linux_sandbox_exe: Option<PathBuf>,
pub(crate) arg0_paths: Arg0DispatchPaths,
pub(crate) config: Arc<Config>,
pub(crate) cli_overrides: Vec<(String, TomlValue)>,
pub(crate) cloud_requirements: Arc<RwLock<CloudRequirementsLoader>>,
@@ -398,7 +402,7 @@ impl CodexMessageProcessor {
auth_manager,
thread_manager,
outgoing,
codex_linux_sandbox_exe,
arg0_paths,
config,
cli_overrides,
cloud_requirements,
@@ -409,7 +413,7 @@ impl CodexMessageProcessor {
auth_manager,
thread_manager,
outgoing: outgoing.clone(),
codex_linux_sandbox_exe,
arg0_paths,
config,
single_client_mode,
cli_overrides,
@@ -425,7 +429,7 @@ impl CodexMessageProcessor {
async fn load_latest_config(&self) -> Result<Config, JSONRPCErrorError> {
let cloud_requirements = self.current_cloud_requirements();
codex_core::config::ConfigBuilder::default()
let mut config = codex_core::config::ConfigBuilder::default()
.cli_overrides(self.cli_overrides.clone())
.cloud_requirements(cloud_requirements)
.build()
@@ -434,7 +438,10 @@ impl CodexMessageProcessor {
code: INTERNAL_ERROR_CODE,
message: format!("failed to reload config: {err}"),
data: None,
})
})?;
config.codex_linux_sandbox_exe = self.arg0_paths.codex_linux_sandbox_exe.clone();
config.main_execve_wrapper_exe = self.arg0_paths.main_execve_wrapper_exe.clone();
Ok(config)
}
fn current_cloud_requirements(&self) -> CloudRequirementsLoader {
@@ -816,6 +823,10 @@ impl CodexMessageProcessor {
ClientRequest::ConfigRequirementsRead { .. } => {
warn!("ConfigRequirementsRead request reached CodexMessageProcessor unexpectedly");
}
ClientRequest::ExternalAgentConfigDetect { .. }
| ClientRequest::ExternalAgentConfigImport { .. } => {
warn!("ExternalAgentConfig request reached CodexMessageProcessor unexpectedly");
}
ClientRequest::GetAccountRateLimits {
request_id,
params: _,
@@ -1741,6 +1752,7 @@ impl CodexMessageProcessor {
None,
None,
managed_network_requirements_enabled,
NetworkProxyAuditMetadata::default(),
)
.await
{
@@ -1758,8 +1770,10 @@ impl CodexMessageProcessor {
None => None,
};
let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config);
let command = params.command;
let exec_params = ExecParams {
command: params.command,
original_command: command.join(" "),
command,
cwd,
expiration: timeout_ms.into(),
env,
@@ -1789,7 +1803,7 @@ impl CodexMessageProcessor {
None => self.config.permissions.sandbox_policy.get().clone(),
};
let codex_linux_sandbox_exe = self.config.codex_linux_sandbox_exe.clone();
let codex_linux_sandbox_exe = self.arg0_paths.codex_linux_sandbox_exe.clone();
let outgoing = self.outgoing.clone();
let request_for_task = request;
let sandbox_cwd = self.config.cwd.clone();
@@ -1854,7 +1868,8 @@ impl CodexMessageProcessor {
approval_policy,
sandbox_mode,
model_provider,
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.clone(),
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
base_instructions,
developer_instructions,
compact_prompt,
@@ -1948,6 +1963,7 @@ impl CodexMessageProcessor {
approval_policy,
sandbox,
config,
service_name,
base_instructions,
developer_instructions,
dynamic_tools,
@@ -2015,7 +2031,12 @@ impl CodexMessageProcessor {
match self
.thread_manager
.start_thread_with_tools(config, core_dynamic_tools, persist_extended_history)
.start_thread_with_tools_and_service_name(
config,
core_dynamic_tools,
persist_extended_history,
service_name,
)
.await
{
Ok(new_conv) => {
@@ -2108,7 +2129,8 @@ impl CodexMessageProcessor {
approval_policy: approval_policy
.map(codex_app_server_protocol::AskForApproval::to_core),
sandbox_mode: sandbox.map(SandboxMode::to_core),
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.clone(),
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
base_instructions,
developer_instructions,
personality,
@@ -2522,6 +2544,7 @@ impl CodexMessageProcessor {
source_kinds,
archived,
cwd,
search_term,
} = params;
let requested_page_size = limit
@@ -2542,6 +2565,7 @@ impl CodexMessageProcessor {
source_kinds,
archived: archived.unwrap_or(false),
cwd: cwd.map(PathBuf::from),
search_term,
},
)
.await
@@ -2794,6 +2818,10 @@ impl CodexMessageProcessor {
.await;
}
pub(crate) fn subscribe_running_assistant_turn_count(&self) -> watch::Receiver<usize> {
self.thread_watch_manager.subscribe_running_turn_count()
}
/// Best-effort: ensure initialized connections are subscribed to this thread.
pub(crate) async fn try_attach_thread_listener(
&mut self,
@@ -3599,6 +3627,7 @@ impl CodexMessageProcessor {
source_kinds: None,
archived: false,
cwd: None,
search_term: None,
},
)
.await
@@ -3625,6 +3654,7 @@ impl CodexMessageProcessor {
source_kinds,
archived,
cwd,
search_term,
} = filters;
let mut cursor_obj: Option<RolloutCursor> = match cursor.as_ref() {
Some(cursor_str) => {
@@ -3667,6 +3697,7 @@ impl CodexMessageProcessor {
allowed_sources,
model_provider_filter.as_deref(),
fallback_provider.as_str(),
search_term.as_deref(),
)
.await
.map_err(|err| JSONRPCErrorError {
@@ -3683,6 +3714,7 @@ impl CodexMessageProcessor {
allowed_sources,
model_provider_filter.as_deref(),
fallback_provider.as_str(),
search_term.as_deref(),
)
.await
.map_err(|err| JSONRPCErrorError {
@@ -4318,7 +4350,8 @@ impl CodexMessageProcessor {
approval_policy,
sandbox_mode,
model_provider,
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.clone(),
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
base_instructions,
developer_instructions,
compact_prompt,
@@ -4329,7 +4362,8 @@ impl CodexMessageProcessor {
}
None => (
ConfigOverrides {
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.clone(),
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
..Default::default()
},
None,
@@ -4513,7 +4547,8 @@ impl CodexMessageProcessor {
approval_policy,
sandbox_mode,
model_provider,
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.clone(),
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
base_instructions,
developer_instructions,
compact_prompt,
@@ -4525,7 +4560,8 @@ impl CodexMessageProcessor {
}
None => (
ConfigOverrides {
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.clone(),
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
..Default::default()
},
None,
@@ -5897,6 +5933,11 @@ impl CodexMessageProcessor {
EventMsg::TurnComplete(_) => "task_complete",
_ => &event.msg.to_string(),
};
let request_event_name = format!("codex/event/{event_formatted}");
tracing::trace!(
conversation_id = %conversation_id,
"app-server event: {request_event_name}"
);
let mut params = match serde_json::to_value(event.clone()) {
Ok(serde_json::Value::Object(map)) => map,
Ok(_) => {
@@ -5931,7 +5972,7 @@ impl CodexMessageProcessor {
.send_notification_to_connections(
&subscribed_connection_ids,
OutgoingNotification {
method: format!("codex/event/{event_formatted}"),
method: request_event_name,
params: Some(params.into()),
},
)
@@ -6514,7 +6555,7 @@ fn skills_to_info(
skills
.iter()
.map(|skill| {
let enabled = !disabled_paths.contains(&skill.path);
let enabled = !disabled_paths.contains(&skill.path_to_skills_md);
codex_app_server_protocol::SkillMetadata {
name: skill.name.clone(),
description: skill.description.clone(),
@@ -6545,7 +6586,7 @@ fn skills_to_info(
.collect(),
}
}),
path: skill.path.clone(),
path: skill.path_to_skills_md.clone(),
scope: skill.scope.into(),
enabled,
}

View File

@@ -0,0 +1,106 @@
use crate::error_code::INTERNAL_ERROR_CODE;
use codex_app_server_protocol::ExternalAgentConfigDetectParams;
use codex_app_server_protocol::ExternalAgentConfigDetectResponse;
use codex_app_server_protocol::ExternalAgentConfigImportParams;
use codex_app_server_protocol::ExternalAgentConfigImportResponse;
use codex_app_server_protocol::ExternalAgentConfigMigrationItem;
use codex_app_server_protocol::ExternalAgentConfigMigrationItemType;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_core::external_agent_config::ExternalAgentConfigDetectOptions;
use codex_core::external_agent_config::ExternalAgentConfigMigrationItem as CoreMigrationItem;
use codex_core::external_agent_config::ExternalAgentConfigMigrationItemType as CoreMigrationItemType;
use codex_core::external_agent_config::ExternalAgentConfigService;
use std::io;
use std::path::PathBuf;
#[derive(Clone)]
pub(crate) struct ExternalAgentConfigApi {
migration_service: ExternalAgentConfigService,
}
impl ExternalAgentConfigApi {
pub(crate) fn new(codex_home: PathBuf) -> Self {
Self {
migration_service: ExternalAgentConfigService::new(codex_home),
}
}
pub(crate) async fn detect(
&self,
params: ExternalAgentConfigDetectParams,
) -> Result<ExternalAgentConfigDetectResponse, JSONRPCErrorError> {
let items = self
.migration_service
.detect(ExternalAgentConfigDetectOptions {
include_home: params.include_home,
cwds: params.cwds,
})
.map_err(map_io_error)?;
Ok(ExternalAgentConfigDetectResponse {
items: items
.into_iter()
.map(|migration_item| ExternalAgentConfigMigrationItem {
item_type: match migration_item.item_type {
CoreMigrationItemType::Config => {
ExternalAgentConfigMigrationItemType::Config
}
CoreMigrationItemType::Skills => {
ExternalAgentConfigMigrationItemType::Skills
}
CoreMigrationItemType::AgentsMd => {
ExternalAgentConfigMigrationItemType::AgentsMd
}
CoreMigrationItemType::McpServerConfig => {
ExternalAgentConfigMigrationItemType::McpServerConfig
}
},
description: migration_item.description,
cwd: migration_item.cwd,
})
.collect(),
})
}
pub(crate) async fn import(
&self,
params: ExternalAgentConfigImportParams,
) -> Result<ExternalAgentConfigImportResponse, JSONRPCErrorError> {
self.migration_service
.import(
params
.migration_items
.into_iter()
.map(|migration_item| CoreMigrationItem {
item_type: match migration_item.item_type {
ExternalAgentConfigMigrationItemType::Config => {
CoreMigrationItemType::Config
}
ExternalAgentConfigMigrationItemType::Skills => {
CoreMigrationItemType::Skills
}
ExternalAgentConfigMigrationItemType::AgentsMd => {
CoreMigrationItemType::AgentsMd
}
ExternalAgentConfigMigrationItemType::McpServerConfig => {
CoreMigrationItemType::McpServerConfig
}
},
description: migration_item.description,
cwd: migration_item.cwd,
})
.collect(),
)
.map_err(map_io_error)?;
Ok(ExternalAgentConfigImportResponse {})
}
}
fn map_io_error(err: io::Error) -> JSONRPCErrorError {
JSONRPCErrorError {
code: INTERNAL_ERROR_CODE,
message: err.to_string(),
data: None,
}
}

View File

@@ -1,5 +1,6 @@
#![deny(clippy::print_stdout, clippy::print_stderr)]
use codex_arg0::Arg0DispatchPaths;
use codex_cloud_requirements::cloud_requirements_loader;
use codex_core::AuthManager;
use codex_core::config::Config;
@@ -12,7 +13,6 @@ use std::collections::HashMap;
use std::collections::HashSet;
use std::io::ErrorKind;
use std::io::Result as IoResult;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::RwLock;
use std::sync::atomic::AtomicBool;
@@ -57,6 +57,7 @@ mod codex_message_processor;
mod config_api;
mod dynamic_tools;
mod error_code;
mod external_agent_config_api;
mod filters;
mod fuzzy_file_search;
mod message_processor;
@@ -94,10 +95,77 @@ enum OutboundControlEvent {
writer: mpsc::Sender<crate::outgoing_message::OutgoingMessage>,
disconnect_sender: Option<CancellationToken>,
initialized: Arc<AtomicBool>,
experimental_api_enabled: Arc<AtomicBool>,
opted_out_notification_methods: Arc<RwLock<HashSet<String>>>,
},
/// Remove state for a closed/disconnected connection.
Closed { connection_id: ConnectionId },
/// Disconnect all connection-oriented clients during graceful restart.
DisconnectAll,
}
#[derive(Default)]
struct ShutdownState {
requested: bool,
forced: bool,
last_logged_running_turn_count: Option<usize>,
}
enum ShutdownAction {
Noop,
Finish,
}
impl ShutdownState {
fn requested(&self) -> bool {
self.requested
}
fn forced(&self) -> bool {
self.forced
}
fn on_ctrl_c(&mut self, connection_count: usize, running_turn_count: usize) {
if self.requested {
self.forced = true;
return;
}
self.requested = true;
self.last_logged_running_turn_count = None;
info!(
"received Ctrl-C; entering graceful restart drain (connections={}, runningAssistantTurns={}, requests still accepted until no assistant turns are running)",
connection_count, running_turn_count,
);
}
fn update(&mut self, running_turn_count: usize, connection_count: usize) -> ShutdownAction {
if !self.requested {
return ShutdownAction::Noop;
}
if self.forced || running_turn_count == 0 {
if self.forced {
info!(
"received second Ctrl-C; forcing restart with {running_turn_count} running assistant turn(s) and {connection_count} connection(s)"
);
} else {
info!(
"Ctrl-C restart: no assistant turns running; stopping acceptor and disconnecting {connection_count} connection(s)"
);
}
return ShutdownAction::Finish;
}
if self.last_logged_running_turn_count != Some(running_turn_count) {
info!(
"Ctrl-C restart: waiting for {running_turn_count} running assistant turn(s) to finish"
);
self.last_logged_running_turn_count = Some(running_turn_count);
}
ShutdownAction::Noop
}
}
fn config_warning_from_error(
@@ -225,13 +293,13 @@ fn log_format_from_env() -> LogFormat {
}
pub async fn run_main(
codex_linux_sandbox_exe: Option<PathBuf>,
arg0_paths: Arg0DispatchPaths,
cli_config_overrides: CliConfigOverrides,
loader_overrides: LoaderOverrides,
default_analytics_enabled: bool,
) -> IoResult<()> {
run_main_with_transport(
codex_linux_sandbox_exe,
arg0_paths,
cli_config_overrides,
loader_overrides,
default_analytics_enabled,
@@ -241,7 +309,7 @@ pub async fn run_main(
}
pub async fn run_main_with_transport(
codex_linux_sandbox_exe: Option<PathBuf>,
arg0_paths: Arg0DispatchPaths,
cli_config_overrides: CliConfigOverrides,
loader_overrides: LoaderOverrides,
default_analytics_enabled: bool,
@@ -253,19 +321,37 @@ pub async fn run_main_with_transport(
let (outbound_control_tx, mut outbound_control_rx) =
mpsc::channel::<OutboundControlEvent>(CHANNEL_CAPACITY);
enum TransportRuntime {
Stdio,
WebSocket {
accept_handle: JoinHandle<()>,
shutdown_token: CancellationToken,
},
}
let mut stdio_handles = Vec::<JoinHandle<()>>::new();
let mut websocket_accept_handle = None;
match transport {
let transport_runtime = match transport {
AppServerTransport::Stdio => {
start_stdio_connection(transport_event_tx.clone(), &mut stdio_handles).await?;
TransportRuntime::Stdio
}
AppServerTransport::WebSocket { bind_address } => {
websocket_accept_handle =
Some(start_websocket_acceptor(bind_address, transport_event_tx.clone()).await?);
let shutdown_token = CancellationToken::new();
let accept_handle = start_websocket_acceptor(
bind_address,
transport_event_tx.clone(),
shutdown_token.clone(),
)
.await?;
TransportRuntime::WebSocket {
accept_handle,
shutdown_token,
}
}
}
let single_client_mode = matches!(transport, AppServerTransport::Stdio);
};
let single_client_mode = matches!(&transport_runtime, TransportRuntime::Stdio);
let shutdown_when_no_connections = single_client_mode;
let graceful_ctrl_c_restart_enabled = !single_client_mode;
// Parse CLI overrides once and derive the base Config eagerly so later
// components do not need to work with raw TOML values.
@@ -419,6 +505,7 @@ pub async fn run_main_with_transport(
writer,
disconnect_sender,
initialized,
experimental_api_enabled,
opted_out_notification_methods,
} => {
outbound_connections.insert(
@@ -426,6 +513,7 @@ pub async fn run_main_with_transport(
OutboundConnectionState::new(
writer,
initialized,
experimental_api_enabled,
opted_out_notification_methods,
disconnect_sender,
),
@@ -434,6 +522,16 @@ pub async fn run_main_with_transport(
OutboundControlEvent::Closed { connection_id } => {
outbound_connections.remove(&connection_id);
}
OutboundControlEvent::DisconnectAll => {
info!(
"disconnecting {} outbound websocket connection(s) for graceful restart",
outbound_connections.len()
);
for connection_state in outbound_connections.values() {
connection_state.request_disconnect();
}
outbound_connections.clear();
}
}
}
envelope = outgoing_rx.recv() => {
@@ -454,7 +552,7 @@ pub async fn run_main_with_transport(
let loader_overrides = loader_overrides_for_config_api;
let mut processor = MessageProcessor::new(MessageProcessorArgs {
outgoing: outgoing_message_sender,
codex_linux_sandbox_exe,
arg0_paths,
config: Arc::new(config),
single_client_mode,
cli_overrides,
@@ -464,11 +562,46 @@ pub async fn run_main_with_transport(
config_warnings,
});
let mut thread_created_rx = processor.thread_created_receiver();
let mut running_turn_count_rx = processor.subscribe_running_assistant_turn_count();
let mut connections = HashMap::<ConnectionId, ConnectionState>::new();
let websocket_accept_shutdown = match &transport_runtime {
TransportRuntime::WebSocket { shutdown_token, .. } => Some(shutdown_token.clone()),
TransportRuntime::Stdio => None,
};
async move {
let mut listen_for_threads = true;
let mut shutdown_state = ShutdownState::default();
loop {
let running_turn_count = {
let running_turn_count = running_turn_count_rx.borrow();
*running_turn_count
};
if matches!(
shutdown_state.update(running_turn_count, connections.len()),
ShutdownAction::Finish
) {
if let Some(shutdown_token) = &websocket_accept_shutdown {
shutdown_token.cancel();
}
let _ = outbound_control_tx
.send(OutboundControlEvent::DisconnectAll)
.await;
break;
}
tokio::select! {
ctrl_c_result = tokio::signal::ctrl_c(), if graceful_ctrl_c_restart_enabled && !shutdown_state.forced() => {
if let Err(err) = ctrl_c_result {
warn!("failed to listen for Ctrl-C during graceful restart drain: {err}");
}
let running_turn_count = *running_turn_count_rx.borrow();
shutdown_state.on_ctrl_c(connections.len(), running_turn_count);
}
changed = running_turn_count_rx.changed(), if graceful_ctrl_c_restart_enabled && shutdown_state.requested() => {
if changed.is_err() {
warn!("running-turn watcher closed during graceful restart drain");
}
}
event = transport_event_rx.recv() => {
let Some(event) = event else {
break;
@@ -480,6 +613,8 @@ pub async fn run_main_with_transport(
disconnect_sender,
} => {
let outbound_initialized = Arc::new(AtomicBool::new(false));
let outbound_experimental_api_enabled =
Arc::new(AtomicBool::new(false));
let outbound_opted_out_notification_methods =
Arc::new(RwLock::new(HashSet::new()));
if outbound_control_tx
@@ -488,6 +623,9 @@ pub async fn run_main_with_transport(
writer,
disconnect_sender,
initialized: Arc::clone(&outbound_initialized),
experimental_api_enabled: Arc::clone(
&outbound_experimental_api_enabled,
),
opted_out_notification_methods: Arc::clone(
&outbound_opted_out_notification_methods,
),
@@ -501,6 +639,7 @@ pub async fn run_main_with_transport(
connection_id,
ConnectionState::new(
outbound_initialized,
outbound_experimental_api_enabled,
outbound_opted_out_notification_methods,
),
);
@@ -550,6 +689,12 @@ pub async fn run_main_with_transport(
"failed to update outbound opted-out notifications"
);
}
connection_state
.outbound_experimental_api_enabled
.store(
connection_state.session.experimental_api_enabled,
std::sync::atomic::Ordering::Release,
);
if !was_initialized && connection_state.session.initialized {
processor.send_initialize_notifications().await;
}
@@ -619,8 +764,13 @@ pub async fn run_main_with_transport(
let _ = processor_handle.await;
let _ = outbound_handle.await;
if let Some(handle) = websocket_accept_handle {
handle.abort();
if let TransportRuntime::WebSocket {
accept_handle,
shutdown_token,
} = transport_runtime
{
shutdown_token.cancel();
let _ = accept_handle.await;
}
for handle in stdio_handles {

View File

@@ -1,6 +1,7 @@
use clap::Parser;
use codex_app_server::AppServerTransport;
use codex_app_server::run_main_with_transport;
use codex_arg0::Arg0DispatchPaths;
use codex_arg0::arg0_dispatch_or_else;
use codex_core::config_loader::LoaderOverrides;
use codex_utils_cli::CliConfigOverrides;
@@ -23,12 +24,7 @@ struct AppServerArgs {
}
fn main() -> anyhow::Result<()> {
arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move {
// Run wrapper mode only after arg0 dispatch so `codex-linux-sandbox`
// invocations don't get misclassified as zsh exec-wrapper calls.
if codex_core::maybe_run_zsh_exec_wrapper_mode()? {
return Ok(());
}
arg0_dispatch_or_else(|arg0_paths: Arg0DispatchPaths| async move {
let args = AppServerArgs::parse();
let managed_config_path = managed_config_path_from_debug_env();
let loader_overrides = LoaderOverrides {
@@ -38,7 +34,7 @@ fn main() -> anyhow::Result<()> {
let transport = args.listen;
run_main_with_transport(
codex_linux_sandbox_exe,
arg0_paths,
CliConfigOverrides::default(),
loader_overrides,
false,

View File

@@ -1,5 +1,4 @@
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::RwLock;
use std::sync::atomic::AtomicBool;
@@ -9,6 +8,7 @@ use crate::codex_message_processor::CodexMessageProcessor;
use crate::codex_message_processor::CodexMessageProcessorArgs;
use crate::config_api::ConfigApi;
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
use crate::external_agent_config_api::ExternalAgentConfigApi;
use crate::outgoing_message::ConnectionId;
use crate::outgoing_message::ConnectionRequestId;
use crate::outgoing_message::OutgoingMessageSender;
@@ -23,6 +23,8 @@ use codex_app_server_protocol::ConfigReadParams;
use codex_app_server_protocol::ConfigValueWriteParams;
use codex_app_server_protocol::ConfigWarningNotification;
use codex_app_server_protocol::ExperimentalApi;
use codex_app_server_protocol::ExternalAgentConfigDetectParams;
use codex_app_server_protocol::ExternalAgentConfigImportParams;
use codex_app_server_protocol::InitializeResponse;
use codex_app_server_protocol::JSONRPCError;
use codex_app_server_protocol::JSONRPCErrorError;
@@ -32,6 +34,7 @@ use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerRequestPayload;
use codex_app_server_protocol::experimental_required_message;
use codex_arg0::Arg0DispatchPaths;
use codex_core::AuthManager;
use codex_core::ThreadManager;
use codex_core::auth::ExternalAuthRefreshContext;
@@ -49,7 +52,9 @@ use codex_core::default_client::set_default_originator;
use codex_feedback::CodexFeedback;
use codex_protocol::ThreadId;
use codex_protocol::protocol::SessionSource;
use futures::FutureExt;
use tokio::sync::broadcast;
use tokio::sync::watch;
use tokio::time::Duration;
use tokio::time::timeout;
use toml::Value as TomlValue;
@@ -124,6 +129,7 @@ pub(crate) struct MessageProcessor {
outgoing: Arc<OutgoingMessageSender>,
codex_message_processor: CodexMessageProcessor,
config_api: ConfigApi,
external_agent_config_api: ExternalAgentConfigApi,
config: Arc<Config>,
config_warnings: Arc<Vec<ConfigWarningNotification>>,
}
@@ -131,13 +137,13 @@ pub(crate) struct MessageProcessor {
#[derive(Clone, Debug, Default)]
pub(crate) struct ConnectionSessionState {
pub(crate) initialized: bool,
experimental_api_enabled: bool,
pub(crate) experimental_api_enabled: bool,
pub(crate) opted_out_notification_methods: HashSet<String>,
}
pub(crate) struct MessageProcessorArgs {
pub(crate) outgoing: Arc<OutgoingMessageSender>,
pub(crate) codex_linux_sandbox_exe: Option<PathBuf>,
pub(crate) arg0_paths: Arg0DispatchPaths,
pub(crate) config: Arc<Config>,
pub(crate) single_client_mode: bool,
pub(crate) cli_overrides: Vec<(String, TomlValue)>,
@@ -153,7 +159,7 @@ impl MessageProcessor {
pub(crate) fn new(args: MessageProcessorArgs) -> Self {
let MessageProcessorArgs {
outgoing,
codex_linux_sandbox_exe,
arg0_paths,
config,
single_client_mode,
cli_overrides,
@@ -182,7 +188,7 @@ impl MessageProcessor {
auth_manager,
thread_manager,
outgoing: outgoing.clone(),
codex_linux_sandbox_exe,
arg0_paths,
config: Arc::clone(&config),
cli_overrides: cli_overrides.clone(),
cloud_requirements: cloud_requirements.clone(),
@@ -195,11 +201,13 @@ impl MessageProcessor {
loader_overrides,
cloud_requirements,
);
let external_agent_config_api = ExternalAgentConfigApi::new(config.codex_home.clone());
Self {
outgoing,
codex_message_processor,
config_api,
external_agent_config_api,
config,
config_warnings: Arc::new(config_warnings),
}
@@ -212,6 +220,12 @@ impl MessageProcessor {
session: &mut ConnectionSessionState,
outbound_initialized: &AtomicBool,
) {
let request_method = request.method.as_str();
tracing::trace!(
?connection_id,
request_id = ?request.id,
"app-server request: {request_method}"
);
let request_id = ConnectionRequestId {
connection_id,
request_id: request.id.clone(),
@@ -355,6 +369,26 @@ impl MessageProcessor {
)
.await;
}
ClientRequest::ExternalAgentConfigDetect { request_id, params } => {
self.handle_external_agent_config_detect(
ConnectionRequestId {
connection_id,
request_id,
},
params,
)
.await;
}
ClientRequest::ExternalAgentConfigImport { request_id, params } => {
self.handle_external_agent_config_import(
ConnectionRequestId {
connection_id,
request_id,
},
params,
)
.await;
}
ClientRequest::ConfigValueWrite { request_id, params } => {
self.handle_config_value_write(
ConnectionRequestId {
@@ -386,8 +420,12 @@ impl MessageProcessor {
.await;
}
other => {
// Box the delegated future so this wrapper's async state machine does not
// inline the full `CodexMessageProcessor::process_request` future, which
// can otherwise push worker-thread stack usage over the edge.
self.codex_message_processor
.process_request(connection_id, other)
.boxed()
.await;
}
}
@@ -427,6 +465,11 @@ impl MessageProcessor {
.await;
}
pub(crate) fn subscribe_running_assistant_turn_count(&self) -> watch::Receiver<usize> {
self.codex_message_processor
.subscribe_running_assistant_turn_count()
}
/// Handle a standalone JSON-RPC response originating from the peer.
pub(crate) async fn process_response(&mut self, response: JSONRPCResponse) {
tracing::info!("<- response: {:?}", response);
@@ -475,4 +518,26 @@ impl MessageProcessor {
Err(error) => self.outgoing.send_error(request_id, error).await,
}
}
async fn handle_external_agent_config_detect(
&self,
request_id: ConnectionRequestId,
params: ExternalAgentConfigDetectParams,
) {
match self.external_agent_config_api.detect(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_external_agent_config_import(
&self,
request_id: ConnectionRequestId,
params: ExternalAgentConfigImportParams,
) {
match self.external_agent_config_api.import(params).await {
Ok(response) => self.outgoing.send_response(request_id, response).await,
Err(error) => self.outgoing.send_error(request_id, error).await,
}
}
}

View File

@@ -275,6 +275,10 @@ impl OutgoingMessageSender {
connection_ids: &[ConnectionId],
notification: ServerNotification,
) {
tracing::trace!(
targeted_connections = connection_ids.len(),
"app-server event: {notification}"
);
let outgoing_message = OutgoingMessage::AppServerNotification(notification);
if connection_ids.is_empty() {
if let Err(err) = self

View File

@@ -15,11 +15,13 @@ use std::sync::Arc;
use tokio::sync::Mutex;
#[cfg(test)]
use tokio::sync::mpsc;
use tokio::sync::watch;
#[derive(Clone)]
pub(crate) struct ThreadWatchManager {
state: Arc<Mutex<ThreadWatchState>>,
outgoing: Option<Arc<OutgoingMessageSender>>,
running_turn_count_tx: watch::Sender<usize>,
}
pub(crate) struct ThreadWatchActiveGuard {
@@ -71,16 +73,20 @@ impl Default for ThreadWatchManager {
impl ThreadWatchManager {
pub(crate) fn new() -> Self {
let (running_turn_count_tx, _running_turn_count_rx) = watch::channel(0);
Self {
state: Arc::new(Mutex::new(ThreadWatchState::default())),
outgoing: None,
running_turn_count_tx,
}
}
pub(crate) fn new_with_outgoing(outgoing: Arc<OutgoingMessageSender>) -> Self {
let (running_turn_count_tx, _running_turn_count_rx) = watch::channel(0);
Self {
state: Arc::new(Mutex::new(ThreadWatchState::default())),
outgoing: Some(outgoing),
running_turn_count_tx,
}
}
@@ -113,6 +119,21 @@ impl ThreadWatchManager {
.collect()
}
#[cfg(test)]
pub(crate) async fn running_turn_count(&self) -> usize {
self.state
.lock()
.await
.runtime_by_thread_id
.values()
.filter(|runtime| runtime.running)
.count()
}
pub(crate) fn subscribe_running_turn_count(&self) -> watch::Receiver<usize> {
self.running_turn_count_tx.subscribe()
}
pub(crate) async fn note_turn_started(&self, thread_id: &str) {
self.update_runtime_for_thread(thread_id, |runtime| {
runtime.is_loaded = true;
@@ -193,10 +214,17 @@ impl ThreadWatchManager {
where
F: FnOnce(&mut ThreadWatchState) -> Option<ThreadStatusChangedNotification>,
{
let notification = {
let (notification, running_turn_count) = {
let mut state = self.state.lock().await;
mutate(&mut state)
let notification = mutate(&mut state);
let running_turn_count = state
.runtime_by_thread_id
.values()
.filter(|runtime| runtime.running)
.count();
(notification, running_turn_count)
};
let _ = self.running_turn_count_tx.send(running_turn_count);
if let Some(notification) = notification
&& let Some(outgoing) = &self.outgoing
@@ -588,6 +616,32 @@ mod tests {
);
}
#[tokio::test]
async fn has_running_turns_tracks_runtime_running_flag_only() {
let manager = ThreadWatchManager::new();
manager
.upsert_thread(test_thread(
INTERACTIVE_THREAD_ID,
codex_app_server_protocol::SessionSource::Cli,
))
.await;
assert_eq!(manager.running_turn_count().await, 0);
let _permission_guard = manager
.note_permission_requested(INTERACTIVE_THREAD_ID)
.await;
assert_eq!(manager.running_turn_count().await, 0);
manager.note_turn_started(INTERACTIVE_THREAD_ID).await;
assert_eq!(manager.running_turn_count().await, 1);
manager
.note_turn_completed(INTERACTIVE_THREAD_ID, false)
.await;
assert_eq!(manager.running_turn_count().await, 0);
}
#[tokio::test]
async fn status_change_emits_notification() {
let (outgoing_tx, mut outgoing_rx) = mpsc::channel(8);

View File

@@ -6,6 +6,7 @@ use crate::outgoing_message::OutgoingError;
use crate::outgoing_message::OutgoingMessage;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::ServerRequest;
use futures::SinkExt;
use futures::StreamExt;
use owo_colors::OwoColorize;
@@ -30,8 +31,9 @@ use tokio::net::TcpListener;
use tokio::net::TcpStream;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use tokio_tungstenite::accept_async;
use tokio_tungstenite::accept_async_with_config;
use tokio_tungstenite::tungstenite::Message as WebSocketMessage;
use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
use tokio_util::sync::CancellationToken;
use tracing::debug;
use tracing::error;
@@ -67,12 +69,6 @@ fn print_websocket_startup_banner(addr: SocketAddr) {
}
}
#[allow(clippy::print_stderr)]
fn print_websocket_connection(peer_addr: SocketAddr) {
let connected_label = colorize("websocket client connected from", Style::new().dimmed());
eprintln!("{connected_label} {peer_addr}");
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AppServerTransport {
Stdio,
@@ -149,6 +145,7 @@ pub(crate) enum TransportEvent {
pub(crate) struct ConnectionState {
pub(crate) outbound_initialized: Arc<AtomicBool>,
pub(crate) outbound_experimental_api_enabled: Arc<AtomicBool>,
pub(crate) outbound_opted_out_notification_methods: Arc<RwLock<HashSet<String>>>,
pub(crate) session: ConnectionSessionState,
}
@@ -156,10 +153,12 @@ pub(crate) struct ConnectionState {
impl ConnectionState {
pub(crate) fn new(
outbound_initialized: Arc<AtomicBool>,
outbound_experimental_api_enabled: Arc<AtomicBool>,
outbound_opted_out_notification_methods: Arc<RwLock<HashSet<String>>>,
) -> Self {
Self {
outbound_initialized,
outbound_experimental_api_enabled,
outbound_opted_out_notification_methods,
session: ConnectionSessionState::default(),
}
@@ -168,6 +167,7 @@ impl ConnectionState {
pub(crate) struct OutboundConnectionState {
pub(crate) initialized: Arc<AtomicBool>,
pub(crate) experimental_api_enabled: Arc<AtomicBool>,
pub(crate) opted_out_notification_methods: Arc<RwLock<HashSet<String>>>,
pub(crate) writer: mpsc::Sender<OutgoingMessage>,
disconnect_sender: Option<CancellationToken>,
@@ -177,11 +177,13 @@ impl OutboundConnectionState {
pub(crate) fn new(
writer: mpsc::Sender<OutgoingMessage>,
initialized: Arc<AtomicBool>,
experimental_api_enabled: Arc<AtomicBool>,
opted_out_notification_methods: Arc<RwLock<HashSet<String>>>,
disconnect_sender: Option<CancellationToken>,
) -> Self {
Self {
initialized,
experimental_api_enabled,
opted_out_notification_methods,
writer,
disconnect_sender,
@@ -192,7 +194,7 @@ impl OutboundConnectionState {
self.disconnect_sender.is_some()
}
fn request_disconnect(&self) {
pub(crate) fn request_disconnect(&self) {
if let Some(disconnect_sender) = &self.disconnect_sender {
disconnect_sender.cancel();
}
@@ -270,6 +272,7 @@ pub(crate) async fn start_stdio_connection(
pub(crate) async fn start_websocket_acceptor(
bind_address: SocketAddr,
transport_event_tx: mpsc::Sender<TransportEvent>,
shutdown_token: CancellationToken,
) -> IoResult<JoinHandle<()>> {
let listener = TcpListener::bind(bind_address).await?;
let local_addr = listener.local_addr()?;
@@ -279,23 +282,31 @@ pub(crate) async fn start_websocket_acceptor(
let connection_counter = Arc::new(AtomicU64::new(1));
Ok(tokio::spawn(async move {
loop {
match listener.accept().await {
Ok((stream, peer_addr)) => {
print_websocket_connection(peer_addr);
let connection_id =
ConnectionId(connection_counter.fetch_add(1, Ordering::Relaxed));
let transport_event_tx_for_connection = transport_event_tx.clone();
tokio::spawn(async move {
run_websocket_connection(
connection_id,
stream,
transport_event_tx_for_connection,
)
.await;
});
tokio::select! {
_ = shutdown_token.cancelled() => {
info!("websocket acceptor shutting down");
break;
}
Err(err) => {
error!("failed to accept websocket connection: {err}");
accept_result = listener.accept() => {
match accept_result {
Ok((stream, peer_addr)) => {
info!(%peer_addr, "websocket client connected");
let connection_id =
ConnectionId(connection_counter.fetch_add(1, Ordering::Relaxed));
let transport_event_tx_for_connection = transport_event_tx.clone();
tokio::spawn(async move {
run_websocket_connection(
connection_id,
stream,
transport_event_tx_for_connection,
)
.await;
});
}
Err(err) => {
error!("failed to accept websocket connection: {err}");
}
}
}
}
}
@@ -307,13 +318,14 @@ async fn run_websocket_connection(
stream: TcpStream,
transport_event_tx: mpsc::Sender<TransportEvent>,
) {
let websocket_stream = match accept_async(stream).await {
Ok(stream) => stream,
Err(err) => {
warn!("failed to complete websocket handshake: {err}");
return;
}
};
let websocket_stream =
match accept_async_with_config(stream, Some(WebSocketConfig::default())).await {
Ok(stream) => stream,
Err(err) => {
warn!("failed to complete websocket handshake: {err}");
return;
}
};
let (writer_tx, writer_rx) = mpsc::channel::<OutgoingMessage>(CHANNEL_CAPACITY);
let writer_tx_for_reader = writer_tx.clone();
@@ -572,6 +584,7 @@ async fn send_message_to_connection(
warn!("dropping message for disconnected connection: {connection_id:?}");
return false;
};
let message = filter_outgoing_message_for_connection(connection_state, message);
if should_skip_notification_for_connection(connection_state, &message) {
return false;
}
@@ -597,6 +610,30 @@ async fn send_message_to_connection(
}
}
fn filter_outgoing_message_for_connection(
connection_state: &OutboundConnectionState,
message: OutgoingMessage,
) -> OutgoingMessage {
let experimental_api_enabled = connection_state
.experimental_api_enabled
.load(Ordering::Acquire);
match message {
OutgoingMessage::Request(ServerRequest::CommandExecutionRequestApproval {
request_id,
mut params,
}) => {
if !experimental_api_enabled {
params.strip_experimental_fields();
}
OutgoingMessage::Request(ServerRequest::CommandExecutionRequestApproval {
request_id,
params,
})
}
_ => message,
}
}
pub(crate) async fn route_outgoing_envelope(
connections: &mut HashMap<ConnectionId, OutboundConnectionState>,
envelope: OutgoingEnvelope,
@@ -636,6 +673,7 @@ mod tests {
use crate::error_code::OVERLOADED_ERROR_CODE;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::path::PathBuf;
use tokio::time::Duration;
use tokio::time::timeout;
@@ -875,6 +913,7 @@ mod tests {
OutboundConnectionState::new(
writer_tx,
initialized,
Arc::new(AtomicBool::new(true)),
opted_out_notification_methods,
None,
),
@@ -900,6 +939,138 @@ mod tests {
);
}
#[tokio::test]
async fn command_execution_request_approval_strips_experimental_fields_without_capability() {
let connection_id = ConnectionId(8);
let (writer_tx, mut writer_rx) = mpsc::channel(1);
let mut connections = HashMap::new();
connections.insert(
connection_id,
OutboundConnectionState::new(
writer_tx,
Arc::new(AtomicBool::new(true)),
Arc::new(AtomicBool::new(false)),
Arc::new(RwLock::new(HashSet::new())),
None,
),
);
route_outgoing_envelope(
&mut connections,
OutgoingEnvelope::ToConnection {
connection_id,
message: OutgoingMessage::Request(ServerRequest::CommandExecutionRequestApproval {
request_id: codex_app_server_protocol::RequestId::Integer(1),
params: codex_app_server_protocol::CommandExecutionRequestApprovalParams {
thread_id: "thr_123".to_string(),
turn_id: "turn_123".to_string(),
item_id: "call_123".to_string(),
approval_id: None,
reason: Some("Need extra read access".to_string()),
network_approval_context: None,
command: Some("cat file".to_string()),
cwd: Some(PathBuf::from("/tmp")),
command_actions: None,
additional_permissions: Some(
codex_app_server_protocol::AdditionalPermissionProfile {
network: None,
file_system: Some(
codex_app_server_protocol::AdditionalFileSystemPermissions {
read: Some(vec![PathBuf::from("/tmp/allowed")]),
write: None,
},
),
macos: None,
},
),
proposed_execpolicy_amendment: None,
proposed_network_policy_amendments: None,
},
}),
},
)
.await;
let message = writer_rx
.recv()
.await
.expect("request should be delivered to the connection");
let json = serde_json::to_value(message).expect("request should serialize");
assert_eq!(json["params"].get("additionalPermissions"), None);
}
#[tokio::test]
async fn command_execution_request_approval_keeps_experimental_fields_with_capability() {
let connection_id = ConnectionId(9);
let (writer_tx, mut writer_rx) = mpsc::channel(1);
let mut connections = HashMap::new();
connections.insert(
connection_id,
OutboundConnectionState::new(
writer_tx,
Arc::new(AtomicBool::new(true)),
Arc::new(AtomicBool::new(true)),
Arc::new(RwLock::new(HashSet::new())),
None,
),
);
route_outgoing_envelope(
&mut connections,
OutgoingEnvelope::ToConnection {
connection_id,
message: OutgoingMessage::Request(ServerRequest::CommandExecutionRequestApproval {
request_id: codex_app_server_protocol::RequestId::Integer(1),
params: codex_app_server_protocol::CommandExecutionRequestApprovalParams {
thread_id: "thr_123".to_string(),
turn_id: "turn_123".to_string(),
item_id: "call_123".to_string(),
approval_id: None,
reason: Some("Need extra read access".to_string()),
network_approval_context: None,
command: Some("cat file".to_string()),
cwd: Some(PathBuf::from("/tmp")),
command_actions: None,
additional_permissions: Some(
codex_app_server_protocol::AdditionalPermissionProfile {
network: None,
file_system: Some(
codex_app_server_protocol::AdditionalFileSystemPermissions {
read: Some(vec![PathBuf::from("/tmp/allowed")]),
write: None,
},
),
macos: None,
},
),
proposed_execpolicy_amendment: None,
proposed_network_policy_amendments: None,
},
}),
},
)
.await;
let message = writer_rx
.recv()
.await
.expect("request should be delivered to the connection");
let json = serde_json::to_value(message).expect("request should serialize");
assert_eq!(
json["params"]["additionalPermissions"],
json!({
"network": null,
"fileSystem": {
"read": ["/tmp/allowed"],
"write": null,
},
"macos": null,
})
);
}
#[tokio::test]
async fn broadcast_does_not_block_on_slow_connection() {
let fast_connection_id = ConnectionId(1);
@@ -916,6 +1087,7 @@ mod tests {
OutboundConnectionState::new(
fast_writer_tx,
Arc::new(AtomicBool::new(true)),
Arc::new(AtomicBool::new(true)),
Arc::new(RwLock::new(HashSet::new())),
Some(fast_disconnect_token.clone()),
),
@@ -925,6 +1097,7 @@ mod tests {
OutboundConnectionState::new(
slow_writer_tx.clone(),
Arc::new(AtomicBool::new(true)),
Arc::new(AtomicBool::new(true)),
Arc::new(RwLock::new(HashSet::new())),
Some(slow_disconnect_token.clone()),
),
@@ -1001,6 +1174,7 @@ mod tests {
OutboundConnectionState::new(
writer_tx,
Arc::new(AtomicBool::new(true)),
Arc::new(AtomicBool::new(true)),
Arc::new(RwLock::new(HashSet::new())),
None,
),

View File

@@ -105,6 +105,7 @@ impl McpProcess {
cmd.stdin(Stdio::piped());
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
cmd.current_dir(codex_home);
cmd.env("CODEX_HOME", codex_home);
cmd.env("RUST_LOG", "debug");
cmd.env_remove(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR);

View File

@@ -24,7 +24,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
} else {
ModelVisibility::Hide
},
supported_in_api: true,
supported_in_api: preset.supported_in_api,
priority,
upgrade: preset.upgrade.as_ref().map(|u| u.into()),
base_instructions: "base instructions".to_string(),
@@ -48,9 +48,9 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
/// Write a models_cache.json file to the codex home directory.
/// This prevents ModelsManager from making network requests to refresh models.
/// The cache will be treated as fresh (within TTL) and used instead of fetching from the network.
/// Uses the built-in model presets from ModelsManager, converted to ModelInfo format.
/// Uses bundled-catalog-derived presets, converted to ModelInfo format.
pub fn write_models_cache(codex_home: &Path) -> std::io::Result<()> {
// Get all presets and filter for show_in_picker (same as builtin_model_presets does)
// Get a stable bundled-catalog-derived preset list and filter for picker-visible entries.
let presets: Vec<&ModelPreset> = all_model_presets()
.iter()
.filter(|preset| preset.show_in_picker)

View File

@@ -3,7 +3,7 @@
// This is an instance of the fork of Bash that we bundle with
// https://www.npmjs.com/package/@openai/codex-shell-tool-mcp.
// Fetching the prebuilt version via DotSlash makes it easier to write
// integration tests for the MCP server.
// integration tests for shell execution flows.
//
// TODO(mbolin): Currently, we use a .tgz artifact that includes binaries for
// multiple platforms, but we could save a bit of space by making arch-specific

View File

@@ -478,6 +478,7 @@ fn assert_permissions_message(item: &ResponseItem) {
AskForApproval::Never,
&Policy::empty(),
&PathBuf::from("/tmp"),
false,
)
.into_text();
assert_eq!(

View File

@@ -28,9 +28,9 @@ use tokio_tungstenite::WebSocketStream;
use tokio_tungstenite::connect_async;
use tokio_tungstenite::tungstenite::Message as WebSocketMessage;
const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(5);
pub(super) const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(5);
type WsClient = WebSocketStream<MaybeTlsStream<tokio::net::TcpStream>>;
pub(super) type WsClient = WebSocketStream<MaybeTlsStream<tokio::net::TcpStream>>;
#[tokio::test]
async fn websocket_transport_routes_per_connection_handshake_and_responses() -> Result<()> {
@@ -78,7 +78,10 @@ async fn websocket_transport_routes_per_connection_handshake_and_responses() ->
Ok(())
}
async fn spawn_websocket_server(codex_home: &Path, bind_addr: SocketAddr) -> Result<Child> {
pub(super) async fn spawn_websocket_server(
codex_home: &Path,
bind_addr: SocketAddr,
) -> Result<Child> {
let program = codex_utils_cargo_bin::cargo_bin("codex-app-server")
.context("should find app-server binary")?;
let mut cmd = Command::new(program);
@@ -106,14 +109,14 @@ async fn spawn_websocket_server(codex_home: &Path, bind_addr: SocketAddr) -> Res
Ok(process)
}
fn reserve_local_addr() -> Result<SocketAddr> {
pub(super) fn reserve_local_addr() -> Result<SocketAddr> {
let listener = std::net::TcpListener::bind("127.0.0.1:0")?;
let addr = listener.local_addr()?;
drop(listener);
Ok(addr)
}
async fn connect_websocket(bind_addr: SocketAddr) -> Result<WsClient> {
pub(super) async fn connect_websocket(bind_addr: SocketAddr) -> Result<WsClient> {
let url = format!("ws://{bind_addr}");
let deadline = Instant::now() + Duration::from_secs(10);
loop {
@@ -129,7 +132,11 @@ async fn connect_websocket(bind_addr: SocketAddr) -> Result<WsClient> {
}
}
async fn send_initialize_request(stream: &mut WsClient, id: i64, client_name: &str) -> Result<()> {
pub(super) async fn send_initialize_request(
stream: &mut WsClient,
id: i64,
client_name: &str,
) -> Result<()> {
let params = InitializeParams {
client_info: ClientInfo {
name: client_name.to_string(),
@@ -157,7 +164,7 @@ async fn send_config_read_request(stream: &mut WsClient, id: i64) -> Result<()>
.await
}
async fn send_request(
pub(super) async fn send_request(
stream: &mut WsClient,
method: &str,
id: i64,
@@ -179,7 +186,10 @@ async fn send_jsonrpc(stream: &mut WsClient, message: JSONRPCMessage) -> Result<
.context("failed to send websocket frame")
}
async fn read_response_for_id(stream: &mut WsClient, id: i64) -> Result<JSONRPCResponse> {
pub(super) async fn read_response_for_id(
stream: &mut WsClient,
id: i64,
) -> Result<JSONRPCResponse> {
let target_id = RequestId::Integer(id);
loop {
let message = read_jsonrpc_message(stream).await?;
@@ -235,7 +245,7 @@ async fn assert_no_message(stream: &mut WsClient, wait_for: Duration) -> Result<
}
}
fn create_config_toml(
pub(super) fn create_config_toml(
codex_home: &Path,
server_uri: &str,
approval_policy: &str,

View File

@@ -0,0 +1,237 @@
use super::connection_handling_websocket::DEFAULT_READ_TIMEOUT;
use super::connection_handling_websocket::WsClient;
use super::connection_handling_websocket::connect_websocket;
use super::connection_handling_websocket::create_config_toml;
use super::connection_handling_websocket::read_response_for_id;
use super::connection_handling_websocket::reserve_local_addr;
use super::connection_handling_websocket::send_initialize_request;
use super::connection_handling_websocket::send_request;
use super::connection_handling_websocket::spawn_websocket_server;
use anyhow::Context;
use anyhow::Result;
use anyhow::bail;
use app_test_support::create_final_assistant_message_sse_response;
use app_test_support::to_response;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
use codex_app_server_protocol::TurnStartParams;
use codex_app_server_protocol::UserInput as V2UserInput;
use core_test_support::responses;
use futures::SinkExt;
use futures::StreamExt;
use std::process::Command as StdCommand;
use tempfile::TempDir;
use tokio::process::Child;
use tokio::time::Duration;
use tokio::time::Instant;
use tokio::time::sleep;
use tokio::time::timeout;
use tokio_tungstenite::tungstenite::Message as WebSocketMessage;
use wiremock::Mock;
use wiremock::matchers::method;
use wiremock::matchers::path_regex;
#[tokio::test]
async fn websocket_transport_ctrl_c_waits_for_running_turn_before_exit() -> Result<()> {
let GracefulCtrlCFixture {
_codex_home,
_server,
mut process,
mut ws,
} = start_ctrl_c_restart_fixture(Duration::from_secs(3)).await?;
send_sigint(&process)?;
assert_process_does_not_exit_within(&mut process, Duration::from_millis(300)).await?;
let status = wait_for_process_exit_within(
&mut process,
Duration::from_secs(10),
"timed out waiting for graceful Ctrl-C restart shutdown",
)
.await?;
assert!(status.success(), "expected graceful exit, got {status}");
expect_websocket_disconnect(&mut ws).await?;
Ok(())
}
#[tokio::test]
async fn websocket_transport_second_ctrl_c_forces_exit_while_turn_running() -> Result<()> {
let GracefulCtrlCFixture {
_codex_home,
_server,
mut process,
mut ws,
} = start_ctrl_c_restart_fixture(Duration::from_secs(3)).await?;
send_sigint(&process)?;
assert_process_does_not_exit_within(&mut process, Duration::from_millis(300)).await?;
send_sigint(&process)?;
let status = wait_for_process_exit_within(
&mut process,
Duration::from_secs(2),
"timed out waiting for forced Ctrl-C restart shutdown",
)
.await?;
assert!(status.success(), "expected graceful exit, got {status}");
expect_websocket_disconnect(&mut ws).await?;
Ok(())
}
struct GracefulCtrlCFixture {
_codex_home: TempDir,
_server: wiremock::MockServer,
process: Child,
ws: WsClient,
}
async fn start_ctrl_c_restart_fixture(turn_delay: Duration) -> Result<GracefulCtrlCFixture> {
let server = responses::start_mock_server().await;
let delayed_turn_response = create_final_assistant_message_sse_response("Done")?;
Mock::given(method("POST"))
.and(path_regex(".*/responses$"))
.respond_with(responses::sse_response(delayed_turn_response).set_delay(turn_delay))
.up_to_n_times(1)
.mount(&server)
.await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri(), "never")?;
let bind_addr = reserve_local_addr()?;
let process = spawn_websocket_server(codex_home.path(), bind_addr).await?;
let mut ws = connect_websocket(bind_addr).await?;
send_initialize_request(&mut ws, 1, "ws_graceful_shutdown").await?;
let init_response = read_response_for_id(&mut ws, 1).await?;
assert_eq!(init_response.id, RequestId::Integer(1));
send_thread_start_request(&mut ws, 2).await?;
let thread_start_response = read_response_for_id(&mut ws, 2).await?;
let ThreadStartResponse { thread, .. } = to_response(thread_start_response)?;
send_turn_start_request(&mut ws, 3, &thread.id).await?;
let turn_start_response = read_response_for_id(&mut ws, 3).await?;
assert_eq!(turn_start_response.id, RequestId::Integer(3));
wait_for_responses_post(&server, Duration::from_secs(5)).await?;
Ok(GracefulCtrlCFixture {
_codex_home: codex_home,
_server: server,
process,
ws,
})
}
async fn send_thread_start_request(stream: &mut WsClient, id: i64) -> Result<()> {
send_request(
stream,
"thread/start",
id,
Some(serde_json::to_value(ThreadStartParams {
model: Some("mock-model".to_string()),
..Default::default()
})?),
)
.await
}
async fn send_turn_start_request(stream: &mut WsClient, id: i64, thread_id: &str) -> Result<()> {
send_request(
stream,
"turn/start",
id,
Some(serde_json::to_value(TurnStartParams {
thread_id: thread_id.to_string(),
input: vec![V2UserInput::Text {
text: "Hello".to_string(),
text_elements: Vec::new(),
}],
..Default::default()
})?),
)
.await
}
async fn wait_for_responses_post(server: &wiremock::MockServer, wait_for: Duration) -> Result<()> {
let deadline = Instant::now() + wait_for;
loop {
let requests = server
.received_requests()
.await
.context("failed to read mock server requests")?;
if requests
.iter()
.any(|request| request.method == "POST" && request.url.path().ends_with("/responses"))
{
return Ok(());
}
if Instant::now() >= deadline {
bail!("timed out waiting for /responses request");
}
sleep(Duration::from_millis(10)).await;
}
}
fn send_sigint(process: &Child) -> Result<()> {
let pid = process
.id()
.context("websocket app-server process has no pid")?;
let status = StdCommand::new("kill")
.arg("-INT")
.arg(pid.to_string())
.status()
.context("failed to invoke kill -INT")?;
if !status.success() {
bail!("kill -INT exited with {status}");
}
Ok(())
}
async fn assert_process_does_not_exit_within(process: &mut Child, window: Duration) -> Result<()> {
match timeout(window, process.wait()).await {
Err(_) => Ok(()),
Ok(Ok(status)) => bail!("process exited too early during graceful drain: {status}"),
Ok(Err(err)) => Err(err).context("failed waiting for process"),
}
}
async fn wait_for_process_exit_within(
process: &mut Child,
window: Duration,
timeout_context: &'static str,
) -> Result<std::process::ExitStatus> {
timeout(window, process.wait())
.await
.context(timeout_context)?
.context("failed waiting for websocket app-server process exit")
}
async fn expect_websocket_disconnect(stream: &mut WsClient) -> Result<()> {
loop {
let frame = timeout(DEFAULT_READ_TIMEOUT, stream.next())
.await
.context("timed out waiting for websocket disconnect")?;
match frame {
None => return Ok(()),
Some(Ok(WebSocketMessage::Close(_))) => return Ok(()),
Some(Ok(WebSocketMessage::Ping(payload))) => {
stream
.send(WebSocketMessage::Pong(payload))
.await
.context("failed to reply to ping while waiting for disconnect")?;
}
Some(Ok(WebSocketMessage::Pong(_))) => {}
Some(Ok(WebSocketMessage::Frame(_))) => {}
Some(Ok(WebSocketMessage::Text(_))) => {}
Some(Ok(WebSocketMessage::Binary(_))) => {}
Some(Err(_)) => return Ok(()),
}
}
}

View File

@@ -9,6 +9,7 @@ use codex_app_server_protocol::ExperimentalFeatureListResponse;
use codex_app_server_protocol::ExperimentalFeatureStage;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_core::config::ConfigBuilder;
use codex_core::features::FEATURES;
use codex_core::features::Stage;
use pretty_assertions::assert_eq;
@@ -20,6 +21,11 @@ const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
#[tokio::test]
async fn experimental_feature_list_returns_feature_metadata_with_stage() -> Result<()> {
let codex_home = TempDir::new()?;
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.build()
.await?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
@@ -63,7 +69,7 @@ async fn experimental_feature_list_returns_feature_metadata_with_stage() -> Resu
display_name,
description,
announcement,
enabled: spec.default_enabled,
enabled: config.features.enabled(spec.id),
default_enabled: spec.default_enabled,
}
})

View File

@@ -5,6 +5,8 @@ mod collaboration_mode_list;
mod compaction;
mod config_rpc;
mod connection_handling_websocket;
#[cfg(unix)]
mod connection_handling_websocket_unix;
mod dynamic_tools;
mod experimental_api;
mod experimental_feature_list;
@@ -16,6 +18,7 @@ mod rate_limits;
mod request_user_input;
mod review;
mod safety_check_downgrade;
mod skill_approval;
mod skills_list;
mod thread_archive;
mod thread_fork;

View File

@@ -12,8 +12,7 @@ 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::openai_models::InputModality;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::openai_models::ModelPreset;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
use tokio::time::timeout;
@@ -21,6 +20,48 @@ use tokio::time::timeout;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
fn model_from_preset(preset: &ModelPreset) -> Model {
Model {
id: preset.id.clone(),
model: preset.model.clone(),
upgrade: preset.upgrade.as_ref().map(|upgrade| upgrade.id.clone()),
display_name: preset.display_name.clone(),
description: preset.description.clone(),
hidden: !preset.show_in_picker,
supported_reasoning_efforts: preset
.supported_reasoning_efforts
.iter()
.map(|preset| ReasoningEffortOption {
reasoning_effort: preset.effort,
description: preset.description.clone(),
})
.collect(),
default_reasoning_effort: preset.default_reasoning_effort,
input_modalities: preset.input_modalities.clone(),
// `write_models_cache()` round-trips through a simplified ModelInfo fixture that does not
// preserve personality placeholders in base instructions, so app-server list results from
// cache report `supports_personality = false`.
// todo(sayan): fix, maybe make roundtrip use ModelInfo only
supports_personality: false,
is_default: preset.is_default,
}
}
fn expected_visible_models() -> Vec<Model> {
// Filter by supported_in_api to support testing with both ChatGPT and non-ChatGPT auth modes.
let mut presets =
ModelPreset::filter_by_auth(codex_core::test_support::all_model_presets().clone(), false);
// Mirror `ModelsManager::build_available_models()` default selection after auth filtering.
ModelPreset::mark_default_by_picker_visibility(&mut presets);
presets
.iter()
.filter(|preset| preset.show_in_picker)
.map(model_from_preset)
.collect()
}
#[tokio::test]
async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
let codex_home = TempDir::new()?;
@@ -48,130 +89,7 @@ async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
next_cursor,
} = to_response::<ModelListResponse>(response)?;
let expected_models = vec![
Model {
id: "gpt-5.2-codex".to_string(),
model: "gpt-5.2-codex".to_string(),
upgrade: None,
display_name: "gpt-5.2-codex".to_string(),
description: "Latest frontier agentic coding model.".to_string(),
hidden: false,
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: "Greater 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,
input_modalities: vec![InputModality::Text, InputModality::Image],
supports_personality: false,
is_default: true,
},
Model {
id: "gpt-5.1-codex-max".to_string(),
model: "gpt-5.1-codex-max".to_string(),
upgrade: Some("gpt-5.2-codex".to_string()),
display_name: "gpt-5.1-codex-max".to_string(),
description: "Codex-optimized flagship for deep and fast reasoning.".to_string(),
hidden: false,
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: "Greater 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,
input_modalities: vec![InputModality::Text, InputModality::Image],
supports_personality: false,
is_default: false,
},
Model {
id: "gpt-5.1-codex-mini".to_string(),
model: "gpt-5.1-codex-mini".to_string(),
upgrade: Some("gpt-5.2-codex".to_string()),
display_name: "gpt-5.1-codex-mini".to_string(),
description: "Optimized for codex. Cheaper, faster, but less capable.".to_string(),
hidden: false,
supported_reasoning_efforts: vec![
ReasoningEffortOption {
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,
input_modalities: vec![InputModality::Text, InputModality::Image],
supports_personality: false,
is_default: false,
},
Model {
id: "gpt-5.2".to_string(),
model: "gpt-5.2".to_string(),
upgrade: Some("gpt-5.2-codex".to_string()),
display_name: "gpt-5.2".to_string(),
description:
"Latest frontier model with improvements across knowledge, reasoning and coding"
.to_string(),
hidden: false,
supported_reasoning_efforts: vec![
ReasoningEffortOption {
reasoning_effort: ReasoningEffort::Low,
description: "Balances speed with some reasoning; useful for straightforward \
queries and short explanations"
.to_string(),
},
ReasoningEffortOption {
reasoning_effort: ReasoningEffort::Medium,
description: "Provides a solid balance of reasoning depth and latency for \
general-purpose tasks"
.to_string(),
},
ReasoningEffortOption {
reasoning_effort: ReasoningEffort::High,
description: "Maximizes reasoning depth for complex or ambiguous problems"
.to_string(),
},
ReasoningEffortOption {
reasoning_effort: ReasoningEffort::XHigh,
description: "Extra high reasoning depth for complex problems".to_string(),
},
],
default_reasoning_effort: ReasoningEffort::Medium,
input_modalities: vec![InputModality::Text, InputModality::Image],
supports_personality: false,
is_default: false,
},
];
let expected_models = expected_visible_models();
assert_eq!(items, expected_models);
assert!(next_cursor.is_none());
@@ -237,8 +155,10 @@ async fn list_models_pagination_works() -> Result<()> {
next_cursor: first_cursor,
} = to_response::<ModelListResponse>(first_response)?;
let expected_models = expected_visible_models();
assert_eq!(first_items.len(), 1);
assert_eq!(first_items[0].id, "gpt-5.2-codex");
assert_eq!(first_items[0].id, expected_models[0].id);
let next_cursor = first_cursor.ok_or_else(|| anyhow!("cursor for second page"))?;
let second_request = mcp
@@ -261,7 +181,7 @@ async fn list_models_pagination_works() -> Result<()> {
} = to_response::<ModelListResponse>(second_response)?;
assert_eq!(second_items.len(), 1);
assert_eq!(second_items[0].id, "gpt-5.1-codex-max");
assert_eq!(second_items[0].id, expected_models[1].id);
let third_cursor = second_cursor.ok_or_else(|| anyhow!("cursor for third page"))?;
let third_request = mcp
@@ -284,7 +204,7 @@ async fn list_models_pagination_works() -> Result<()> {
} = to_response::<ModelListResponse>(third_response)?;
assert_eq!(third_items.len(), 1);
assert_eq!(third_items[0].id, "gpt-5.1-codex-mini");
assert_eq!(third_items[0].id, expected_models[2].id);
let fourth_cursor = third_cursor.ok_or_else(|| anyhow!("cursor for fourth page"))?;
let fourth_request = mcp
@@ -307,7 +227,7 @@ async fn list_models_pagination_works() -> Result<()> {
} = to_response::<ModelListResponse>(fourth_response)?;
assert_eq!(fourth_items.len(), 1);
assert_eq!(fourth_items[0].id, "gpt-5.2");
assert_eq!(fourth_items[0].id, expected_models[3].id);
assert!(fourth_cursor.is_none());
Ok(())
}

View File

@@ -0,0 +1,138 @@
use anyhow::Result;
use app_test_support::McpProcess;
use app_test_support::create_final_assistant_message_sse_response;
use app_test_support::create_mock_responses_server_sequence;
use app_test_support::to_response;
use app_test_support::write_mock_responses_config_toml;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
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::UserInput as V2UserInput;
use codex_core::features::Feature;
use core_test_support::responses;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
use tokio::time::timeout;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
fn write_skill_with_script(
home: &Path,
name: &str,
script_body: &str,
) -> Result<std::path::PathBuf> {
let skill_dir = home.join("skills").join(name);
let scripts_dir = skill_dir.join("scripts");
fs::create_dir_all(&scripts_dir)?;
fs::write(
skill_dir.join("SKILL.md"),
format!("---\nname: {name}\ndescription: {name} skill\n---\n"),
)?;
let script_path = scripts_dir.join("run.py");
fs::write(&script_path, script_body)?;
Ok(script_path)
}
fn shell_command_response(tool_call_id: &str, command: &str) -> Result<String> {
let arguments = serde_json::to_string(&json!({
"command": command,
"timeout_ms": 500,
}))?;
Ok(responses::sse(vec![
responses::ev_response_created("resp-1"),
responses::ev_function_call(tool_call_id, "shell_command", &arguments),
responses::ev_completed("resp-1"),
]))
}
fn command_for_script(script_path: &Path) -> Result<String> {
let runner = if cfg!(windows) { "python" } else { "python3" };
let script_path = script_path.to_string_lossy().into_owned();
Ok(shlex::try_join([runner, script_path.as_str()])?)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn skill_request_approval_round_trip_on_shell_command_skill_script_exec() -> Result<()> {
let codex_home = tempfile::TempDir::new()?;
let script_path = write_skill_with_script(codex_home.path(), "demo", "print('hello')")?;
let tool_call_id = "skill-call";
let command = command_for_script(&script_path)?;
let server = create_mock_responses_server_sequence(vec![
shell_command_response(tool_call_id, &command)?,
create_final_assistant_message_sse_response("done")?,
])
.await;
write_mock_responses_config_toml(
codex_home.path(),
&server.uri(),
&BTreeMap::from([(Feature::SkillApproval, true)]),
8192,
Some(false),
"mock_provider",
"compact",
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let thread_start_id = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("mock-model".to_string()),
..Default::default()
})
.await?;
let thread_start_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(thread_start_id)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response(thread_start_resp)?;
let turn_start_id = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "ask something".to_string(),
text_elements: Vec::new(),
}],
model: Some("mock-model".to_string()),
..Default::default()
})
.await?;
let turn_start_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(turn_start_id)),
)
.await??;
let TurnStartResponse { .. } = to_response::<TurnStartResponse>(turn_start_resp)?;
let server_req = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_request_message(),
)
.await??;
let ServerRequest::SkillRequestApproval { request_id, params } = server_req else {
panic!("expected SkillRequestApproval request, got: {server_req:?}");
};
assert_eq!(params.item_id, tool_call_id);
assert_eq!(params.skill_name, "demo");
mcp.send_response(request_id, serde_json::json!({ "decision": "approve" }))
.await?;
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
)
.await??;
Ok(())
}

View File

@@ -78,6 +78,7 @@ async fn list_threads_with_sort(
source_kinds,
archived,
cwd: None,
search_term: None,
})
.await?;
let resp: JSONRPCResponse = timeout(
@@ -491,6 +492,7 @@ async fn thread_list_respects_cwd_filter() -> Result<()> {
source_kinds: None,
archived: None,
cwd: Some(target_cwd.to_string_lossy().into_owned()),
search_term: None,
})
.await?;
let resp: JSONRPCResponse = timeout(
@@ -511,6 +513,86 @@ async fn thread_list_respects_cwd_filter() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn thread_list_respects_search_term_filter() -> Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join("config.toml"),
r#"
model = "mock-model"
approval_policy = "never"
suppress_unstable_features_warning = true
[features]
sqlite = true
"#,
)?;
let older_match = create_fake_rollout(
codex_home.path(),
"2025-01-02T10-00-00",
"2025-01-02T10:00:00Z",
"match: needle",
Some("mock_provider"),
None,
)?;
let _non_match = create_fake_rollout(
codex_home.path(),
"2025-01-02T11-00-00",
"2025-01-02T11:00:00Z",
"no hit here",
Some("mock_provider"),
None,
)?;
let newer_match = create_fake_rollout(
codex_home.path(),
"2025-01-02T12-00-00",
"2025-01-02T12:00:00Z",
"needle suffix",
Some("mock_provider"),
None,
)?;
// `thread/list` only applies `search_term` on the sqlite path. In this test we
// create rollouts manually, so we must also create the sqlite DB and mark backfill
// complete; otherwise app-server will permanently use filesystem fallback.
let state_db = codex_state::StateRuntime::init(
codex_home.path().to_path_buf(),
"mock_provider".into(),
None,
)
.await?;
state_db.mark_backfill_complete(None).await?;
let mut mcp = init_mcp(codex_home.path()).await?;
let request_id = mcp
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
cursor: None,
limit: Some(10),
sort_key: None,
model_providers: Some(vec!["mock_provider".to_string()]),
source_kinds: None,
archived: None,
cwd: None,
search_term: Some("needle".to_string()),
})
.await?;
let resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let ThreadListResponse {
data, next_cursor, ..
} = to_response::<ThreadListResponse>(resp)?;
assert_eq!(next_cursor, None);
let ids: Vec<_> = data.iter().map(|thread| thread.id.as_str()).collect();
assert_eq!(ids, vec![newer_match, older_match]);
Ok(())
}
#[tokio::test]
async fn thread_list_empty_source_kinds_defaults_to_interactive_only() -> Result<()> {
let codex_home = TempDir::new()?;
@@ -1335,6 +1417,7 @@ async fn thread_list_invalid_cursor_returns_error() -> Result<()> {
source_kinds: None,
archived: None,
cwd: None,
search_term: None,
})
.await?;
let error: JSONRPCError = timeout(

View File

@@ -289,6 +289,7 @@ async fn thread_name_set_is_reflected_in_read_list_and_resume() -> Result<()> {
source_kinds: None,
archived: None,
cwd: None,
search_term: None,
})
.await?;
let list_resp: JSONRPCResponse = timeout(

View File

@@ -146,6 +146,34 @@ model_reasoning_effort = "high"
Ok(())
}
#[tokio::test]
async fn thread_start_accepts_metrics_service_name() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").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 req_id = mcp
.send_thread_start_request(ThreadStartParams {
service_name: Some("my_app_server_client".to_string()),
..Default::default()
})
.await?;
let resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(req_id)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(resp)?;
assert!(!thread.id.is_empty(), "thread id should not be empty");
Ok(())
}
#[tokio::test]
async fn thread_start_ephemeral_remains_pathless() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;

View File

@@ -3,7 +3,7 @@
// Running these tests with the patched zsh fork:
//
// The suite resolves the shared test-only zsh DotSlash file at
// `exec-server/tests/suite/zsh` via DotSlash on first use, so `dotslash` and
// `app-server/tests/suite/zsh` via DotSlash on first use, so `dotslash` and
// network access are required the first time the artifact is fetched.
use anyhow::Result;
@@ -35,7 +35,6 @@ use core_test_support::responses;
use core_test_support::skip_if_no_network;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use tempfile::TempDir;
use tokio::time::timeout;
@@ -444,32 +443,17 @@ async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2()
return Ok(());
}
eprintln!("using zsh path for zsh-fork test: {}", zsh_path.display());
let zsh_path_for_config = {
// App-server config accepts only a zsh path, not extra argv. Use a
// wrapper so this test can force `-df` and downgrade `-lc` to `-c`
// to avoid rc/login-shell startup noise.
let path = workspace.join("zsh-no-rc");
std::fs::write(
&path,
format!(
r#"#!/bin/sh
if [ "$1" = "-lc" ]; then
shift
set -- -c "$@"
fi
exec "{}" -df "$@"
"#,
zsh_path.display()
),
)?;
let mut permissions = std::fs::metadata(&path)?.permissions();
permissions.set_mode(0o755);
std::fs::set_permissions(&path, permissions)?;
path
};
let first_file = workspace.join("first.txt");
let second_file = workspace.join("second.txt");
std::fs::write(&first_file, "one")?;
std::fs::write(&second_file, "two")?;
let shell_command = format!(
"/bin/rm {} && /bin/rm {}",
first_file.display(),
second_file.display()
);
let tool_call_arguments = serde_json::to_string(&serde_json::json!({
"command": "/usr/bin/true && /usr/bin/true",
"command": shell_command,
"workdir": serde_json::Value::Null,
"timeout_ms": 5000
}))?;
@@ -495,13 +479,13 @@ exec "{}" -df "$@"
create_config_toml(
&codex_home,
&server.uri(),
"on-request",
"untrusted",
&BTreeMap::from([
(Feature::ShellZshFork, true),
(Feature::UnifiedExec, false),
(Feature::ShellSnapshot, false),
]),
&zsh_path_for_config,
&zsh_path,
)?;
let mut mcp = create_zsh_test_mcp_process(&codex_home, &workspace).await?;
@@ -525,21 +509,17 @@ exec "{}" -df "$@"
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "run true true".to_string(),
text: "remove both files".to_string(),
text_elements: Vec::new(),
}],
cwd: Some(workspace.clone()),
approval_policy: Some(codex_app_server_protocol::AskForApproval::OnRequest),
sandbox_policy: Some(if cfg!(target_os = "linux") {
// The zsh exec-bridge wrapper uses a Unix socket back to the parent
// process. Linux restricted sandbox seccomp denies connect(2), so use
// full access here; this test is validating zsh approval/decline
// behavior, not Linux sandboxing.
codex_app_server_protocol::SandboxPolicy::DangerFullAccess
} else {
codex_app_server_protocol::SandboxPolicy::ReadOnly {
access: codex_app_server_protocol::ReadOnlyAccess::FullAccess,
}
approval_policy: Some(codex_app_server_protocol::AskForApproval::UnlessTrusted),
sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::WorkspaceWrite {
writable_roots: vec![workspace.clone().try_into()?],
read_only_access: codex_app_server_protocol::ReadOnlyAccess::FullAccess,
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
}),
model: Some("mock-model".to_string()),
effort: Some(codex_protocol::openai_models::ReasoningEffort::Medium),
@@ -554,7 +534,8 @@ exec "{}" -df "$@"
.await??;
let TurnStartResponse { turn } = to_response::<TurnStartResponse>(turn_resp)?;
let mut approval_ids = Vec::new();
let mut approved_subcommand_strings = Vec::new();
let mut approved_subcommand_ids = Vec::new();
let mut saw_parent_approval = false;
let target_decisions = [
CommandExecutionApprovalDecision::Accept,
@@ -573,38 +554,45 @@ exec "{}" -df "$@"
};
assert_eq!(params.item_id, "call-zsh-fork-subcommand-decline");
assert_eq!(params.thread_id, thread.id);
let is_target_subcommand = params.command.as_deref() == Some("/usr/bin/true");
let approval_command = params
.command
.as_deref()
.expect("approval command should be present");
let is_target_subcommand = (approval_command.starts_with("/bin/rm ")
|| approval_command.starts_with("/usr/bin/rm "))
&& (approval_command.contains(&first_file.display().to_string())
|| approval_command.contains(&second_file.display().to_string()));
if is_target_subcommand {
approval_ids.push(
assert!(
approval_command.contains(&first_file.display().to_string())
|| approval_command.contains(&second_file.display().to_string()),
"expected zsh subcommand approval for one of the rm commands, got: {approval_command}"
);
approved_subcommand_ids.push(
params
.approval_id
.clone()
.expect("approval_id must be present for zsh subcommand approvals"),
);
approved_subcommand_strings.push(approval_command.to_string());
}
let is_parent_approval = approval_command.contains(&zsh_path.display().to_string())
&& approval_command.contains(&shell_command);
let decision = if is_target_subcommand {
let decision = target_decisions[target_decision_index].clone();
target_decision_index += 1;
decision
} else {
let command = params
.command
.as_deref()
.expect("approval command should be present");
} else if is_parent_approval {
assert!(
!saw_parent_approval,
"unexpected extra non-target approval: {command}"
);
assert!(
command.contains("zsh-no-rc"),
"expected parent zsh wrapper approval, got: {command}"
);
assert!(
command.contains("/usr/bin/true && /usr/bin/true"),
"expected tool command in parent approval, got: {command}"
"unexpected extra non-target approval: {approval_command}"
);
saw_parent_approval = true;
CommandExecutionApprovalDecision::Accept
} else {
// Login shells may run startup helpers (for example path_helper on macOS)
// before the parent shell command or target subcommands are reached.
CommandExecutionApprovalDecision::Accept
};
mcp.send_response(
request_id,
@@ -613,8 +601,15 @@ exec "{}" -df "$@"
.await?;
}
assert_eq!(approval_ids.len(), 2);
assert_ne!(approval_ids[0], approval_ids[1]);
assert!(
saw_parent_approval,
"expected parent shell approval request"
);
assert_eq!(approved_subcommand_ids.len(), 2);
assert_ne!(approved_subcommand_ids[0], approved_subcommand_ids[1]);
assert_eq!(approved_subcommand_strings.len(), 2);
assert!(approved_subcommand_strings[0].contains(&first_file.display().to_string()));
assert!(approved_subcommand_strings[1].contains(&second_file.display().to_string()));
let parent_completed_command_execution = timeout(DEFAULT_READ_TIMEOUT, async {
loop {
let completed_notif = mcp
@@ -741,7 +736,7 @@ stream_max_retries = 0
fn find_test_zsh_path() -> Result<Option<std::path::PathBuf>> {
let repo_root = codex_utils_cargo_bin::repo_root()?;
let dotslash_zsh = repo_root.join("codex-rs/exec-server/tests/suite/zsh");
let dotslash_zsh = repo_root.join("codex-rs/app-server/tests/suite/zsh");
if !dotslash_zsh.is_file() {
eprintln!(
"skipping zsh fork test: shared zsh DotSlash file not found at {}",

View File

@@ -3,7 +3,7 @@
// This is the patched zsh fork built by
// `.github/workflows/shell-tool-mcp.yml` for the shell-tool-mcp package.
// Fetching the prebuilt version via DotSlash makes it easier to write
// integration tests that exercise the zsh fork behavior in exec-server tests.
// integration tests that exercise the zsh fork behavior in app-server tests.
//
// TODO(mbolin): Currently, we use a .tgz artifact that includes binaries for
// multiple platforms, but we could save a bit of space by making arch-specific

View File

@@ -15,6 +15,7 @@ workspace = true
anyhow = { workspace = true }
codex-apply-patch = { workspace = true }
codex-linux-sandbox = { workspace = true }
codex-shell-escalation = { workspace = true }
codex-utils-home-dir = { workspace = true }
dotenvy = { workspace = true }
tempfile = { workspace = true }

View File

@@ -12,22 +12,36 @@ use tempfile::TempDir;
const LINUX_SANDBOX_ARG0: &str = "codex-linux-sandbox";
const APPLY_PATCH_ARG0: &str = "apply_patch";
const MISSPELLED_APPLY_PATCH_ARG0: &str = "applypatch";
#[cfg(unix)]
const EXECVE_WRAPPER_ARG0: &str = "codex-execve-wrapper";
const LOCK_FILENAME: &str = ".lock";
const TOKIO_WORKER_STACK_SIZE_BYTES: usize = 16 * 1024 * 1024;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Arg0DispatchPaths {
pub codex_linux_sandbox_exe: Option<PathBuf>,
pub main_execve_wrapper_exe: Option<PathBuf>,
}
/// Keeps the per-session PATH entry alive and locked for the process lifetime.
pub struct Arg0PathEntryGuard {
_temp_dir: TempDir,
_lock_file: File,
paths: Arg0DispatchPaths,
}
impl Arg0PathEntryGuard {
fn new(temp_dir: TempDir, lock_file: File) -> Self {
fn new(temp_dir: TempDir, lock_file: File, paths: Arg0DispatchPaths) -> Self {
Self {
_temp_dir: temp_dir,
_lock_file: lock_file,
paths,
}
}
pub fn paths(&self) -> &Arg0DispatchPaths {
&self.paths
}
}
pub fn arg0_dispatch() -> Option<Arg0PathEntryGuard> {
@@ -39,6 +53,32 @@ pub fn arg0_dispatch() -> Option<Arg0PathEntryGuard> {
.and_then(|s| s.to_str())
.unwrap_or("");
#[cfg(unix)]
if exe_name == EXECVE_WRAPPER_ARG0 {
let mut args = std::env::args();
let _ = args.next();
let file = match args.next() {
Some(file) => file,
None => std::process::exit(1),
};
let argv = args.collect::<Vec<_>>();
let runtime = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(runtime) => runtime,
Err(_) => std::process::exit(1),
};
let exit_code = runtime.block_on(
codex_shell_escalation::run_shell_escalation_execve_wrapper(file, argv),
);
match exit_code {
Ok(exit_code) => std::process::exit(exit_code),
Err(_) => std::process::exit(1),
}
}
if exe_name == LINUX_SANDBOX_ARG0 {
// Safety: [`run_main`] never returns.
codex_linux_sandbox::run_main();
@@ -96,33 +136,43 @@ pub fn arg0_dispatch() -> Option<Arg0PathEntryGuard> {
/// 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`].
/// error. Note that `main_fn` receives [`Arg0DispatchPaths`], which
/// contains the helper executable paths needed to construct
/// [`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,
F: FnOnce(Arg0DispatchPaths) -> 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();
let path_entry = arg0_dispatch();
// Regular invocation create a Tokio runtime and execute the provided
// async entry-point.
let runtime = build_runtime()?;
runtime.block_on(async move {
let codex_linux_sandbox_exe: Option<PathBuf> = if cfg!(target_os = "linux") {
std::env::current_exe().ok()
} else {
None
let current_exe = std::env::current_exe().ok();
let paths = Arg0DispatchPaths {
codex_linux_sandbox_exe: if cfg!(target_os = "linux") {
current_exe.or_else(|| {
path_entry
.as_ref()
.and_then(|path_entry| path_entry.paths().codex_linux_sandbox_exe.clone())
})
} else {
None
},
main_execve_wrapper_exe: path_entry
.as_ref()
.and_then(|path_entry| path_entry.paths().main_execve_wrapper_exe.clone()),
};
main_fn(codex_linux_sandbox_exe).await
main_fn(paths).await
})
}
@@ -227,6 +277,8 @@ pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result<Arg0PathEntryGu
MISSPELLED_APPLY_PATCH_ARG0,
#[cfg(target_os = "linux")]
LINUX_SANDBOX_ARG0,
#[cfg(unix)]
EXECVE_WRAPPER_ARG0,
] {
let exe = std::env::current_exe()?;
@@ -271,7 +323,30 @@ pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result<Arg0PathEntryGu
std::env::set_var("PATH", updated_path_env_var);
}
Ok(Arg0PathEntryGuard::new(temp_dir, lock_file))
let paths = Arg0DispatchPaths {
codex_linux_sandbox_exe: {
#[cfg(target_os = "linux")]
{
Some(path.join(LINUX_SANDBOX_ARG0))
}
#[cfg(not(target_os = "linux"))]
{
None
}
},
main_execve_wrapper_exe: {
#[cfg(unix)]
{
Some(path.join(EXECVE_WRAPPER_ARG0))
}
#[cfg(not(unix))]
{
None
}
},
};
Ok(Arg0PathEntryGuard::new(temp_dir, lock_file, paths))
}
fn janitor_cleanup(temp_root: &Path) -> std::io::Result<()> {

View File

@@ -7,6 +7,7 @@ use std::path::PathBuf;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::config::NetworkProxyAuditMetadata;
use codex_core::exec_env::create_env;
use codex_core::landlock::spawn_command_under_linux_sandbox;
#[cfg(target_os = "macos")]
@@ -223,6 +224,7 @@ async fn run_command_under_sandbox(
None,
None,
managed_network_requirements_enabled,
NetworkProxyAuditMetadata::default(),
)
.await
.map_err(|err| anyhow::anyhow!("failed to start managed network proxy: {err}"))?,

View File

@@ -3,6 +3,7 @@ use clap::CommandFactory;
use clap::Parser;
use clap_complete::Shell;
use clap_complete::generate;
use codex_arg0::Arg0DispatchPaths;
use codex_arg0::arg0_dispatch_or_else;
use codex_chatgpt::apply_command::ApplyCommand;
use codex_chatgpt::apply_command::run_apply_command;
@@ -543,18 +544,13 @@ fn stage_str(stage: codex_core::features::Stage) -> &'static str {
}
fn main() -> anyhow::Result<()> {
arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move {
// Run wrapper mode only after arg0 dispatch so `codex-linux-sandbox`
// invocations don't get misclassified as zsh exec-wrapper calls.
if codex_core::maybe_run_zsh_exec_wrapper_mode()? {
return Ok(());
}
cli_main(codex_linux_sandbox_exe).await?;
arg0_dispatch_or_else(|arg0_paths: Arg0DispatchPaths| async move {
cli_main(arg0_paths).await?;
Ok(())
})
}
async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()> {
async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
let MultitoolCli {
config_overrides: mut root_config_overrides,
feature_toggles,
@@ -572,7 +568,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
&mut interactive.config_overrides,
root_config_overrides.clone(),
);
let exit_info = run_interactive_tui(interactive, codex_linux_sandbox_exe).await?;
let exit_info = run_interactive_tui(interactive, arg0_paths.clone()).await?;
handle_app_exit(exit_info)?;
}
Some(Subcommand::Exec(mut exec_cli)) => {
@@ -580,7 +576,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
&mut exec_cli.config_overrides,
root_config_overrides.clone(),
);
codex_exec::run_main(exec_cli, codex_linux_sandbox_exe).await?;
codex_exec::run_main(exec_cli, arg0_paths.clone()).await?;
}
Some(Subcommand::Review(review_args)) => {
let mut exec_cli = ExecCli::try_parse_from(["codex", "exec"])?;
@@ -589,10 +585,10 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
&mut exec_cli.config_overrides,
root_config_overrides.clone(),
);
codex_exec::run_main(exec_cli, codex_linux_sandbox_exe).await?;
codex_exec::run_main(exec_cli, arg0_paths.clone()).await?;
}
Some(Subcommand::McpServer) => {
codex_mcp_server::run_main(codex_linux_sandbox_exe, root_config_overrides).await?;
codex_mcp_server::run_main(arg0_paths.clone(), root_config_overrides).await?;
}
Some(Subcommand::Mcp(mut mcp_cli)) => {
// Propagate any root-level config overrides (e.g. `-c key=value`).
@@ -603,7 +599,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
None => {
let transport = app_server_cli.listen;
codex_app_server::run_main_with_transport(
codex_linux_sandbox_exe,
arg0_paths.clone(),
root_config_overrides,
codex_core::config_loader::LoaderOverrides::default(),
app_server_cli.analytics_default_enabled,
@@ -647,7 +643,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
all,
config_overrides,
);
let exit_info = run_interactive_tui(interactive, codex_linux_sandbox_exe).await?;
let exit_info = run_interactive_tui(interactive, arg0_paths.clone()).await?;
handle_app_exit(exit_info)?;
}
Some(Subcommand::Fork(ForkCommand {
@@ -664,7 +660,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
all,
config_overrides,
);
let exit_info = run_interactive_tui(interactive, codex_linux_sandbox_exe).await?;
let exit_info = run_interactive_tui(interactive, arg0_paths.clone()).await?;
handle_app_exit(exit_info)?;
}
Some(Subcommand::Login(mut login_cli)) => {
@@ -713,7 +709,8 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
&mut cloud_cli.config_overrides,
root_config_overrides.clone(),
);
codex_cloud_tasks::run_main(cloud_cli, codex_linux_sandbox_exe).await?;
codex_cloud_tasks::run_main(cloud_cli, arg0_paths.codex_linux_sandbox_exe.clone())
.await?;
}
Some(Subcommand::Sandbox(sandbox_args)) => match sandbox_args.cmd {
SandboxCommand::Macos(mut seatbelt_cli) => {
@@ -723,7 +720,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
);
codex_cli::debug_sandbox::run_command_under_seatbelt(
seatbelt_cli,
codex_linux_sandbox_exe,
arg0_paths.codex_linux_sandbox_exe.clone(),
)
.await?;
}
@@ -734,7 +731,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
);
codex_cli::debug_sandbox::run_command_under_landlock(
landlock_cli,
codex_linux_sandbox_exe,
arg0_paths.codex_linux_sandbox_exe.clone(),
)
.await?;
}
@@ -745,7 +742,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
);
codex_cli::debug_sandbox::run_command_under_windows(
windows_cli,
codex_linux_sandbox_exe,
arg0_paths.codex_linux_sandbox_exe.clone(),
)
.await?;
}
@@ -892,7 +889,7 @@ fn prepend_config_flags(
async fn run_interactive_tui(
mut interactive: TuiCli,
codex_linux_sandbox_exe: Option<PathBuf>,
arg0_paths: Arg0DispatchPaths,
) -> std::io::Result<AppExitInfo> {
if let Some(prompt) = interactive.prompt.take() {
// Normalize CRLF/CR to LF so CLI-provided text can't leak `\r` into TUI state.
@@ -917,7 +914,7 @@ async fn run_interactive_tui(
}
}
codex_tui::run_main(interactive, codex_linux_sandbox_exe).await
codex_tui::run_main(interactive, arg0_paths).await
}
fn confirm(prompt: &str) -> std::io::Result<bool> {

View File

@@ -176,6 +176,7 @@ impl From<&ResponsesApiRequest> for ResponseCreateWsRequest {
include: request.include.clone(),
prompt_cache_key: request.prompt_cache_key.clone(),
text: request.text.clone(),
generate: None,
client_metadata: None,
}
}
@@ -200,6 +201,8 @@ pub struct ResponseCreateWsRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<TextControls>,
#[serde(skip_serializing_if = "Option::is_none")]
pub generate: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_metadata: Option<HashMap<String, String>>,
}

View File

@@ -50,7 +50,9 @@ codex-utils-pty = { workspace = true }
codex-utils-readiness = { workspace = true }
codex-secrets = { workspace = true }
codex-utils-string = { workspace = true }
codex-utils-stream-parser = { workspace = true }
codex-windows-sandbox = { package = "codex-windows-sandbox", path = "../windows-sandbox-rs" }
csv = { workspace = true }
dirs = { workspace = true }
dunce = { workspace = true }
encoding_rs = { workspace = true }
@@ -59,7 +61,6 @@ eventsource-stream = { workspace = true }
futures = { workspace = true }
http = { workspace = true }
indexmap = { workspace = true }
indoc = { workspace = true }
keyring = { workspace = true, features = ["crypto-rust"] }
libc = { workspace = true }
notify = { workspace = true }
@@ -138,6 +139,9 @@ windows-sys = { version = "0.52", features = [
[target.'cfg(any(target_os = "freebsd", target_os = "openbsd"))'.dependencies]
keyring = { workspace = true, features = ["sync-secret-service"] }
[target.'cfg(unix)'.dependencies]
codex-shell-escalation = { workspace = true }
[dev-dependencies]
assert_cmd = { workspace = true }
assert_matches = { workspace = true }
@@ -145,6 +149,7 @@ codex-arg0 = { workspace = true }
codex-otel = { workspace = true, features = [
"disable-default-metrics-exporter",
] }
codex-test-macros = { workspace = true }
codex-utils-cargo-bin = { workspace = true }
core_test_support = { workspace = true }
ctor = { workspace = true }

View File

@@ -29,6 +29,12 @@
"$ref": "#/definitions/AgentRoleToml"
},
"properties": {
"job_max_runtime_seconds": {
"description": "Default maximum runtime in seconds for agent job workers.",
"format": "uint64",
"minimum": 1.0,
"type": "integer"
},
"max_depth": {
"description": "Maximum nesting depth allowed for spawned agent threads. Root sessions start at depth 0.",
"format": "int32",
@@ -367,9 +373,15 @@
"prevent_idle_sleep": {
"type": "boolean"
},
"realtime_conversation": {
"type": "boolean"
},
"remote_models": {
"type": "boolean"
},
"request_permissions": {
"type": "boolean"
},
"request_rule": {
"type": "boolean"
},
@@ -394,6 +406,9 @@
"shell_zsh_fork": {
"type": "boolean"
},
"skill_approval": {
"type": "boolean"
},
"skill_env_var_dependency_prompt": {
"type": "boolean"
},
@@ -415,6 +430,9 @@
"use_linux_sandbox_bwrap": {
"type": "boolean"
},
"voice_transcription": {
"type": "boolean"
},
"web_search": {
"type": "boolean"
},
@@ -1648,9 +1666,15 @@
"prevent_idle_sleep": {
"type": "boolean"
},
"realtime_conversation": {
"type": "boolean"
},
"remote_models": {
"type": "boolean"
},
"request_permissions": {
"type": "boolean"
},
"request_rule": {
"type": "boolean"
},
@@ -1675,6 +1699,9 @@
"shell_zsh_fork": {
"type": "boolean"
},
"skill_approval": {
"type": "boolean"
},
"skill_env_var_dependency_prompt": {
"type": "boolean"
},
@@ -1696,6 +1723,9 @@
"use_linux_sandbox_bwrap": {
"type": "boolean"
},
"voice_transcription": {
"type": "boolean"
},
"web_search": {
"type": "boolean"
},
@@ -2016,6 +2046,14 @@
],
"description": "User-level skill config entries keyed by SKILL.md path."
},
"sqlite_home": {
"allOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
}
],
"description": "Directory where Codex stores the SQLite state DB. Defaults to `$CODEX_SQLITE_HOME` when set. Otherwise uses `$CODEX_HOME`."
},
"suppress_unstable_features_warning": {
"description": "Suppress warnings about unstable (under development) features.",
"type": "boolean"

File diff suppressed because one or more lines are too long

View File

@@ -83,7 +83,7 @@ impl AgentControl {
let new_thread = match session_source {
Some(session_source) => {
state
.spawn_new_thread_with_source(config, self.clone(), session_source, false)
.spawn_new_thread_with_source(config, self.clone(), session_source, false, None)
.await?
}
None => state.spawn_new_thread(config, self.clone()).await?,

View File

@@ -30,7 +30,6 @@ struct ActiveAgents {
used_agent_nicknames: HashSet<String>,
nickname_reset_count: usize,
}
fn session_depth(session_source: &SessionSource) -> i32 {
match session_source {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) => *depth,

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