Compare commits

..

85 Commits

Author SHA1 Message Date
Yaroslav Volovich
baf0a90cae refactor: simplify thread history synthetic-id detection 2026-02-25 21:22:16 +00:00
Yaroslav Volovich
d7602c3081 fix: make resume cwd filtering and thread renames deterministic 2026-02-25 20:45:27 +00:00
Yaroslav Volovich
5d91f2b2c2 test: skip sandbox-sensitive core tests on gha linux arm 2026-02-25 20:37:45 +00:00
Yaroslav Volovich
d52f1a5d37 test: stabilize bazel arm flakes 2026-02-25 20:37:45 +00:00
Yaroslav Volovich
e0ab8c645c docs: clarify turn-based thread fork contracts 2026-02-25 20:37:45 +00:00
Yaroslav Volovich
d2f8b95bbf codex: address PR review feedback (#12577) 2026-02-25 20:37:45 +00:00
Yaroslav Volovich
8b57fdd973 app-server: support turn-based thread forks 2026-02-25 20:37:45 +00:00
Michael Bolin
be5bca6f8d fix: harden zsh fork tests and keep subcommand approvals deterministic (#12809)
## Why
The prior
`turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2`
assertion was brittle under Bazel: command approval payloads in the test
could include environment-dependent wrapper/command formatting
differences, which makes exact command-string matching flaky even when
behavior is correct.

(This regression was knowingly introduced in
https://github.com/openai/codex/pull/12800, but it was urgent to land
that PR.)

## What changed
- Hardened
`turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2`
in
[`turn_start_zsh_fork.rs`](https://github.com/openai/codex/blob/main/codex-rs/app-server/tests/suite/v2/turn_start_zsh_fork.rs):
- Replaced strict `approval_command.starts_with("/bin/rm")` checks with
intent-based subcommand matching.
- Subcommand approvals are now recognized by file-target semantics
(`first.txt` or `second.txt`) plus `rm` intent.
- Parent approval recognition is now more tolerant of command-format
differences while still requiring a definitive parent command context.
- Uses a defensive loop that waits for all target subcommand decisions
and the parent approval request.
- Preserved the existing regression and unit test fixes from earlier
commits in `unix_escalation.rs` and `skill_approval.rs`.

## Verification
- Ran the zsh fork subcommand decline regression under this change:
-
`turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2`
- Confirmed the test is now robust against approval-command-string
variation instead of hardcoding one expected command shape.
2026-02-25 12:23:30 -08:00
Eric Traut
f6fdfbeb98 Update Codex docs success link (#12805)
Fix a stale documentation link in the sign-in flow
2026-02-25 12:02:41 -08:00
Ahmed Ibrahim
3f30746237 Add simple realtime text logs (#12807)
Update realtime debug logs to include the actual text payloads in both
input and output paths.

- In `core/src/realtime_conversation.rs`:
- `handle_start`: add extracted assistant text output to the
`[realtime-text]` debug log.
- `handle_text`: add incoming text input (`params.text`) to the
`[realtime-text]` debug log.

No tests were run (per request).
2026-02-25 12:01:48 -08:00
Owen Lin
a0fd94bde6 feat(app-server): add ThreadItem::DynamicToolCall (#12732)
Previously, clients would call `thread/start` with dynamic_tools set,
and when a model invokes a dynamic tool, it would just make the
server->client `item/tool/call` request and wait for the client's
response to complete the tool call. This works, but it doesn't have an
`item/started` or `item/completed` event.

Now we are doing this:
- [new] emit `item/started` with `DynamicToolCall` populated with the
call arguments
- send an `item/tool/call` server request
- [new] once the client responds, emit `item/completed` with
`DynamicToolCall` populated with the response.

Also, with `persistExtendedHistory: true`, dynamic tool calls are now
reconstructable in `thread/read` and `thread/resume` as
`ThreadItem::DynamicToolCall`.
2026-02-25 12:00:10 -08:00
Rasmus Rygaard
73eaebbd1c Propagate session ID when compacting (#12802)
We propagate the session ID when sending requests for inference but we
don't do the same for compaction requests. This makes it hard to link
compaction requests to their session for debugging purposes
2026-02-25 19:17:38 +00:00
Michael Bolin
648a420cbf fix: enforce sandbox envelope for zsh fork execution (#12800)
## Why
Zsh fork execution was still able to bypass the `WorkspaceWrite` model
in edge cases because the fork path reconstructed command execution
without preserving sandbox wrappers, and command extraction only
accepted shell invocations in a narrow positional shape. This can allow
commands to run with broader filesystem access than expected, which
breaks the sandbox safety model.

## What changed
- Preserved the sandboxed `ExecRequest` produced by
`attempt.env_for(...)` when entering the zsh fork path in
[`unix_escalation.rs`](https://github.com/openai/codex/blob/main/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs).
- Updated `CoreShellCommandExecutor` to execute the sandboxed command
and working directory captured from `attempt.env_for(...)`, instead of
re-running a freshly reconstructed shell command.
- Made zsh-fork script extraction robust to wrapped invocations by
scanning command arguments for `-c`/`-lc` rather than only matching the
first positional form.
- Added unit tests in `unix_escalation.rs` to lock in wrapper-tolerant
parsing behavior and keep unsupported shell forms rejected.
- Tightened the regression in
[`skill_approval.rs`](https://github.com/openai/codex/blob/main/codex-rs/core/tests/suite/skill_approval.rs):
- `shell_zsh_fork_still_enforces_workspace_write_sandbox` now uses an
explicit `WorkspaceWrite` policy with `exclude_tmpdir_env_var: true` and
`exclude_slash_tmp: true`.
- The test attempts to write to `/tmp/...`, which is only reliably
outside writable roots with those explicit exclusions set.

## Verification
- Added and passed the new unit tests around `extract_shell_script`
parsing behavior with wrapped command shapes.
  - `extract_shell_script_supports_wrapped_command_prefixes`
  - `extract_shell_script_rejects_unsupported_shell_invocation`
- Verified the regression with the focused integration test:
`shell_zsh_fork_still_enforces_workspace_write_sandbox`.

## Manual Testing

Prior to this change, if I ran Codex via:

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

and asked:

```
what is the output of /bin/ps
```

it would run it, even though the default sandbox should prevent the
agent from running `/bin/ps` because it is setuid on MacOS.

But with this change, I now see the expected failure because it is
blocked by the sandbox:

```
/bin/ps exited with status 1 and produced no output in this environment.
```
2026-02-25 11:05:27 -08:00
pakrym-oai
9d7013eab0 Handle websocket timeout (#12791)
Sometimes websockets will timeout with 400 error, ensure we retry it.
2026-02-25 10:31:37 -08:00
jif-oai
7b39e76a66 Revert "fix(bazel): replace askama templates with include_str! in memories" (#12795)
Reverts openai/codex#11778
2026-02-25 18:06:17 +00:00
Ahmed Ibrahim
947092283a Add app-server v2 thread realtime API (#12715)
Add experimental `thread/realtime/*` v2 requests and notifications, then
route app-server realtime events through that thread-scoped surface with
integration coverage.

---------

Co-authored-by: Codex <noreply@openai.com>
2026-02-25 09:59:10 -08:00
Curtis 'Fjord' Hawthorne
0543d0a022 Promote js_repl to experimental with Node requirement (#12712)
## Summary

- Promote `js_repl` to an experimental feature that users can enable
from `/experimental`.
- Add `js_repl` experimental metadata, including the Node prerequisite
and activation guidance.
- Add regression coverage for the feature metadata and the
`/experimental` popup.

## What Changed

- Changed `Feature::JsRepl` from `Stage::UnderDevelopment` to
`Stage::Experimental`.
- Added experimental metadata for `js_repl` in `core/src/features.rs`:
  - name: `JavaScript REPL`
- description: calls out interactive website debugging, inline
JavaScript execution, and the required Node version (`>= v24.13.1`)
- announcement: tells users to enable it, then start a new chat or
restart Codex
- Added a core unit test that verifies:
  - `js_repl` is experimental
  - `js_repl` is disabled by default
- the hardcoded Node version in the description matches
`node-version.txt`
- Added a TUI test that opens the `/experimental` popup and verifies the
rendered `js_repl` entry includes the Node requirement text.

## Testing

- `just fmt`
- `cargo test -p codex-tui`
- `cargo test -p codex-core` (unit-test phase passed; stopped during the
long `tests/all.rs` integration suite)
2026-02-25 09:44:52 -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
381 changed files with 30574 additions and 5940 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"

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

234
MODULE.bazel.lock generated

File diff suppressed because one or more lines are too long

960
codex-rs/Cargo.lock generated

File diff suppressed because it is too large Load Diff

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
@@ -147,6 +149,7 @@ allocative = "0.3.3"
ansi-to-tui = "7.0.0"
anyhow = "1"
arboard = { version = "3", features = ["wayland-data-control"] }
askama = "0.15.4"
assert_cmd = "2"
assert_matches = "1.5.0"
async-channel = "2.3.1"
@@ -162,6 +165,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 +179,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,7 +188,6 @@ 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"

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": [
{
@@ -1981,6 +2052,38 @@
],
"type": "object"
},
"ThreadRealtimeAudioChunk": {
"description": "EXPERIMENTAL - thread realtime audio chunk.",
"properties": {
"data": {
"type": "string"
},
"numChannels": {
"format": "uint16",
"minimum": 0.0,
"type": "integer"
},
"sampleRate": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"samplesPerChannel": {
"format": "uint32",
"minimum": 0.0,
"type": [
"integer",
"null"
]
}
},
"required": [
"data",
"numChannels",
"sampleRate"
],
"type": "object"
},
"ThreadResumeParams": {
"description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nThe precedence is: history > path > thread_id. If using history or path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.",
"properties": {
@@ -2190,6 +2293,12 @@
"type": "null"
}
]
},
"serviceName": {
"type": [
"string",
"null"
]
}
},
"type": "object"
@@ -3390,6 +3499,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

@@ -500,6 +500,50 @@
],
"type": "object"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
"properties": {
"text": {
"type": "string"
},
"type": {
"enum": [
"inputText"
],
"title": "InputTextDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"text",
"type"
],
"title": "InputTextDynamicToolCallOutputContentItem",
"type": "object"
},
{
"properties": {
"imageUrl": {
"type": "string"
},
"type": {
"enum": [
"inputImage"
],
"title": "InputImageDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"imageUrl",
"type"
],
"title": "InputImageDynamicToolCallOutputContentItem",
"type": "object"
}
]
},
"EventMsg": {
"description": "Response event from the agent NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen.",
"oneOf": [
@@ -1613,6 +1657,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 +1717,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 +1820,94 @@
"title": "DynamicToolCallRequestEventMsg",
"type": "object"
},
{
"properties": {
"arguments": {
"description": "Dynamic tool call arguments."
},
"call_id": {
"description": "Identifier for the corresponding DynamicToolCallRequest.",
"type": "string"
},
"content_items": {
"description": "Dynamic tool response content items.",
"items": {
"$ref": "#/definitions/DynamicToolCallOutputContentItem"
},
"type": "array"
},
"duration": {
"allOf": [
{
"$ref": "#/definitions/Duration"
}
],
"description": "The duration of the dynamic tool call."
},
"error": {
"description": "Optional error text when the tool call failed before producing a response.",
"type": [
"string",
"null"
]
},
"success": {
"description": "Whether the tool call succeeded.",
"type": "boolean"
},
"tool": {
"description": "Dynamic tool name.",
"type": "string"
},
"turn_id": {
"description": "Turn ID that this dynamic tool call belongs to.",
"type": "string"
},
"type": {
"enum": [
"dynamic_tool_call_response"
],
"title": "DynamicToolCallResponseEventMsgType",
"type": "string"
}
},
"required": [
"arguments",
"call_id",
"content_items",
"duration",
"success",
"tool",
"turn_id",
"type"
],
"title": "DynamicToolCallResponseEventMsg",
"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 +3421,29 @@
}
]
},
"FileSystemPermissions": {
"properties": {
"read": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"write": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
}
},
"type": "object"
},
"FunctionCallOutputBody": {
"anyOf": [
{
@@ -3461,6 +3637,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 +3873,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 +4014,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 +7147,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 +7207,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 +7310,94 @@
"title": "DynamicToolCallRequestEventMsg",
"type": "object"
},
{
"properties": {
"arguments": {
"description": "Dynamic tool call arguments."
},
"call_id": {
"description": "Identifier for the corresponding DynamicToolCallRequest.",
"type": "string"
},
"content_items": {
"description": "Dynamic tool response content items.",
"items": {
"$ref": "#/definitions/DynamicToolCallOutputContentItem"
},
"type": "array"
},
"duration": {
"allOf": [
{
"$ref": "#/definitions/Duration"
}
],
"description": "The duration of the dynamic tool call."
},
"error": {
"description": "Optional error text when the tool call failed before producing a response.",
"type": [
"string",
"null"
]
},
"success": {
"description": "Whether the tool call succeeded.",
"type": "boolean"
},
"tool": {
"description": "Dynamic tool name.",
"type": "string"
},
"turn_id": {
"description": "Turn ID that this dynamic tool call belongs to.",
"type": "string"
},
"type": {
"enum": [
"dynamic_tool_call_response"
],
"title": "DynamicToolCallResponseEventMsgType",
"type": "string"
}
},
"required": [
"arguments",
"call_id",
"content_items",
"duration",
"success",
"tool",
"turn_id",
"type"
],
"title": "DynamicToolCallResponseEventMsg",
"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

@@ -778,6 +778,58 @@
],
"type": "object"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
"properties": {
"text": {
"type": "string"
},
"type": {
"enum": [
"inputText"
],
"title": "InputTextDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"text",
"type"
],
"title": "InputTextDynamicToolCallOutputContentItem",
"type": "object"
},
{
"properties": {
"imageUrl": {
"type": "string"
},
"type": {
"enum": [
"inputImage"
],
"title": "InputImageDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"imageUrl",
"type"
],
"title": "InputImageDynamicToolCallOutputContentItem",
"type": "object"
}
]
},
"DynamicToolCallStatus": {
"enum": [
"inProgress",
"completed",
"failed"
],
"type": "string"
},
"ErrorNotification": {
"properties": {
"error": {
@@ -1965,6 +2017,59 @@
"title": "McpToolCallThreadItem",
"type": "object"
},
{
"properties": {
"arguments": true,
"contentItems": {
"items": {
"$ref": "#/definitions/DynamicToolCallOutputContentItem"
},
"type": [
"array",
"null"
]
},
"durationMs": {
"description": "The duration of the dynamic tool call in milliseconds.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"id": {
"type": "string"
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
"success": {
"type": [
"boolean",
"null"
]
},
"tool": {
"type": "string"
},
"type": {
"enum": [
"dynamicToolCall"
],
"title": "DynamicToolCallThreadItemType",
"type": "string"
}
},
"required": [
"arguments",
"id",
"status",
"tool",
"type"
],
"title": "DynamicToolCallThreadItem",
"type": "object"
},
{
"properties": {
"agentsStates": {
@@ -2177,6 +2282,120 @@
],
"type": "object"
},
"ThreadRealtimeAudioChunk": {
"description": "EXPERIMENTAL - thread realtime audio chunk.",
"properties": {
"data": {
"type": "string"
},
"numChannels": {
"format": "uint16",
"minimum": 0.0,
"type": "integer"
},
"sampleRate": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"samplesPerChannel": {
"format": "uint32",
"minimum": 0.0,
"type": [
"integer",
"null"
]
}
},
"required": [
"data",
"numChannels",
"sampleRate"
],
"type": "object"
},
"ThreadRealtimeClosedNotification": {
"description": "EXPERIMENTAL - emitted when thread realtime transport closes.",
"properties": {
"reason": {
"type": [
"string",
"null"
]
},
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"type": "object"
},
"ThreadRealtimeErrorNotification": {
"description": "EXPERIMENTAL - emitted when thread realtime encounters an error.",
"properties": {
"message": {
"type": "string"
},
"threadId": {
"type": "string"
}
},
"required": [
"message",
"threadId"
],
"type": "object"
},
"ThreadRealtimeItemAddedNotification": {
"description": "EXPERIMENTAL - raw non-audio thread realtime item emitted by the backend.",
"properties": {
"item": true,
"threadId": {
"type": "string"
}
},
"required": [
"item",
"threadId"
],
"type": "object"
},
"ThreadRealtimeOutputAudioDeltaNotification": {
"description": "EXPERIMENTAL - streamed output audio emitted by thread realtime.",
"properties": {
"audio": {
"$ref": "#/definitions/ThreadRealtimeAudioChunk"
},
"threadId": {
"type": "string"
}
},
"required": [
"audio",
"threadId"
],
"type": "object"
},
"ThreadRealtimeStartedNotification": {
"description": "EXPERIMENTAL - emitted when thread realtime startup is accepted.",
"properties": {
"sessionId": {
"type": [
"string",
"null"
]
},
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"type": "object"
},
"ThreadStartedNotification": {
"properties": {
"thread": {
@@ -3453,6 +3672,106 @@
"title": "FuzzyFileSearch/sessionCompletedNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/realtime/started"
],
"title": "Thread/realtime/startedNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadRealtimeStartedNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/realtime/startedNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/realtime/itemAdded"
],
"title": "Thread/realtime/itemAddedNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadRealtimeItemAddedNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/realtime/itemAddedNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/realtime/outputAudio/delta"
],
"title": "Thread/realtime/outputAudio/deltaNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadRealtimeOutputAudioDeltaNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/realtime/outputAudio/deltaNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/realtime/error"
],
"title": "Thread/realtime/errorNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadRealtimeErrorNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/realtime/errorNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/realtime/closed"
],
"title": "Thread/realtime/closedNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadRealtimeClosedNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/realtime/closedNotification",
"type": "object"
},
{
"description": "Notifies the user of world-writable directories on Windows, which cannot be protected by the sandbox.",
"properties": {

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

@@ -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

@@ -185,6 +185,58 @@
],
"type": "string"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
"properties": {
"text": {
"type": "string"
},
"type": {
"enum": [
"inputText"
],
"title": "InputTextDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"text",
"type"
],
"title": "InputTextDynamicToolCallOutputContentItem",
"type": "object"
},
{
"properties": {
"imageUrl": {
"type": "string"
},
"type": {
"enum": [
"inputImage"
],
"title": "InputImageDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"imageUrl",
"type"
],
"title": "InputImageDynamicToolCallOutputContentItem",
"type": "object"
}
]
},
"DynamicToolCallStatus": {
"enum": [
"inProgress",
"completed",
"failed"
],
"type": "string"
},
"FileUpdateChange": {
"properties": {
"diff": {
@@ -633,6 +685,59 @@
"title": "McpToolCallThreadItem",
"type": "object"
},
{
"properties": {
"arguments": true,
"contentItems": {
"items": {
"$ref": "#/definitions/DynamicToolCallOutputContentItem"
},
"type": [
"array",
"null"
]
},
"durationMs": {
"description": "The duration of the dynamic tool call in milliseconds.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"id": {
"type": "string"
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
"success": {
"type": [
"boolean",
"null"
]
},
"tool": {
"type": "string"
},
"type": {
"enum": [
"dynamicToolCall"
],
"title": "DynamicToolCallThreadItemType",
"type": "string"
}
},
"required": [
"arguments",
"id",
"status",
"tool",
"type"
],
"title": "DynamicToolCallThreadItem",
"type": "object"
},
{
"properties": {
"agentsStates": {

View File

@@ -185,6 +185,58 @@
],
"type": "string"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
"properties": {
"text": {
"type": "string"
},
"type": {
"enum": [
"inputText"
],
"title": "InputTextDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"text",
"type"
],
"title": "InputTextDynamicToolCallOutputContentItem",
"type": "object"
},
{
"properties": {
"imageUrl": {
"type": "string"
},
"type": {
"enum": [
"inputImage"
],
"title": "InputImageDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"imageUrl",
"type"
],
"title": "InputImageDynamicToolCallOutputContentItem",
"type": "object"
}
]
},
"DynamicToolCallStatus": {
"enum": [
"inProgress",
"completed",
"failed"
],
"type": "string"
},
"FileUpdateChange": {
"properties": {
"diff": {
@@ -633,6 +685,59 @@
"title": "McpToolCallThreadItem",
"type": "object"
},
{
"properties": {
"arguments": true,
"contentItems": {
"items": {
"$ref": "#/definitions/DynamicToolCallOutputContentItem"
},
"type": [
"array",
"null"
]
},
"durationMs": {
"description": "The duration of the dynamic tool call in milliseconds.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"id": {
"type": "string"
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
"success": {
"type": [
"boolean",
"null"
]
},
"tool": {
"type": "string"
},
"type": {
"enum": [
"dynamicToolCall"
],
"title": "DynamicToolCallThreadItemType",
"type": "string"
}
},
"required": [
"arguments",
"id",
"status",
"tool",
"type"
],
"title": "DynamicToolCallThreadItem",
"type": "object"
},
{
"properties": {
"agentsStates": {

View File

@@ -299,6 +299,58 @@
],
"type": "string"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
"properties": {
"text": {
"type": "string"
},
"type": {
"enum": [
"inputText"
],
"title": "InputTextDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"text",
"type"
],
"title": "InputTextDynamicToolCallOutputContentItem",
"type": "object"
},
{
"properties": {
"imageUrl": {
"type": "string"
},
"type": {
"enum": [
"inputImage"
],
"title": "InputImageDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"imageUrl",
"type"
],
"title": "InputImageDynamicToolCallOutputContentItem",
"type": "object"
}
]
},
"DynamicToolCallStatus": {
"enum": [
"inProgress",
"completed",
"failed"
],
"type": "string"
},
"FileUpdateChange": {
"properties": {
"diff": {
@@ -747,6 +799,59 @@
"title": "McpToolCallThreadItem",
"type": "object"
},
{
"properties": {
"arguments": true,
"contentItems": {
"items": {
"$ref": "#/definitions/DynamicToolCallOutputContentItem"
},
"type": [
"array",
"null"
]
},
"durationMs": {
"description": "The duration of the dynamic tool call in milliseconds.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"id": {
"type": "string"
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
"success": {
"type": [
"boolean",
"null"
]
},
"tool": {
"type": "string"
},
"type": {
"enum": [
"dynamicToolCall"
],
"title": "DynamicToolCallThreadItemType",
"type": "string"
}
},
"required": [
"arguments",
"id",
"status",
"tool",
"type"
],
"title": "DynamicToolCallThreadItem",
"type": "object"
},
{
"properties": {
"agentsStates": {

View File

@@ -345,6 +345,58 @@
],
"type": "string"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
"properties": {
"text": {
"type": "string"
},
"type": {
"enum": [
"inputText"
],
"title": "InputTextDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"text",
"type"
],
"title": "InputTextDynamicToolCallOutputContentItem",
"type": "object"
},
{
"properties": {
"imageUrl": {
"type": "string"
},
"type": {
"enum": [
"inputImage"
],
"title": "InputImageDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"imageUrl",
"type"
],
"title": "InputImageDynamicToolCallOutputContentItem",
"type": "object"
}
]
},
"DynamicToolCallStatus": {
"enum": [
"inProgress",
"completed",
"failed"
],
"type": "string"
},
"FileUpdateChange": {
"properties": {
"diff": {
@@ -1207,6 +1259,59 @@
"title": "McpToolCallThreadItem",
"type": "object"
},
{
"properties": {
"arguments": true,
"contentItems": {
"items": {
"$ref": "#/definitions/DynamicToolCallOutputContentItem"
},
"type": [
"array",
"null"
]
},
"durationMs": {
"description": "The duration of the dynamic tool call in milliseconds.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"id": {
"type": "string"
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
"success": {
"type": [
"boolean",
"null"
]
},
"tool": {
"type": "string"
},
"type": {
"enum": [
"dynamicToolCall"
],
"title": "DynamicToolCallThreadItemType",
"type": "string"
}
},
"required": [
"arguments",
"id",
"status",
"tool",
"type"
],
"title": "DynamicToolCallThreadItem",
"type": "object"
},
{
"properties": {
"agentsStates": {

View File

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

View File

@@ -299,6 +299,58 @@
],
"type": "string"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
"properties": {
"text": {
"type": "string"
},
"type": {
"enum": [
"inputText"
],
"title": "InputTextDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"text",
"type"
],
"title": "InputTextDynamicToolCallOutputContentItem",
"type": "object"
},
{
"properties": {
"imageUrl": {
"type": "string"
},
"type": {
"enum": [
"inputImage"
],
"title": "InputImageDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"imageUrl",
"type"
],
"title": "InputImageDynamicToolCallOutputContentItem",
"type": "object"
}
]
},
"DynamicToolCallStatus": {
"enum": [
"inProgress",
"completed",
"failed"
],
"type": "string"
},
"FileUpdateChange": {
"properties": {
"diff": {
@@ -980,6 +1032,59 @@
"title": "McpToolCallThreadItem",
"type": "object"
},
{
"properties": {
"arguments": true,
"contentItems": {
"items": {
"$ref": "#/definitions/DynamicToolCallOutputContentItem"
},
"type": [
"array",
"null"
]
},
"durationMs": {
"description": "The duration of the dynamic tool call in milliseconds.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"id": {
"type": "string"
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
"success": {
"type": [
"boolean",
"null"
]
},
"tool": {
"type": "string"
},
"type": {
"enum": [
"dynamicToolCall"
],
"title": "DynamicToolCallThreadItemType",
"type": "string"
}
},
"required": [
"arguments",
"id",
"status",
"tool",
"type"
],
"title": "DynamicToolCallThreadItem",
"type": "object"
},
{
"properties": {
"agentsStates": {

View File

@@ -299,6 +299,58 @@
],
"type": "string"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
"properties": {
"text": {
"type": "string"
},
"type": {
"enum": [
"inputText"
],
"title": "InputTextDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"text",
"type"
],
"title": "InputTextDynamicToolCallOutputContentItem",
"type": "object"
},
{
"properties": {
"imageUrl": {
"type": "string"
},
"type": {
"enum": [
"inputImage"
],
"title": "InputImageDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"imageUrl",
"type"
],
"title": "InputImageDynamicToolCallOutputContentItem",
"type": "object"
}
]
},
"DynamicToolCallStatus": {
"enum": [
"inProgress",
"completed",
"failed"
],
"type": "string"
},
"FileUpdateChange": {
"properties": {
"diff": {
@@ -980,6 +1032,59 @@
"title": "McpToolCallThreadItem",
"type": "object"
},
{
"properties": {
"arguments": true,
"contentItems": {
"items": {
"$ref": "#/definitions/DynamicToolCallOutputContentItem"
},
"type": [
"array",
"null"
]
},
"durationMs": {
"description": "The duration of the dynamic tool call in milliseconds.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"id": {
"type": "string"
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
"success": {
"type": [
"boolean",
"null"
]
},
"tool": {
"type": "string"
},
"type": {
"enum": [
"dynamicToolCall"
],
"title": "DynamicToolCallThreadItemType",
"type": "string"
}
},
"required": [
"arguments",
"id",
"status",
"tool",
"type"
],
"title": "DynamicToolCallThreadItem",
"type": "object"
},
{
"properties": {
"agentsStates": {

View File

@@ -0,0 +1,20 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "EXPERIMENTAL - emitted when thread realtime transport closes.",
"properties": {
"reason": {
"type": [
"string",
"null"
]
},
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"title": "ThreadRealtimeClosedNotification",
"type": "object"
}

View File

@@ -0,0 +1,18 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "EXPERIMENTAL - emitted when thread realtime encounters an error.",
"properties": {
"message": {
"type": "string"
},
"threadId": {
"type": "string"
}
},
"required": [
"message",
"threadId"
],
"title": "ThreadRealtimeErrorNotification",
"type": "object"
}

View File

@@ -0,0 +1,16 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "EXPERIMENTAL - raw non-audio thread realtime item emitted by the backend.",
"properties": {
"item": true,
"threadId": {
"type": "string"
}
},
"required": [
"item",
"threadId"
],
"title": "ThreadRealtimeItemAddedNotification",
"type": "object"
}

View File

@@ -0,0 +1,52 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ThreadRealtimeAudioChunk": {
"description": "EXPERIMENTAL - thread realtime audio chunk.",
"properties": {
"data": {
"type": "string"
},
"numChannels": {
"format": "uint16",
"minimum": 0.0,
"type": "integer"
},
"sampleRate": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"samplesPerChannel": {
"format": "uint32",
"minimum": 0.0,
"type": [
"integer",
"null"
]
}
},
"required": [
"data",
"numChannels",
"sampleRate"
],
"type": "object"
}
},
"description": "EXPERIMENTAL - streamed output audio emitted by thread realtime.",
"properties": {
"audio": {
"$ref": "#/definitions/ThreadRealtimeAudioChunk"
},
"threadId": {
"type": "string"
}
},
"required": [
"audio",
"threadId"
],
"title": "ThreadRealtimeOutputAudioDeltaNotification",
"type": "object"
}

View File

@@ -0,0 +1,20 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "EXPERIMENTAL - emitted when thread realtime startup is accepted.",
"properties": {
"sessionId": {
"type": [
"string",
"null"
]
},
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"title": "ThreadRealtimeStartedNotification",
"type": "object"
}

View File

@@ -345,6 +345,58 @@
],
"type": "string"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
"properties": {
"text": {
"type": "string"
},
"type": {
"enum": [
"inputText"
],
"title": "InputTextDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"text",
"type"
],
"title": "InputTextDynamicToolCallOutputContentItem",
"type": "object"
},
{
"properties": {
"imageUrl": {
"type": "string"
},
"type": {
"enum": [
"inputImage"
],
"title": "InputImageDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"imageUrl",
"type"
],
"title": "InputImageDynamicToolCallOutputContentItem",
"type": "object"
}
]
},
"DynamicToolCallStatus": {
"enum": [
"inProgress",
"completed",
"failed"
],
"type": "string"
},
"FileUpdateChange": {
"properties": {
"diff": {
@@ -1207,6 +1259,59 @@
"title": "McpToolCallThreadItem",
"type": "object"
},
{
"properties": {
"arguments": true,
"contentItems": {
"items": {
"$ref": "#/definitions/DynamicToolCallOutputContentItem"
},
"type": [
"array",
"null"
]
},
"durationMs": {
"description": "The duration of the dynamic tool call in milliseconds.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"id": {
"type": "string"
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
"success": {
"type": [
"boolean",
"null"
]
},
"tool": {
"type": "string"
},
"type": {
"enum": [
"dynamicToolCall"
],
"title": "DynamicToolCallThreadItemType",
"type": "string"
}
},
"required": [
"arguments",
"id",
"status",
"tool",
"type"
],
"title": "DynamicToolCallThreadItem",
"type": "object"
},
{
"properties": {
"agentsStates": {

View File

@@ -299,6 +299,58 @@
],
"type": "string"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
"properties": {
"text": {
"type": "string"
},
"type": {
"enum": [
"inputText"
],
"title": "InputTextDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"text",
"type"
],
"title": "InputTextDynamicToolCallOutputContentItem",
"type": "object"
},
{
"properties": {
"imageUrl": {
"type": "string"
},
"type": {
"enum": [
"inputImage"
],
"title": "InputImageDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"imageUrl",
"type"
],
"title": "InputImageDynamicToolCallOutputContentItem",
"type": "object"
}
]
},
"DynamicToolCallStatus": {
"enum": [
"inProgress",
"completed",
"failed"
],
"type": "string"
},
"FileUpdateChange": {
"properties": {
"diff": {
@@ -980,6 +1032,59 @@
"title": "McpToolCallThreadItem",
"type": "object"
},
{
"properties": {
"arguments": true,
"contentItems": {
"items": {
"$ref": "#/definitions/DynamicToolCallOutputContentItem"
},
"type": [
"array",
"null"
]
},
"durationMs": {
"description": "The duration of the dynamic tool call in milliseconds.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"id": {
"type": "string"
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
"success": {
"type": [
"boolean",
"null"
]
},
"tool": {
"type": "string"
},
"type": {
"enum": [
"dynamicToolCall"
],
"title": "DynamicToolCallThreadItemType",
"type": "string"
}
},
"required": [
"arguments",
"id",
"status",
"tool",
"type"
],
"title": "DynamicToolCallThreadItem",
"type": "object"
},
{
"properties": {
"agentsStates": {

View File

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

View File

@@ -345,6 +345,58 @@
],
"type": "string"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
"properties": {
"text": {
"type": "string"
},
"type": {
"enum": [
"inputText"
],
"title": "InputTextDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"text",
"type"
],
"title": "InputTextDynamicToolCallOutputContentItem",
"type": "object"
},
{
"properties": {
"imageUrl": {
"type": "string"
},
"type": {
"enum": [
"inputImage"
],
"title": "InputImageDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"imageUrl",
"type"
],
"title": "InputImageDynamicToolCallOutputContentItem",
"type": "object"
}
]
},
"DynamicToolCallStatus": {
"enum": [
"inProgress",
"completed",
"failed"
],
"type": "string"
},
"FileUpdateChange": {
"properties": {
"diff": {
@@ -1207,6 +1259,59 @@
"title": "McpToolCallThreadItem",
"type": "object"
},
{
"properties": {
"arguments": true,
"contentItems": {
"items": {
"$ref": "#/definitions/DynamicToolCallOutputContentItem"
},
"type": [
"array",
"null"
]
},
"durationMs": {
"description": "The duration of the dynamic tool call in milliseconds.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"id": {
"type": "string"
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
"success": {
"type": [
"boolean",
"null"
]
},
"tool": {
"type": "string"
},
"type": {
"enum": [
"dynamicToolCall"
],
"title": "DynamicToolCallThreadItemType",
"type": "string"
}
},
"required": [
"arguments",
"id",
"status",
"tool",
"type"
],
"title": "DynamicToolCallThreadItem",
"type": "object"
},
{
"properties": {
"agentsStates": {

View File

@@ -299,6 +299,58 @@
],
"type": "string"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
"properties": {
"text": {
"type": "string"
},
"type": {
"enum": [
"inputText"
],
"title": "InputTextDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"text",
"type"
],
"title": "InputTextDynamicToolCallOutputContentItem",
"type": "object"
},
{
"properties": {
"imageUrl": {
"type": "string"
},
"type": {
"enum": [
"inputImage"
],
"title": "InputImageDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"imageUrl",
"type"
],
"title": "InputImageDynamicToolCallOutputContentItem",
"type": "object"
}
]
},
"DynamicToolCallStatus": {
"enum": [
"inProgress",
"completed",
"failed"
],
"type": "string"
},
"FileUpdateChange": {
"properties": {
"diff": {
@@ -980,6 +1032,59 @@
"title": "McpToolCallThreadItem",
"type": "object"
},
{
"properties": {
"arguments": true,
"contentItems": {
"items": {
"$ref": "#/definitions/DynamicToolCallOutputContentItem"
},
"type": [
"array",
"null"
]
},
"durationMs": {
"description": "The duration of the dynamic tool call in milliseconds.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"id": {
"type": "string"
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
"success": {
"type": [
"boolean",
"null"
]
},
"tool": {
"type": "string"
},
"type": {
"enum": [
"dynamicToolCall"
],
"title": "DynamicToolCallThreadItemType",
"type": "string"
}
},
"required": [
"arguments",
"id",
"status",
"tool",
"type"
],
"title": "DynamicToolCallThreadItem",
"type": "object"
},
{
"properties": {
"agentsStates": {

View File

@@ -299,6 +299,58 @@
],
"type": "string"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
"properties": {
"text": {
"type": "string"
},
"type": {
"enum": [
"inputText"
],
"title": "InputTextDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"text",
"type"
],
"title": "InputTextDynamicToolCallOutputContentItem",
"type": "object"
},
{
"properties": {
"imageUrl": {
"type": "string"
},
"type": {
"enum": [
"inputImage"
],
"title": "InputImageDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"imageUrl",
"type"
],
"title": "InputImageDynamicToolCallOutputContentItem",
"type": "object"
}
]
},
"DynamicToolCallStatus": {
"enum": [
"inProgress",
"completed",
"failed"
],
"type": "string"
},
"FileUpdateChange": {
"properties": {
"diff": {
@@ -980,6 +1032,59 @@
"title": "McpToolCallThreadItem",
"type": "object"
},
{
"properties": {
"arguments": true,
"contentItems": {
"items": {
"$ref": "#/definitions/DynamicToolCallOutputContentItem"
},
"type": [
"array",
"null"
]
},
"durationMs": {
"description": "The duration of the dynamic tool call in milliseconds.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"id": {
"type": "string"
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
"success": {
"type": [
"boolean",
"null"
]
},
"tool": {
"type": "string"
},
"type": {
"enum": [
"dynamicToolCall"
],
"title": "DynamicToolCallThreadItemType",
"type": "string"
}
},
"required": [
"arguments",
"id",
"status",
"tool",
"type"
],
"title": "DynamicToolCallThreadItem",
"type": "object"
},
{
"properties": {
"agentsStates": {

View File

@@ -299,6 +299,58 @@
],
"type": "string"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
"properties": {
"text": {
"type": "string"
},
"type": {
"enum": [
"inputText"
],
"title": "InputTextDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"text",
"type"
],
"title": "InputTextDynamicToolCallOutputContentItem",
"type": "object"
},
{
"properties": {
"imageUrl": {
"type": "string"
},
"type": {
"enum": [
"inputImage"
],
"title": "InputImageDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"imageUrl",
"type"
],
"title": "InputImageDynamicToolCallOutputContentItem",
"type": "object"
}
]
},
"DynamicToolCallStatus": {
"enum": [
"inProgress",
"completed",
"failed"
],
"type": "string"
},
"FileUpdateChange": {
"properties": {
"diff": {
@@ -747,6 +799,59 @@
"title": "McpToolCallThreadItem",
"type": "object"
},
{
"properties": {
"arguments": true,
"contentItems": {
"items": {
"$ref": "#/definitions/DynamicToolCallOutputContentItem"
},
"type": [
"array",
"null"
]
},
"durationMs": {
"description": "The duration of the dynamic tool call in milliseconds.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"id": {
"type": "string"
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
"success": {
"type": [
"boolean",
"null"
]
},
"tool": {
"type": "string"
},
"type": {
"enum": [
"dynamicToolCall"
],
"title": "DynamicToolCallThreadItemType",
"type": "string"
}
},
"required": [
"arguments",
"id",
"status",
"tool",
"type"
],
"title": "DynamicToolCallThreadItem",
"type": "object"
},
{
"properties": {
"agentsStates": {

View File

@@ -299,6 +299,58 @@
],
"type": "string"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
"properties": {
"text": {
"type": "string"
},
"type": {
"enum": [
"inputText"
],
"title": "InputTextDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"text",
"type"
],
"title": "InputTextDynamicToolCallOutputContentItem",
"type": "object"
},
{
"properties": {
"imageUrl": {
"type": "string"
},
"type": {
"enum": [
"inputImage"
],
"title": "InputImageDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"imageUrl",
"type"
],
"title": "InputImageDynamicToolCallOutputContentItem",
"type": "object"
}
]
},
"DynamicToolCallStatus": {
"enum": [
"inProgress",
"completed",
"failed"
],
"type": "string"
},
"FileUpdateChange": {
"properties": {
"diff": {
@@ -747,6 +799,59 @@
"title": "McpToolCallThreadItem",
"type": "object"
},
{
"properties": {
"arguments": true,
"contentItems": {
"items": {
"$ref": "#/definitions/DynamicToolCallOutputContentItem"
},
"type": [
"array",
"null"
]
},
"durationMs": {
"description": "The duration of the dynamic tool call in milliseconds.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"id": {
"type": "string"
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
"success": {
"type": [
"boolean",
"null"
]
},
"tool": {
"type": "string"
},
"type": {
"enum": [
"dynamicToolCall"
],
"title": "DynamicToolCallThreadItemType",
"type": "string"
}
},
"required": [
"arguments",
"id",
"status",
"tool",
"type"
],
"title": "DynamicToolCallThreadItem",
"type": "object"
},
{
"properties": {
"agentsStates": {

View File

@@ -299,6 +299,58 @@
],
"type": "string"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
"properties": {
"text": {
"type": "string"
},
"type": {
"enum": [
"inputText"
],
"title": "InputTextDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"text",
"type"
],
"title": "InputTextDynamicToolCallOutputContentItem",
"type": "object"
},
{
"properties": {
"imageUrl": {
"type": "string"
},
"type": {
"enum": [
"inputImage"
],
"title": "InputImageDynamicToolCallOutputContentItemType",
"type": "string"
}
},
"required": [
"imageUrl",
"type"
],
"title": "InputImageDynamicToolCallOutputContentItem",
"type": "object"
}
]
},
"DynamicToolCallStatus": {
"enum": [
"inProgress",
"completed",
"failed"
],
"type": "string"
},
"FileUpdateChange": {
"properties": {
"diff": {
@@ -747,6 +799,59 @@
"title": "McpToolCallThreadItem",
"type": "object"
},
{
"properties": {
"arguments": true,
"contentItems": {
"items": {
"$ref": "#/definitions/DynamicToolCallOutputContentItem"
},
"type": [
"array",
"null"
]
},
"durationMs": {
"description": "The duration of the dynamic tool call in milliseconds.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"id": {
"type": "string"
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
"success": {
"type": [
"boolean",
"null"
]
},
"tool": {
"type": "string"
},
"type": {
"enum": [
"dynamicToolCall"
],
"title": "DynamicToolCallThreadItemType",
"type": "string"
}
},
"required": [
"arguments",
"id",
"status",
"tool",
"type"
],
"title": "DynamicToolCallThreadItem",
"type": "object"
},
{
"properties": {
"agentsStates": {

File diff suppressed because one or more lines are too long

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 DynamicToolCallOutputContentItem = { "type": "inputText", text: string, } | { "type": "inputImage", imageUrl: string, };

View File

@@ -0,0 +1,39 @@
// 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 { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem";
import type { JsonValue } from "./serde_json/JsonValue";
export type DynamicToolCallResponseEvent = {
/**
* Identifier for the corresponding DynamicToolCallRequest.
*/
call_id: string,
/**
* Turn ID that this dynamic tool call belongs to.
*/
turn_id: string,
/**
* Dynamic tool name.
*/
tool: string,
/**
* Dynamic tool call arguments.
*/
arguments: JsonValue,
/**
* Dynamic tool response content items.
*/
content_items: Array<DynamicToolCallOutputContentItem>,
/**
* Whether the tool call succeeded.
*/
success: boolean,
/**
* Optional error text when the tool call failed before producing a response.
*/
error: string | null,
/**
* The duration of the dynamic tool call.
*/
duration: string, };

View File

@@ -24,6 +24,7 @@ import type { CollabWaitingEndEvent } from "./CollabWaitingEndEvent";
import type { ContextCompactedEvent } from "./ContextCompactedEvent";
import type { DeprecationNoticeEvent } from "./DeprecationNoticeEvent";
import type { DynamicToolCallRequest } from "./DynamicToolCallRequest";
import type { DynamicToolCallResponseEvent } from "./DynamicToolCallResponseEvent";
import type { ElicitationRequestEvent } from "./ElicitationRequestEvent";
import type { ErrorEvent } from "./ErrorEvent";
import type { ExecApprovalRequestEvent } from "./ExecApprovalRequestEvent";
@@ -56,6 +57,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 +80,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": "dynamic_tool_call_response" } & DynamicToolCallResponseEvent | { "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

@@ -30,6 +30,11 @@ import type { ReasoningTextDeltaNotification } from "./v2/ReasoningTextDeltaNoti
import type { TerminalInteractionNotification } from "./v2/TerminalInteractionNotification";
import type { ThreadArchivedNotification } from "./v2/ThreadArchivedNotification";
import type { ThreadNameUpdatedNotification } from "./v2/ThreadNameUpdatedNotification";
import type { ThreadRealtimeClosedNotification } from "./v2/ThreadRealtimeClosedNotification";
import type { ThreadRealtimeErrorNotification } from "./v2/ThreadRealtimeErrorNotification";
import type { ThreadRealtimeItemAddedNotification } from "./v2/ThreadRealtimeItemAddedNotification";
import type { ThreadRealtimeOutputAudioDeltaNotification } from "./v2/ThreadRealtimeOutputAudioDeltaNotification";
import type { ThreadRealtimeStartedNotification } from "./v2/ThreadRealtimeStartedNotification";
import type { ThreadStartedNotification } from "./v2/ThreadStartedNotification";
import type { ThreadStatusChangedNotification } from "./v2/ThreadStatusChangedNotification";
import type { ThreadTokenUsageUpdatedNotification } from "./v2/ThreadTokenUsageUpdatedNotification";
@@ -44,4 +49,4 @@ import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldW
/**
* Notification sent from the server to the client.
*/
export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification } | { "method": "authStatusChange", "params": AuthStatusChangeNotification } | { "method": "loginChatGptComplete", "params": LoginChatGptCompleteNotification } | { "method": "sessionConfigured", "params": SessionConfiguredNotification };
export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification } | { "method": "authStatusChange", "params": AuthStatusChangeNotification } | { "method": "loginChatGptComplete", "params": LoginChatGptCompleteNotification } | { "method": "sessionConfigured", "params": SessionConfiguredNotification };

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

@@ -53,7 +53,9 @@ export type { ConversationSummary } from "./ConversationSummary";
export type { CreditsSnapshot } from "./CreditsSnapshot";
export type { CustomPrompt } from "./CustomPrompt";
export type { DeprecationNoticeEvent } from "./DeprecationNoticeEvent";
export type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem";
export type { DynamicToolCallRequest } from "./DynamicToolCallRequest";
export type { DynamicToolCallResponseEvent } from "./DynamicToolCallResponseEvent";
export type { ElicitationRequestEvent } from "./ElicitationRequestEvent";
export type { ErrorEvent } from "./ErrorEvent";
export type { EventMsg } from "./EventMsg";
@@ -71,6 +73,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 +119,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 +138,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 +210,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,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 DynamicToolCallStatus = "inProgress" | "completed" | "failed";

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

@@ -19,6 +19,15 @@ export type ThreadForkParams = {threadId: string, /**
* If specified, the thread_id param will be ignored.
*/
path?: string | null, /**
* [UNSTABLE] Fork after the specified historical turn (inclusive).
* When omitted, the full thread history is copied.
*
* The value should come from `thread/read(includeTurns=true)` for the same source thread.
* Requests are rejected for legacy histories whose replayed turn ids are not stable, for
* in-progress turns, and for turns that do not yet contain both a user message and an agent
* response.
*/
forkAfterTurnId?: string | null, /**
* Configuration overrides for the forked thread, if any.
*/
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, /**

View File

@@ -8,6 +8,8 @@ import type { CollabAgentTool } from "./CollabAgentTool";
import type { CollabAgentToolCallStatus } from "./CollabAgentToolCallStatus";
import type { CommandAction } from "./CommandAction";
import type { CommandExecutionStatus } from "./CommandExecutionStatus";
import type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem";
import type { DynamicToolCallStatus } from "./DynamicToolCallStatus";
import type { FileUpdateChange } from "./FileUpdateChange";
import type { McpToolCallError } from "./McpToolCallError";
import type { McpToolCallResult } from "./McpToolCallResult";
@@ -50,6 +52,10 @@ durationMs: number | null, } | { "type": "fileChange", id: string, changes: Arra
/**
* The duration of the MCP tool call in milliseconds.
*/
durationMs: number | null, } | { "type": "dynamicToolCall", id: string, tool: string, arguments: JsonValue, status: DynamicToolCallStatus, contentItems: Array<DynamicToolCallOutputContentItem> | null, success: boolean | null,
/**
* The duration of the dynamic tool call in milliseconds.
*/
durationMs: number | null, } | { "type": "collabAgentToolCall",
/**
* Unique identifier for this collab tool call.

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

@@ -0,0 +1,8 @@
// 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.
/**
* EXPERIMENTAL - thread realtime audio chunk.
*/
export type ThreadRealtimeAudioChunk = { data: string, sampleRate: number, numChannels: number, samplesPerChannel: number | null, };

View File

@@ -0,0 +1,8 @@
// 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.
/**
* EXPERIMENTAL - emitted when thread realtime transport closes.
*/
export type ThreadRealtimeClosedNotification = { threadId: string, reason: string | null, };

View File

@@ -0,0 +1,8 @@
// 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.
/**
* EXPERIMENTAL - emitted when thread realtime encounters an error.
*/
export type ThreadRealtimeErrorNotification = { threadId: string, message: string, };

View File

@@ -0,0 +1,9 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { JsonValue } from "../serde_json/JsonValue";
/**
* EXPERIMENTAL - raw non-audio thread realtime item emitted by the backend.
*/
export type ThreadRealtimeItemAddedNotification = { threadId: string, item: JsonValue, };

View File

@@ -0,0 +1,9 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ThreadRealtimeAudioChunk } from "./ThreadRealtimeAudioChunk";
/**
* EXPERIMENTAL - streamed output audio emitted by thread realtime.
*/
export type ThreadRealtimeOutputAudioDeltaNotification = { threadId: string, audio: ThreadRealtimeAudioChunk, };

View File

@@ -0,0 +1,8 @@
// 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.
/**
* EXPERIMENTAL - emitted when thread realtime startup is accepted.
*/
export type ThreadRealtimeStartedNotification = { threadId: string, sessionId: 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";
@@ -58,6 +61,7 @@ export type { DeprecationNoticeNotification } from "./DeprecationNoticeNotificat
export type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem";
export type { DynamicToolCallParams } from "./DynamicToolCallParams";
export type { DynamicToolCallResponse } from "./DynamicToolCallResponse";
export type { DynamicToolCallStatus } from "./DynamicToolCallStatus";
export type { DynamicToolSpec } from "./DynamicToolSpec";
export type { ErrorNotification } from "./ErrorNotification";
export type { ExecPolicyAmendment } from "./ExecPolicyAmendment";
@@ -65,6 +69,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 +113,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 +140,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";
@@ -165,6 +180,12 @@ export type { ThreadLoadedListResponse } from "./ThreadLoadedListResponse";
export type { ThreadNameUpdatedNotification } from "./ThreadNameUpdatedNotification";
export type { ThreadReadParams } from "./ThreadReadParams";
export type { ThreadReadResponse } from "./ThreadReadResponse";
export type { ThreadRealtimeAudioChunk } from "./ThreadRealtimeAudioChunk";
export type { ThreadRealtimeClosedNotification } from "./ThreadRealtimeClosedNotification";
export type { ThreadRealtimeErrorNotification } from "./ThreadRealtimeErrorNotification";
export type { ThreadRealtimeItemAddedNotification } from "./ThreadRealtimeItemAddedNotification";
export type { ThreadRealtimeOutputAudioDeltaNotification } from "./ThreadRealtimeOutputAudioDeltaNotification";
export type { ThreadRealtimeStartedNotification } from "./ThreadRealtimeStartedNotification";
export type { ThreadResumeParams } from "./ThreadResumeParams";
export type { ThreadResumeResponse } from "./ThreadResumeResponse";
export type { ThreadRollbackParams } from "./ThreadRollbackParams";

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

@@ -7,6 +7,7 @@ use crate::export::GeneratedSchema;
use crate::export::write_json_schema;
use crate::protocol::v1;
use crate::protocol::v2;
use codex_experimental_api_macros::ExperimentalApi;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
@@ -268,6 +269,26 @@ client_request_definitions! {
params: v2::TurnInterruptParams,
response: v2::TurnInterruptResponse,
},
#[experimental("thread/realtime/start")]
ThreadRealtimeStart => "thread/realtime/start" {
params: v2::ThreadRealtimeStartParams,
response: v2::ThreadRealtimeStartResponse,
},
#[experimental("thread/realtime/appendAudio")]
ThreadRealtimeAppendAudio => "thread/realtime/appendAudio" {
params: v2::ThreadRealtimeAppendAudioParams,
response: v2::ThreadRealtimeAppendAudioResponse,
},
#[experimental("thread/realtime/appendText")]
ThreadRealtimeAppendText => "thread/realtime/appendText" {
params: v2::ThreadRealtimeAppendTextParams,
response: v2::ThreadRealtimeAppendTextResponse,
},
#[experimental("thread/realtime/stop")]
ThreadRealtimeStop => "thread/realtime/stop" {
params: v2::ThreadRealtimeStopParams,
response: v2::ThreadRealtimeStopResponse,
},
ReviewStart => "review/start" {
params: v2::ReviewStartParams,
response: v2::ReviewStartResponse,
@@ -350,6 +371,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 +530,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 +545,7 @@ macro_rules! server_request_definitions {
}
#[derive(Debug, Clone, PartialEq, JsonSchema)]
#[allow(clippy::large_enum_variant)]
pub enum ServerRequestPayload {
$( $variant($params), )*
}
@@ -576,7 +607,16 @@ macro_rules! server_notification_definitions {
),* $(,)?
) => {
/// Notification sent from the server to the client.
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS, Display)]
#[derive(
Serialize,
Deserialize,
Debug,
Clone,
JsonSchema,
TS,
Display,
ExperimentalApi,
)]
#[serde(tag = "method", content = "params", rename_all = "camelCase")]
#[strum(serialize_all = "camelCase")]
pub enum ServerNotification {
@@ -671,6 +711,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,
@@ -808,6 +853,16 @@ server_notification_definitions! {
ConfigWarning => "configWarning" (v2::ConfigWarningNotification),
FuzzyFileSearchSessionUpdated => "fuzzyFileSearch/sessionUpdated" (FuzzyFileSearchSessionUpdatedNotification),
FuzzyFileSearchSessionCompleted => "fuzzyFileSearch/sessionCompleted" (FuzzyFileSearchSessionCompletedNotification),
#[experimental("thread/realtime/started")]
ThreadRealtimeStarted => "thread/realtime/started" (v2::ThreadRealtimeStartedNotification),
#[experimental("thread/realtime/itemAdded")]
ThreadRealtimeItemAdded => "thread/realtime/itemAdded" (v2::ThreadRealtimeItemAddedNotification),
#[experimental("thread/realtime/outputAudio/delta")]
ThreadRealtimeOutputAudioDelta => "thread/realtime/outputAudio/delta" (v2::ThreadRealtimeOutputAudioDeltaNotification),
#[experimental("thread/realtime/error")]
ThreadRealtimeError => "thread/realtime/error" (v2::ThreadRealtimeErrorNotification),
#[experimental("thread/realtime/closed")]
ThreadRealtimeClosed => "thread/realtime/closed" (v2::ThreadRealtimeClosedNotification),
/// Notifies the user of world-writable directories on Windows, which cannot be protected by the sandbox.
WindowsWorldWritableWarning => "windows/worldWritableWarning" (v2::WindowsWorldWritableWarningNotification),
@@ -1335,6 +1390,31 @@ mod tests {
Ok(())
}
#[test]
fn serialize_thread_realtime_start() -> Result<()> {
let request = ClientRequest::ThreadRealtimeStart {
request_id: RequestId::Integer(9),
params: v2::ThreadRealtimeStartParams {
thread_id: "thr_123".to_string(),
prompt: "You are on a call".to_string(),
session_id: Some("sess_456".to_string()),
},
};
assert_eq!(
json!({
"method": "thread/realtime/start",
"id": 9,
"params": {
"threadId": "thr_123",
"prompt": "You are on a call",
"sessionId": "sess_456"
}
}),
serde_json::to_value(&request)?,
);
Ok(())
}
#[test]
fn serialize_thread_status_changed_notification() -> Result<()> {
let notification =
@@ -1357,6 +1437,37 @@ mod tests {
Ok(())
}
#[test]
fn serialize_thread_realtime_output_audio_delta_notification() -> Result<()> {
let notification = ServerNotification::ThreadRealtimeOutputAudioDelta(
v2::ThreadRealtimeOutputAudioDeltaNotification {
thread_id: "thr_123".to_string(),
audio: v2::ThreadRealtimeAudioChunk {
data: "AQID".to_string(),
sample_rate: 24_000,
num_channels: 1,
samples_per_channel: Some(512),
},
},
);
assert_eq!(
json!({
"method": "thread/realtime/outputAudio/delta",
"params": {
"threadId": "thr_123",
"audio": {
"data": "AQID",
"sampleRate": 24000,
"numChannels": 1,
"samplesPerChannel": 512
}
}
}),
serde_json::to_value(&notification)?,
);
Ok(())
}
#[test]
fn mock_experimental_method_is_marked_experimental() {
let request = ClientRequest::MockExperimentalMethod {
@@ -1366,4 +1477,74 @@ mod tests {
let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&request);
assert_eq!(reason, Some("mock/experimentalMethod"));
}
#[test]
fn thread_realtime_start_is_marked_experimental() {
let request = ClientRequest::ThreadRealtimeStart {
request_id: RequestId::Integer(1),
params: v2::ThreadRealtimeStartParams {
thread_id: "thr_123".to_string(),
prompt: "You are on a call".to_string(),
session_id: None,
},
};
let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&request);
assert_eq!(reason, Some("thread/realtime/start"));
}
#[test]
fn thread_realtime_started_notification_is_marked_experimental() {
let notification =
ServerNotification::ThreadRealtimeStarted(v2::ThreadRealtimeStartedNotification {
thread_id: "thr_123".to_string(),
session_id: Some("sess_456".to_string()),
});
let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&notification);
assert_eq!(reason, Some("thread/realtime/started"));
}
#[test]
fn thread_realtime_output_audio_delta_notification_is_marked_experimental() {
let notification = ServerNotification::ThreadRealtimeOutputAudioDelta(
v2::ThreadRealtimeOutputAudioDeltaNotification {
thread_id: "thr_123".to_string(),
audio: v2::ThreadRealtimeAudioChunk {
data: "AQID".to_string(),
sample_rate: 24_000,
num_channels: 1,
samples_per_channel: Some(512),
},
},
);
let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&notification);
assert_eq!(reason, Some("thread/realtime/outputAudio/delta"));
}
#[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

@@ -1,8 +1,23 @@
//! Reconstruct app-server turn history from persisted rollout items.
//!
//! This module translates the low-level rollout log into the higher-level `Turn` / `ThreadItem`
//! structures returned by app-server APIs. The same reducer is used both for replaying stored
//! rollouts and for incrementally rebuilding the current turn from live `EventMsg` values.
//! Callers must feed items in recorded order; reordering lifecycle events can silently attach work
//! to the wrong turn while still producing a superficially valid transcript.
//!
//! Modern rollouts persist explicit turn lifecycle events, which means replay can preserve stable
//! turn ids. Legacy rollouts may only contain message/tool events, so replay has to synthesize
//! turn ids while rebuilding them. Consumers that need durable turn anchors, such as turn-based
//! `thread/fork`, should reject those synthesized ids rather than treating them as stable.
use crate::protocol::v2::CollabAgentState;
use crate::protocol::v2::CollabAgentTool;
use crate::protocol::v2::CollabAgentToolCallStatus;
use crate::protocol::v2::CommandAction;
use crate::protocol::v2::CommandExecutionStatus;
use crate::protocol::v2::DynamicToolCallOutputContentItem;
use crate::protocol::v2::DynamicToolCallStatus;
use crate::protocol::v2::FileUpdateChange;
use crate::protocol::v2::McpToolCallError;
use crate::protocol::v2::McpToolCallResult;
@@ -22,6 +37,7 @@ use codex_protocol::protocol::AgentReasoningRawContentEvent;
use codex_protocol::protocol::AgentStatus;
use codex_protocol::protocol::CompactedItem;
use codex_protocol::protocol::ContextCompactedEvent;
use codex_protocol::protocol::DynamicToolCallResponseEvent;
use codex_protocol::protocol::ErrorEvent;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ExecCommandBeginEvent;
@@ -54,7 +70,10 @@ use codex_protocol::protocol::PatchApplyStatus as CorePatchApplyStatus;
/// Convert persisted [`RolloutItem`] entries into a sequence of [`Turn`] values.
///
/// When available, this uses `TurnContext.turn_id` as the canonical turn id so
/// resumed/rebuilt thread history preserves the original turn identifiers.
/// resumed/rebuilt thread history preserves the original turn identifiers. Callers that also need
/// to distinguish between stable and replay-synthesized turn ids should drive
/// [`ThreadHistoryBuilder`] directly and inspect [`ThreadHistoryBuilder::has_synthetic_turn_ids`]
/// before consuming the final turn list.
pub fn build_turns_from_rollout_items(items: &[RolloutItem]) -> Vec<Turn> {
let mut builder = ThreadHistoryBuilder::new();
for item in items {
@@ -63,10 +82,19 @@ pub fn build_turns_from_rollout_items(items: &[RolloutItem]) -> Vec<Turn> {
builder.finish()
}
/// Stateful reducer that groups rollout lifecycle items into app-server turns.
///
/// The builder owns the in-progress turn boundary while replay is in flight, then emits the
/// finalized `Turn` list once all items have been applied. It also tracks whether replay had to
/// synthesize any turn ids so app-server can share the same reduction rules for stored history
/// and live notifications without exposing that metadata in every convenience return type.
/// Reusing a builder across unrelated transcripts without calling [`Self::reset`] can leak late
/// events from one transcript into the next.
pub struct ThreadHistoryBuilder {
turns: Vec<Turn>,
current_turn: Option<PendingTurn>,
next_item_index: i64,
has_synthetic_turn_ids: bool,
}
impl Default for ThreadHistoryBuilder {
@@ -76,23 +104,47 @@ impl Default for ThreadHistoryBuilder {
}
impl ThreadHistoryBuilder {
/// Create an empty reducer with no active turn.
///
/// The builder expects to receive the rollout from the beginning of a transcript, in order.
/// Starting midway through a rollout can still produce `Turn` values, but turn boundaries and
/// synthetic-id detection may no longer describe the actual persisted history.
pub fn new() -> Self {
Self {
turns: Vec::new(),
current_turn: None,
next_item_index: 1,
has_synthetic_turn_ids: false,
}
}
/// Clear all accumulated turns and return the builder to its initial state.
pub fn reset(&mut self) {
*self = Self::new();
}
pub fn finish(mut self) -> Vec<Turn> {
self.finish_current_turn();
self.turns
/// Finalize replay and return only the reconstructed turns.
pub fn finish(self) -> Vec<Turn> {
let mut this = self;
this.finish_current_turn();
this.turns
}
/// Return whether replay had to synthesize any turn ids so far.
///
/// This becomes true when replay encounters legacy history without explicit turn lifecycle
/// events and must mint replacement ids while rebuilding turns. Callers that want to use turn
/// ids as stable anchors should check this before trusting replayed ids.
pub fn has_synthetic_turn_ids(&self) -> bool {
self.has_synthetic_turn_ids
}
/// Return the current turn view without consuming the builder.
///
/// When a turn is actively streaming, this returns that in-progress turn. Otherwise it falls
/// back to the most recently completed turn. This is useful for live UI updates, but a caller
/// that treats the result as committed persisted history can accidentally display incomplete
/// state after a partially applied event stream.
pub fn active_turn_snapshot(&self) -> Option<Turn> {
self.current_turn
.as_ref()
@@ -100,6 +152,7 @@ impl ThreadHistoryBuilder {
.or_else(|| self.turns.last().cloned())
}
/// Return whether replay is currently inside an unfinished turn boundary.
pub fn has_active_turn(&self) -> bool {
self.current_turn.is_some()
}
@@ -125,6 +178,12 @@ impl ThreadHistoryBuilder {
EventMsg::ExecCommandEnd(payload) => self.handle_exec_command_end(payload),
EventMsg::PatchApplyBegin(payload) => self.handle_patch_apply_begin(payload),
EventMsg::PatchApplyEnd(payload) => self.handle_patch_apply_end(payload),
EventMsg::DynamicToolCallRequest(payload) => {
self.handle_dynamic_tool_call_request(payload)
}
EventMsg::DynamicToolCallResponse(payload) => {
self.handle_dynamic_tool_call_response(payload)
}
EventMsg::McpToolCallBegin(payload) => self.handle_mcp_tool_call_begin(payload),
EventMsg::McpToolCallEnd(payload) => self.handle_mcp_tool_call_end(payload),
EventMsg::ViewImageToolCall(payload) => self.handle_view_image_tool_call(payload),
@@ -160,6 +219,12 @@ impl ThreadHistoryBuilder {
}
}
/// Apply one persisted rollout line to the reducer.
///
/// Unlike [`Self::handle_event`], this accepts the outer `RolloutItem` wrapper so replay can
/// preserve rollback markers and explicit turn boundaries exactly as they were recorded on
/// disk. Feed items in file order; replaying a suffix or shuffling lines can still produce
/// syntactically valid turns while silently attaching items to the wrong exchange.
pub fn handle_rollout_item(&mut self, item: &RolloutItem) {
match item {
RolloutItem::EventMsg(event) => self.handle_event(event),
@@ -382,6 +447,49 @@ impl ThreadHistoryBuilder {
}
}
fn handle_dynamic_tool_call_request(
&mut self,
payload: &codex_protocol::dynamic_tools::DynamicToolCallRequest,
) {
let item = ThreadItem::DynamicToolCall {
id: payload.call_id.clone(),
tool: payload.tool.clone(),
arguments: payload.arguments.clone(),
status: DynamicToolCallStatus::InProgress,
content_items: None,
success: None,
duration_ms: None,
};
if payload.turn_id.is_empty() {
self.upsert_item_in_current_turn(item);
} else {
self.upsert_item_in_turn_id(&payload.turn_id, item);
}
}
fn handle_dynamic_tool_call_response(&mut self, payload: &DynamicToolCallResponseEvent) {
let status = if payload.success {
DynamicToolCallStatus::Completed
} else {
DynamicToolCallStatus::Failed
};
let duration_ms = i64::try_from(payload.duration.as_millis()).ok();
let item = ThreadItem::DynamicToolCall {
id: payload.call_id.clone(),
tool: payload.tool.clone(),
arguments: payload.arguments.clone(),
status,
content_items: Some(convert_dynamic_tool_content_items(&payload.content_items)),
success: Some(payload.success),
duration_ms,
};
if payload.turn_id.is_empty() {
self.upsert_item_in_current_turn(item);
} else {
self.upsert_item_in_turn_id(&payload.turn_id, item);
}
}
fn handle_mcp_tool_call_begin(&mut self, payload: &McpToolCallBeginEvent) {
let item = ThreadItem::McpToolCall {
id: payload.call_id.clone(),
@@ -809,8 +917,15 @@ impl ThreadHistoryBuilder {
}
fn new_turn(&mut self, id: Option<String>) -> PendingTurn {
let id = match id {
Some(id) => id,
None => {
self.has_synthetic_turn_ids = true;
Uuid::now_v7().to_string()
}
};
PendingTurn {
id: id.unwrap_or_else(|| Uuid::now_v7().to_string()),
id,
items: Vec::new(),
error: None,
status: TurnStatus::Completed,
@@ -898,6 +1013,10 @@ fn render_review_output_text(output: &ReviewOutputEvent) -> String {
}
}
/// Convert core patch-apply change records into the app-server wire format.
///
/// The returned list is sorted by path so repeated replays of the same patch event stay stable for
/// clients and snapshot tests.
pub fn convert_patch_changes(
changes: &HashMap<std::path::PathBuf, codex_protocol::protocol::FileChange>,
) -> Vec<FileUpdateChange> {
@@ -913,6 +1032,23 @@ pub fn convert_patch_changes(
converted
}
fn convert_dynamic_tool_content_items(
items: &[codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem],
) -> Vec<DynamicToolCallOutputContentItem> {
items
.iter()
.cloned()
.map(|item| match item {
codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem::InputText { text } => {
DynamicToolCallOutputContentItem::InputText { text }
}
codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem::InputImage {
image_url,
} => DynamicToolCallOutputContentItem::InputImage { image_url },
})
.collect()
}
fn map_patch_change_kind(change: &codex_protocol::protocol::FileChange) -> PatchChangeKind {
match change {
codex_protocol::protocol::FileChange::Add { .. } => PatchChangeKind::Add,
@@ -1002,6 +1138,7 @@ impl From<&PendingTurn> for Turn {
mod tests {
use super::*;
use codex_protocol::ThreadId;
use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem;
use codex_protocol::items::TurnItem as CoreTurnItem;
use codex_protocol::items::UserMessageItem as CoreUserMessageItem;
use codex_protocol::models::MessagePhase as CoreMessagePhase;
@@ -1012,6 +1149,7 @@ mod tests {
use codex_protocol::protocol::AgentReasoningRawContentEvent;
use codex_protocol::protocol::CodexErrorInfo;
use codex_protocol::protocol::CompactedItem;
use codex_protocol::protocol::DynamicToolCallResponseEvent;
use codex_protocol::protocol::ExecCommandEndEvent;
use codex_protocol::protocol::ExecCommandSource;
use codex_protocol::protocol::ItemStartedEvent;
@@ -1127,6 +1265,66 @@ mod tests {
);
}
#[test]
fn reports_synthetic_turn_ids_for_legacy_history() {
let items = vec![
RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
message: "legacy".into(),
images: None,
text_elements: Vec::new(),
local_images: Vec::new(),
})),
RolloutItem::EventMsg(EventMsg::AgentMessage(AgentMessageEvent {
message: "reply".into(),
phase: None,
})),
];
let mut builder = ThreadHistoryBuilder::new();
for item in &items {
builder.handle_rollout_item(item);
}
assert!(builder.has_synthetic_turn_ids());
assert_eq!(builder.finish().len(), 1);
}
#[test]
fn reports_no_synthetic_turn_ids_when_turn_boundaries_are_explicit() {
let turn_id = "turn-explicit";
let items = vec![
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
turn_id: turn_id.to_string(),
model_context_window: Some(128_000),
collaboration_mode_kind: Default::default(),
})),
RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
message: "modern".into(),
images: None,
text_elements: Vec::new(),
local_images: Vec::new(),
})),
RolloutItem::EventMsg(EventMsg::AgentMessage(AgentMessageEvent {
message: "reply".into(),
phase: None,
})),
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: turn_id.to_string(),
last_agent_message: Some("reply".into()),
})),
];
let mut builder = ThreadHistoryBuilder::new();
for item in &items {
builder.handle_rollout_item(item);
}
assert!(!builder.has_synthetic_turn_ids());
let turns = builder.finish();
assert_eq!(turns.len(), 1);
assert_eq!(turns[0].id, turn_id);
}
#[test]
fn ignores_non_plan_item_lifecycle_events() {
let turn_id = "turn-1";
@@ -1606,6 +1804,65 @@ mod tests {
);
}
#[test]
fn reconstructs_dynamic_tool_items_from_request_and_response_events() {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".into(),
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
EventMsg::UserMessage(UserMessageEvent {
message: "run dynamic tool".into(),
images: None,
text_elements: Vec::new(),
local_images: Vec::new(),
}),
EventMsg::DynamicToolCallRequest(
codex_protocol::dynamic_tools::DynamicToolCallRequest {
call_id: "dyn-1".into(),
turn_id: "turn-1".into(),
tool: "lookup_ticket".into(),
arguments: serde_json::json!({"id":"ABC-123"}),
},
),
EventMsg::DynamicToolCallResponse(DynamicToolCallResponseEvent {
call_id: "dyn-1".into(),
turn_id: "turn-1".into(),
tool: "lookup_ticket".into(),
arguments: serde_json::json!({"id":"ABC-123"}),
content_items: vec![CoreDynamicToolCallOutputContentItem::InputText {
text: "Ticket is open".into(),
}],
success: true,
error: None,
duration: Duration::from_millis(42),
}),
];
let items = events
.into_iter()
.map(RolloutItem::EventMsg)
.collect::<Vec<_>>();
let turns = build_turns_from_rollout_items(&items);
assert_eq!(turns.len(), 1);
assert_eq!(turns[0].items.len(), 2);
assert_eq!(
turns[0].items[1],
ThreadItem::DynamicToolCall {
id: "dyn-1".into(),
tool: "lookup_ticket".into(),
arguments: serde_json::json!({"id":"ABC-123"}),
status: DynamicToolCallStatus::Completed,
content_items: Some(vec![DynamicToolCallOutputContentItem::InputText {
text: "Ticket is open".into(),
}]),
success: Some(true),
duration_ms: Some(42),
}
);
}
#[test]
fn reconstructs_declined_exec_and_patch_items() {
let events = vec![

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;
@@ -39,6 +46,7 @@ use codex_protocol::protocol::PatchApplyStatus as CorePatchApplyStatus;
use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot;
use codex_protocol::protocol::RateLimitWindow as CoreRateLimitWindow;
use codex_protocol::protocol::ReadOnlyAccess as CoreReadOnlyAccess;
use codex_protocol::protocol::RealtimeAudioFrame as CoreRealtimeAudioFrame;
use codex_protocol::protocol::RejectConfig as CoreRejectConfig;
use codex_protocol::protocol::SessionSource as CoreSessionSource;
use codex_protocol::protocol::SkillDependencies as CoreSkillDependencies;
@@ -633,6 +641,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 +747,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 +783,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 +1037,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 +1720,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>,
@@ -1708,6 +1869,17 @@ pub struct ThreadForkParams {
#[ts(optional = nullable)]
pub path: Option<PathBuf>,
/// [UNSTABLE] Fork after the specified historical turn (inclusive).
/// When omitted, the full thread history is copied.
///
/// The value should come from `thread/read(includeTurns=true)` for the same source thread.
/// Requests are rejected for legacy histories whose replayed turn ids are not stable, for
/// in-progress turns, and for turns that do not yet contain both a user message and an agent
/// response.
#[experimental("thread/fork.forkAfterTurnId")]
#[ts(optional = nullable)]
pub fork_after_turn_id: Option<String>,
/// Configuration overrides for the forked thread, if any.
#[ts(optional = nullable)]
pub model: Option<String>,
@@ -1861,6 +2033,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)]
@@ -2388,6 +2563,157 @@ pub struct ErrorNotification {
pub turn_id: String,
}
/// EXPERIMENTAL - thread realtime audio chunk.
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadRealtimeAudioChunk {
pub data: String,
pub sample_rate: u32,
pub num_channels: u16,
pub samples_per_channel: Option<u32>,
}
impl From<CoreRealtimeAudioFrame> for ThreadRealtimeAudioChunk {
fn from(value: CoreRealtimeAudioFrame) -> Self {
let CoreRealtimeAudioFrame {
data,
sample_rate,
num_channels,
samples_per_channel,
} = value;
Self {
data,
sample_rate,
num_channels,
samples_per_channel,
}
}
}
impl From<ThreadRealtimeAudioChunk> for CoreRealtimeAudioFrame {
fn from(value: ThreadRealtimeAudioChunk) -> Self {
let ThreadRealtimeAudioChunk {
data,
sample_rate,
num_channels,
samples_per_channel,
} = value;
Self {
data,
sample_rate,
num_channels,
samples_per_channel,
}
}
}
/// EXPERIMENTAL - start a thread-scoped realtime session.
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadRealtimeStartParams {
pub thread_id: String,
pub prompt: String,
#[ts(optional = nullable)]
pub session_id: Option<String>,
}
/// EXPERIMENTAL - response for starting thread realtime.
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadRealtimeStartResponse {}
/// EXPERIMENTAL - append audio input to thread realtime.
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadRealtimeAppendAudioParams {
pub thread_id: String,
pub audio: ThreadRealtimeAudioChunk,
}
/// EXPERIMENTAL - response for appending realtime audio input.
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadRealtimeAppendAudioResponse {}
/// EXPERIMENTAL - append text input to thread realtime.
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadRealtimeAppendTextParams {
pub thread_id: String,
pub text: String,
}
/// EXPERIMENTAL - response for appending realtime text input.
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadRealtimeAppendTextResponse {}
/// EXPERIMENTAL - stop thread realtime.
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadRealtimeStopParams {
pub thread_id: String,
}
/// EXPERIMENTAL - response for stopping thread realtime.
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadRealtimeStopResponse {}
/// EXPERIMENTAL - emitted when thread realtime startup is accepted.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadRealtimeStartedNotification {
pub thread_id: String,
pub session_id: Option<String>,
}
/// EXPERIMENTAL - raw non-audio thread realtime item emitted by the backend.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadRealtimeItemAddedNotification {
pub thread_id: String,
pub item: JsonValue,
}
/// EXPERIMENTAL - streamed output audio emitted by thread realtime.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadRealtimeOutputAudioDeltaNotification {
pub thread_id: String,
pub audio: ThreadRealtimeAudioChunk,
}
/// EXPERIMENTAL - emitted when thread realtime encounters an error.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadRealtimeErrorNotification {
pub thread_id: String,
pub message: String,
}
/// EXPERIMENTAL - emitted when thread realtime transport closes.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadRealtimeClosedNotification {
pub thread_id: String,
pub reason: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -2742,6 +3068,19 @@ pub enum ThreadItem {
},
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
DynamicToolCall {
id: String,
tool: String,
arguments: JsonValue,
status: DynamicToolCallStatus,
content_items: Option<Vec<DynamicToolCallOutputContentItem>>,
success: Option<bool>,
/// The duration of the dynamic tool call in milliseconds.
#[ts(type = "number | null")]
duration_ms: Option<i64>,
},
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
CollabAgentToolCall {
/// Unique identifier for this collab tool call.
id: String,
@@ -2790,6 +3129,7 @@ impl ThreadItem {
| ThreadItem::CommandExecution { id, .. }
| ThreadItem::FileChange { id, .. }
| ThreadItem::McpToolCall { id, .. }
| ThreadItem::DynamicToolCall { id, .. }
| ThreadItem::CollabAgentToolCall { id, .. }
| ThreadItem::WebSearch { id, .. }
| ThreadItem::ImageView { id, .. }
@@ -2970,6 +3310,15 @@ pub enum McpToolCallStatus {
Failed,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub enum DynamicToolCallStatus {
InProgress,
Completed,
Failed,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -3359,7 +3708,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 +3729,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 +3745,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 +3798,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

@@ -121,8 +121,8 @@ 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/fork` — fork an existing thread into a new thread id by copying the stored history. Experimental `forkAfterTurnId` lets clients fork from a selected historical turn (modern turn-id-stable histories only); 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`, `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`).
@@ -135,6 +135,10 @@ Example with notification opt-out:
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode".
- `turn/steer` — add user input to an already in-flight turn without starting a new turn; returns the active `turnId` that accepted the input.
- `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`.
- `thread/realtime/start` — start a thread-scoped realtime session (experimental); returns `{}` and streams `thread/realtime/*` notifications.
- `thread/realtime/appendAudio` — append an input audio chunk to the active realtime session (experimental); returns `{}`.
- `thread/realtime/appendText` — append text input to the active realtime session (experimental); returns `{}`.
- `thread/realtime/stop` — stop the active realtime session for the thread (experimental); returns `{}`.
- `review/start` — kick off Codexs automated reviewer for a thread; responds like `turn/start` and emits `item/started`/`item/completed` notifications with `enteredReviewMode` and `exitedReviewMode` items, plus a final assistant `agentMessage` containing the review.
- `command/exec` — run a single command under the server sandbox without starting a thread/turn (handy for utilities and validation).
- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options and optional `upgrade` model ids.
@@ -153,6 +157,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 +176,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": [
{
@@ -208,7 +215,7 @@ To continue a stored session, call `thread/resume` with the `thread.id` you prev
{ "id": 11, "result": { "thread": { "id": "thr_123", } } }
```
To branch from a stored session, call `thread/fork` with the `thread.id`. This creates a new thread id and emits a `thread/started` notification for it:
To branch from a stored session, call `thread/fork` with the `thread.id`. This creates a new thread id and emits a `thread/started` notification for it. To fork from a selected prior turn instead of the full history, pass experimental `forkAfterTurnId`:
```json
{ "method": "thread/fork", "id": 12, "params": { "threadId": "thr_123" } }
@@ -216,6 +223,19 @@ To branch from a stored session, call `thread/fork` with the `thread.id`. This c
{ "method": "thread/started", "params": { "thread": { } } }
```
```json
{ "method": "thread/fork", "id": 13, "params": {
"threadId": "thr_123",
"forkAfterTurnId": "turn_abc",
"cwd": "/Users/me/project-worktree"
} }
```
The `forkAfterTurnId` value should come from `thread/read(includeTurns=true)` for that source
thread. Anchored forks are rejected for legacy histories that only have replay-synthesized turn
ids, for turns that are still in progress, and for turns that do not yet include both the user's
input and the model's response.
Experimental API: `thread/start`, `thread/resume`, and `thread/fork` accept `persistExtendedHistory: true` to persist a richer subset of ThreadItems for non-lossy history when calling `thread/read`, `thread/resume`, and `thread/fork` later. This does not backfill events that were not persisted previously.
### Example: List threads (with pagination & filters)
@@ -229,6 +249,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:
@@ -549,6 +570,8 @@ Notes:
Event notifications are the server-initiated event stream for thread lifecycles, turn lifecycles, and the items within them. After you start or resume a thread, keep reading stdout for `thread/started`, `thread/archived`, `thread/unarchived`, `turn/*`, and `item/*` notifications.
Thread realtime uses a separate thread-scoped notification surface. `thread/realtime/*` notifications are ephemeral transport events, not `ThreadItem`s, and are not returned by `thread/read`, `thread/resume`, or `thread/fork`.
### Notification opt-out
Clients can suppress specific notifications per connection by sending exact method names in `initialize.params.capabilities.optOutNotificationMethods`.
@@ -570,6 +593,18 @@ The fuzzy file search session API emits per-query notifications:
- `fuzzyFileSearch/sessionUpdated``{ sessionId, query, files }` with the current matching files for the active query.
- `fuzzyFileSearch/sessionCompleted``{ sessionId, query }` once indexing/matching for that query has completed.
### Thread realtime events (experimental)
The thread realtime API emits thread-scoped notifications for session lifecycle and streaming media:
- `thread/realtime/started``{ threadId, sessionId }` once realtime starts for the thread (experimental).
- `thread/realtime/itemAdded``{ threadId, item }` for non-audio realtime items (experimental). `item` is forwarded as raw JSON while the upstream websocket item schema remains unstable.
- `thread/realtime/outputAudio/delta``{ threadId, audio }` for streamed output audio chunks (experimental). `audio` uses camelCase fields (`data`, `sampleRate`, `numChannels`, `samplesPerChannel`).
- `thread/realtime/error``{ threadId, message }` when realtime encounters a transport or backend error (experimental).
- `thread/realtime/closed``{ threadId, reason }` when the realtime transport closes (experimental).
Because audio is intentionally separate from `ThreadItem`, clients can opt out of `thread/realtime/outputAudio/delta` independently with `optOutNotificationMethods`.
### Windows sandbox setup events
- `windowsSandbox/setupCompleted``{ mode, success, error }` after a `windowsSandbox/setupStart` request finishes.
@@ -660,15 +695,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
@@ -702,6 +737,13 @@ When a dynamic tool is invoked during a turn, the server sends an `item/tool/cal
}
```
The server also emits item lifecycle notifications around the request:
1. `item/started` with `item.type = "dynamicToolCall"`, `status = "inProgress"`, plus `tool` and `arguments`.
2. `item/tool/call` request.
3. Client response.
4. `item/completed` with `item.type = "dynamicToolCall"`, final `status`, and the returned `contentItems`/`success`.
The client must respond with content items. Use `inputText` for text and `inputImage` for image URLs/data URLs:
```json
@@ -1072,6 +1114,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;
@@ -26,7 +27,9 @@ use codex_app_server_protocol::CommandExecutionRequestApprovalResponse;
use codex_app_server_protocol::CommandExecutionStatus;
use codex_app_server_protocol::ContextCompactedNotification;
use codex_app_server_protocol::DeprecationNoticeNotification;
use codex_app_server_protocol::DynamicToolCallOutputContentItem;
use codex_app_server_protocol::DynamicToolCallParams;
use codex_app_server_protocol::DynamicToolCallStatus;
use codex_app_server_protocol::ErrorNotification;
use codex_app_server_protocol::ExecCommandApprovalParams;
use codex_app_server_protocol::ExecCommandApprovalResponse;
@@ -45,6 +48,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,9 +58,17 @@ 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;
use codex_app_server_protocol::ThreadRealtimeClosedNotification;
use codex_app_server_protocol::ThreadRealtimeErrorNotification;
use codex_app_server_protocol::ThreadRealtimeItemAddedNotification;
use codex_app_server_protocol::ThreadRealtimeOutputAudioDeltaNotification;
use codex_app_server_protocol::ThreadRealtimeStartedNotification;
use codex_app_server_protocol::ThreadRollbackResponse;
use codex_app_server_protocol::ThreadTokenUsage;
use codex_app_server_protocol::ThreadTokenUsageUpdatedNotification;
@@ -91,12 +104,14 @@ use codex_protocol::protocol::ExecCommandEndEvent;
use codex_protocol::protocol::McpToolCallBeginEvent;
use codex_protocol::protocol::McpToolCallEndEvent;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::RealtimeEvent;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::ReviewOutputEvent;
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;
@@ -166,6 +181,73 @@ pub(crate) async fn apply_bespoke_event_handling(
.await;
}
}
EventMsg::RealtimeConversationStarted(event) => {
if let ApiVersion::V2 = api_version {
let notification = ThreadRealtimeStartedNotification {
thread_id: conversation_id.to_string(),
session_id: event.session_id,
};
outgoing
.send_server_notification(ServerNotification::ThreadRealtimeStarted(
notification,
))
.await;
}
}
EventMsg::RealtimeConversationRealtime(event) => {
if let ApiVersion::V2 = api_version {
match event.payload {
RealtimeEvent::SessionCreated { .. } => {}
RealtimeEvent::SessionUpdated { .. } => {}
RealtimeEvent::AudioOut(audio) => {
let notification = ThreadRealtimeOutputAudioDeltaNotification {
thread_id: conversation_id.to_string(),
audio: audio.into(),
};
outgoing
.send_server_notification(
ServerNotification::ThreadRealtimeOutputAudioDelta(notification),
)
.await;
}
RealtimeEvent::ConversationItemAdded(item) => {
let notification = ThreadRealtimeItemAddedNotification {
thread_id: conversation_id.to_string(),
item,
};
outgoing
.send_server_notification(ServerNotification::ThreadRealtimeItemAdded(
notification,
))
.await;
}
RealtimeEvent::Error(message) => {
let notification = ThreadRealtimeErrorNotification {
thread_id: conversation_id.to_string(),
message,
};
outgoing
.send_server_notification(ServerNotification::ThreadRealtimeError(
notification,
))
.await;
}
}
}
}
EventMsg::RealtimeConversationClosed(event) => {
if let ApiVersion::V2 = api_version {
let notification = ThreadRealtimeClosedNotification {
thread_id: conversation_id.to_string(),
reason: event.reason,
};
outgoing
.send_server_notification(ServerNotification::ThreadRealtimeClosed(
notification,
))
.await;
}
}
EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
call_id,
turn_id,
@@ -263,6 +345,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 +409,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 +429,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,15 +518,66 @@ 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;
let turn_id = request.turn_id;
let tool = request.tool;
let arguments = request.arguments;
let item = ThreadItem::DynamicToolCall {
id: call_id.clone(),
tool: tool.clone(),
arguments: arguments.clone(),
status: DynamicToolCallStatus::InProgress,
content_items: None,
success: None,
duration_ms: None,
};
let notification = ItemStartedNotification {
thread_id: conversation_id.to_string(),
turn_id: turn_id.clone(),
item,
};
outgoing
.send_server_notification(ServerNotification::ItemStarted(notification))
.await;
let params = DynamicToolCallParams {
thread_id: conversation_id.to_string(),
turn_id: request.turn_id,
turn_id: turn_id.clone(),
call_id: call_id.clone(),
tool: request.tool,
arguments: request.arguments,
tool: tool.clone(),
arguments: arguments.clone(),
};
let rx = outgoing
.send_request(ServerRequestPayload::DynamicToolCall(params))
@@ -458,6 +604,46 @@ pub(crate) async fn apply_bespoke_event_handling(
.await;
}
}
EventMsg::DynamicToolCallResponse(response) => {
if matches!(api_version, ApiVersion::V2) {
let status = if response.success {
DynamicToolCallStatus::Completed
} else {
DynamicToolCallStatus::Failed
};
let duration_ms = i64::try_from(response.duration.as_millis()).ok();
let item = ThreadItem::DynamicToolCall {
id: response.call_id,
tool: response.tool,
arguments: response.arguments,
status,
content_items: Some(
response
.content_items
.into_iter()
.map(|item| match item {
CoreDynamicToolCallOutputContentItem::InputText { text } => {
DynamicToolCallOutputContentItem::InputText { text }
}
CoreDynamicToolCallOutputContentItem::InputImage { image_url } => {
DynamicToolCallOutputContentItem::InputImage { image_url }
}
})
.collect(),
),
success: Some(response.success),
duration_ms,
};
let notification = ItemCompletedNotification {
thread_id: conversation_id.to_string(),
turn_id: response.turn_id,
item,
};
outgoing
.send_server_notification(ServerNotification::ItemCompleted(notification))
.await;
}
}
// TODO(celia): properly construct McpToolCall TurnItem in core.
EventMsg::McpToolCallBegin(begin_event) => {
let notification = construct_mcp_tool_call_notification(
@@ -1875,6 +2061,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

@@ -131,6 +131,7 @@ use codex_app_server_protocol::ThreadCompactStartParams;
use codex_app_server_protocol::ThreadCompactStartResponse;
use codex_app_server_protocol::ThreadForkParams;
use codex_app_server_protocol::ThreadForkResponse;
use codex_app_server_protocol::ThreadHistoryBuilder;
use codex_app_server_protocol::ThreadItem;
use codex_app_server_protocol::ThreadListParams;
use codex_app_server_protocol::ThreadListResponse;
@@ -138,6 +139,14 @@ use codex_app_server_protocol::ThreadLoadedListParams;
use codex_app_server_protocol::ThreadLoadedListResponse;
use codex_app_server_protocol::ThreadReadParams;
use codex_app_server_protocol::ThreadReadResponse;
use codex_app_server_protocol::ThreadRealtimeAppendAudioParams;
use codex_app_server_protocol::ThreadRealtimeAppendAudioResponse;
use codex_app_server_protocol::ThreadRealtimeAppendTextParams;
use codex_app_server_protocol::ThreadRealtimeAppendTextResponse;
use codex_app_server_protocol::ThreadRealtimeStartParams;
use codex_app_server_protocol::ThreadRealtimeStartResponse;
use codex_app_server_protocol::ThreadRealtimeStopParams;
use codex_app_server_protocol::ThreadRealtimeStopResponse;
use codex_app_server_protocol::ThreadResumeParams;
use codex_app_server_protocol::ThreadResumeResponse;
use codex_app_server_protocol::ThreadRollbackParams;
@@ -168,6 +177,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 +199,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;
@@ -233,6 +244,9 @@ use codex_protocol::dynamic_tools::DynamicToolSpec as CoreDynamicToolSpec;
use codex_protocol::items::TurnItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::AgentStatus;
use codex_protocol::protocol::ConversationAudioParams;
use codex_protocol::protocol::ConversationStartParams;
use codex_protocol::protocol::ConversationTextParams;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::GitInfo as CoreGitInfo;
use codex_protocol::protocol::InitialHistory;
@@ -269,6 +283,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 +303,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 +353,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 +377,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 +414,7 @@ impl CodexMessageProcessor {
auth_manager,
thread_manager,
outgoing,
codex_linux_sandbox_exe,
arg0_paths,
config,
cli_overrides,
cloud_requirements,
@@ -409,7 +425,7 @@ impl CodexMessageProcessor {
auth_manager,
thread_manager,
outgoing: outgoing.clone(),
codex_linux_sandbox_exe,
arg0_paths,
config,
single_client_mode,
cli_overrides,
@@ -425,7 +441,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 +450,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 {
@@ -618,6 +637,22 @@ impl CodexMessageProcessor {
self.turn_interrupt(to_connection_request_id(request_id), params)
.await;
}
ClientRequest::ThreadRealtimeStart { request_id, params } => {
self.thread_realtime_start(to_connection_request_id(request_id), params)
.await;
}
ClientRequest::ThreadRealtimeAppendAudio { request_id, params } => {
self.thread_realtime_append_audio(to_connection_request_id(request_id), params)
.await;
}
ClientRequest::ThreadRealtimeAppendText { request_id, params } => {
self.thread_realtime_append_text(to_connection_request_id(request_id), params)
.await;
}
ClientRequest::ThreadRealtimeStop { request_id, params } => {
self.thread_realtime_stop(to_connection_request_id(request_id), params)
.await;
}
ClientRequest::ReviewStart { request_id, params } => {
self.review_start(to_connection_request_id(request_id), params)
.await;
@@ -816,6 +851,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 +1780,7 @@ impl CodexMessageProcessor {
None,
None,
managed_network_requirements_enabled,
NetworkProxyAuditMetadata::default(),
)
.await
{
@@ -1758,8 +1798,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 +1831,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 +1896,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 +1991,7 @@ impl CodexMessageProcessor {
approval_policy,
sandbox,
config,
service_name,
base_instructions,
developer_instructions,
dynamic_tools,
@@ -2015,7 +2059,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 +2157,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,
@@ -2188,7 +2238,7 @@ impl CodexMessageProcessor {
return;
};
let (_, thread) = match self.load_thread(&thread_id).await {
let (thread_id, thread) = match self.load_thread(&thread_id).await {
Ok(v) => v,
Err(error) => {
self.outgoing.send_error(request_id, error).await;
@@ -2196,17 +2246,54 @@ impl CodexMessageProcessor {
}
};
let expected_name = name.clone();
if let Err(err) = thread.submit(Op::SetThreadName { name }).await {
self.send_internal_error(request_id, format!("failed to set thread name: {err}"))
.await;
return;
}
if let Err(err) = self
.wait_for_thread_name_persisted(thread_id, &expected_name)
.await
{
self.send_internal_error(
request_id,
format!("failed to observe persisted thread name: {err}"),
)
.await;
return;
}
self.outgoing
.send_response(request_id, ThreadSetNameResponse {})
.await;
}
async fn wait_for_thread_name_persisted(
&self,
thread_id: ThreadId,
expected_name: &str,
) -> std::io::Result<()> {
let codex_home = self.config.codex_home.clone();
tokio::time::timeout(Duration::from_secs(2), async {
loop {
match find_thread_name_by_id(&codex_home, &thread_id).await? {
Some(name) if name == expected_name => return Ok(()),
_ => tokio::time::sleep(Duration::from_millis(10)).await,
}
}
})
.await
.map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!("timed out waiting for thread name {expected_name:?}"),
)
})?
}
async fn thread_unarchive(
&mut self,
request_id: ConnectionRequestId,
@@ -2522,6 +2609,7 @@ impl CodexMessageProcessor {
source_kinds,
archived,
cwd,
search_term,
} = params;
let requested_page_size = limit
@@ -2542,6 +2630,7 @@ impl CodexMessageProcessor {
source_kinds,
archived: archived.unwrap_or(false),
cwd: cwd.map(PathBuf::from),
search_term,
},
)
.await
@@ -2794,6 +2883,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,
@@ -3269,6 +3362,7 @@ impl CodexMessageProcessor {
let ThreadForkParams {
thread_id,
path,
fork_after_turn_id,
model,
model_provider,
cwd,
@@ -3382,21 +3476,71 @@ impl CodexMessageProcessor {
};
let fallback_model_provider = config.model_provider_id.clone();
let anchored_fork_history = if let Some(fork_after_turn_id) = fork_after_turn_id.as_deref()
{
let source_items = match read_rollout_items_from_rollout(rollout_path.as_path()).await {
Ok(items) => items,
Err(err) => {
let (code, message) = match err.kind() {
std::io::ErrorKind::NotFound => (
INVALID_REQUEST_ERROR_CODE,
format!("failed to load rollout `{}`: {err}", rollout_path.display()),
),
_ => (
INTERNAL_ERROR_CODE,
format!(
"failed to read rollout `{}` for thread fork: {err}",
rollout_path.display()
),
),
};
let error = JSONRPCErrorError {
code,
message,
data: None,
};
self.outgoing.send_error(request_id, error).await;
return;
}
};
match resolve_thread_fork_history_from_anchor(
source_items.as_slice(),
fork_after_turn_id,
) {
Ok(history_items) => Some(InitialHistory::Forked(history_items)),
Err(message) => {
self.send_invalid_request_error(request_id, message).await;
return;
}
}
} else {
None
};
let NewThread {
thread_id,
session_configured,
..
} = match self
.thread_manager
.fork_thread(
usize::MAX,
config,
rollout_path.clone(),
persist_extended_history,
)
.await
{
} = match if let Some(initial_history) = anchored_fork_history {
self.thread_manager
.resume_thread_with_history(
config,
initial_history,
self.auth_manager.clone(),
persist_extended_history,
)
.await
} else {
self.thread_manager
.fork_thread(
usize::MAX,
config,
rollout_path.clone(),
persist_extended_history,
)
.await
} {
Ok(thread) => thread,
Err(err) => {
let (code, message) = match err {
@@ -3599,6 +3743,7 @@ impl CodexMessageProcessor {
source_kinds: None,
archived: false,
cwd: None,
search_term: None,
},
)
.await
@@ -3625,6 +3770,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 +3813,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 +3830,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 +4466,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 +4478,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 +4663,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 +4676,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,
@@ -5482,6 +5634,177 @@ impl CodexMessageProcessor {
}
}
async fn prepare_realtime_conversation_thread(
&mut self,
request_id: ConnectionRequestId,
thread_id: &str,
) -> Option<(ThreadId, Arc<CodexThread>)> {
let (thread_id, thread) = match self.load_thread(thread_id).await {
Ok(v) => v,
Err(error) => {
self.outgoing.send_error(request_id, error).await;
return None;
}
};
if let Err(error) = self
.ensure_conversation_listener(
thread_id,
request_id.connection_id,
false,
ApiVersion::V2,
)
.await
{
self.outgoing.send_error(request_id, error).await;
return None;
}
if !thread.enabled(Feature::RealtimeConversation) {
self.send_invalid_request_error(
request_id,
format!("thread {thread_id} does not support realtime conversation"),
)
.await;
return None;
}
Some((thread_id, thread))
}
async fn thread_realtime_start(
&mut self,
request_id: ConnectionRequestId,
params: ThreadRealtimeStartParams,
) {
let Some((_, thread)) = self
.prepare_realtime_conversation_thread(request_id.clone(), &params.thread_id)
.await
else {
return;
};
let submit = thread
.submit(Op::RealtimeConversationStart(ConversationStartParams {
prompt: params.prompt,
session_id: params.session_id,
}))
.await;
match submit {
Ok(_) => {
self.outgoing
.send_response(request_id, ThreadRealtimeStartResponse::default())
.await;
}
Err(err) => {
self.send_internal_error(
request_id,
format!("failed to start realtime conversation: {err}"),
)
.await;
}
}
}
async fn thread_realtime_append_audio(
&mut self,
request_id: ConnectionRequestId,
params: ThreadRealtimeAppendAudioParams,
) {
let Some((_, thread)) = self
.prepare_realtime_conversation_thread(request_id.clone(), &params.thread_id)
.await
else {
return;
};
let submit = thread
.submit(Op::RealtimeConversationAudio(ConversationAudioParams {
frame: params.audio.into(),
}))
.await;
match submit {
Ok(_) => {
self.outgoing
.send_response(request_id, ThreadRealtimeAppendAudioResponse::default())
.await;
}
Err(err) => {
self.send_internal_error(
request_id,
format!("failed to append realtime conversation audio: {err}"),
)
.await;
}
}
}
async fn thread_realtime_append_text(
&mut self,
request_id: ConnectionRequestId,
params: ThreadRealtimeAppendTextParams,
) {
let Some((_, thread)) = self
.prepare_realtime_conversation_thread(request_id.clone(), &params.thread_id)
.await
else {
return;
};
let submit = thread
.submit(Op::RealtimeConversationText(ConversationTextParams {
text: params.text,
}))
.await;
match submit {
Ok(_) => {
self.outgoing
.send_response(request_id, ThreadRealtimeAppendTextResponse::default())
.await;
}
Err(err) => {
self.send_internal_error(
request_id,
format!("failed to append realtime conversation text: {err}"),
)
.await;
}
}
}
async fn thread_realtime_stop(
&mut self,
request_id: ConnectionRequestId,
params: ThreadRealtimeStopParams,
) {
let Some((_, thread)) = self
.prepare_realtime_conversation_thread(request_id.clone(), &params.thread_id)
.await
else {
return;
};
let submit = thread.submit(Op::RealtimeConversationClose).await;
match submit {
Ok(_) => {
self.outgoing
.send_response(request_id, ThreadRealtimeStopResponse::default())
.await;
}
Err(err) => {
self.send_internal_error(
request_id,
format!("failed to stop realtime conversation: {err}"),
)
.await;
}
}
}
fn build_review_turn(turn_id: String, display_text: &str) -> Turn {
let items = if display_text.is_empty() {
Vec::new()
@@ -5897,6 +6220,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 +6259,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 +6842,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 +6873,7 @@ fn skills_to_info(
.collect(),
}
}),
path: skill.path.clone(),
path: skill.path_to_skills_md.clone(),
scope: skill.scope.into(),
enabled,
}
@@ -6630,6 +6958,112 @@ async fn sync_default_client_residency_requirement(
}
}
/// Return the exact rollout prefix that should seed a turn-anchored `thread/fork`.
///
/// The selected turn stays in the returned history, while every later turn is removed. The helper
/// validates that the anchor is a stable, already-materialized exchange: legacy replay-synthesized
/// ids are rejected, the turn must not still be in progress, and the turn must include both a
/// user message and an agent response. The cutoff uses both the next explicit `TurnStarted` event
/// and the next user-message boundary so the fork cannot accidentally retain trailing non-user
/// events that conceptually belong to a later turn.
fn resolve_thread_fork_history_from_anchor(
rollout_items: &[RolloutItem],
fork_after_turn_id: &str,
) -> Result<Vec<RolloutItem>, String> {
let mut history_builder = ThreadHistoryBuilder::new();
for item in rollout_items {
history_builder.handle_rollout_item(item);
}
if history_builder.has_synthetic_turn_ids() {
return Err(
"turn-based forking is not supported for legacy thread history; use full thread/fork"
.to_string(),
);
}
let turns = history_builder.finish();
let Some(target_turn_idx) = turns.iter().position(|turn| turn.id == fork_after_turn_id) else {
return Err(format!(
"fork turn not found in source thread history: {fork_after_turn_id}"
));
};
let target_turn = &turns[target_turn_idx];
let has_user_message = target_turn
.items
.iter()
.any(|item| matches!(item, ThreadItem::UserMessage { .. }));
let has_agent_message = target_turn
.items
.iter()
.any(|item| matches!(item, ThreadItem::AgentMessage { .. }));
if matches!(target_turn.status, TurnStatus::InProgress) {
return Err("fork turn must be completed/interrupted/failed, not in progress".to_string());
}
if !has_user_message {
return Err("fork turn must contain a user message".to_string());
}
if !has_agent_message {
return Err("fork turn must contain an agent message".to_string());
}
let Some(next_turn) = turns.get(target_turn_idx.saturating_add(1)) else {
return Ok(rollout_items.to_vec());
};
let next_turn_id = next_turn.id.as_str();
let next_turn_started_idx = rollout_items
.iter()
.position(|item| {
matches!(
item,
RolloutItem::EventMsg(EventMsg::TurnStarted(payload))
if payload.turn_id == next_turn_id
)
})
.ok_or_else(|| {
format!("failed to locate boundary after fork turn: missing next turn `{next_turn_id}`")
})?;
let target_terminal_idx = rollout_items.iter().rposition(|item| {
matches!(
item,
RolloutItem::EventMsg(EventMsg::TurnComplete(payload))
if payload.turn_id == fork_after_turn_id
) || matches!(
item,
RolloutItem::EventMsg(EventMsg::TurnAborted(payload))
if payload.turn_id.as_deref() == Some(fork_after_turn_id)
)
});
let next_user_boundary_idx = target_terminal_idx
.and_then(|terminal_idx| {
rollout_items
.iter()
.enumerate()
.skip(terminal_idx.saturating_add(1))
.find_map(|(idx, item)| is_user_turn_rollout_boundary(item).then_some(idx))
})
.unwrap_or(usize::MAX);
let cut_idx = next_turn_started_idx.min(next_user_boundary_idx);
Ok(rollout_items[..cut_idx].to_vec())
}
/// Return whether this rollout item starts a new user turn for truncation purposes.
///
/// Anchored forks use this as a fallback boundary when explicit turn lifecycle events are present
/// but a later turn also emitted standalone user-message markers that should not leak into the
/// forked prefix.
fn is_user_turn_rollout_boundary(item: &RolloutItem) -> bool {
match item {
RolloutItem::ResponseItem(ResponseItem::Message { role, .. }) => role == "user",
RolloutItem::EventMsg(EventMsg::UserMessage(_)) => true,
_ => false,
}
}
/// Derive the effective [`Config`] by layering three override sources.
///
/// Precedence (lowest to highest):

View File

@@ -1,3 +1,4 @@
use codex_app_server_protocol::DynamicToolCallOutputContentItem;
use codex_app_server_protocol::DynamicToolCallResponse;
use codex_core::CodexThread;
use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem;
@@ -15,65 +16,23 @@ pub(crate) async fn on_call_response(
conversation: Arc<CodexThread>,
) {
let response = receiver.await;
let value = match response {
Ok(Ok(value)) => value,
let (response, _error) = match response {
Ok(Ok(value)) => decode_response(value),
Ok(Err(err)) => {
error!("request failed with client error: {err:?}");
let fallback = CoreDynamicToolResponse {
content_items: vec![CoreDynamicToolCallOutputContentItem::InputText {
text: "dynamic tool request failed".to_string(),
}],
success: false,
};
if let Err(err) = conversation
.submit(Op::DynamicToolResponse {
id: call_id.clone(),
response: fallback,
})
.await
{
error!("failed to submit DynamicToolResponse: {err}");
}
return;
fallback_response("dynamic tool request failed")
}
Err(err) => {
error!("request failed: {err:?}");
let fallback = CoreDynamicToolResponse {
content_items: vec![CoreDynamicToolCallOutputContentItem::InputText {
text: "dynamic tool request failed".to_string(),
}],
success: false,
};
if let Err(err) = conversation
.submit(Op::DynamicToolResponse {
id: call_id.clone(),
response: fallback,
})
.await
{
error!("failed to submit DynamicToolResponse: {err}");
}
return;
fallback_response("dynamic tool request failed")
}
};
let response = serde_json::from_value::<DynamicToolCallResponse>(value).unwrap_or_else(|err| {
error!("failed to deserialize DynamicToolCallResponse: {err}");
DynamicToolCallResponse {
content_items: vec![
codex_app_server_protocol::DynamicToolCallOutputContentItem::InputText {
text: "dynamic tool response was invalid".to_string(),
},
],
success: false,
}
});
let DynamicToolCallResponse {
content_items,
success,
} = response;
let response = CoreDynamicToolResponse {
} = response.clone();
let core_response = CoreDynamicToolResponse {
content_items: content_items
.into_iter()
.map(CoreDynamicToolCallOutputContentItem::from)
@@ -82,11 +41,33 @@ pub(crate) async fn on_call_response(
};
if let Err(err) = conversation
.submit(Op::DynamicToolResponse {
id: call_id,
response,
id: call_id.clone(),
response: core_response,
})
.await
{
error!("failed to submit DynamicToolResponse: {err}");
}
}
fn decode_response(value: serde_json::Value) -> (DynamicToolCallResponse, Option<String>) {
match serde_json::from_value::<DynamicToolCallResponse>(value) {
Ok(response) => (response, None),
Err(err) => {
error!("failed to deserialize DynamicToolCallResponse: {err}");
fallback_response("dynamic tool response was invalid")
}
}
}
fn fallback_response(message: &str) -> (DynamicToolCallResponse, Option<String>) {
(
DynamicToolCallResponse {
content_items: vec![DynamicToolCallOutputContentItem::InputText {
text: message.to_string(),
}],
success: false,
},
Some(message.to_string()),
)
}

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 {

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