diff --git a/.bazelrc b/.bazelrc
index c58667b46e..de01a87443 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -107,8 +107,9 @@ common:ci-bazel --config=ci
common:ci-bazel --build_metadata=TAG_workflow=bazel
# Keep code-mode integration cases out of ordinary Bazel legs. The
# Windows-cross config below re-enables them after generating its Windows V8
-# snapshot on the Windows runner.
-common:ci-bazel --test_env=CODEX_BAZEL_TEST_SKIP_FILTERS=suite::code_mode::
+# snapshot on the Windows runner. Also skip the ConPTY Ctrl-C integration test
+# until Windows Bazel CI can reliably interrupt foreground processes.
+common:ci-bazel --test_env=CODEX_BAZEL_TEST_SKIP_FILTERS=suite::code_mode::,tests::windows_tests::conpty_ctrl_c_interrupts_powershell_foreground_child
# Shared config for Bazel-backed Rust linting.
build:clippy --aspects=@rules_rust//rust:defs.bzl%rust_clippy_aspect
@@ -191,11 +192,12 @@ common:ci-windows-cross --strategy=TestRunner=local
common:ci-windows-cross --strategy=V8Mksnapshot=local
common:ci-windows-cross --local_test_jobs=4
common:ci-windows-cross --test_env=RUST_TEST_THREADS=1
-# Native Windows CI still covers the PowerShell tests. The cross-built gnullvm
-# binaries currently hang in PowerShell AST parser tests when those binaries are
-# run on the Windows runner. Keep V8-backed code-mode tests enabled except for
-# the hidden dynamic-tool callback test, which currently times out on Windows.
-common:ci-windows-cross --test_env=CODEX_BAZEL_TEST_SKIP_FILTERS=powershell,suite::code_mode::code_mode_can_call_hidden_dynamic_tools
+# Native Windows CI still covers the PowerShell parser-process tests. The
+# cross-built gnullvm binaries currently hang in those tests when run on the
+# Windows runner. Keep V8-backed code-mode tests enabled except for the hidden
+# dynamic-tool callback test, which currently times out on Windows. This config
+# replaces the base skip list, so repeat the exact ConPTY Ctrl-C test exclusion.
+common:ci-windows-cross --test_env=CODEX_BAZEL_TEST_SKIP_FILTERS=command_safety::powershell_parser::tests::,suite::code_mode::code_mode_can_call_hidden_dynamic_tools,tests::windows_tests::conpty_ctrl_c_interrupts_powershell_foreground_child
common:ci-windows-cross --platforms=//:windows_x86_64_gnullvm
common:ci-windows-cross --extra_execution_platforms=//:rbe,//:windows_x86_64_msvc
common:ci-windows-cross --extra_toolchains=//:windows_gnullvm_tests_on_msvc_host_toolchain
diff --git a/.codex/environments/environment.toml b/.codex/environments/environment.toml
index 39c40eb07f..b3c5910fbc 100644
--- a/.codex/environments/environment.toml
+++ b/.codex/environments/environment.toml
@@ -2,9 +2,6 @@
version = 1
name = "codex"
-[setup]
-script = "python ./.codex/environments/setup.py"
-
[[actions]]
name = "Run"
icon = "run"
diff --git a/.codex/environments/setup.py b/.codex/environments/setup.py
deleted file mode 100644
index a9d78c9405..0000000000
--- a/.codex/environments/setup.py
+++ /dev/null
@@ -1,65 +0,0 @@
-#!/usr/bin/env python3
-
-"""Set up ignored files that should be shared with Codex worktrees."""
-
-import shutil
-import subprocess
-from functools import cache
-from pathlib import Path
-
-
-@cache
-def worktree_paths() -> tuple[Path, Path]:
- script_dir = Path(__file__).resolve().parent
- worktree_root = git_path(script_dir / "../..", "--show-toplevel")
- common_git_dir = git_path(worktree_root, "--git-common-dir")
- return worktree_root, common_git_dir.parent
-
-
-def git_path(working_directory: Path, argument: str) -> Path:
- output = subprocess.check_output(
- [
- "git",
- "-C",
- str(working_directory),
- "rev-parse",
- "--path-format=absolute",
- argument,
- ],
- text=True,
- )
- return Path(output.strip())
-
-
-def copy_from_main_worktree_to_worktree(repo_relative_path: str) -> None:
- relative_path = Path(repo_relative_path)
- if relative_path.is_absolute() or ".." in relative_path.parts:
- raise ValueError(f"path must be repository-relative: {repo_relative_path}")
-
- worktree_root, main_worktree = worktree_paths()
- source_path = main_worktree / relative_path
- destination_path = worktree_root / relative_path
-
- print(f" source: {source_path}")
- print(f" destination: {destination_path}")
-
- if source_path == destination_path:
- print(" result: running in the main worktree; nothing to copy")
- elif destination_path.exists():
- print(" result: destination already exists; nothing to copy")
- elif not source_path.is_file():
- print(" result: source does not exist; nothing to copy")
- else:
- destination_path.parent.mkdir(parents=True, exist_ok=True)
- shutil.copy2(source_path, destination_path)
- print(f" result: copied {repo_relative_path}")
-
-
-def main() -> None:
- print("Codex environment setup:")
- # See codex-rs/docs/bazel.md for the repository's Bazel workflow.
- copy_from_main_worktree_to_worktree("user.bazelrc")
-
-
-if __name__ == "__main__":
- main()
diff --git a/.codex/skills/code-review/SKILL.md b/.codex/skills/code-review/SKILL.md
index eec0787c20..ccd37a9868 100644
--- a/.codex/skills/code-review/SKILL.md
+++ b/.codex/skills/code-review/SKILL.md
@@ -3,7 +3,7 @@ name: code-review
description: Run a final code review on a pull request
---
-Use subagents to review code using all code-review-* skills in this repository other than this orchestrator. One subagent per skill. Pass full skill path to subagents. Use xhigh reasoning.
+Use subagents to review code using all code-review-* skills other than this orchestrator. One subagent per skill. Pass full skill path to subagents. Use xhigh reasoning.
You must return every single issue from every subagent. You can return an unlimited number of findings.
Use raw Markdown to report findings.
diff --git a/.github/dotslash-zsh-config.json b/.github/dotslash-zsh-config.json
index 37285f19e2..2c2b11eec5 100644
--- a/.github/dotslash-zsh-config.json
+++ b/.github/dotslash-zsh-config.json
@@ -5,21 +5,25 @@
"macos-aarch64": {
"name": "codex-zsh-aarch64-apple-darwin.tar.gz",
"format": "tar.gz",
+ "hash": "sha256",
"path": "codex-zsh/bin/zsh"
},
"macos-x86_64": {
"name": "codex-zsh-x86_64-apple-darwin.tar.gz",
"format": "tar.gz",
+ "hash": "sha256",
"path": "codex-zsh/bin/zsh"
},
"linux-x86_64": {
"name": "codex-zsh-x86_64-unknown-linux-musl.tar.gz",
"format": "tar.gz",
+ "hash": "sha256",
"path": "codex-zsh/bin/zsh"
},
"linux-aarch64": {
"name": "codex-zsh-aarch64-unknown-linux-musl.tar.gz",
"format": "tar.gz",
+ "hash": "sha256",
"path": "codex-zsh/bin/zsh"
}
}
diff --git a/.github/scripts/build-codex-package-archive.sh b/.github/scripts/build-codex-package-archive.sh
index 80da4cf20c..229213cc53 100644
--- a/.github/scripts/build-codex-package-archive.sh
+++ b/.github/scripts/build-codex-package-archive.sh
@@ -9,6 +9,7 @@ Usage: build-codex-package-archive.sh \
--entrypoint-dir
\
--archive-dir \
[--bwrap-bin ] \
+ [--zsh-manifest ] \
[--codex-command-runner-bin ] \
[--codex-windows-sandbox-setup-bin ] \
[--target-suffixed-entrypoint]
@@ -48,6 +49,10 @@ while [[ $# -gt 0 ]]; do
bwrap_bin_provided="true"
shift 2
;;
+ --zsh-manifest)
+ resource_args+=(--zsh-manifest "${2:?--zsh-manifest requires a value}")
+ shift 2
+ ;;
--codex-command-runner-bin)
resource_args+=(
--codex-command-runner-bin
diff --git a/.github/scripts/check_ci_results.py b/.github/scripts/check_ci_results.py
new file mode 100644
index 0000000000..36d1eed3b4
--- /dev/null
+++ b/.github/scripts/check_ci_results.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+
+"""Fail a terminal CI job unless every serialized dependency succeeded.
+
+Parent workflows pass GitHub's `toJSON(needs)` object through the NEEDS
+environment variable. Treat skipped and cancelled dependencies as failures too:
+for a required fan-in job, only an explicit success is safe to accept.
+"""
+
+import json
+import os
+
+
+def main() -> None:
+ # Keep result policy in one script so blocking-ci and postmerge-ci cannot
+ # drift in how they interpret dependency conclusions.
+ needs = json.loads(os.environ["NEEDS"])
+ failures = sorted(
+ (name, dependency["result"])
+ for name, dependency in needs.items()
+ if dependency["result"] != "success"
+ )
+
+ if failures:
+ print("CI dependencies did not succeed:")
+ for name, result in failures:
+ print(f"{name}: {result}")
+ raise SystemExit(1)
+
+ print("All CI dependencies succeeded.")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/.github/scripts/v8_canary_changes.py b/.github/scripts/v8_canary_changes.py
index 6d9693c60a..0acc4ec3f6 100644
--- a/.github/scripts/v8_canary_changes.py
+++ b/.github/scripts/v8_canary_changes.py
@@ -1,12 +1,44 @@
#!/usr/bin/env python3
+"""Decide which V8 canary work is needed for a commit range.
+
+The workflow deliberately has no trigger-level path filters because it is both
+directly triggered for pull requests and called by postmerge-ci. Keeping the
+patterns here gives those entrypoints one source of truth; unrelated events
+still run metadata but skip the expensive build matrices.
+"""
+
import argparse
import subprocess
import tomllib
+from fnmatch import fnmatchcase
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
+# These patterns replace the old pull_request/push path filters. Include parent
+# workflow changes because they can alter whether the canary is invoked.
+CANARY_PATH_PATTERNS = {
+ ".bazelrc",
+ ".github/actions/setup-bazel-ci/**",
+ ".github/scripts/run_bazel_with_buildbuddy.py",
+ ".github/scripts/rusty_v8_bazel.py",
+ ".github/scripts/rusty_v8_module_bazel.py",
+ ".github/scripts/v8_canary_changes.py",
+ ".github/workflows/postmerge-ci.yml",
+ ".github/workflows/rusty-v8-release.yml",
+ ".github/workflows/v8-canary.yml",
+ "MODULE.bazel",
+ "MODULE.bazel.lock",
+ "codex-rs/Cargo.toml",
+ "patches/BUILD.bazel",
+ "patches/llvm_*.patch",
+ "patches/rules_cc_*.patch",
+ "patches/v8_*.patch",
+ "third_party/v8/**",
+}
+# Windows source builds are a narrower, more expensive subset of the canary.
+# A V8 version change also requires them even when no path below changed.
WINDOWS_SOURCE_BUILD_PATHS = {
".github/scripts/rusty_v8_bazel.py",
".github/scripts/rusty_v8_module_bazel.py",
@@ -16,6 +48,30 @@ WINDOWS_SOURCE_BUILD_PATHS = {
}
+def matching_canary_paths(changed_files: set[str]) -> set[str]:
+ """Return changed paths that require the general V8 build matrix."""
+ return {
+ path
+ for path in changed_files
+ if any(fnmatchcase(path, pattern) for pattern in CANARY_PATH_PATTERNS)
+ }
+
+
+def canary_required(
+ changed_files: set[str],
+ base_v8_version: str,
+ head_v8_version: str,
+ *,
+ force: bool = False,
+) -> bool:
+ """Return whether the general V8 build matrix should run."""
+ return (
+ force
+ or base_v8_version != head_v8_version
+ or bool(matching_canary_paths(changed_files))
+ )
+
+
def resolved_v8_version(cargo_lock: bytes) -> str:
versions = sorted(
{
@@ -36,6 +92,7 @@ def windows_source_required(
*,
force: bool = False,
) -> bool:
+ """Return whether Windows must rebuild rusty_v8 from source."""
return (
force
or base_v8_version != head_v8_version
@@ -58,6 +115,8 @@ def merge_base(base: str, head: str, *, root: Path = ROOT) -> str:
def changed_files(base: str, head: str, *, root: Path = ROOT) -> set[str]:
+ # Three-dot diff gives PRs merge-base semantics while remaining equivalent
+ # to before/after for ordinary linear pushes to main.
output = git_output(
"diff",
"--name-only",
@@ -79,25 +138,44 @@ def parse_args() -> argparse.Namespace:
def main() -> None:
args = parse_args()
if args.force:
- required = True
- reason = "manual workflow dispatch"
+ # workflow_dispatch has no comparison range, and callers use it as a
+ # manual retry path, so it intentionally runs every variant.
+ canary = True
+ canary_reason = "manual workflow dispatch"
+ windows_source = True
+ windows_source_reason = "manual workflow dispatch"
elif not args.base or not args.head:
raise SystemExit("--base and --head are required unless --force is set")
else:
files = changed_files(args.base, args.head)
base_version = v8_version_at_revision(merge_base(args.base, args.head))
head_version = v8_version_at_revision(args.head)
- required = windows_source_required(files, base_version, head_version)
+
+ matched_canary_paths = sorted(matching_canary_paths(files))
+ canary = canary_required(files, base_version, head_version)
+ windows_source = windows_source_required(files, base_version, head_version)
if base_version != head_version:
- reason = f"v8 version changed from {base_version} to {head_version}"
+ canary_reason = (
+ f"v8 version changed from {base_version} to {head_version}"
+ )
+ windows_source_reason = canary_reason
else:
- matched_paths = sorted(files & WINDOWS_SOURCE_BUILD_PATHS)
- reason = (
- ", ".join(matched_paths) if matched_paths else "no relevant changes"
+ canary_reason = (
+ ", ".join(matched_canary_paths)
+ if matched_canary_paths
+ else "no relevant changes"
+ )
+ matched_windows_paths = sorted(files & WINDOWS_SOURCE_BUILD_PATHS)
+ windows_source_reason = (
+ ", ".join(matched_windows_paths)
+ if matched_windows_paths
+ else "no relevant changes"
)
- print(f"windows_source_required={str(required).lower()}")
- print(f"windows_source_reason={reason}")
+ print(f"canary_required={str(canary).lower()}")
+ print(f"canary_reason={canary_reason}")
+ print(f"windows_source_required={str(windows_source).lower()}")
+ print(f"windows_source_reason={windows_source_reason}")
if __name__ == "__main__":
diff --git a/.github/workflows/bazel.yml b/.github/workflows/bazel.yml
index 7a6db869e0..10dd8670de 100644
--- a/.github/workflows/bazel.yml
+++ b/.github/workflows/bazel.yml
@@ -4,10 +4,7 @@ name: Bazel
# https://github.com/cerisier/toolchains_llvm_bootstrapped/blob/main/.github/workflows/ci.yaml
on:
- pull_request: {}
- push:
- branches:
- - main
+ workflow_call:
workflow_dispatch:
concurrency:
diff --git a/.github/workflows/blob-size-policy.yml b/.github/workflows/blob-size-policy.yml
index ea50e8ee2e..fddc51afe7 100644
--- a/.github/workflows/blob-size-policy.yml
+++ b/.github/workflows/blob-size-policy.yml
@@ -1,7 +1,7 @@
name: blob-size-policy
on:
- pull_request: {}
+ workflow_call:
jobs:
check:
@@ -14,13 +14,24 @@ jobs:
fetch-depth: 0
persist-credentials: false
- - name: Determine PR comparison range
+ - name: Determine comparison range
id: range
shell: bash
run: |
set -euo pipefail
- echo "base=${{ github.event.pull_request.base.sha }}" >> "$GITHUB_OUTPUT"
- echo "head=${{ github.event.pull_request.head.sha }}" >> "$GITHUB_OUTPUT"
+
+ # PRs inspect the proposed diff; main pushes inspect only the commit
+ # range that just landed. Both paths feed the same blob-size checker.
+ if [[ "${{ github.event_name }}" == "pull_request" ]]; then
+ base='${{ github.event.pull_request.base.sha }}'
+ head='${{ github.event.pull_request.head.sha }}'
+ else
+ base='${{ github.event.before }}'
+ head='${{ github.sha }}'
+ fi
+
+ echo "base=$base" >> "$GITHUB_OUTPUT"
+ echo "head=$head" >> "$GITHUB_OUTPUT"
- name: Check changed blob sizes
env:
diff --git a/.github/workflows/blocking-ci.yml b/.github/workflows/blocking-ci.yml
new file mode 100644
index 0000000000..fe66fdd509
--- /dev/null
+++ b/.github/workflows/blocking-ci.yml
@@ -0,0 +1,79 @@
+name: blocking-ci
+
+# This is the single entrypoint for checks that block a PR merge. It also runs
+# after pushes to main so the same check family stays grouped in the Actions UI.
+on:
+ pull_request: {}
+ push:
+ branches: [main]
+
+jobs:
+ # Keep reusable workflow calls alphabetized. The `required` job below is the
+ # version-controlled list that the main-branch ruleset should require.
+ bazel:
+ name: Bazel
+ uses: ./.github/workflows/bazel.yml
+ secrets: inherit
+
+ blob-size-policy:
+ name: Blob size policy
+ uses: ./.github/workflows/blob-size-policy.yml
+ secrets: inherit
+
+ cargo-deny:
+ name: cargo-deny
+ uses: ./.github/workflows/cargo-deny.yml
+ secrets: inherit
+
+ codespell:
+ name: Codespell
+ uses: ./.github/workflows/codespell.yml
+ secrets: inherit
+
+ repo-checks:
+ name: repo-checks
+ uses: ./.github/workflows/repo-checks.yml
+ secrets: inherit
+
+ rust-ci:
+ name: rust-ci
+ uses: ./.github/workflows/rust-ci.yml
+ secrets: inherit
+
+ sdk:
+ name: sdk
+ uses: ./.github/workflows/sdk.yml
+ secrets: inherit
+
+ required:
+ name: CI required
+ # Without `always()`, GitHub skips this job after a failed dependency and a
+ # required check can appear successful instead of reporting the failure.
+ if: ${{ always() }}
+ needs:
+ - bazel
+ - blob-size-policy
+ - cargo-deny
+ - codespell
+ - repo-checks
+ - rust-ci
+ - sdk
+ runs-on: ubuntu-24.04
+ steps:
+ # Keep the helper on the same revision as the caller and child workflows.
+ # CI workflow uploads are restricted, so this repository does not need a
+ # separate trusted-base checkout for the terminal policy step. Using the
+ # PR head also lets the introducing PR exercise a newly added helper.
+ #
+ # During the initial rollout, PR branches created before
+ # check_ci_results.py exists must rebase onto main before this gate can
+ # run.
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
+ persist-credentials: false
+
+ - name: Require successful dependencies
+ env:
+ NEEDS: ${{ toJSON(needs) }}
+ run: python3 .github/scripts/check_ci_results.py
diff --git a/.github/workflows/cargo-deny.yml b/.github/workflows/cargo-deny.yml
index aa37d7cec2..be00e1147d 100644
--- a/.github/workflows/cargo-deny.yml
+++ b/.github/workflows/cargo-deny.yml
@@ -1,10 +1,7 @@
name: cargo-deny
on:
- pull_request:
- push:
- branches:
- - main
+ workflow_call:
# Cargo's libgit2 transport has been flaky when fetching git dependencies with
# nested submodules. Prefer the system git CLI across every Cargo invocation.
diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml
index b642a6dbe1..a1751c8d29 100644
--- a/.github/workflows/codespell.yml
+++ b/.github/workflows/codespell.yml
@@ -3,10 +3,7 @@
name: Codespell
on:
- push:
- branches: [main]
- pull_request:
- branches: [main]
+ workflow_call:
permissions:
contents: read
diff --git a/.github/workflows/issue-labeler.yml b/.github/workflows/issue-labeler.yml
index 2c4eb6aa68..0f11d2af5e 100644
--- a/.github/workflows/issue-labeler.yml
+++ b/.github/workflows/issue-labeler.yml
@@ -78,6 +78,7 @@ jobs:
30. automations - Issues involving scheduled automation tasks or heartbeats.
31. pets - Issues involving pets avatars and animations.
32. agent - Fallback only for core agent loop or agent-related issues that do not fit app-server, connectivity, subagent, session, config, plan, computer-use, browser, memory, imagen, remote, performance, automations, or pets.
+ 33. aws-bedrock - Issues involving the Amazon Bedrock model provider or Bedrock Mantle. Do not use for unrelated AWS services or generic AWS mentions.
Issue number: ${{ github.event.issue.number }}
diff --git a/.github/workflows/postmerge-ci.yml b/.github/workflows/postmerge-ci.yml
new file mode 100644
index 0000000000..ac2b46d1db
--- /dev/null
+++ b/.github/workflows/postmerge-ci.yml
@@ -0,0 +1,40 @@
+name: postmerge-ci
+
+# This is the single entrypoint for main-only CI that is intentionally outside
+# the merge-blocking suite. It keeps the broader postmerge signal in one run.
+on:
+ push:
+ branches: [main]
+
+jobs:
+ # Keep reusable workflow calls alphabetized. Each child retains its own
+ # workflow_dispatch trigger so maintainers can rerun flaky suites directly.
+ rust-ci-full:
+ name: rust-ci-full
+ uses: ./.github/workflows/rust-ci-full.yml
+ secrets: inherit
+
+ v8-canary:
+ name: v8-canary
+ uses: ./.github/workflows/v8-canary.yml
+ secrets: inherit
+
+ results:
+ name: Postmerge CI results
+ needs:
+ - rust-ci-full
+ - v8-canary
+ if: ${{ always() }}
+ runs-on: ubuntu-24.04
+ steps:
+ # Postmerge runs use the pushed main commit, so this helper always comes
+ # from the same revision that defined the parent workflow.
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ ref: ${{ github.sha }}
+ persist-credentials: false
+
+ - name: Require successful dependencies
+ env:
+ NEEDS: ${{ toJSON(needs) }}
+ run: python3 .github/scripts/check_ci_results.py
diff --git a/.github/workflows/ci.yml b/.github/workflows/repo-checks.yml
similarity index 95%
rename from .github/workflows/ci.yml
rename to .github/workflows/repo-checks.yml
index a78ae034ed..3d0504172d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/repo-checks.yml
@@ -1,8 +1,7 @@
-name: ci
+name: repo-checks
on:
- pull_request: {}
- push: { branches: [main] }
+ workflow_call:
jobs:
build-test:
@@ -29,6 +28,9 @@ jobs:
- name: Test Codex package builder
run: python3 -m unittest discover -s scripts/codex_package -p 'test_*.py'
+ - name: Test standalone installer
+ run: python3 -m unittest discover -s scripts/install -p 'test_*.py'
+
- name: Setup pnpm
uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
with:
diff --git a/.github/workflows/rust-ci-full.yml b/.github/workflows/rust-ci-full.yml
index 7ad1d4b3ad..cb99c8911e 100644
--- a/.github/workflows/rust-ci-full.yml
+++ b/.github/workflows/rust-ci-full.yml
@@ -1,8 +1,10 @@
name: rust-ci-full
on:
+ workflow_call:
push:
branches:
- - main
+ # Main pushes enter through postmerge-ci. Keep this opt-in branch trigger
+ # for developers who want the full suite before merging.
- "**full-ci**"
workflow_dispatch:
diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml
index f693994d1a..cf1afdfe2d 100644
--- a/.github/workflows/rust-ci.yml
+++ b/.github/workflows/rust-ci.yml
@@ -1,6 +1,6 @@
name: rust-ci
on:
- pull_request: {}
+ workflow_call:
workflow_dispatch:
# Cargo's libgit2 transport has been flaky when fetching git dependencies with
diff --git a/.github/workflows/rust-release-zsh.yml b/.github/workflows/rust-release-zsh.yml
index b55d2e714b..e814ec32e1 100644
--- a/.github/workflows/rust-release-zsh.yml
+++ b/.github/workflows/rust-release-zsh.yml
@@ -1,15 +1,56 @@
name: rust-release-zsh
on:
- workflow_call:
+ push:
+ tags:
+ - "codex-zsh-v*.*.*"
env:
ZSH_COMMIT: 77045ef899e53b9598bebc5a41db93a548a40ca6
ZSH_PATCH: codex-rs/shell-escalation/patches/zsh-exec-wrapper.patch
+concurrency:
+ group: ${{ github.workflow }}::${{ github.ref_name }}
+ cancel-in-progress: false
+
jobs:
+ metadata:
+ runs-on: ubuntu-latest
+ outputs:
+ release_tag: ${{ steps.release_tag.outputs.release_tag }}
+
+ steps:
+ - name: Validate release tag
+ id: release_tag
+ env:
+ RELEASE_TAG: ${{ github.ref_name }}
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ if [[ ! "${RELEASE_TAG}" =~ ^codex-zsh-v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+ echo "Tag ${RELEASE_TAG} does not match codex-zsh-vX.Y.Z." >&2
+ exit 1
+ fi
+
+ echo "release_tag=${RELEASE_TAG}" >> "${GITHUB_OUTPUT}"
+
+ - name: Ensure release does not exist
+ env:
+ GH_TOKEN: ${{ github.token }}
+ RELEASE_TAG: ${{ steps.release_tag.outputs.release_tag }}
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ if gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" > /dev/null 2>&1; then
+ echo "Release ${RELEASE_TAG} already exists; publish changed artifacts under a new tag." >&2
+ exit 1
+ fi
+
linux:
name: Build zsh (Linux) - ${{ matrix.variant }} - ${{ matrix.target }}
+ needs: metadata
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
container:
@@ -62,6 +103,7 @@ jobs:
darwin:
name: Build zsh (macOS) - ${{ matrix.variant }} - ${{ matrix.target }}
+ needs: metadata
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
@@ -101,3 +143,40 @@ jobs:
with:
name: codex-zsh-${{ matrix.target }}
path: dist/zsh/${{ matrix.target }}/*
+
+ publish-release:
+ needs:
+ - metadata
+ - linux
+ - darwin
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ actions: read
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ path: dist
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
+ with:
+ tag_name: ${{ needs.metadata.outputs.release_tag }}
+ name: ${{ needs.metadata.outputs.release_tag }}
+ files: dist/**
+ # Keep zsh artifact releases out of Codex's normal "latest release" channel.
+ prerelease: true
+
+ - name: Publish DotSlash manifest
+ uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ tag: ${{ needs.metadata.outputs.release_tag }}
+ config: .github/dotslash-zsh-config.json
diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml
index a2646e3eae..49d24abc18 100644
--- a/.github/workflows/rust-release.yml
+++ b/.github/workflows/rust-release.yml
@@ -14,6 +14,9 @@ on:
tags:
- "rust-v*.*.*"
+env:
+ CODEX_ZSH_RELEASE_TAG: codex-zsh-v0.1.0
+
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
@@ -354,6 +357,15 @@ jobs:
cp target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg "$dest/codex-${{ matrix.target }}.dmg"
fi
+ - name: Download packaged zsh manifest
+ if: ${{ runner.os != 'macOS' }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ curl -fsSL \
+ "https://github.com/${GITHUB_REPOSITORY}/releases/download/${CODEX_ZSH_RELEASE_TAG}/codex-zsh" \
+ -o "${RUNNER_TEMP}/codex-zsh"
+
- name: Build Codex package archive
if: ${{ runner.os != 'macOS' }}
shell: bash
@@ -366,7 +378,8 @@ jobs:
--target "$TARGET" \
--bundle "$BUNDLE" \
--entrypoint-dir "target/${TARGET}/release" \
- --archive-dir "dist/${TARGET}"
+ --archive-dir "dist/${TARGET}" \
+ --zsh-manifest "${RUNNER_TEMP}/codex-zsh"
- name: Build Python runtime wheel
if: ${{ matrix.bundle == 'primary' && runner.os != 'macOS' }}
@@ -697,6 +710,14 @@ jobs:
cp "target/${{ matrix.target }}/release/${binary}" "$dest/${binary}-${{ matrix.target }}"
done
+ - name: Download packaged zsh manifest
+ shell: bash
+ run: |
+ set -euo pipefail
+ curl -fsSL \
+ "https://github.com/${GITHUB_REPOSITORY}/releases/download/${CODEX_ZSH_RELEASE_TAG}/codex-zsh" \
+ -o "${RUNNER_TEMP}/codex-zsh"
+
- name: Build Codex package archive
shell: bash
env:
@@ -708,7 +729,8 @@ jobs:
--target "$TARGET" \
--bundle "$BUNDLE" \
--entrypoint-dir "target/${TARGET}/release" \
- --archive-dir "dist/${TARGET}"
+ --archive-dir "dist/${TARGET}" \
+ --zsh-manifest "${RUNNER_TEMP}/codex-zsh"
- name: Build Python runtime wheel
if: ${{ matrix.bundle == 'primary' }}
@@ -1043,11 +1065,6 @@ jobs:
with:
publish: true
- zsh-release-assets:
- name: zsh release assets
- needs: tag-check
- uses: ./.github/workflows/rust-release-zsh.yml
-
release:
needs:
- tag-check
@@ -1055,7 +1072,6 @@ jobs:
- finalize-macos
- build-windows
- argument-comment-lint-release-assets
- - zsh-release-assets
if: >-
${{
always() &&
@@ -1063,8 +1079,7 @@ jobs:
needs.build.result == 'success' &&
needs.finalize-macos.result == 'success' &&
needs.build-windows.result == 'success' &&
- needs.argument-comment-lint-release-assets.result == 'success' &&
- needs.zsh-release-assets.result == 'success'
+ needs.argument-comment-lint-release-assets.result == 'success'
}}
name: release
runs-on: ubuntu-latest
@@ -1113,7 +1128,7 @@ jobs:
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: dist
- pattern: "{*-symbols,argument-comment-lint-*,codex-zsh-*,python-runtime-wheel-*}"
+ pattern: "{*-symbols,argument-comment-lint-*,python-runtime-wheel-*}"
- name: List
run: ls -R dist/
@@ -1236,13 +1251,6 @@ jobs:
tag: ${{ github.ref_name }}
config: .github/dotslash-config.json
- - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- tag: ${{ github.ref_name }}
- config: .github/dotslash-zsh-config.json
-
- uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml
index dcb9406798..41f8874712 100644
--- a/.github/workflows/sdk.yml
+++ b/.github/workflows/sdk.yml
@@ -1,9 +1,7 @@
name: sdk
on:
- push:
- branches: [main]
- pull_request: {}
+ workflow_call:
jobs:
python-sdk:
diff --git a/.github/workflows/v8-canary.yml b/.github/workflows/v8-canary.yml
index efa06aaad7..53d9585108 100644
--- a/.github/workflows/v8-canary.yml
+++ b/.github/workflows/v8-canary.yml
@@ -1,44 +1,12 @@
name: v8-canary
+# Do not use trigger-level path filters here. This workflow is also called by
+# postmerge-ci, and GitHub cannot share a path filter between pull_request and
+# workflow_call. v8_canary_changes.py is the single source of truth instead;
+# unrelated events run only the cheap metadata job below.
on:
- pull_request:
- paths:
- - ".bazelrc"
- - ".github/actions/setup-bazel-ci/**"
- - ".github/scripts/run_bazel_with_buildbuddy.py"
- - ".github/scripts/rusty_v8_bazel.py"
- - ".github/scripts/rusty_v8_module_bazel.py"
- - ".github/scripts/v8_canary_changes.py"
- - ".github/workflows/rusty-v8-release.yml"
- - ".github/workflows/v8-canary.yml"
- - "MODULE.bazel"
- - "MODULE.bazel.lock"
- - "codex-rs/Cargo.toml"
- - "patches/BUILD.bazel"
- - "patches/llvm_*.patch"
- - "patches/rules_cc_*.patch"
- - "patches/v8_*.patch"
- - "third_party/v8/**"
- push:
- branches:
- - main
- paths:
- - ".bazelrc"
- - ".github/actions/setup-bazel-ci/**"
- - ".github/scripts/run_bazel_with_buildbuddy.py"
- - ".github/scripts/rusty_v8_bazel.py"
- - ".github/scripts/rusty_v8_module_bazel.py"
- - ".github/scripts/v8_canary_changes.py"
- - ".github/workflows/rusty-v8-release.yml"
- - ".github/workflows/v8-canary.yml"
- - "MODULE.bazel"
- - "MODULE.bazel.lock"
- - "codex-rs/Cargo.toml"
- - "patches/BUILD.bazel"
- - "patches/llvm_*.patch"
- - "patches/rules_cc_*.patch"
- - "patches/v8_*.patch"
- - "third_party/v8/**"
+ workflow_call:
+ pull_request: {}
workflow_dispatch:
# Cargo's libgit2 transport has been flaky when fetching git dependencies with
@@ -54,6 +22,10 @@ jobs:
metadata:
runs-on: ubuntu-latest
outputs:
+ # A stale PR head can contain the old detector, which does not emit this
+ # output. Missing must mean "run" so older branches cannot silently skip
+ # the expensive V8 coverage while reporting success.
+ canary_required: ${{ steps.changes.outputs.canary_required || 'true' }}
v8_version: ${{ steps.v8_version.outputs.version }}
windows_source_required: ${{ steps.changes.outputs.windows_source_required }}
@@ -77,7 +49,7 @@ jobs:
version="$(python3 .github/scripts/rusty_v8_bazel.py resolved-v8-crate-version)"
echo "version=${version}" >> "$GITHUB_OUTPUT"
- - name: Detect whether Windows source artifacts need rebuilding
+ - name: Detect V8 canary changes
id: changes
env:
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
@@ -87,6 +59,8 @@ jobs:
run: |
set -euo pipefail
+ # Manual runs have no meaningful before/after range. Force every V8
+ # variant so workflow_dispatch remains a reliable retry path.
if [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then
output="$(python3 .github/scripts/v8_canary_changes.py --force)"
else
@@ -104,6 +78,8 @@ jobs:
build:
name: Build ${{ matrix.variant }} ${{ matrix.target }}
needs: metadata
+ # Metadata always runs; only relevant changes pay for the large matrix.
+ if: ${{ needs.metadata.outputs.canary_required == 'true' }}
runs-on: ${{ matrix.runner }}
permissions:
contents: read
diff --git a/.worktreeinclude b/.worktreeinclude
new file mode 100644
index 0000000000..74824b01f9
--- /dev/null
+++ b/.worktreeinclude
@@ -0,0 +1 @@
+user.bazelrc
diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock
index 8ae5bef17c..3356b66c14 100644
--- a/MODULE.bazel.lock
+++ b/MODULE.bazel.lock
@@ -804,7 +804,7 @@
"critical-section_1.2.0": "{\"dependencies\":[],\"features\":{\"restore-state-bool\":[],\"restore-state-none\":[],\"restore-state-u16\":[],\"restore-state-u32\":[],\"restore-state-u64\":[],\"restore-state-u8\":[],\"restore-state-usize\":[],\"std\":[\"restore-state-bool\"]}}",
"crossbeam-channel_0.5.15": "{\"dependencies\":[{\"default_features\":false,\"name\":\"crossbeam-utils\",\"req\":\"^0.8.18\"},{\"kind\":\"dev\",\"name\":\"num_cpus\",\"req\":\"^1.13.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"signal-hook\",\"req\":\"^0.3\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"crossbeam-utils/std\"]}}",
"crossbeam-deque_0.8.6": "{\"dependencies\":[{\"default_features\":false,\"name\":\"crossbeam-epoch\",\"req\":\"^0.9.17\"},{\"default_features\":false,\"name\":\"crossbeam-utils\",\"req\":\"^0.8.18\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"crossbeam-epoch/std\",\"crossbeam-utils/std\"]}}",
- "crossbeam-epoch_0.9.18": "{\"dependencies\":[{\"default_features\":false,\"name\":\"crossbeam-utils\",\"req\":\"^0.8.18\"},{\"name\":\"loom-crate\",\"optional\":true,\"package\":\"loom\",\"req\":\"^0.7.1\",\"target\":\"cfg(crossbeam_loom)\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"loom\":[\"loom-crate\",\"crossbeam-utils/loom\"],\"nightly\":[\"crossbeam-utils/nightly\"],\"std\":[\"alloc\",\"crossbeam-utils/std\"]}}",
+ "crossbeam-epoch_0.9.20": "{\"dependencies\":[{\"default_features\":false,\"name\":\"crossbeam-utils\",\"req\":\"^0.8.18\"},{\"name\":\"loom-crate\",\"optional\":true,\"package\":\"loom\",\"req\":\"^0.7.1\",\"target\":\"cfg(crossbeam_loom)\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"loom\":[\"loom-crate\",\"crossbeam-utils/loom\"],\"nightly\":[\"crossbeam-utils/nightly\"],\"std\":[\"alloc\",\"crossbeam-utils/std\"]}}",
"crossbeam-queue_0.3.12": "{\"dependencies\":[{\"default_features\":false,\"name\":\"crossbeam-utils\",\"req\":\"^0.8.18\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"nightly\":[\"crossbeam-utils/nightly\"],\"std\":[\"alloc\",\"crossbeam-utils/std\"]}}",
"crossbeam-utils_0.8.21": "{\"dependencies\":[{\"name\":\"loom\",\"optional\":true,\"req\":\"^0.7.1\",\"target\":\"cfg(crossbeam_loom)\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"}],\"features\":{\"default\":[\"std\"],\"nightly\":[],\"std\":[]}}",
"crossterm_winapi_0.9.1": "{\"dependencies\":[{\"features\":[\"winbase\",\"consoleapi\",\"processenv\",\"handleapi\",\"synchapi\",\"impl-default\"],\"name\":\"winapi\",\"req\":\"^0.3.8\",\"target\":\"cfg(windows)\"}],\"features\":{}}",
@@ -1343,7 +1343,7 @@
"pkcs8_0.10.2": "{\"dependencies\":[{\"features\":[\"oid\"],\"name\":\"der\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"},{\"name\":\"pkcs5\",\"optional\":true,\"req\":\"^0.7\"},{\"default_features\":false,\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"spki\",\"req\":\"^0.7.1\"},{\"default_features\":false,\"name\":\"subtle\",\"optional\":true,\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"3des\":[\"encryption\",\"pkcs5/3des\"],\"alloc\":[\"der/alloc\",\"der/zeroize\",\"spki/alloc\"],\"des-insecure\":[\"encryption\",\"pkcs5/des-insecure\"],\"encryption\":[\"alloc\",\"pkcs5/alloc\",\"pkcs5/pbes2\",\"rand_core\"],\"getrandom\":[\"rand_core/getrandom\"],\"pem\":[\"alloc\",\"der/pem\",\"spki/pem\"],\"sha1-insecure\":[\"encryption\",\"pkcs5/sha1-insecure\"],\"std\":[\"alloc\",\"der/std\",\"spki/std\"]}}",
"pkg-config_0.3.32": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"}],\"features\":{}}",
"plain_0.2.3": "{\"dependencies\":[],\"features\":{}}",
- "plist_1.8.0": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"^0.22.0\"},{\"name\":\"indexmap\",\"req\":\"^2.1.0\"},{\"name\":\"quick_xml\",\"package\":\"quick-xml\",\"req\":\"^0.38.0\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.2\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.2\"},{\"kind\":\"dev\",\"name\":\"serde_yaml\",\"req\":\"^0.8.21\"},{\"features\":[\"parsing\",\"formatting\"],\"name\":\"time\",\"req\":\"^0.3.30\"}],\"features\":{\"default\":[\"serde\"],\"enable_unstable_features_that_may_break_with_minor_version_bumps\":[]}}",
+ "plist_1.9.0": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"^0.22.0\"},{\"name\":\"indexmap\",\"req\":\"^2.1.0\"},{\"name\":\"quick_xml\",\"package\":\"quick-xml\",\"req\":\"^0.39.2\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.2\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.2\"},{\"kind\":\"dev\",\"name\":\"serde_yaml\",\"req\":\"^0.8.21\"},{\"features\":[\"parsing\",\"formatting\"],\"name\":\"time\",\"req\":\"^0.3.47\"}],\"features\":{\"default\":[\"serde\"],\"enable_unstable_features_that_may_break_with_minor_version_bumps\":[]}}",
"png_0.18.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"approx\",\"req\":\"^0.5.1\"},{\"name\":\"bitflags\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"byteorder\",\"req\":\"^1.5.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.0\"},{\"name\":\"crc32fast\",\"req\":\"^1.2.0\"},{\"default_features\":false,\"features\":[\"cargo_bench_support\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"fdeflate\",\"req\":\"^0.3.3\"},{\"name\":\"flate2\",\"req\":\"^1.0.35\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3\"},{\"features\":[\"simd\"],\"name\":\"miniz_oxide\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9.2\"}],\"features\":{\"benchmarks\":[],\"unstable\":[\"crc32fast/nightly\"],\"zlib-rs\":[\"flate2/zlib-rs\"]}}",
"polling_3.11.0": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"concurrent-queue\",\"req\":\"^2.2.0\",\"target\":\"cfg(windows)\"},{\"kind\":\"dev\",\"name\":\"easy-parallel\",\"req\":\"^3.1.0\"},{\"kind\":\"dev\",\"name\":\"fastrand\",\"req\":\"^2.0.0\"},{\"name\":\"hermit-abi\",\"req\":\"^0.5.0\",\"target\":\"cfg(target_os = \\\"hermit\\\")\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.9\",\"target\":\"cfg(windows)\"},{\"default_features\":false,\"features\":[\"event\",\"fs\",\"pipe\",\"process\",\"std\",\"time\"],\"name\":\"rustix\",\"req\":\"^1.0.5\",\"target\":\"cfg(any(unix, target_os = \\\"fuchsia\\\", target_os = \\\"vxworks\\\"))\"},{\"kind\":\"dev\",\"name\":\"signal-hook\",\"req\":\"^0.3.17\",\"target\":\"cfg(all(unix, not(target_os=\\\"vita\\\")))\"},{\"kind\":\"dev\",\"name\":\"socket2\",\"req\":\"^0.6.0\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.37\"},{\"features\":[\"Wdk_Foundation\",\"Wdk_Storage_FileSystem\",\"Win32_Foundation\",\"Win32_Networking_WinSock\",\"Win32_Security\",\"Win32_Storage_FileSystem\",\"Win32_System_IO\",\"Win32_System_LibraryLoader\",\"Win32_System_Threading\",\"Win32_System_WindowsProgramming\"],\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{}}",
"poly1305_0.8.0": "{\"dependencies\":[{\"name\":\"cpufeatures\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"},{\"name\":\"opaque-debug\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"universal-hash\",\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"std\":[\"universal-hash/std\"]}}",
@@ -1390,7 +1390,8 @@
"pulldown-cmark_0.10.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.1\"},{\"name\":\"bitflags\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"getopts\",\"optional\":true,\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.4\"},{\"name\":\"memchr\",\"req\":\"^2.5\"},{\"name\":\"pulldown-cmark-escape\",\"optional\":true,\"req\":\"^0.10.0\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.6\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.61\"},{\"name\":\"unicase\",\"req\":\"^2.6\"}],\"features\":{\"default\":[\"getopts\",\"html\"],\"gen-tests\":[],\"html\":[\"pulldown-cmark-escape\"],\"simd\":[\"pulldown-cmark-escape?/simd\"]}}",
"pxfm_0.1.27": "{\"dependencies\":[{\"name\":\"num-traits\",\"req\":\"^0.2.3\"}],\"features\":{}}",
"quick-error_2.0.1": "{\"dependencies\":[],\"features\":{}}",
- "quick-xml_0.38.4": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\">=0.4, <0.8\"},{\"name\":\"document-features\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"encoding_rs\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"memchr\",\"req\":\"^2.1\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.4\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1\"},{\"name\":\"serde\",\"optional\":true,\"req\":\">=1.0.139\"},{\"kind\":\"dev\",\"name\":\"serde-value\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.206\"},{\"default_features\":false,\"features\":[\"io-util\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.10\"},{\"default_features\":false,\"features\":[\"macros\",\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.21\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"}],\"features\":{\"async-tokio\":[\"tokio\"],\"default\":[],\"encoding\":[\"encoding_rs\"],\"escape-html\":[],\"overlapped-lists\":[],\"serde-types\":[\"serde/derive\"],\"serialize\":[\"serde\"]}}",
+ "quick-xml_0.39.4": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\">=0.4, <0.9\"},{\"name\":\"document-features\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"encoding_rs\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"memchr\",\"req\":\"^2.1\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.4\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1\"},{\"name\":\"serde\",\"optional\":true,\"req\":\">=1.0.180\"},{\"kind\":\"dev\",\"name\":\"serde-value\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.206\"},{\"default_features\":false,\"features\":[\"io-util\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.10\"},{\"default_features\":false,\"features\":[\"macros\",\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.21\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"}],\"features\":{\"async-tokio\":[\"tokio\"],\"default\":[],\"encoding\":[\"encoding_rs\"],\"escape-html\":[],\"overlapped-lists\":[],\"serde-types\":[\"serde/derive\"],\"serialize\":[\"serde\"]}}",
+ "quick-xml_0.41.0": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\">=0.4, <0.9\"},{\"name\":\"document-features\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"encoding_rs\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"memchr\",\"req\":\"^2.1\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.4\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1\"},{\"name\":\"serde\",\"optional\":true,\"req\":\">=1.0.180\"},{\"kind\":\"dev\",\"name\":\"serde-value\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.206\"},{\"default_features\":false,\"features\":[\"io-util\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.10\"},{\"default_features\":false,\"features\":[\"macros\",\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.21\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"}],\"features\":{\"async-tokio\":[\"tokio\"],\"default\":[],\"encoding\":[\"encoding_rs\"],\"escape-html\":[],\"overlapped-lists\":[],\"serde-types\":[\"serde/derive\"],\"serialize\":[\"serde\"]}}",
"quickcheck_1.1.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"env_logger\",\"optional\":true,\"req\":\"^0.11\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"},{\"default_features\":false,\"features\":[\"sys_rng\"],\"name\":\"rand\",\"req\":\"^0.10\"}],\"features\":{\"default\":[\"regex\",\"use_logging\"],\"regex\":[\"env_logger/regex\"],\"use_logging\":[\"log\",\"env_logger\"]}}",
"quinn-proto_0.11.14": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.0.1\"},{\"kind\":\"dev\",\"name\":\"assert_matches\",\"req\":\"^1.1\"},{\"default_features\":false,\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.9\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"fastbloom\",\"optional\":true,\"req\":\"^0.14\"},{\"default_features\":false,\"features\":[\"wasm_js\"],\"name\":\"getrandom\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"},{\"name\":\"lru-slab\",\"req\":\"^0.1.2\"},{\"name\":\"qlog\",\"optional\":true,\"req\":\"^0.15.2\"},{\"name\":\"rand\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rand_pcg\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14\"},{\"features\":[\"wasm32_unknown_unknown_js\"],\"name\":\"ring\",\"req\":\"^0.17\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17\"},{\"name\":\"rustc-hash\",\"req\":\"^2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.5\"},{\"features\":[\"web\"],\"name\":\"rustls-pki-types\",\"req\":\"^1.7\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"rustls-platform-verifier\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"slab\",\"req\":\"^0.4.6\"},{\"name\":\"thiserror\",\"req\":\"^2.0.3\"},{\"features\":[\"alloc\",\"alloc\"],\"name\":\"tinyvec\",\"req\":\"^1.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"req\":\"^0.1.10\"},{\"default_features\":false,\"features\":[\"env-filter\",\"fmt\",\"ansi\",\"time\",\"local-time\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.45\"},{\"name\":\"web-time\",\"req\":\"^1\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"}],\"features\":{\"__rustls-post-quantum-test\":[],\"aws-lc-rs\":[\"dep:aws-lc-rs\",\"aws-lc-rs?/aws-lc-sys\",\"aws-lc-rs?/prebuilt-nasm\"],\"aws-lc-rs-fips\":[\"aws-lc-rs\",\"aws-lc-rs?/fips\"],\"bloom\":[\"dep:fastbloom\"],\"default\":[\"rustls-ring\",\"log\",\"bloom\"],\"log\":[\"tracing/log\"],\"platform-verifier\":[\"dep:rustls-platform-verifier\"],\"qlog\":[\"dep:qlog\"],\"ring\":[\"dep:ring\"],\"rustls\":[\"rustls-ring\"],\"rustls-aws-lc-rs\":[\"dep:rustls\",\"rustls?/aws-lc-rs\",\"aws-lc-rs\"],\"rustls-aws-lc-rs-fips\":[\"rustls-aws-lc-rs\",\"aws-lc-rs-fips\"],\"rustls-log\":[\"rustls?/logging\"],\"rustls-ring\":[\"dep:rustls\",\"rustls?/ring\",\"ring\"]}}",
"quinn-udp_0.5.14": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cfg_aliases\",\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"async_tokio\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7\"},{\"name\":\"libc\",\"req\":\"^0.2.158\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"once_cell\",\"req\":\"^1.19\",\"target\":\"cfg(windows)\"},{\"name\":\"socket2\",\"req\":\">=0.5, <0.7\",\"target\":\"cfg(not(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\")))\"},{\"features\":[\"sync\",\"rt\",\"rt-multi-thread\",\"net\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.28.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.10\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_IO\",\"Win32_Networking_WinSock\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <=0.60\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"tracing\",\"log\"],\"direct-log\":[\"dep:log\"],\"fast-apple-datapath\":[],\"log\":[\"tracing/log\"]}}",
@@ -1749,7 +1750,7 @@
"wayland-client_0.31.12": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"futures-channel\",\"req\":\"^0.3.16\"},{\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"},{\"features\":[\"event\"],\"name\":\"rustix\",\"req\":\"^1.0.2\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.2\"},{\"name\":\"wayland-backend\",\"req\":\"^0.3.12\"},{\"name\":\"wayland-scanner\",\"req\":\"^0.31.8\"}],\"features\":{}}",
"wayland-protocols-wlr_0.3.10": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2\"},{\"name\":\"wayland-backend\",\"req\":\"^0.3.12\"},{\"name\":\"wayland-client\",\"optional\":true,\"req\":\"^0.31.12\"},{\"name\":\"wayland-protocols\",\"req\":\"^0.32.10\"},{\"name\":\"wayland-scanner\",\"req\":\"^0.31.8\"},{\"name\":\"wayland-server\",\"optional\":true,\"req\":\"^0.31.11\"}],\"features\":{\"client\":[\"wayland-client\",\"wayland-protocols/client\"],\"server\":[\"wayland-server\",\"wayland-protocols/server\"]}}",
"wayland-protocols_0.32.10": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2\"},{\"name\":\"wayland-backend\",\"req\":\"^0.3.12\"},{\"name\":\"wayland-client\",\"optional\":true,\"req\":\"^0.31.12\"},{\"name\":\"wayland-scanner\",\"req\":\"^0.31.8\"},{\"name\":\"wayland-server\",\"optional\":true,\"req\":\"^0.31.11\"}],\"features\":{\"client\":[\"wayland-client\"],\"server\":[\"wayland-server\"],\"staging\":[],\"unstable\":[]}}",
- "wayland-scanner_0.31.8": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.11\"},{\"name\":\"quick-xml\",\"req\":\"^0.38.3\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"similar\",\"req\":\"^2\"}],\"features\":{}}",
+ "wayland-scanner_0.31.10": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.11\"},{\"name\":\"quick-xml\",\"req\":\"^0.39\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"similar\",\"req\":\"^2\"}],\"features\":{}}",
"wayland-sys_0.31.8": "{\"dependencies\":[{\"name\":\"dlib\",\"optional\":true,\"req\":\"^0.5.1\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"memoffset\",\"optional\":true,\"req\":\"^0.9\"},{\"name\":\"once_cell\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.7\"}],\"features\":{\"client\":[\"dep:dlib\",\"dep:log\"],\"cursor\":[\"client\"],\"dlopen\":[\"once_cell\"],\"egl\":[\"client\"],\"server\":[\"libc\",\"memoffset\",\"dep:dlib\",\"dep:log\"]}}",
"web-sys_0.3.85": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"default_features\":false,\"name\":\"js-sys\",\"req\":\"=0.3.85\"},{\"default_features\":false,\"name\":\"wasm-bindgen\",\"req\":\"=0.2.108\"}],\"features\":{\"AbortController\":[],\"AbortSignal\":[\"EventTarget\"],\"AddEventListenerOptions\":[],\"AesCbcParams\":[],\"AesCtrParams\":[],\"AesDerivedKeyParams\":[],\"AesGcmParams\":[],\"AesKeyAlgorithm\":[],\"AesKeyGenParams\":[],\"Algorithm\":[],\"AlignSetting\":[],\"AllowedBluetoothDevice\":[],\"AllowedUsbDevice\":[],\"AlphaOption\":[],\"AnalyserNode\":[\"AudioNode\",\"EventTarget\"],\"AnalyserOptions\":[],\"AngleInstancedArrays\":[],\"Animation\":[\"EventTarget\"],\"AnimationEffect\":[],\"AnimationEvent\":[\"Event\"],\"AnimationEventInit\":[],\"AnimationPlayState\":[],\"AnimationPlaybackEvent\":[\"Event\"],\"AnimationPlaybackEventInit\":[],\"AnimationPropertyDetails\":[],\"AnimationPropertyValueDetails\":[],\"AnimationTimeline\":[],\"AssignedNodesOptions\":[],\"AttestationConveyancePreference\":[],\"Attr\":[\"EventTarget\",\"Node\"],\"AttributeNameValue\":[],\"AudioBuffer\":[],\"AudioBufferOptions\":[],\"AudioBufferSourceNode\":[\"AudioNode\",\"AudioScheduledSourceNode\",\"EventTarget\"],\"AudioBufferSourceOptions\":[],\"AudioConfiguration\":[],\"AudioContext\":[\"BaseAudioContext\",\"EventTarget\"],\"AudioContextLatencyCategory\":[],\"AudioContextOptions\":[],\"AudioContextState\":[],\"AudioData\":[],\"AudioDataCopyToOptions\":[],\"AudioDataInit\":[],\"AudioDecoder\":[],\"AudioDecoderConfig\":[],\"AudioDecoderInit\":[],\"AudioDecoderSupport\":[],\"AudioDestinationNode\":[\"AudioNode\",\"EventTarget\"],\"AudioEncoder\":[],\"AudioEncoderConfig\":[],\"AudioEncoderInit\":[],\"AudioEncoderSupport\":[],\"AudioListener\":[],\"AudioNode\":[\"EventTarget\"],\"AudioNodeOptions\":[],\"AudioParam\":[],\"AudioParamMap\":[],\"AudioProcessingEvent\":[\"Event\"],\"AudioSampleFormat\":[],\"AudioScheduledSourceNode\":[\"AudioNode\",\"EventTarget\"],\"AudioSinkInfo\":[],\"AudioSinkOptions\":[],\"AudioSinkType\":[],\"AudioStreamTrack\":[\"EventTarget\",\"MediaStreamTrack\"],\"AudioTrack\":[],\"AudioTrackList\":[\"EventTarget\"],\"AudioWorklet\":[\"Worklet\"],\"AudioWorkletGlobalScope\":[\"WorkletGlobalScope\"],\"AudioWorkletNode\":[\"AudioNode\",\"EventTarget\"],\"AudioWorkletNodeOptions\":[],\"AudioWorkletProcessor\":[],\"AuthenticationExtensionsClientInputs\":[],\"AuthenticationExtensionsClientInputsJson\":[],\"AuthenticationExtensionsClientOutputs\":[],\"AuthenticationExtensionsClientOutputsJson\":[],\"AuthenticationExtensionsDevicePublicKeyInputs\":[],\"AuthenticationExtensionsDevicePublicKeyOutputs\":[],\"AuthenticationExtensionsLargeBlobInputs\":[],\"AuthenticationExtensionsLargeBlobOutputs\":[],\"AuthenticationExtensionsPrfInputs\":[],\"AuthenticationExtensionsPrfOutputs\":[],\"AuthenticationExtensionsPrfValues\":[],\"AuthenticationResponseJson\":[],\"AuthenticatorAssertionResponse\":[\"AuthenticatorResponse\"],\"AuthenticatorAssertionResponseJson\":[],\"AuthenticatorAttachment\":[],\"AuthenticatorAttestationResponse\":[\"AuthenticatorResponse\"],\"AuthenticatorAttestationResponseJson\":[],\"AuthenticatorResponse\":[],\"AuthenticatorSelectionCriteria\":[],\"AuthenticatorTransport\":[],\"AutoKeyword\":[],\"AutocompleteInfo\":[],\"BarProp\":[],\"BaseAudioContext\":[\"EventTarget\"],\"BaseComputedKeyframe\":[],\"BaseKeyframe\":[],\"BasePropertyIndexedKeyframe\":[],\"BasicCardRequest\":[],\"BasicCardResponse\":[],\"BasicCardType\":[],\"BatteryManager\":[\"EventTarget\"],\"BeforeUnloadEvent\":[\"Event\"],\"BinaryType\":[],\"BiquadFilterNode\":[\"AudioNode\",\"EventTarget\"],\"BiquadFilterOptions\":[],\"BiquadFilterType\":[],\"Blob\":[],\"BlobEvent\":[\"Event\"],\"BlobEventInit\":[],\"BlobPropertyBag\":[],\"BlockParsingOptions\":[],\"Bluetooth\":[\"EventTarget\"],\"BluetoothAdvertisingEvent\":[\"Event\"],\"BluetoothAdvertisingEventInit\":[],\"BluetoothCharacteristicProperties\":[],\"BluetoothDataFilterInit\":[],\"BluetoothDevice\":[\"EventTarget\"],\"BluetoothLeScanFilterInit\":[],\"BluetoothManufacturerDataMap\":[],\"BluetoothPermissionDescriptor\":[],\"BluetoothPermissionResult\":[\"EventTarget\",\"PermissionStatus\"],\"BluetoothPermissionStorage\":[],\"BluetoothRemoteGattCharacteristic\":[\"EventTarget\"],\"BluetoothRemoteGattDescriptor\":[],\"BluetoothRemoteGattServer\":[],\"BluetoothRemoteGattService\":[\"EventTarget\"],\"BluetoothServiceDataMap\":[],\"BluetoothUuid\":[],\"BoxQuadOptions\":[],\"BroadcastChannel\":[\"EventTarget\"],\"BrowserElementDownloadOptions\":[],\"BrowserElementExecuteScriptOptions\":[],\"BrowserFeedWriter\":[],\"BrowserFindCaseSensitivity\":[],\"BrowserFindDirection\":[],\"ByteLengthQueuingStrategy\":[],\"Cache\":[],\"CacheBatchOperation\":[],\"CacheQueryOptions\":[],\"CacheStorage\":[],\"CacheStorageNamespace\":[],\"CanvasCaptureMediaStream\":[\"EventTarget\",\"MediaStream\"],\"CanvasCaptureMediaStreamTrack\":[\"EventTarget\",\"MediaStreamTrack\"],\"CanvasGradient\":[],\"CanvasPattern\":[],\"CanvasRenderingContext2d\":[],\"CanvasWindingRule\":[],\"CaretChangedReason\":[],\"CaretPosition\":[],\"CaretStateChangedEventInit\":[],\"CdataSection\":[\"CharacterData\",\"EventTarget\",\"Node\",\"Text\"],\"ChannelCountMode\":[],\"ChannelInterpretation\":[],\"ChannelMergerNode\":[\"AudioNode\",\"EventTarget\"],\"ChannelMergerOptions\":[],\"ChannelSplitterNode\":[\"AudioNode\",\"EventTarget\"],\"ChannelSplitterOptions\":[],\"CharacterData\":[\"EventTarget\",\"Node\"],\"CheckerboardReason\":[],\"CheckerboardReport\":[],\"CheckerboardReportService\":[],\"ChromeFilePropertyBag\":[],\"ChromeWorker\":[\"EventTarget\",\"Worker\"],\"Client\":[],\"ClientQueryOptions\":[],\"ClientRectsAndTexts\":[],\"ClientType\":[],\"Clients\":[],\"Clipboard\":[\"EventTarget\"],\"ClipboardEvent\":[\"Event\"],\"ClipboardEventInit\":[],\"ClipboardItem\":[],\"ClipboardItemOptions\":[],\"ClipboardPermissionDescriptor\":[],\"ClipboardUnsanitizedFormats\":[],\"CloseEvent\":[\"Event\"],\"CloseEventInit\":[],\"CodecState\":[],\"CollectedClientData\":[],\"ColorSpaceConversion\":[],\"Comment\":[\"CharacterData\",\"EventTarget\",\"Node\"],\"CompositeOperation\":[],\"CompositionEvent\":[\"Event\",\"UiEvent\"],\"CompositionEventInit\":[],\"CompressionFormat\":[],\"CompressionStream\":[],\"ComputedEffectTiming\":[],\"ConnStatusDict\":[],\"ConnectionType\":[],\"ConsoleCounter\":[],\"ConsoleCounterError\":[],\"ConsoleEvent\":[],\"ConsoleInstance\":[],\"ConsoleInstanceOptions\":[],\"ConsoleLevel\":[],\"ConsoleLogLevel\":[],\"ConsoleProfileEvent\":[],\"ConsoleStackEntry\":[],\"ConsoleTimerError\":[],\"ConsoleTimerLogOrEnd\":[],\"ConsoleTimerStart\":[],\"ConstantSourceNode\":[\"AudioNode\",\"AudioScheduledSourceNode\",\"EventTarget\"],\"ConstantSourceOptions\":[],\"ConstrainBooleanParameters\":[],\"ConstrainDomStringParameters\":[],\"ConstrainDoubleRange\":[],\"ConstrainLongRange\":[],\"ContextAttributes2d\":[],\"ConvertCoordinateOptions\":[],\"ConvolverNode\":[\"AudioNode\",\"EventTarget\"],\"ConvolverOptions\":[],\"CookieChangeEvent\":[\"Event\"],\"CookieChangeEventInit\":[],\"CookieInit\":[],\"CookieListItem\":[],\"CookieSameSite\":[],\"CookieStore\":[\"EventTarget\"],\"CookieStoreDeleteOptions\":[],\"CookieStoreGetOptions\":[],\"CookieStoreManager\":[],\"Coordinates\":[],\"CountQueuingStrategy\":[],\"Credential\":[],\"CredentialCreationOptions\":[],\"CredentialPropertiesOutput\":[],\"CredentialRequestOptions\":[],\"CredentialsContainer\":[],\"Crypto\":[],\"CryptoKey\":[],\"CryptoKeyPair\":[],\"CssAnimation\":[\"Animation\",\"EventTarget\"],\"CssBoxType\":[],\"CssConditionRule\":[\"CssGroupingRule\",\"CssRule\"],\"CssCounterStyleRule\":[\"CssRule\"],\"CssFontFaceRule\":[\"CssRule\"],\"CssFontFeatureValuesRule\":[\"CssRule\"],\"CssGroupingRule\":[\"CssRule\"],\"CssImportRule\":[\"CssRule\"],\"CssKeyframeRule\":[\"CssRule\"],\"CssKeyframesRule\":[\"CssRule\"],\"CssMediaRule\":[\"CssConditionRule\",\"CssGroupingRule\",\"CssRule\"],\"CssNamespaceRule\":[\"CssRule\"],\"CssPageRule\":[\"CssRule\"],\"CssPseudoElement\":[],\"CssRule\":[],\"CssRuleList\":[],\"CssStyleDeclaration\":[],\"CssStyleRule\":[\"CssRule\"],\"CssStyleSheet\":[\"StyleSheet\"],\"CssStyleSheetParsingMode\":[],\"CssSupportsRule\":[\"CssConditionRule\",\"CssGroupingRule\",\"CssRule\"],\"CssTransition\":[\"Animation\",\"EventTarget\"],\"CustomElementRegistry\":[],\"CustomEvent\":[\"Event\"],\"CustomEventInit\":[],\"DataTransfer\":[],\"DataTransferItem\":[],\"DataTransferItemList\":[],\"DateTimeValue\":[],\"DecoderDoctorNotification\":[],\"DecoderDoctorNotificationType\":[],\"DecompressionStream\":[],\"DedicatedWorkerGlobalScope\":[\"EventTarget\",\"WorkerGlobalScope\"],\"DelayNode\":[\"AudioNode\",\"EventTarget\"],\"DelayOptions\":[],\"DeviceAcceleration\":[],\"DeviceAccelerationInit\":[],\"DeviceLightEvent\":[\"Event\"],\"DeviceLightEventInit\":[],\"DeviceMotionEvent\":[\"Event\"],\"DeviceMotionEventInit\":[],\"DeviceOrientationEvent\":[\"Event\"],\"DeviceOrientationEventInit\":[],\"DeviceProximityEvent\":[\"Event\"],\"DeviceProximityEventInit\":[],\"DeviceRotationRate\":[],\"DeviceRotationRateInit\":[],\"DhKeyDeriveParams\":[],\"DirectionSetting\":[],\"Directory\":[],\"DirectoryPickerOptions\":[],\"DisplayMediaStreamConstraints\":[],\"DisplayNameOptions\":[],\"DisplayNameResult\":[],\"DistanceModelType\":[],\"DnsCacheDict\":[],\"DnsCacheEntry\":[],\"DnsLookupDict\":[],\"Document\":[\"EventTarget\",\"Node\"],\"DocumentFragment\":[\"EventTarget\",\"Node\"],\"DocumentTimeline\":[\"AnimationTimeline\"],\"DocumentTimelineOptions\":[],\"DocumentType\":[\"EventTarget\",\"Node\"],\"DomError\":[],\"DomException\":[],\"DomImplementation\":[],\"DomMatrix\":[\"DomMatrixReadOnly\"],\"DomMatrix2dInit\":[],\"DomMatrixInit\":[],\"DomMatrixReadOnly\":[],\"DomParser\":[],\"DomPoint\":[\"DomPointReadOnly\"],\"DomPointInit\":[],\"DomPointReadOnly\":[],\"DomQuad\":[],\"DomQuadInit\":[],\"DomQuadJson\":[],\"DomRect\":[\"DomRectReadOnly\"],\"DomRectInit\":[],\"DomRectList\":[],\"DomRectReadOnly\":[],\"DomRequest\":[\"EventTarget\"],\"DomRequestReadyState\":[],\"DomStringList\":[],\"DomStringMap\":[],\"DomTokenList\":[],\"DomWindowResizeEventDetail\":[],\"DoubleRange\":[],\"DragEvent\":[\"Event\",\"MouseEvent\",\"UiEvent\"],\"DragEventInit\":[],\"DynamicsCompressorNode\":[\"AudioNode\",\"EventTarget\"],\"DynamicsCompressorOptions\":[],\"EcKeyAlgorithm\":[],\"EcKeyGenParams\":[],\"EcKeyImportParams\":[],\"EcdhKeyDeriveParams\":[],\"EcdsaParams\":[],\"EffectTiming\":[],\"Element\":[\"EventTarget\",\"Node\"],\"ElementCreationOptions\":[],\"ElementDefinitionOptions\":[],\"EncodedAudioChunk\":[],\"EncodedAudioChunkInit\":[],\"EncodedAudioChunkMetadata\":[],\"EncodedAudioChunkType\":[],\"EncodedVideoChunk\":[],\"EncodedVideoChunkInit\":[],\"EncodedVideoChunkMetadata\":[],\"EncodedVideoChunkType\":[],\"EndingTypes\":[],\"ErrorCallback\":[],\"ErrorEvent\":[\"Event\"],\"ErrorEventInit\":[],\"Event\":[],\"EventInit\":[],\"EventListener\":[],\"EventListenerOptions\":[],\"EventModifierInit\":[],\"EventSource\":[\"EventTarget\"],\"EventSourceInit\":[],\"EventTarget\":[],\"Exception\":[],\"ExtBlendMinmax\":[],\"ExtColorBufferFloat\":[],\"ExtColorBufferHalfFloat\":[],\"ExtDisjointTimerQuery\":[],\"ExtFragDepth\":[],\"ExtSRgb\":[],\"ExtShaderTextureLod\":[],\"ExtTextureFilterAnisotropic\":[],\"ExtTextureNorm16\":[],\"ExtendableCookieChangeEvent\":[\"Event\",\"ExtendableEvent\"],\"ExtendableCookieChangeEventInit\":[],\"ExtendableEvent\":[\"Event\"],\"ExtendableEventInit\":[],\"ExtendableMessageEvent\":[\"Event\",\"ExtendableEvent\"],\"ExtendableMessageEventInit\":[],\"External\":[],\"FakePluginMimeEntry\":[],\"FakePluginTagInit\":[],\"FetchEvent\":[\"Event\",\"ExtendableEvent\"],\"FetchEventInit\":[],\"FetchObserver\":[\"EventTarget\"],\"FetchReadableStreamReadDataArray\":[],\"FetchReadableStreamReadDataDone\":[],\"FetchState\":[],\"File\":[\"Blob\"],\"FileCallback\":[],\"FileList\":[],\"FilePickerAcceptType\":[],\"FilePickerOptions\":[],\"FilePropertyBag\":[],\"FileReader\":[\"EventTarget\"],\"FileReaderSync\":[],\"FileSystem\":[],\"FileSystemCreateWritableOptions\":[],\"FileSystemDirectoryEntry\":[\"FileSystemEntry\"],\"FileSystemDirectoryHandle\":[\"FileSystemHandle\"],\"FileSystemDirectoryReader\":[],\"FileSystemEntriesCallback\":[],\"FileSystemEntry\":[],\"FileSystemEntryCallback\":[],\"FileSystemFileEntry\":[\"FileSystemEntry\"],\"FileSystemFileHandle\":[\"FileSystemHandle\"],\"FileSystemFlags\":[],\"FileSystemGetDirectoryOptions\":[],\"FileSystemGetFileOptions\":[],\"FileSystemHandle\":[],\"FileSystemHandleKind\":[],\"FileSystemHandlePermissionDescriptor\":[],\"FileSystemPermissionDescriptor\":[],\"FileSystemPermissionMode\":[],\"FileSystemReadWriteOptions\":[],\"FileSystemRemoveOptions\":[],\"FileSystemSyncAccessHandle\":[],\"FileSystemWritableFileStream\":[\"WritableStream\"],\"FillMode\":[],\"FlashClassification\":[],\"FlowControlType\":[],\"FocusEvent\":[\"Event\",\"UiEvent\"],\"FocusEventInit\":[],\"FocusOptions\":[],\"FontData\":[],\"FontFace\":[],\"FontFaceDescriptors\":[],\"FontFaceLoadStatus\":[],\"FontFaceSet\":[\"EventTarget\"],\"FontFaceSetIterator\":[],\"FontFaceSetIteratorResult\":[],\"FontFaceSetLoadEvent\":[\"Event\"],\"FontFaceSetLoadEventInit\":[],\"FontFaceSetLoadStatus\":[],\"FormData\":[],\"FrameType\":[],\"FuzzingFunctions\":[],\"GainNode\":[\"AudioNode\",\"EventTarget\"],\"GainOptions\":[],\"Gamepad\":[],\"GamepadButton\":[],\"GamepadEffectParameters\":[],\"GamepadEvent\":[\"Event\"],\"GamepadEventInit\":[],\"GamepadHand\":[],\"GamepadHapticActuator\":[],\"GamepadHapticActuatorType\":[],\"GamepadHapticEffectType\":[],\"GamepadHapticsResult\":[],\"GamepadMappingType\":[],\"GamepadPose\":[],\"GamepadTouch\":[],\"Geolocation\":[],\"GestureEvent\":[\"Event\",\"UiEvent\"],\"GetAnimationsOptions\":[],\"GetRootNodeOptions\":[],\"GetUserMediaRequest\":[],\"Gpu\":[],\"GpuAdapter\":[],\"GpuAdapterInfo\":[],\"GpuAddressMode\":[],\"GpuAutoLayoutMode\":[],\"GpuBindGroup\":[],\"GpuBindGroupDescriptor\":[],\"GpuBindGroupEntry\":[],\"GpuBindGroupLayout\":[],\"GpuBindGroupLayoutDescriptor\":[],\"GpuBindGroupLayoutEntry\":[],\"GpuBlendComponent\":[],\"GpuBlendFactor\":[],\"GpuBlendOperation\":[],\"GpuBlendState\":[],\"GpuBuffer\":[],\"GpuBufferBinding\":[],\"GpuBufferBindingLayout\":[],\"GpuBufferBindingType\":[],\"GpuBufferDescriptor\":[],\"GpuBufferMapState\":[],\"GpuCanvasAlphaMode\":[],\"GpuCanvasConfiguration\":[],\"GpuCanvasContext\":[],\"GpuCanvasToneMapping\":[],\"GpuCanvasToneMappingMode\":[],\"GpuColorDict\":[],\"GpuColorTargetState\":[],\"GpuCommandBuffer\":[],\"GpuCommandBufferDescriptor\":[],\"GpuCommandEncoder\":[],\"GpuCommandEncoderDescriptor\":[],\"GpuCompareFunction\":[],\"GpuCompilationInfo\":[],\"GpuCompilationMessage\":[],\"GpuCompilationMessageType\":[],\"GpuComputePassDescriptor\":[],\"GpuComputePassEncoder\":[],\"GpuComputePassTimestampWrites\":[],\"GpuComputePipeline\":[],\"GpuComputePipelineDescriptor\":[],\"GpuCopyExternalImageDestInfo\":[],\"GpuCopyExternalImageSourceInfo\":[],\"GpuCullMode\":[],\"GpuDepthStencilState\":[],\"GpuDevice\":[\"EventTarget\"],\"GpuDeviceDescriptor\":[],\"GpuDeviceLostInfo\":[],\"GpuDeviceLostReason\":[],\"GpuError\":[],\"GpuErrorFilter\":[],\"GpuExtent3dDict\":[],\"GpuExternalTexture\":[],\"GpuExternalTextureBindingLayout\":[],\"GpuExternalTextureDescriptor\":[],\"GpuFeatureName\":[],\"GpuFilterMode\":[],\"GpuFragmentState\":[],\"GpuFrontFace\":[],\"GpuIndexFormat\":[],\"GpuInternalError\":[\"GpuError\"],\"GpuLoadOp\":[],\"GpuMipmapFilterMode\":[],\"GpuMultisampleState\":[],\"GpuObjectDescriptorBase\":[],\"GpuOrigin2dDict\":[],\"GpuOrigin3dDict\":[],\"GpuOutOfMemoryError\":[\"GpuError\"],\"GpuPipelineDescriptorBase\":[],\"GpuPipelineError\":[\"DomException\"],\"GpuPipelineErrorInit\":[],\"GpuPipelineErrorReason\":[],\"GpuPipelineLayout\":[],\"GpuPipelineLayoutDescriptor\":[],\"GpuPowerPreference\":[],\"GpuPrimitiveState\":[],\"GpuPrimitiveTopology\":[],\"GpuProgrammableStage\":[],\"GpuQuerySet\":[],\"GpuQuerySetDescriptor\":[],\"GpuQueryType\":[],\"GpuQueue\":[],\"GpuQueueDescriptor\":[],\"GpuRenderBundle\":[],\"GpuRenderBundleDescriptor\":[],\"GpuRenderBundleEncoder\":[],\"GpuRenderBundleEncoderDescriptor\":[],\"GpuRenderPassColorAttachment\":[],\"GpuRenderPassDepthStencilAttachment\":[],\"GpuRenderPassDescriptor\":[],\"GpuRenderPassEncoder\":[],\"GpuRenderPassLayout\":[],\"GpuRenderPassTimestampWrites\":[],\"GpuRenderPipeline\":[],\"GpuRenderPipelineDescriptor\":[],\"GpuRequestAdapterOptions\":[],\"GpuSampler\":[],\"GpuSamplerBindingLayout\":[],\"GpuSamplerBindingType\":[],\"GpuSamplerDescriptor\":[],\"GpuShaderModule\":[],\"GpuShaderModuleCompilationHint\":[],\"GpuShaderModuleDescriptor\":[],\"GpuStencilFaceState\":[],\"GpuStencilOperation\":[],\"GpuStorageTextureAccess\":[],\"GpuStorageTextureBindingLayout\":[],\"GpuStoreOp\":[],\"GpuSupportedFeatures\":[],\"GpuSupportedLimits\":[],\"GpuTexelCopyBufferInfo\":[],\"GpuTexelCopyBufferLayout\":[],\"GpuTexelCopyTextureInfo\":[],\"GpuTexture\":[],\"GpuTextureAspect\":[],\"GpuTextureBindingLayout\":[],\"GpuTextureDescriptor\":[],\"GpuTextureDimension\":[],\"GpuTextureFormat\":[],\"GpuTextureSampleType\":[],\"GpuTextureView\":[],\"GpuTextureViewDescriptor\":[],\"GpuTextureViewDimension\":[],\"GpuUncapturedErrorEvent\":[\"Event\"],\"GpuUncapturedErrorEventInit\":[],\"GpuValidationError\":[\"GpuError\"],\"GpuVertexAttribute\":[],\"GpuVertexBufferLayout\":[],\"GpuVertexFormat\":[],\"GpuVertexState\":[],\"GpuVertexStepMode\":[],\"GroupedHistoryEventInit\":[],\"HalfOpenInfoDict\":[],\"HardwareAcceleration\":[],\"HashChangeEvent\":[\"Event\"],\"HashChangeEventInit\":[],\"Headers\":[],\"HeadersGuardEnum\":[],\"Hid\":[\"EventTarget\"],\"HidCollectionInfo\":[],\"HidConnectionEvent\":[\"Event\"],\"HidConnectionEventInit\":[],\"HidDevice\":[\"EventTarget\"],\"HidDeviceFilter\":[],\"HidDeviceRequestOptions\":[],\"HidInputReportEvent\":[\"Event\"],\"HidInputReportEventInit\":[],\"HidReportInfo\":[],\"HidReportItem\":[],\"HidUnitSystem\":[],\"HiddenPluginEventInit\":[],\"History\":[],\"HitRegionOptions\":[],\"HkdfParams\":[],\"HmacDerivedKeyParams\":[],\"HmacImportParams\":[],\"HmacKeyAlgorithm\":[],\"HmacKeyGenParams\":[],\"HtmlAllCollection\":[],\"HtmlAnchorElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlAreaElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlAudioElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"HtmlMediaElement\",\"Node\"],\"HtmlBaseElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlBodyElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlBrElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlButtonElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlCanvasElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlCollection\":[],\"HtmlDListElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDataElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDataListElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDetailsElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDialogElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDirectoryElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDivElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDocument\":[\"Document\",\"EventTarget\",\"Node\"],\"HtmlElement\":[\"Element\",\"EventTarget\",\"Node\"],\"HtmlEmbedElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlFieldSetElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlFontElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlFormControlsCollection\":[\"HtmlCollection\"],\"HtmlFormElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlFrameElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlFrameSetElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlHeadElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlHeadingElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlHrElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlHtmlElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlIFrameElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlImageElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlInputElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlLabelElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlLegendElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlLiElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlLinkElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlMapElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlMediaElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlMenuElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlMenuItemElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlMetaElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlMeterElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlModElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlOListElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlObjectElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlOptGroupElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlOptionElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlOptionsCollection\":[\"HtmlCollection\"],\"HtmlOutputElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlParagraphElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlParamElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlPictureElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlPreElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlProgressElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlQuoteElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlScriptElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlSelectElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlSlotElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlSourceElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlSpanElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlStyleElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTableCaptionElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTableCellElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTableColElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTableElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTableRowElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTableSectionElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTemplateElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTextAreaElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTimeElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTitleElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTrackElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlUListElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlUnknownElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlVideoElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"HtmlMediaElement\",\"Node\"],\"HttpConnDict\":[],\"HttpConnInfo\":[],\"HttpConnectionElement\":[],\"IdbCursor\":[],\"IdbCursorDirection\":[],\"IdbCursorWithValue\":[\"IdbCursor\"],\"IdbDatabase\":[\"EventTarget\"],\"IdbFactory\":[],\"IdbFileHandle\":[\"EventTarget\"],\"IdbFileMetadataParameters\":[],\"IdbFileRequest\":[\"DomRequest\",\"EventTarget\"],\"IdbIndex\":[],\"IdbIndexParameters\":[],\"IdbKeyRange\":[],\"IdbLocaleAwareKeyRange\":[\"IdbKeyRange\"],\"IdbMutableFile\":[\"EventTarget\"],\"IdbObjectStore\":[],\"IdbObjectStoreParameters\":[],\"IdbOpenDbOptions\":[],\"IdbOpenDbRequest\":[\"EventTarget\",\"IdbRequest\"],\"IdbRequest\":[\"EventTarget\"],\"IdbRequestReadyState\":[],\"IdbTransaction\":[\"EventTarget\"],\"IdbTransactionDurability\":[],\"IdbTransactionMode\":[],\"IdbTransactionOptions\":[],\"IdbVersionChangeEvent\":[\"Event\"],\"IdbVersionChangeEventInit\":[],\"IdleDeadline\":[],\"IdleRequestOptions\":[],\"IirFilterNode\":[\"AudioNode\",\"EventTarget\"],\"IirFilterOptions\":[],\"ImageBitmap\":[],\"ImageBitmapOptions\":[],\"ImageBitmapRenderingContext\":[],\"ImageCapture\":[],\"ImageCaptureError\":[],\"ImageCaptureErrorEvent\":[\"Event\"],\"ImageCaptureErrorEventInit\":[],\"ImageData\":[],\"ImageDecodeOptions\":[],\"ImageDecodeResult\":[],\"ImageDecoder\":[],\"ImageDecoderInit\":[],\"ImageEncodeOptions\":[],\"ImageOrientation\":[],\"ImageTrack\":[\"EventTarget\"],\"ImageTrackList\":[],\"InputDeviceInfo\":[\"MediaDeviceInfo\"],\"InputEvent\":[\"Event\",\"UiEvent\"],\"InputEventInit\":[],\"IntersectionObserver\":[],\"IntersectionObserverEntry\":[],\"IntersectionObserverEntryInit\":[],\"IntersectionObserverInit\":[],\"IntlUtils\":[],\"IsInputPendingOptions\":[],\"IterableKeyAndValueResult\":[],\"IterableKeyOrValueResult\":[],\"IterationCompositeOperation\":[],\"JsonWebKey\":[],\"KeyAlgorithm\":[],\"KeyEvent\":[],\"KeyFrameRequestEvent\":[\"Event\"],\"KeyIdsInitData\":[],\"KeyboardEvent\":[\"Event\",\"UiEvent\"],\"KeyboardEventInit\":[],\"KeyframeAnimationOptions\":[],\"KeyframeEffect\":[\"AnimationEffect\"],\"KeyframeEffectOptions\":[],\"L10nElement\":[],\"L10nValue\":[],\"LargeBlobSupport\":[],\"LatencyMode\":[],\"LifecycleCallbacks\":[],\"LineAlignSetting\":[],\"ListBoxObject\":[],\"LocalMediaStream\":[\"EventTarget\",\"MediaStream\"],\"LocaleInfo\":[],\"Location\":[],\"Lock\":[],\"LockInfo\":[],\"LockManager\":[],\"LockManagerSnapshot\":[],\"LockMode\":[],\"LockOptions\":[],\"MathMlElement\":[\"Element\",\"EventTarget\",\"Node\"],\"MediaCapabilities\":[],\"MediaCapabilitiesInfo\":[],\"MediaConfiguration\":[],\"MediaDecodingConfiguration\":[],\"MediaDecodingType\":[],\"MediaDeviceInfo\":[],\"MediaDeviceKind\":[],\"MediaDevices\":[\"EventTarget\"],\"MediaElementAudioSourceNode\":[\"AudioNode\",\"EventTarget\"],\"MediaElementAudioSourceOptions\":[],\"MediaEncodingConfiguration\":[],\"MediaEncodingType\":[],\"MediaEncryptedEvent\":[\"Event\"],\"MediaError\":[],\"MediaImage\":[],\"MediaKeyError\":[\"Event\"],\"MediaKeyMessageEvent\":[\"Event\"],\"MediaKeyMessageEventInit\":[],\"MediaKeyMessageType\":[],\"MediaKeyNeededEventInit\":[],\"MediaKeySession\":[\"EventTarget\"],\"MediaKeySessionType\":[],\"MediaKeyStatus\":[],\"MediaKeyStatusMap\":[],\"MediaKeySystemAccess\":[],\"MediaKeySystemConfiguration\":[],\"MediaKeySystemMediaCapability\":[],\"MediaKeySystemStatus\":[],\"MediaKeys\":[],\"MediaKeysPolicy\":[],\"MediaKeysRequirement\":[],\"MediaList\":[],\"MediaMetadata\":[],\"MediaMetadataInit\":[],\"MediaPositionState\":[],\"MediaQueryList\":[\"EventTarget\"],\"MediaQueryListEvent\":[\"Event\"],\"MediaQueryListEventInit\":[],\"MediaRecorder\":[\"EventTarget\"],\"MediaRecorderErrorEvent\":[\"Event\"],\"MediaRecorderErrorEventInit\":[],\"MediaRecorderOptions\":[],\"MediaSession\":[],\"MediaSessionAction\":[],\"MediaSessionActionDetails\":[],\"MediaSessionPlaybackState\":[],\"MediaSource\":[\"EventTarget\"],\"MediaSourceEndOfStreamError\":[],\"MediaSourceEnum\":[],\"MediaSourceReadyState\":[],\"MediaStream\":[\"EventTarget\"],\"MediaStreamAudioDestinationNode\":[\"AudioNode\",\"EventTarget\"],\"MediaStreamAudioSourceNode\":[\"AudioNode\",\"EventTarget\"],\"MediaStreamAudioSourceOptions\":[],\"MediaStreamConstraints\":[],\"MediaStreamError\":[],\"MediaStreamEvent\":[\"Event\"],\"MediaStreamEventInit\":[],\"MediaStreamTrack\":[\"EventTarget\"],\"MediaStreamTrackEvent\":[\"Event\"],\"MediaStreamTrackEventInit\":[],\"MediaStreamTrackGenerator\":[\"EventTarget\",\"MediaStreamTrack\"],\"MediaStreamTrackGeneratorInit\":[],\"MediaStreamTrackProcessor\":[],\"MediaStreamTrackProcessorInit\":[],\"MediaStreamTrackState\":[],\"MediaTrackCapabilities\":[],\"MediaTrackConstraintSet\":[],\"MediaTrackConstraints\":[],\"MediaTrackSettings\":[],\"MediaTrackSupportedConstraints\":[],\"MemoryAttribution\":[],\"MemoryAttributionContainer\":[],\"MemoryBreakdownEntry\":[],\"MemoryMeasurement\":[],\"MessageChannel\":[],\"MessageEvent\":[\"Event\"],\"MessageEventInit\":[],\"MessagePort\":[\"EventTarget\"],\"MidiAccess\":[\"EventTarget\"],\"MidiConnectionEvent\":[\"Event\"],\"MidiConnectionEventInit\":[],\"MidiInput\":[\"EventTarget\",\"MidiPort\"],\"MidiInputMap\":[],\"MidiMessageEvent\":[\"Event\"],\"MidiMessageEventInit\":[],\"MidiOptions\":[],\"MidiOutput\":[\"EventTarget\",\"MidiPort\"],\"MidiOutputMap\":[],\"MidiPort\":[\"EventTarget\"],\"MidiPortConnectionState\":[],\"MidiPortDeviceState\":[],\"MidiPortType\":[],\"MimeType\":[],\"MimeTypeArray\":[],\"MouseEvent\":[\"Event\",\"UiEvent\"],\"MouseEventInit\":[],\"MouseScrollEvent\":[\"Event\",\"MouseEvent\",\"UiEvent\"],\"MozDebug\":[],\"MutationEvent\":[\"Event\"],\"MutationObserver\":[],\"MutationObserverInit\":[],\"MutationObservingInfo\":[],\"MutationRecord\":[],\"NamedNodeMap\":[],\"NativeOsFileReadOptions\":[],\"NativeOsFileWriteAtomicOptions\":[],\"NavigationType\":[],\"Navigator\":[],\"NavigatorAutomationInformation\":[],\"NavigatorUaBrandVersion\":[],\"NavigatorUaData\":[],\"NetworkCommandOptions\":[],\"NetworkInformation\":[\"EventTarget\"],\"NetworkResultOptions\":[],\"Node\":[\"EventTarget\"],\"NodeFilter\":[],\"NodeIterator\":[],\"NodeList\":[],\"Notification\":[\"EventTarget\"],\"NotificationAction\":[],\"NotificationDirection\":[],\"NotificationEvent\":[\"Event\",\"ExtendableEvent\"],\"NotificationEventInit\":[],\"NotificationOptions\":[],\"NotificationPermission\":[],\"ObserverCallback\":[],\"OesElementIndexUint\":[],\"OesStandardDerivatives\":[],\"OesTextureFloat\":[],\"OesTextureFloatLinear\":[],\"OesTextureHalfFloat\":[],\"OesTextureHalfFloatLinear\":[],\"OesVertexArrayObject\":[],\"OfflineAudioCompletionEvent\":[\"Event\"],\"OfflineAudioCompletionEventInit\":[],\"OfflineAudioContext\":[\"BaseAudioContext\",\"EventTarget\"],\"OfflineAudioContextOptions\":[],\"OfflineResourceList\":[\"EventTarget\"],\"OffscreenCanvas\":[\"EventTarget\"],\"OffscreenCanvasRenderingContext2d\":[],\"OpenFilePickerOptions\":[],\"OpenWindowEventDetail\":[],\"OptionalEffectTiming\":[],\"OrientationLockType\":[],\"OrientationType\":[],\"OscillatorNode\":[\"AudioNode\",\"AudioScheduledSourceNode\",\"EventTarget\"],\"OscillatorOptions\":[],\"OscillatorType\":[],\"OverSampleType\":[],\"OvrMultiview2\":[],\"PageTransitionEvent\":[\"Event\"],\"PageTransitionEventInit\":[],\"PaintRequest\":[],\"PaintRequestList\":[],\"PaintWorkletGlobalScope\":[\"WorkletGlobalScope\"],\"PannerNode\":[\"AudioNode\",\"EventTarget\"],\"PannerOptions\":[],\"PanningModelType\":[],\"ParityType\":[],\"Path2d\":[],\"PaymentAddress\":[],\"PaymentComplete\":[],\"PaymentMethodChangeEvent\":[\"Event\",\"PaymentRequestUpdateEvent\"],\"PaymentMethodChangeEventInit\":[],\"PaymentRequestUpdateEvent\":[\"Event\"],\"PaymentRequestUpdateEventInit\":[],\"PaymentResponse\":[],\"Pbkdf2Params\":[],\"PcImplIceConnectionState\":[],\"PcImplIceGatheringState\":[],\"PcImplSignalingState\":[],\"PcObserverStateType\":[],\"Performance\":[\"EventTarget\"],\"PerformanceEntry\":[],\"PerformanceEntryEventInit\":[],\"PerformanceEntryFilterOptions\":[],\"PerformanceMark\":[\"PerformanceEntry\"],\"PerformanceMeasure\":[\"PerformanceEntry\"],\"PerformanceNavigation\":[],\"PerformanceNavigationTiming\":[\"PerformanceEntry\",\"PerformanceResourceTiming\"],\"PerformanceObserver\":[],\"PerformanceObserverEntryList\":[],\"PerformanceObserverInit\":[],\"PerformanceResourceTiming\":[\"PerformanceEntry\"],\"PerformanceServerTiming\":[],\"PerformanceTiming\":[],\"PeriodicWave\":[],\"PeriodicWaveConstraints\":[],\"PeriodicWaveOptions\":[],\"PermissionDescriptor\":[],\"PermissionName\":[],\"PermissionState\":[],\"PermissionStatus\":[\"EventTarget\"],\"Permissions\":[],\"PictureInPictureEvent\":[\"Event\"],\"PictureInPictureEventInit\":[],\"PictureInPictureWindow\":[\"EventTarget\"],\"PlaneLayout\":[],\"PlaybackDirection\":[],\"Plugin\":[],\"PluginArray\":[],\"PluginCrashedEventInit\":[],\"PointerEvent\":[\"Event\",\"MouseEvent\",\"UiEvent\"],\"PointerEventInit\":[],\"PopStateEvent\":[\"Event\"],\"PopStateEventInit\":[],\"PopupBlockedEvent\":[\"Event\"],\"PopupBlockedEventInit\":[],\"Position\":[],\"PositionAlignSetting\":[],\"PositionError\":[],\"PositionOptions\":[],\"PremultiplyAlpha\":[],\"Presentation\":[],\"PresentationAvailability\":[\"EventTarget\"],\"PresentationConnection\":[\"EventTarget\"],\"PresentationConnectionAvailableEvent\":[\"Event\"],\"PresentationConnectionAvailableEventInit\":[],\"PresentationConnectionBinaryType\":[],\"PresentationConnectionCloseEvent\":[\"Event\"],\"PresentationConnectionCloseEventInit\":[],\"PresentationConnectionClosedReason\":[],\"PresentationConnectionList\":[\"EventTarget\"],\"PresentationConnectionState\":[],\"PresentationReceiver\":[],\"PresentationRequest\":[\"EventTarget\"],\"PresentationStyle\":[],\"ProcessingInstruction\":[\"CharacterData\",\"EventTarget\",\"Node\"],\"ProfileTimelineLayerRect\":[],\"ProfileTimelineMarker\":[],\"ProfileTimelineMessagePortOperationType\":[],\"ProfileTimelineStackFrame\":[],\"ProfileTimelineWorkerOperationType\":[],\"ProgressEvent\":[\"Event\"],\"ProgressEventInit\":[],\"PromiseNativeHandler\":[],\"PromiseRejectionEvent\":[\"Event\"],\"PromiseRejectionEventInit\":[],\"PublicKeyCredential\":[\"Credential\"],\"PublicKeyCredentialCreationOptions\":[],\"PublicKeyCredentialCreationOptionsJson\":[],\"PublicKeyCredentialDescriptor\":[],\"PublicKeyCredentialDescriptorJson\":[],\"PublicKeyCredentialEntity\":[],\"PublicKeyCredentialHints\":[],\"PublicKeyCredentialParameters\":[],\"PublicKeyCredentialRequestOptions\":[],\"PublicKeyCredentialRequestOptionsJson\":[],\"PublicKeyCredentialRpEntity\":[],\"PublicKeyCredentialType\":[],\"PublicKeyCredentialUserEntity\":[],\"PublicKeyCredentialUserEntityJson\":[],\"PushEncryptionKeyName\":[],\"PushEvent\":[\"Event\",\"ExtendableEvent\"],\"PushEventInit\":[],\"PushManager\":[],\"PushMessageData\":[],\"PushPermissionState\":[],\"PushSubscription\":[],\"PushSubscriptionInit\":[],\"PushSubscriptionJson\":[],\"PushSubscriptionKeys\":[],\"PushSubscriptionOptions\":[],\"PushSubscriptionOptionsInit\":[],\"QueryOptions\":[],\"QueuingStrategy\":[],\"QueuingStrategyInit\":[],\"RadioNodeList\":[\"NodeList\"],\"Range\":[],\"RcwnPerfStats\":[],\"RcwnStatus\":[],\"ReadableByteStreamController\":[],\"ReadableStream\":[],\"ReadableStreamByobReader\":[],\"ReadableStreamByobRequest\":[],\"ReadableStreamDefaultController\":[],\"ReadableStreamDefaultReader\":[],\"ReadableStreamGetReaderOptions\":[],\"ReadableStreamIteratorOptions\":[],\"ReadableStreamReadResult\":[],\"ReadableStreamReaderMode\":[],\"ReadableStreamType\":[],\"ReadableWritablePair\":[],\"RecordingState\":[],\"ReferrerPolicy\":[],\"RegisterRequest\":[],\"RegisterResponse\":[],\"RegisteredKey\":[],\"RegistrationOptions\":[],\"RegistrationResponseJson\":[],\"Request\":[],\"RequestCache\":[],\"RequestCredentials\":[],\"RequestDestination\":[],\"RequestDeviceOptions\":[],\"RequestInit\":[],\"RequestMediaKeySystemAccessNotification\":[],\"RequestMode\":[],\"RequestRedirect\":[],\"ResidentKeyRequirement\":[],\"ResizeObserver\":[],\"ResizeObserverBoxOptions\":[],\"ResizeObserverEntry\":[],\"ResizeObserverOptions\":[],\"ResizeObserverSize\":[],\"ResizeQuality\":[],\"Response\":[],\"ResponseInit\":[],\"ResponseType\":[],\"RsaHashedImportParams\":[],\"RsaOaepParams\":[],\"RsaOtherPrimesInfo\":[],\"RsaPssParams\":[],\"RtcAnswerOptions\":[],\"RtcBundlePolicy\":[],\"RtcCertificate\":[],\"RtcCertificateExpiration\":[],\"RtcCodecStats\":[],\"RtcConfiguration\":[],\"RtcDataChannel\":[\"EventTarget\"],\"RtcDataChannelEvent\":[\"Event\"],\"RtcDataChannelEventInit\":[],\"RtcDataChannelInit\":[],\"RtcDataChannelState\":[],\"RtcDataChannelType\":[],\"RtcDegradationPreference\":[],\"RtcEncodedAudioFrame\":[],\"RtcEncodedAudioFrameMetadata\":[],\"RtcEncodedAudioFrameOptions\":[],\"RtcEncodedVideoFrame\":[],\"RtcEncodedVideoFrameMetadata\":[],\"RtcEncodedVideoFrameOptions\":[],\"RtcEncodedVideoFrameType\":[],\"RtcFecParameters\":[],\"RtcIceCandidate\":[],\"RtcIceCandidateInit\":[],\"RtcIceCandidatePairStats\":[],\"RtcIceCandidateStats\":[],\"RtcIceComponentStats\":[],\"RtcIceConnectionState\":[],\"RtcIceCredentialType\":[],\"RtcIceGatheringState\":[],\"RtcIceServer\":[],\"RtcIceTransportPolicy\":[],\"RtcIdentityAssertion\":[],\"RtcIdentityAssertionResult\":[],\"RtcIdentityProvider\":[],\"RtcIdentityProviderDetails\":[],\"RtcIdentityProviderOptions\":[],\"RtcIdentityProviderRegistrar\":[],\"RtcIdentityValidationResult\":[],\"RtcInboundRtpStreamStats\":[],\"RtcMediaStreamStats\":[],\"RtcMediaStreamTrackStats\":[],\"RtcOfferAnswerOptions\":[],\"RtcOfferOptions\":[],\"RtcOutboundRtpStreamStats\":[],\"RtcPeerConnection\":[\"EventTarget\"],\"RtcPeerConnectionIceErrorEvent\":[\"Event\"],\"RtcPeerConnectionIceEvent\":[\"Event\"],\"RtcPeerConnectionIceEventInit\":[],\"RtcPeerConnectionState\":[],\"RtcPriorityType\":[],\"RtcRtcpParameters\":[],\"RtcRtpCapabilities\":[],\"RtcRtpCodecCapability\":[],\"RtcRtpCodecParameters\":[],\"RtcRtpContributingSource\":[],\"RtcRtpEncodingParameters\":[],\"RtcRtpHeaderExtensionCapability\":[],\"RtcRtpHeaderExtensionParameters\":[],\"RtcRtpParameters\":[],\"RtcRtpReceiver\":[],\"RtcRtpScriptTransform\":[],\"RtcRtpScriptTransformer\":[\"EventTarget\"],\"RtcRtpSender\":[],\"RtcRtpSourceEntry\":[],\"RtcRtpSourceEntryType\":[],\"RtcRtpSynchronizationSource\":[],\"RtcRtpTransceiver\":[],\"RtcRtpTransceiverDirection\":[],\"RtcRtpTransceiverInit\":[],\"RtcRtxParameters\":[],\"RtcSdpType\":[],\"RtcSessionDescription\":[],\"RtcSessionDescriptionInit\":[],\"RtcSignalingState\":[],\"RtcStats\":[],\"RtcStatsIceCandidatePairState\":[],\"RtcStatsIceCandidateType\":[],\"RtcStatsReport\":[],\"RtcStatsReportInternal\":[],\"RtcStatsType\":[],\"RtcTrackEvent\":[\"Event\"],\"RtcTrackEventInit\":[],\"RtcTransformEvent\":[\"Event\"],\"RtcTransportStats\":[],\"RtcdtmfSender\":[\"EventTarget\"],\"RtcdtmfToneChangeEvent\":[\"Event\"],\"RtcdtmfToneChangeEventInit\":[],\"RtcrtpContributingSourceStats\":[],\"RtcrtpStreamStats\":[],\"SFrameTransform\":[\"EventTarget\"],\"SFrameTransformErrorEvent\":[\"Event\"],\"SFrameTransformErrorEventInit\":[],\"SFrameTransformErrorEventType\":[],\"SFrameTransformOptions\":[],\"SFrameTransformRole\":[],\"SaveFilePickerOptions\":[],\"Scheduler\":[],\"SchedulerPostTaskOptions\":[],\"Scheduling\":[],\"Screen\":[\"EventTarget\"],\"ScreenColorGamut\":[],\"ScreenDetailed\":[\"EventTarget\",\"Screen\"],\"ScreenDetails\":[\"EventTarget\"],\"ScreenLuminance\":[],\"ScreenOrientation\":[\"EventTarget\"],\"ScriptProcessorNode\":[\"AudioNode\",\"EventTarget\"],\"ScrollAreaEvent\":[\"Event\",\"UiEvent\"],\"ScrollBehavior\":[],\"ScrollBoxObject\":[],\"ScrollIntoViewContainer\":[],\"ScrollIntoViewOptions\":[],\"ScrollLogicalPosition\":[],\"ScrollOptions\":[],\"ScrollRestoration\":[],\"ScrollSetting\":[],\"ScrollState\":[],\"ScrollToOptions\":[],\"ScrollViewChangeEventInit\":[],\"SecurityPolicyViolationEvent\":[\"Event\"],\"SecurityPolicyViolationEventDisposition\":[],\"SecurityPolicyViolationEventInit\":[],\"Selection\":[],\"SelectionMode\":[],\"Serial\":[\"EventTarget\"],\"SerialInputSignals\":[],\"SerialOptions\":[],\"SerialOutputSignals\":[],\"SerialPort\":[\"EventTarget\"],\"SerialPortFilter\":[],\"SerialPortInfo\":[],\"SerialPortRequestOptions\":[],\"ServerSocketOptions\":[],\"ServiceWorker\":[\"EventTarget\"],\"ServiceWorkerContainer\":[\"EventTarget\"],\"ServiceWorkerGlobalScope\":[\"EventTarget\",\"WorkerGlobalScope\"],\"ServiceWorkerRegistration\":[\"EventTarget\"],\"ServiceWorkerState\":[],\"ServiceWorkerUpdateViaCache\":[],\"ShadowRoot\":[\"DocumentFragment\",\"EventTarget\",\"Node\"],\"ShadowRootInit\":[],\"ShadowRootMode\":[],\"ShareData\":[],\"SharedWorker\":[\"EventTarget\"],\"SharedWorkerGlobalScope\":[\"EventTarget\",\"WorkerGlobalScope\"],\"SignResponse\":[],\"SocketElement\":[],\"SocketOptions\":[],\"SocketReadyState\":[],\"SocketsDict\":[],\"SourceBuffer\":[\"EventTarget\"],\"SourceBufferAppendMode\":[],\"SourceBufferList\":[\"EventTarget\"],\"SpeechGrammar\":[],\"SpeechGrammarList\":[],\"SpeechRecognition\":[\"EventTarget\"],\"SpeechRecognitionAlternative\":[],\"SpeechRecognitionError\":[\"Event\"],\"SpeechRecognitionErrorCode\":[],\"SpeechRecognitionErrorInit\":[],\"SpeechRecognitionEvent\":[\"Event\"],\"SpeechRecognitionEventInit\":[],\"SpeechRecognitionResult\":[],\"SpeechRecognitionResultList\":[],\"SpeechSynthesis\":[\"EventTarget\"],\"SpeechSynthesisErrorCode\":[],\"SpeechSynthesisErrorEvent\":[\"Event\",\"SpeechSynthesisEvent\"],\"SpeechSynthesisErrorEventInit\":[],\"SpeechSynthesisEvent\":[\"Event\"],\"SpeechSynthesisEventInit\":[],\"SpeechSynthesisUtterance\":[\"EventTarget\"],\"SpeechSynthesisVoice\":[],\"StereoPannerNode\":[\"AudioNode\",\"EventTarget\"],\"StereoPannerOptions\":[],\"Storage\":[],\"StorageEstimate\":[],\"StorageEvent\":[\"Event\"],\"StorageEventInit\":[],\"StorageManager\":[],\"StorageType\":[],\"StreamPipeOptions\":[],\"StyleRuleChangeEventInit\":[],\"StyleSheet\":[],\"StyleSheetApplicableStateChangeEventInit\":[],\"StyleSheetChangeEventInit\":[],\"StyleSheetList\":[],\"SubmitEvent\":[\"Event\"],\"SubmitEventInit\":[],\"SubtleCrypto\":[],\"SupportedType\":[],\"SvcOutputMetadata\":[],\"SvgAngle\":[],\"SvgAnimateElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgAnimationElement\",\"SvgElement\"],\"SvgAnimateMotionElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgAnimationElement\",\"SvgElement\"],\"SvgAnimateTransformElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgAnimationElement\",\"SvgElement\"],\"SvgAnimatedAngle\":[],\"SvgAnimatedBoolean\":[],\"SvgAnimatedEnumeration\":[],\"SvgAnimatedInteger\":[],\"SvgAnimatedLength\":[],\"SvgAnimatedLengthList\":[],\"SvgAnimatedNumber\":[],\"SvgAnimatedNumberList\":[],\"SvgAnimatedPreserveAspectRatio\":[],\"SvgAnimatedRect\":[],\"SvgAnimatedString\":[],\"SvgAnimatedTransformList\":[],\"SvgAnimationElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgBoundingBoxOptions\":[],\"SvgCircleElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgClipPathElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgComponentTransferFunctionElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgDefsElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgDescElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgElement\":[\"Element\",\"EventTarget\",\"Node\"],\"SvgEllipseElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgFilterElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgForeignObjectElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgGeometryElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgGradientElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgGraphicsElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgImageElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgLength\":[],\"SvgLengthList\":[],\"SvgLineElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgLinearGradientElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGradientElement\"],\"SvgMarkerElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgMaskElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgMatrix\":[],\"SvgMetadataElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgNumber\":[],\"SvgNumberList\":[],\"SvgPathElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgPathSeg\":[],\"SvgPathSegArcAbs\":[\"SvgPathSeg\"],\"SvgPathSegArcRel\":[\"SvgPathSeg\"],\"SvgPathSegClosePath\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoCubicAbs\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoCubicRel\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoCubicSmoothAbs\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoCubicSmoothRel\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoQuadraticAbs\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoQuadraticRel\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoQuadraticSmoothAbs\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoQuadraticSmoothRel\":[\"SvgPathSeg\"],\"SvgPathSegLinetoAbs\":[\"SvgPathSeg\"],\"SvgPathSegLinetoHorizontalAbs\":[\"SvgPathSeg\"],\"SvgPathSegLinetoHorizontalRel\":[\"SvgPathSeg\"],\"SvgPathSegLinetoRel\":[\"SvgPathSeg\"],\"SvgPathSegLinetoVerticalAbs\":[\"SvgPathSeg\"],\"SvgPathSegLinetoVerticalRel\":[\"SvgPathSeg\"],\"SvgPathSegList\":[],\"SvgPathSegMovetoAbs\":[\"SvgPathSeg\"],\"SvgPathSegMovetoRel\":[\"SvgPathSeg\"],\"SvgPatternElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgPoint\":[],\"SvgPointList\":[],\"SvgPolygonElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgPolylineElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgPreserveAspectRatio\":[],\"SvgRadialGradientElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGradientElement\"],\"SvgRect\":[],\"SvgRectElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgScriptElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgSetElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgAnimationElement\",\"SvgElement\"],\"SvgStopElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgStringList\":[],\"SvgStyleElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgSwitchElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgSymbolElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgTextContentElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgTextElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\",\"SvgTextContentElement\",\"SvgTextPositioningElement\"],\"SvgTextPathElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\",\"SvgTextContentElement\"],\"SvgTextPositioningElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\",\"SvgTextContentElement\"],\"SvgTitleElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgTransform\":[],\"SvgTransformList\":[],\"SvgUnitTypes\":[],\"SvgUseElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgViewElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgZoomAndPan\":[],\"SvgaElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgfeBlendElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeColorMatrixElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeComponentTransferElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeCompositeElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeConvolveMatrixElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeDiffuseLightingElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeDisplacementMapElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeDistantLightElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeDropShadowElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeFloodElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeFuncAElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgComponentTransferFunctionElement\",\"SvgElement\"],\"SvgfeFuncBElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgComponentTransferFunctionElement\",\"SvgElement\"],\"SvgfeFuncGElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgComponentTransferFunctionElement\",\"SvgElement\"],\"SvgfeFuncRElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgComponentTransferFunctionElement\",\"SvgElement\"],\"SvgfeGaussianBlurElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeImageElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeMergeElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeMergeNodeElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeMorphologyElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeOffsetElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfePointLightElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeSpecularLightingElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeSpotLightElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeTileElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeTurbulenceElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvggElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgmPathElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgsvgElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgtSpanElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\",\"SvgTextContentElement\",\"SvgTextPositioningElement\"],\"TaskController\":[\"AbortController\"],\"TaskControllerInit\":[],\"TaskPriority\":[],\"TaskPriorityChangeEvent\":[\"Event\"],\"TaskPriorityChangeEventInit\":[],\"TaskSignal\":[\"AbortSignal\",\"EventTarget\"],\"TaskSignalAnyInit\":[],\"TcpReadyState\":[],\"TcpServerSocket\":[\"EventTarget\"],\"TcpServerSocketEvent\":[\"Event\"],\"TcpServerSocketEventInit\":[],\"TcpSocket\":[\"EventTarget\"],\"TcpSocketBinaryType\":[],\"TcpSocketErrorEvent\":[\"Event\"],\"TcpSocketErrorEventInit\":[],\"TcpSocketEvent\":[\"Event\"],\"TcpSocketEventInit\":[],\"Text\":[\"CharacterData\",\"EventTarget\",\"Node\"],\"TextDecodeOptions\":[],\"TextDecoder\":[],\"TextDecoderOptions\":[],\"TextEncoder\":[],\"TextMetrics\":[],\"TextTrack\":[\"EventTarget\"],\"TextTrackCue\":[\"EventTarget\"],\"TextTrackCueList\":[],\"TextTrackKind\":[],\"TextTrackList\":[\"EventTarget\"],\"TextTrackMode\":[],\"TimeEvent\":[\"Event\"],\"TimeRanges\":[],\"ToggleEvent\":[\"Event\"],\"ToggleEventInit\":[],\"TokenBinding\":[],\"TokenBindingStatus\":[],\"Touch\":[],\"TouchEvent\":[\"Event\",\"UiEvent\"],\"TouchEventInit\":[],\"TouchInit\":[],\"TouchList\":[],\"TrackEvent\":[\"Event\"],\"TrackEventInit\":[],\"TransformStream\":[],\"TransformStreamDefaultController\":[],\"Transformer\":[],\"TransitionEvent\":[\"Event\"],\"TransitionEventInit\":[],\"Transport\":[],\"TreeBoxObject\":[],\"TreeCellInfo\":[],\"TreeView\":[],\"TreeWalker\":[],\"U2f\":[],\"U2fClientData\":[],\"ULongRange\":[],\"UaDataValues\":[],\"UaLowEntropyJson\":[],\"UdpMessageEventInit\":[],\"UdpOptions\":[],\"UiEvent\":[\"Event\"],\"UiEventInit\":[],\"UnderlyingSink\":[],\"UnderlyingSource\":[],\"Url\":[],\"UrlSearchParams\":[],\"Usb\":[\"EventTarget\"],\"UsbAlternateInterface\":[],\"UsbConfiguration\":[],\"UsbConnectionEvent\":[\"Event\"],\"UsbConnectionEventInit\":[],\"UsbControlTransferParameters\":[],\"UsbDevice\":[],\"UsbDeviceFilter\":[],\"UsbDeviceRequestOptions\":[],\"UsbDirection\":[],\"UsbEndpoint\":[],\"UsbEndpointType\":[],\"UsbInTransferResult\":[],\"UsbInterface\":[],\"UsbIsochronousInTransferPacket\":[],\"UsbIsochronousInTransferResult\":[],\"UsbIsochronousOutTransferPacket\":[],\"UsbIsochronousOutTransferResult\":[],\"UsbOutTransferResult\":[],\"UsbPermissionDescriptor\":[],\"UsbPermissionResult\":[\"EventTarget\",\"PermissionStatus\"],\"UsbPermissionStorage\":[],\"UsbRecipient\":[],\"UsbRequestType\":[],\"UsbTransferStatus\":[],\"UserActivation\":[],\"UserProximityEvent\":[\"Event\"],\"UserProximityEventInit\":[],\"UserVerificationRequirement\":[],\"ValidityState\":[],\"ValueEvent\":[\"Event\"],\"ValueEventInit\":[],\"VideoColorPrimaries\":[],\"VideoColorSpace\":[],\"VideoColorSpaceInit\":[],\"VideoConfiguration\":[],\"VideoDecoder\":[],\"VideoDecoderConfig\":[],\"VideoDecoderInit\":[],\"VideoDecoderSupport\":[],\"VideoEncoder\":[],\"VideoEncoderConfig\":[],\"VideoEncoderEncodeOptions\":[],\"VideoEncoderInit\":[],\"VideoEncoderSupport\":[],\"VideoFacingModeEnum\":[],\"VideoFrame\":[],\"VideoFrameBufferInit\":[],\"VideoFrameCopyToOptions\":[],\"VideoFrameInit\":[],\"VideoMatrixCoefficients\":[],\"VideoPixelFormat\":[],\"VideoPlaybackQuality\":[],\"VideoStreamTrack\":[\"EventTarget\",\"MediaStreamTrack\"],\"VideoTrack\":[],\"VideoTrackList\":[\"EventTarget\"],\"VideoTransferCharacteristics\":[],\"ViewTransition\":[],\"VisibilityState\":[],\"VisualViewport\":[\"EventTarget\"],\"VoidCallback\":[],\"VrDisplay\":[\"EventTarget\"],\"VrDisplayCapabilities\":[],\"VrEye\":[],\"VrEyeParameters\":[],\"VrFieldOfView\":[],\"VrFrameData\":[],\"VrLayer\":[],\"VrMockController\":[],\"VrMockDisplay\":[],\"VrPose\":[],\"VrServiceTest\":[],\"VrStageParameters\":[],\"VrSubmitFrameResult\":[],\"VttCue\":[\"EventTarget\",\"TextTrackCue\"],\"VttRegion\":[],\"WakeLock\":[],\"WakeLockSentinel\":[\"EventTarget\"],\"WakeLockType\":[],\"WatchAdvertisementsOptions\":[],\"WaveShaperNode\":[\"AudioNode\",\"EventTarget\"],\"WaveShaperOptions\":[],\"WebGl2RenderingContext\":[],\"WebGlActiveInfo\":[],\"WebGlBuffer\":[],\"WebGlContextAttributes\":[],\"WebGlContextEvent\":[\"Event\"],\"WebGlContextEventInit\":[],\"WebGlFramebuffer\":[],\"WebGlPowerPreference\":[],\"WebGlProgram\":[],\"WebGlQuery\":[],\"WebGlRenderbuffer\":[],\"WebGlRenderingContext\":[],\"WebGlSampler\":[],\"WebGlShader\":[],\"WebGlShaderPrecisionFormat\":[],\"WebGlSync\":[],\"WebGlTexture\":[],\"WebGlTransformFeedback\":[],\"WebGlUniformLocation\":[],\"WebGlVertexArrayObject\":[],\"WebKitCssMatrix\":[\"DomMatrix\",\"DomMatrixReadOnly\"],\"WebSocket\":[\"EventTarget\"],\"WebSocketDict\":[],\"WebSocketElement\":[],\"WebTransport\":[],\"WebTransportBidirectionalStream\":[],\"WebTransportCloseInfo\":[],\"WebTransportCongestionControl\":[],\"WebTransportDatagramDuplexStream\":[],\"WebTransportDatagramStats\":[],\"WebTransportError\":[\"DomException\"],\"WebTransportErrorOptions\":[],\"WebTransportErrorSource\":[],\"WebTransportHash\":[],\"WebTransportOptions\":[],\"WebTransportReceiveStream\":[\"ReadableStream\"],\"WebTransportReceiveStreamStats\":[],\"WebTransportReliabilityMode\":[],\"WebTransportSendStream\":[\"WritableStream\"],\"WebTransportSendStreamOptions\":[],\"WebTransportSendStreamStats\":[],\"WebTransportStats\":[],\"WebglColorBufferFloat\":[],\"WebglCompressedTextureAstc\":[],\"WebglCompressedTextureAtc\":[],\"WebglCompressedTextureEtc\":[],\"WebglCompressedTextureEtc1\":[],\"WebglCompressedTexturePvrtc\":[],\"WebglCompressedTextureS3tc\":[],\"WebglCompressedTextureS3tcSrgb\":[],\"WebglDebugRendererInfo\":[],\"WebglDebugShaders\":[],\"WebglDepthTexture\":[],\"WebglDrawBuffers\":[],\"WebglLoseContext\":[],\"WebglMultiDraw\":[],\"WellKnownDirectory\":[],\"WgslLanguageFeatures\":[],\"WheelEvent\":[\"Event\",\"MouseEvent\",\"UiEvent\"],\"WheelEventInit\":[],\"WidevineCdmManifest\":[],\"Window\":[\"EventTarget\"],\"WindowClient\":[\"Client\"],\"Worker\":[\"EventTarget\"],\"WorkerDebuggerGlobalScope\":[\"EventTarget\"],\"WorkerGlobalScope\":[\"EventTarget\"],\"WorkerLocation\":[],\"WorkerNavigator\":[],\"WorkerOptions\":[],\"WorkerType\":[],\"Worklet\":[],\"WorkletGlobalScope\":[],\"WorkletOptions\":[],\"WritableStream\":[],\"WritableStreamDefaultController\":[],\"WritableStreamDefaultWriter\":[],\"WriteCommandType\":[],\"WriteParams\":[],\"XPathExpression\":[],\"XPathNsResolver\":[],\"XPathResult\":[],\"XmlDocument\":[\"Document\",\"EventTarget\",\"Node\"],\"XmlHttpRequest\":[\"EventTarget\",\"XmlHttpRequestEventTarget\"],\"XmlHttpRequestEventTarget\":[\"EventTarget\"],\"XmlHttpRequestResponseType\":[],\"XmlHttpRequestUpload\":[\"EventTarget\",\"XmlHttpRequestEventTarget\"],\"XmlSerializer\":[],\"XrBoundedReferenceSpace\":[\"EventTarget\",\"XrReferenceSpace\",\"XrSpace\"],\"XrEye\":[],\"XrFrame\":[],\"XrHand\":[],\"XrHandJoint\":[],\"XrHandedness\":[],\"XrInputSource\":[],\"XrInputSourceArray\":[],\"XrInputSourceEvent\":[\"Event\"],\"XrInputSourceEventInit\":[],\"XrInputSourcesChangeEvent\":[\"Event\"],\"XrInputSourcesChangeEventInit\":[],\"XrJointPose\":[\"XrPose\"],\"XrJointSpace\":[\"EventTarget\",\"XrSpace\"],\"XrLayer\":[\"EventTarget\"],\"XrPermissionDescriptor\":[],\"XrPermissionStatus\":[\"EventTarget\",\"PermissionStatus\"],\"XrPose\":[],\"XrReferenceSpace\":[\"EventTarget\",\"XrSpace\"],\"XrReferenceSpaceEvent\":[\"Event\"],\"XrReferenceSpaceEventInit\":[],\"XrReferenceSpaceType\":[],\"XrRenderState\":[],\"XrRenderStateInit\":[],\"XrRigidTransform\":[],\"XrSession\":[\"EventTarget\"],\"XrSessionEvent\":[\"Event\"],\"XrSessionEventInit\":[],\"XrSessionInit\":[],\"XrSessionMode\":[],\"XrSessionSupportedPermissionDescriptor\":[],\"XrSpace\":[\"EventTarget\"],\"XrSystem\":[\"EventTarget\"],\"XrTargetRayMode\":[],\"XrView\":[],\"XrViewerPose\":[\"XrPose\"],\"XrViewport\":[],\"XrVisibilityState\":[],\"XrWebGlLayer\":[\"EventTarget\",\"XrLayer\"],\"XrWebGlLayerInit\":[],\"XsltProcessor\":[],\"console\":[],\"css\":[],\"default\":[\"std\"],\"gpu_buffer_usage\":[],\"gpu_color_write\":[],\"gpu_map_mode\":[],\"gpu_shader_stage\":[],\"gpu_texture_usage\":[],\"std\":[\"wasm-bindgen/std\",\"js-sys/std\"]}}",
"web-time_1.1.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"futures-channel\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_feature = \\\"atomics\\\"))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_feature = \\\"atomics\\\"))\"},{\"features\":[\"js\"],\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.2\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"name\":\"js-sys\",\"req\":\"^0.3.20\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"features\":[\"macro\"],\"kind\":\"dev\",\"name\":\"pollster\",\"req\":\"^0.3\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"wasm-bindgen\",\"req\":\"^0.2.70\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-futures\",\"req\":\"^0.4\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"features\":[\"WorkerGlobalScope\"],\"kind\":\"dev\",\"name\":\"web-sys\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_feature = \\\"atomics\\\"))\"},{\"features\":[\"CssStyleDeclaration\",\"Document\",\"Element\",\"HtmlTableElement\",\"HtmlTableRowElement\",\"Performance\",\"Window\"],\"kind\":\"dev\",\"name\":\"web-sys\",\"req\":\"^0.3\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"}],\"features\":{\"serde\":[\"dep:serde\"]}}",
diff --git a/README.md b/README.md
index 77c8d2199c..bb2a9d18c3 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ curl -fsSL https://chatgpt.com/codex/install.sh | sh
Run the following on Windows to install Codex CLI:
-```
+```shell
powershell -ExecutionPolicy ByPass -c "irm https://chatgpt.com/codex/install.ps1 | iex"
```
diff --git a/cliff.toml b/cliff.toml
deleted file mode 100644
index f31e1bd89c..0000000000
--- a/cliff.toml
+++ /dev/null
@@ -1,46 +0,0 @@
-# https://git-cliff.org/docs/configuration
-
-[changelog]
-header = """
-# Changelog
-
-You can install any of these versions: `npm install -g @openai/codex@`
-"""
-
-body = """
-{% if version -%}
-## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
-{%- else %}
-## [unreleased]
-{% endif %}
-
-{%- for group, commits in commits | group_by(attribute="group") %}
-### {{ group | striptags | trim }}
-
-{% for commit in commits %}- {% if commit.scope %}*({{ commit.scope }})* {% endif %}{% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}
-{% endfor %}
-
-{%- endfor -%}
-"""
-
-footer = """
-
-"""
-
-trim = true
-postprocessors = []
-
-[git]
-conventional_commits = true
-
-commit_parsers = [
- { message = "^feat", group = "🚀 Features" },
- { message = "^fix", group = "🪲 Bug Fixes" },
- { message = "^bump", group = "🛳️ Release" },
- # Fallback – skip anything that didn't match the above rules.
- { message = ".*", group = "💼 Other" },
-]
-
-filter_unconventional = false
-sort_commits = "oldest"
-topo_order = false
\ No newline at end of file
diff --git a/codex-rs/.cargo/audit.toml b/codex-rs/.cargo/audit.toml
index c1d8ff87ee..56feb5edb3 100644
--- a/codex-rs/.cargo/audit.toml
+++ b/codex-rs/.cargo/audit.toml
@@ -1,5 +1,5 @@
[advisories]
-# Reviewed 2026-06-07. Keep this list in sync with ../deny.toml.
+# Reviewed 2026-07-02. Keep this list in sync with ../deny.toml.
ignore = [
"RUSTSEC-2024-0388", # derivative 2.2.0 via starlark/starlark_syntax; upstream crate is unmaintained
"RUSTSEC-2025-0057", # fxhash 0.2.1 via starlark_map/bm25; upstream crate is unmaintained
@@ -10,4 +10,6 @@ ignore = [
"RUSTSEC-2026-0118", # hickory-proto via rama-dns/rama-tcp; remove when rama updates to hickory 0.26.1 or hickory-net
"RUSTSEC-2026-0119", # hickory-proto via rama-dns/rama-tcp; remove when rama updates to hickory 0.26.1 or hickory-net
"RUSTSEC-2026-0173", # proc-macro-error2 via i18n-embed-fl/age/codex-secrets; remove when local secrets storage migrates off age or age drops i18n-embed-fl
+ "RUSTSEC-2026-0194", # quick-xml via plist/syntect and wayland-scanner; trusted inputs only; remove when rust-plist#191 and wayland-rs#938 are released
+ "RUSTSEC-2026-0195", # quick-xml via plist/syntect and wayland-scanner; trusted inputs only; remove when rust-plist#191 and wayland-rs#938 are released
]
diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock
index 2c50f268f0..c0ff4a585a 100644
--- a/codex-rs/Cargo.lock
+++ b/codex-rs/Cargo.lock
@@ -2173,9 +2173,11 @@ dependencies = [
"futures",
"gethostname",
"hmac 0.12.1",
+ "httpdate",
"jsonwebtoken",
"owo-colors",
"pretty_assertions",
+ "rand 0.9.3",
"serde",
"serde_json",
"sha2 0.10.9",
@@ -2314,6 +2316,7 @@ name = "codex-cli"
version = "0.0.0"
dependencies = [
"anyhow",
+ "app_test_support",
"assert_cmd",
"assert_matches",
"clap",
@@ -2503,6 +2506,7 @@ dependencies = [
"codex-code-mode-protocol",
"codex-protocol",
"deno_core_icudata",
+ "futures",
"pretty_assertions",
"serde_json",
"tokio",
@@ -2514,6 +2518,18 @@ dependencies = [
[[package]]
name = "codex-code-mode-host"
version = "0.0.0"
+dependencies = [
+ "anyhow",
+ "codex-code-mode",
+ "codex-code-mode-protocol",
+ "codex-protocol",
+ "codex-utils-cargo-bin",
+ "pretty_assertions",
+ "serde_json",
+ "tempfile",
+ "tokio",
+ "tokio-util",
+]
[[package]]
name = "codex-code-mode-protocol"
@@ -2557,6 +2573,7 @@ dependencies = [
"multimap",
"pretty_assertions",
"prost 0.14.3",
+ "regex-lite",
"schemars 0.8.22",
"serde",
"serde_ignored",
@@ -2919,7 +2936,9 @@ dependencies = [
"opentelemetry_sdk",
"pretty_assertions",
"prost 0.14.3",
+ "rcgen",
"reqwest 0.12.28",
+ "rustls",
"serde",
"serde_json",
"serial_test",
@@ -3009,6 +3028,7 @@ dependencies = [
"codex-tools",
"codex-utils-absolute-path",
"pretty_assertions",
+ "serde_json",
"tokio",
]
@@ -3058,6 +3078,7 @@ dependencies = [
"anyhow",
"codex-login",
"codex-protocol",
+ "mime_guess",
"pretty_assertions",
"sentry",
"tracing",
@@ -3244,6 +3265,7 @@ dependencies = [
"clap",
"codex-core",
"codex-install-context",
+ "codex-network-proxy",
"codex-process-hardening",
"codex-protocol",
"codex-sandboxing",
@@ -3356,6 +3378,7 @@ name = "codex-mcp-extension"
version = "0.0.0"
dependencies = [
"codex-config",
+ "codex-connectors-extension",
"codex-core",
"codex-core-plugins",
"codex-exec-server",
@@ -3692,7 +3715,7 @@ dependencies = [
"icu_provider",
"landlock",
"pretty_assertions",
- "quick-xml",
+ "quick-xml 0.41.0",
"reqwest 0.12.28",
"schemars 0.8.22",
"seccompiler",
@@ -3757,7 +3780,6 @@ dependencies = [
"base64 0.22.1",
"bytes",
"codex-api",
- "codex-client",
"codex-config",
"codex-exec-server",
"codex-keyring-store",
@@ -4801,9 +4823,9 @@ dependencies = [
[[package]]
name = "crossbeam-epoch"
-version = "0.9.18"
+version = "0.9.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
dependencies = [
"crossbeam-utils",
]
@@ -10252,13 +10274,13 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "plist"
-version = "1.8.0"
+version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
+checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1"
dependencies = [
"base64 0.22.1",
"indexmap 2.14.0",
- "quick-xml",
+ "quick-xml 0.39.4",
"serde",
"time",
]
@@ -10691,9 +10713,18 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quick-xml"
-version = "0.38.4"
+version = "0.39.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
+checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
dependencies = [
"memchr",
"serde",
@@ -14654,12 +14685,12 @@ dependencies = [
[[package]]
name = "wayland-scanner"
-version = "0.31.8"
+version = "0.31.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3"
+checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a"
dependencies = [
"proc-macro2",
- "quick-xml",
+ "quick-xml 0.39.4",
"quote",
]
diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml
index ef17cb9ce5..2f73c216a9 100644
--- a/codex-rs/Cargo.toml
+++ b/codex-rs/Cargo.toml
@@ -166,6 +166,7 @@ codex-code-mode-protocol = { path = "code-mode-protocol" }
codex-home = { path = "codex-home" }
codex-config = { path = "config" }
codex-connectors = { path = "connectors" }
+codex-connectors-extension = { path = "ext/connectors" }
codex-context-fragments = { path = "context-fragments" }
codex-core = { path = "core" }
codex-core-api = { path = "core-api" }
@@ -317,6 +318,7 @@ glob = "0.3"
globset = "0.4"
hmac = "0.12.1"
http = "1.3.1"
+httpdate = "1.0.3"
iana-time-zone = "0.1.64"
icu_decimal = "2.1"
icu_locale_core = "2.1"
@@ -360,7 +362,7 @@ portable-pty = "0.9.0"
predicates = "3"
pretty_assertions = "1.4.1"
pulldown-cmark = "0.10"
-quick-xml = "0.38.4"
+quick-xml = "0.41.0"
rand = "0.9"
ratatui = "0.29.0"
ratatui-macros = "0.6.0"
diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs
index c183ad925a..0f759fd938 100644
--- a/codex-rs/analytics/src/analytics_client_tests.rs
+++ b/codex-rs/analytics/src/analytics_client_tests.rs
@@ -198,6 +198,7 @@ fn sample_thread_with_metadata(
parent_thread_id,
preview: "first prompt".to_string(),
ephemeral,
+ history_mode: Default::default(),
model_provider: "openai".to_string(),
created_at: 1,
updated_at: 2,
@@ -558,6 +559,7 @@ async fn ingest_rejected_turn_steer(
response: Box::new(sample_thread_resume_response(
"thread-2", /*ephemeral*/ false, "gpt-5",
)),
+ thread_originator: None,
},
out,
)
@@ -631,6 +633,7 @@ async fn ingest_turn_prerequisites(
response: Box::new(sample_thread_start_response(
"thread-2", /*ephemeral*/ false, "gpt-5",
)),
+ thread_originator: None,
},
out,
)
@@ -654,6 +657,7 @@ async fn ingest_turn_prerequisites(
connection_id: 7,
request_id: RequestId::Integer(3),
response: Box::new(sample_turn_start_response("turn-2")),
+ thread_originator: None,
},
out,
)
@@ -720,6 +724,7 @@ async fn ingest_review_prerequisites(
response: Box::new(sample_thread_start_response(
"thread-1", /*ephemeral*/ false, "gpt-5",
)),
+ thread_originator: None,
},
events,
)
@@ -1652,6 +1657,7 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize
/*ephemeral*/ false,
"gpt-5",
)),
+ thread_originator: None,
},
&mut events,
)
@@ -1697,6 +1703,7 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize
response: Box::new(sample_thread_resume_response(
"thread-1", /*ephemeral*/ true, "gpt-5",
)),
+ thread_originator: None,
},
&mut events,
)
@@ -1741,6 +1748,157 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize
);
}
+#[tokio::test]
+async fn thread_originator_overrides_shared_connection_across_thread_events() {
+ let mut reducer = AnalyticsReducer::default();
+ let mut events = Vec::new();
+
+ reducer
+ .ingest(sample_initialize_fact(/*connection_id*/ 7), &mut events)
+ .await;
+ for (request_id, thread_id, thread_originator) in [
+ (1, "thread-work", Some(TEST_PRODUCT_CLIENT_ID.to_string())),
+ (2, "thread-default", None),
+ ] {
+ reducer
+ .ingest(
+ AnalyticsFact::ClientResponse {
+ connection_id: 7,
+ request_id: RequestId::Integer(request_id),
+ response: Box::new(sample_thread_start_response(
+ thread_id, /*ephemeral*/ false, "gpt-5",
+ )),
+ thread_originator,
+ },
+ &mut events,
+ )
+ .await;
+ }
+
+ let initialized = serde_json::to_value(&events).expect("serialize thread events");
+ assert_eq!(
+ initialized
+ .as_array()
+ .expect("thread events")
+ .iter()
+ .map(|event| {
+ json!({
+ "thread_id": event["event_params"]["thread_id"],
+ "app_server_client": event["event_params"]["app_server_client"],
+ })
+ })
+ .collect::>(),
+ vec![
+ json!({
+ "thread_id": "thread-work",
+ "app_server_client": {
+ "product_client_id": TEST_PRODUCT_CLIENT_ID,
+ "client_name": "codex-tui",
+ "client_version": "1.0.0",
+ "rpc_transport": "websocket",
+ "experimental_api_enabled": false,
+ },
+ }),
+ json!({
+ "thread_id": "thread-default",
+ "app_server_client": {
+ "product_client_id": DEFAULT_ORIGINATOR,
+ "client_name": "codex-tui",
+ "client_version": "1.0.0",
+ "rpc_transport": "websocket",
+ "experimental_api_enabled": false,
+ },
+ }),
+ ]
+ );
+
+ events.clear();
+ reducer
+ .ingest(
+ AnalyticsFact::ClientRequest {
+ connection_id: 7,
+ request_id: RequestId::Integer(3),
+ request: Box::new(sample_turn_start_request(
+ "thread-work",
+ /*request_id*/ 3,
+ )),
+ },
+ &mut events,
+ )
+ .await;
+ reducer
+ .ingest(
+ AnalyticsFact::ClientResponse {
+ connection_id: 7,
+ request_id: RequestId::Integer(3),
+ response: Box::new(sample_turn_start_response("turn-1")),
+ thread_originator: None,
+ },
+ &mut events,
+ )
+ .await;
+ ingest_completed_command_execution_item(&mut reducer, &mut events, "thread-work", "item-work")
+ .await;
+ ingest_complete_child_turn(&mut reducer, &mut events, "thread-work", "turn-1").await;
+ reducer
+ .ingest(
+ AnalyticsFact::Custom(CustomAnalyticsFact::Compaction(Box::new(
+ CodexCompactionEvent {
+ thread_id: "thread-work".to_string(),
+ turn_id: "turn-compact".to_string(),
+ trigger: CompactionTrigger::Manual,
+ reason: CompactionReason::UserRequested,
+ implementation: CompactionImplementation::Responses,
+ phase: CompactionPhase::StandaloneTurn,
+ strategy: CompactionStrategy::Memento,
+ status: CompactionStatus::Completed,
+ codex_error_kind: None,
+ codex_error_http_status_code: None,
+ active_context_tokens_before: 131_000,
+ active_context_tokens_after: 64_000,
+ retained_image_count: None,
+ compaction_summary_tokens: None,
+ cached_input_tokens: None,
+ started_at: 100,
+ completed_at: 101,
+ duration_ms: Some(1200),
+ },
+ ))),
+ &mut events,
+ )
+ .await;
+
+ let lifecycle = serde_json::to_value(&events).expect("serialize lifecycle events");
+ assert_eq!(
+ lifecycle
+ .as_array()
+ .expect("lifecycle events")
+ .iter()
+ .map(|event| {
+ json!({
+ "event_type": event["event_type"],
+ "product_client_id":
+ event["event_params"]["app_server_client"]["product_client_id"],
+ })
+ })
+ .collect::>(),
+ vec![
+ json!({
+ "event_type": "codex_command_execution_event",
+ "product_client_id": TEST_PRODUCT_CLIENT_ID,
+ }),
+ json!({
+ "event_type": "codex_turn_event",
+ "product_client_id": TEST_PRODUCT_CLIENT_ID,
+ }),
+ json!({
+ "event_type": "codex_compaction_event",
+ "product_client_id": TEST_PRODUCT_CLIENT_ID,
+ }),
+ ]
+ );
+}
+
#[tokio::test]
async fn unrelated_client_requests_are_ignored_by_reducer() {
let mut reducer = AnalyticsReducer::default();
@@ -1767,6 +1925,7 @@ async fn unrelated_client_requests_are_ignored_by_reducer() {
connection_id: 7,
request_id: RequestId::Integer(3),
response: Box::new(sample_turn_start_response("turn-2")),
+ thread_originator: None,
},
&mut events,
)
@@ -1792,6 +1951,7 @@ async fn unrelated_client_responses_are_ignored_by_reducer() {
response: Box::new(ClientResponsePayload::ThreadArchive(
ThreadArchiveResponse {},
)),
+ thread_originator: None,
},
&mut events,
)
@@ -1851,6 +2011,7 @@ async fn compaction_event_ingests_custom_fact() {
Some(AppServerThreadSource::Subagent),
Some(parent_thread_id.to_string()),
)),
+ thread_originator: None,
},
&mut events,
)
@@ -1971,6 +2132,7 @@ async fn guardian_review_event_ingests_custom_fact_with_optional_target_item() {
/*ephemeral*/ false,
"gpt-5",
)),
+ thread_originator: None,
},
&mut events,
)
@@ -2499,6 +2661,7 @@ async fn item_review_summaries_do_not_cross_threads_with_reused_item_ids() {
response: Box::new(sample_thread_start_response(
"thread-2", /*ephemeral*/ false, "gpt-5",
)),
+ thread_originator: None,
},
&mut events,
)
@@ -2749,7 +2912,7 @@ async fn subagent_thread_started_publishes_without_initialize() {
}
#[tokio::test]
-async fn subagent_events_use_inherited_connection_unless_turn_connection_is_explicit() {
+async fn subagent_events_keep_thread_originator_with_explicit_turn_connection() {
let mut reducer = AnalyticsReducer::default();
let mut events = Vec::new();
let parent_thread_id =
@@ -2786,6 +2949,7 @@ async fn subagent_events_use_inherited_connection_unless_turn_connection_is_expl
/*ephemeral*/ false,
"gpt-5",
)),
+ thread_originator: None,
},
&mut events,
)
@@ -2908,6 +3072,7 @@ async fn subagent_events_use_inherited_connection_unless_turn_connection_is_expl
connection_id: 8,
request_id: RequestId::Integer(3),
response: Box::new(sample_turn_start_response("turn-explicit")),
+ thread_originator: None,
},
&mut events,
)
@@ -2918,7 +3083,11 @@ async fn subagent_events_use_inherited_connection_unless_turn_connection_is_expl
};
assert_eq!(
event.event_params.app_server_client.product_client_id,
- DEFAULT_ORIGINATOR
+ "parent-client"
+ );
+ assert_eq!(
+ event.event_params.app_server_client.client_name.as_deref(),
+ Some("codex-tui")
);
}
@@ -3868,6 +4037,7 @@ async fn accepted_turn_steer_emits_expected_event() {
connection_id: 7,
request_id: RequestId::Integer(4),
response: Box::new(sample_turn_steer_response("turn-2")),
+ thread_originator: None,
},
&mut out,
)
@@ -4039,6 +4209,7 @@ async fn turn_start_error_response_discards_pending_start_request() {
connection_id: 7,
request_id: RequestId::Integer(3),
response: Box::new(sample_turn_start_response("turn-2")),
+ thread_originator: None,
},
&mut out,
)
@@ -4367,6 +4538,7 @@ async fn accepted_steers_increment_turn_steer_count() {
connection_id: 7,
request_id: RequestId::Integer(4),
response: Box::new(sample_turn_steer_response("turn-2")),
+ thread_originator: None,
},
&mut out,
)
@@ -4414,6 +4586,7 @@ async fn accepted_steers_increment_turn_steer_count() {
connection_id: 7,
request_id: RequestId::Integer(6),
response: Box::new(sample_turn_steer_response("turn-2")),
+ thread_originator: None,
},
&mut out,
)
diff --git a/codex-rs/analytics/src/client.rs b/codex-rs/analytics/src/client.rs
index 5996734f8e..f957a665a0 100644
--- a/codex-rs/analytics/src/client.rs
+++ b/codex-rs/analytics/src/client.rs
@@ -435,6 +435,31 @@ impl AnalyticsEventsClient {
connection_id: u64,
request_id: RequestId,
response: ClientResponsePayload,
+ ) {
+ self.track_response_inner(
+ connection_id,
+ request_id,
+ response,
+ /*thread_originator*/ None,
+ );
+ }
+
+ pub fn track_response_with_thread_originator(
+ &self,
+ connection_id: u64,
+ request_id: RequestId,
+ response: ClientResponsePayload,
+ thread_originator: String,
+ ) {
+ self.track_response_inner(connection_id, request_id, response, Some(thread_originator));
+ }
+
+ fn track_response_inner(
+ &self,
+ connection_id: u64,
+ request_id: RequestId,
+ response: ClientResponsePayload,
+ thread_originator: Option,
) {
if !matches!(
response,
@@ -450,6 +475,7 @@ impl AnalyticsEventsClient {
connection_id,
request_id,
response: Box::new(response),
+ thread_originator,
});
}
diff --git a/codex-rs/analytics/src/client_tests.rs b/codex-rs/analytics/src/client_tests.rs
index bbc5b5f0b6..ac50d6ebc7 100644
--- a/codex-rs/analytics/src/client_tests.rs
+++ b/codex-rs/analytics/src/client_tests.rs
@@ -283,6 +283,7 @@ fn sample_thread(thread_id: &str) -> Thread {
parent_thread_id: None,
preview: "first prompt".to_string(),
ephemeral: false,
+ history_mode: Default::default(),
model_provider: "openai".to_string(),
created_at: 1,
updated_at: 2,
diff --git a/codex-rs/analytics/src/facts.rs b/codex-rs/analytics/src/facts.rs
index 89b8708648..3346a6e19a 100644
--- a/codex-rs/analytics/src/facts.rs
+++ b/codex-rs/analytics/src/facts.rs
@@ -464,6 +464,7 @@ pub(crate) enum AnalyticsFact {
connection_id: u64,
request_id: RequestId,
response: Box,
+ thread_originator: Option,
},
ErrorResponse {
connection_id: u64,
diff --git a/codex-rs/analytics/src/reducer.rs b/codex-rs/analytics/src/reducer.rs
index 522a307688..29f21bac52 100644
--- a/codex-rs/analytics/src/reducer.rs
+++ b/codex-rs/analytics/src/reducer.rs
@@ -165,6 +165,20 @@ struct ConnectionState {
struct ThreadAnalyticsState {
connection_id: Option,
metadata: Option,
+ originator: Option,
+}
+
+impl ThreadAnalyticsState {
+ fn app_server_client(
+ &self,
+ connection_state: &ConnectionState,
+ ) -> CodexAppServerClientMetadata {
+ let mut app_server_client = connection_state.app_server_client.clone();
+ if let Some(originator) = self.originator.as_ref() {
+ app_server_client.product_client_id.clone_from(originator);
+ }
+ app_server_client
+ }
}
#[derive(Clone, Copy)]
@@ -434,9 +448,11 @@ impl AnalyticsReducer {
connection_id,
request_id,
response,
+ thread_originator,
} => {
if let Some(response) = response.into_client_response(request_id) {
- self.ingest_response(connection_id, response, out).await;
+ self.ingest_response(connection_id, response, thread_originator, out)
+ .await;
}
}
AnalyticsFact::ErrorResponse {
@@ -575,6 +591,9 @@ impl AnalyticsReducer {
.and_then(|parent_thread_id| self.threads.get(parent_thread_id))
.and_then(|thread| thread.connection_id);
let thread_state = self.threads.entry(input.thread_id.clone()).or_default();
+ thread_state
+ .originator
+ .get_or_insert_with(|| input.product_client_id.clone());
thread_state
.metadata
.get_or_insert_with(|| ThreadMetadataState {
@@ -597,7 +616,7 @@ impl AnalyticsReducer {
input: GuardianReviewEventParams,
out: &mut Vec,
) {
- let Some((connection_state, thread_metadata)) =
+ let Some((connection_state, thread_state, thread_metadata)) =
self.thread_context_or_warn(AnalyticsDropSite::guardian(&input))
else {
return;
@@ -607,7 +626,7 @@ impl AnalyticsReducer {
event_type: "codex_guardian_review",
event_params: GuardianReviewEventPayload {
session_id: thread_metadata.session_id.clone(),
- app_server_client: connection_state.app_server_client.clone(),
+ app_server_client: thread_state.app_server_client(connection_state),
runtime: connection_state.runtime.clone(),
guardian_review: input,
},
@@ -874,6 +893,7 @@ impl AnalyticsReducer {
&mut self,
connection_id: u64,
response: ClientResponse,
+ thread_originator: Option,
out: &mut Vec,
) {
match response {
@@ -883,6 +903,7 @@ impl AnalyticsReducer {
response.thread,
response.model,
ThreadInitializationMode::New,
+ thread_originator,
out,
);
}
@@ -892,6 +913,7 @@ impl AnalyticsReducer {
response.thread,
response.model,
ThreadInitializationMode::Resumed,
+ thread_originator,
out,
);
}
@@ -901,6 +923,7 @@ impl AnalyticsReducer {
response.thread,
response.model,
ThreadInitializationMode::Forked,
+ thread_originator,
out,
);
}
@@ -1248,7 +1271,7 @@ impl AnalyticsReducer {
else {
return;
};
- let Some((connection_state, thread_metadata)) = self
+ let Some((connection_state, thread_state, thread_metadata)) = self
.thread_context_or_warn(AnalyticsDropSite::tool_item(¬ification, item_id))
else {
return;
@@ -1260,6 +1283,7 @@ impl AnalyticsReducer {
started_at_ms,
completed_at_ms,
connection_state,
+ thread_state,
thread_metadata,
review_summary: self.item_review_summaries.get(&key),
}) {
@@ -1316,6 +1340,7 @@ impl AnalyticsReducer {
thread: codex_app_server_protocol::Thread,
model: String,
initialization_mode: ThreadInitializationMode,
+ thread_originator: Option,
out: &mut Vec,
) {
let session_source: SessionSource = thread.source.into();
@@ -1333,20 +1358,20 @@ impl AnalyticsReducer {
parent_thread_id,
initialization_mode,
);
- self.threads.insert(
- thread_id.clone(),
- ThreadAnalyticsState {
- connection_id: Some(connection_id),
- metadata: Some(thread_metadata.clone()),
- },
- );
+ let thread_state = self.threads.entry(thread_id.clone()).or_default();
+ if let Some(originator) = thread_originator {
+ thread_state.originator = Some(originator);
+ }
+ thread_state.connection_id = Some(connection_id);
+ thread_state.metadata = Some(thread_metadata.clone());
+ let app_server_client = thread_state.app_server_client(connection_state);
out.push(TrackEventRequest::ThreadInitialized(
ThreadInitializedEvent {
event_type: "codex_thread_initialized",
event_params: ThreadInitializedEventParams {
thread_id,
session_id,
- app_server_client: connection_state.app_server_client.clone(),
+ app_server_client,
runtime: connection_state.runtime.clone(),
model,
ephemeral: thread.ephemeral,
@@ -1362,7 +1387,7 @@ impl AnalyticsReducer {
}
fn ingest_compaction(&mut self, input: CodexCompactionEvent, out: &mut Vec) {
- let Some((connection_state, thread_metadata)) =
+ let Some((connection_state, thread_state, thread_metadata)) =
self.thread_context_or_warn(AnalyticsDropSite::compaction(&input))
else {
return;
@@ -1373,7 +1398,7 @@ impl AnalyticsReducer {
event_params: codex_compaction_event_params(
input,
thread_metadata.session_id.clone(),
- connection_state.app_server_client.clone(),
+ thread_state.app_server_client(connection_state),
connection_state.runtime.clone(),
thread_metadata.thread_source.clone(),
thread_metadata.subagent_source.clone(),
@@ -1384,7 +1409,7 @@ impl AnalyticsReducer {
}
fn ingest_goal(&mut self, input: CodexGoalEvent, out: &mut Vec) {
- let Some((connection_state, thread_metadata)) =
+ let Some((connection_state, thread_state, thread_metadata)) =
self.thread_context_or_warn(AnalyticsDropSite::goal(&input))
else {
return;
@@ -1394,7 +1419,7 @@ impl AnalyticsReducer {
event_params: codex_goal_event_params(
input,
thread_metadata.session_id.clone(),
- connection_state.app_server_client.clone(),
+ thread_state.app_server_client(connection_state),
connection_state.runtime.clone(),
thread_metadata.thread_source.clone(),
thread_metadata.subagent_source.clone(),
@@ -1483,11 +1508,11 @@ impl AnalyticsReducer {
return;
};
let drop_site = AnalyticsDropSite::turn_steer(&pending_request.thread_id);
- let Some(thread_metadata) = self
- .threads
- .get(drop_site.thread_id)
- .and_then(|thread| thread.metadata.as_ref())
- else {
+ let Some(thread_state) = self.threads.get(drop_site.thread_id) else {
+ warn_missing_analytics_context(&drop_site, MissingAnalyticsContext::ThreadMetadata);
+ return;
+ };
+ let Some(thread_metadata) = thread_state.metadata.as_ref() else {
warn_missing_analytics_context(&drop_site, MissingAnalyticsContext::ThreadMetadata);
return;
};
@@ -1498,7 +1523,7 @@ impl AnalyticsReducer {
session_id: thread_metadata.session_id.clone(),
expected_turn_id: Some(pending_request.expected_turn_id),
accepted_turn_id,
- app_server_client: connection_state.app_server_client.clone(),
+ app_server_client: thread_state.app_server_client(connection_state),
runtime: connection_state.runtime.clone(),
thread_source: thread_metadata.thread_source.clone(),
subagent_source: thread_metadata.subagent_source.clone(),
@@ -1529,7 +1554,7 @@ impl AnalyticsReducer {
&pending_review,
);
}
- let Some((connection_state, thread_metadata)) =
+ let Some((connection_state, thread_state, thread_metadata)) =
self.thread_context_or_warn(AnalyticsDropSite::review(&pending_review))
else {
return;
@@ -1541,7 +1566,7 @@ impl AnalyticsReducer {
turn_id: pending_review.turn_id,
item_id: pending_review.item_id,
review_id: pending_review.review_id,
- app_server_client: connection_state.app_server_client.clone(),
+ app_server_client: thread_state.app_server_client(connection_state),
runtime: connection_state.runtime.clone(),
thread_source: thread_metadata.thread_source.clone(),
subagent_source: thread_metadata.subagent_source.clone(),
@@ -1610,18 +1635,18 @@ impl AnalyticsReducer {
);
return;
};
- let Some(thread_metadata) = self
- .threads
- .get(drop_site.thread_id)
- .and_then(|thread| thread.metadata.as_ref())
- else {
+ let Some(thread_state) = self.threads.get(drop_site.thread_id) else {
+ warn_missing_analytics_context(&drop_site, MissingAnalyticsContext::ThreadMetadata);
+ return;
+ };
+ let Some(thread_metadata) = thread_state.metadata.as_ref() else {
warn_missing_analytics_context(&drop_site, MissingAnalyticsContext::ThreadMetadata);
return;
};
let turn_event = TrackEventRequest::TurnEvent(Box::new(CodexTurnEventRequest {
event_type: "codex_turn_event",
event_params: codex_turn_event_params(
- connection_state.app_server_client.clone(),
+ thread_state.app_server_client(connection_state),
connection_state.runtime.clone(),
turn_id.to_string(),
turn_state,
@@ -1663,17 +1688,18 @@ impl AnalyticsReducer {
fn thread_context_or_warn(
&self,
drop_site: AnalyticsDropSite<'_>,
- ) -> Option<(&ConnectionState, &ThreadMetadataState)> {
+ ) -> Option<(
+ &ConnectionState,
+ &ThreadAnalyticsState,
+ &ThreadMetadataState,
+ )> {
let connection_state = self.thread_connection_or_warn(drop_site)?;
- let Some(thread_metadata) = self
- .threads
- .get(drop_site.thread_id)
- .and_then(|thread| thread.metadata.as_ref())
- else {
+ let thread_state = self.threads.get(drop_site.thread_id)?;
+ let Some(thread_metadata) = thread_state.metadata.as_ref() else {
warn_missing_analytics_context(&drop_site, MissingAnalyticsContext::ThreadMetadata);
return None;
};
- Some((connection_state, thread_metadata))
+ Some((connection_state, thread_state, thread_metadata))
}
}
@@ -1743,6 +1769,7 @@ struct ToolItemEventInput<'a> {
started_at_ms: u64,
completed_at_ms: u64,
connection_state: &'a ConnectionState,
+ thread_state: &'a ThreadAnalyticsState,
thread_metadata: &'a ThreadMetadataState,
review_summary: Option<&'a ItemReviewSummary>,
}
@@ -1755,6 +1782,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option {
started_at_ms,
completed_at_ms,
connection_state,
+ thread_state,
thread_metadata,
review_summary,
} = input;
@@ -1784,6 +1812,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option {
started_at_ms,
completed_at_ms,
connection_state,
+ thread_state,
thread_metadata,
review_summary,
},
@@ -1825,6 +1854,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option {
started_at_ms,
completed_at_ms,
connection_state,
+ thread_state,
thread_metadata,
review_summary,
},
@@ -1866,6 +1896,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option {
started_at_ms,
completed_at_ms,
connection_state,
+ thread_state,
thread_metadata,
review_summary,
},
@@ -1910,6 +1941,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option {
started_at_ms,
completed_at_ms,
connection_state,
+ thread_state,
thread_metadata,
review_summary,
},
@@ -1954,6 +1986,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option {
started_at_ms,
completed_at_ms,
connection_state,
+ thread_state,
thread_metadata,
review_summary,
},
@@ -2009,6 +2042,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option {
started_at_ms,
completed_at_ms,
connection_state,
+ thread_state,
thread_metadata,
review_summary,
},
@@ -2045,6 +2079,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option {
started_at_ms,
completed_at_ms,
connection_state,
+ thread_state,
thread_metadata,
review_summary,
},
@@ -2100,6 +2135,7 @@ struct ToolItemContext<'a> {
started_at_ms: u64,
completed_at_ms: u64,
connection_state: &'a ConnectionState,
+ thread_state: &'a ThreadAnalyticsState,
thread_metadata: &'a ThreadMetadataState,
review_summary: Option<&'a ItemReviewSummary>,
}
@@ -2118,7 +2154,9 @@ fn tool_item_base(
thread_id: thread_id.to_string(),
turn_id: turn_id.to_string(),
item_id,
- app_server_client: context.connection_state.app_server_client.clone(),
+ app_server_client: context
+ .thread_state
+ .app_server_client(context.connection_state),
runtime: context.connection_state.runtime.clone(),
thread_source: thread_metadata.thread_source.clone(),
subagent_source: thread_metadata.subagent_source.clone(),
diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs
index b7a958eff1..4eec8a2348 100644
--- a/codex-rs/app-server-client/src/lib.rs
+++ b/codex-rs/app-server-client/src/lib.rs
@@ -78,9 +78,6 @@ pub use crate::remote::RemoteAppServerEndpoint;
/// module exists so clients can remove a direct `codex-core` dependency
/// while legacy startup/config paths are migrated to RPCs.
pub mod legacy_core {
- pub use codex_core::check_execpolicy_for_warnings;
- pub use codex_core::format_exec_policy_error_with_source;
-
pub mod config {
pub use codex_core::config::*;
diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json
index 68c242504f..2f39f821b6 100644
--- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json
+++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json
@@ -556,6 +556,13 @@
},
"ConsumeAccountRateLimitResetCreditParams": {
"properties": {
+ "creditId": {
+ "description": "Opaque reset-credit identifier to redeem. When omitted, the backend selects the next available credit.",
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"idempotencyKey": {
"description": "Identifies one logical reset attempt. A UUID is recommended; reuse the same value when retrying that attempt.",
"type": "string"
@@ -1619,6 +1626,12 @@
"null"
]
},
+ "threadId": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"timeoutSecs": {
"format": "int64",
"type": [
@@ -1780,13 +1793,29 @@
"type": "object"
},
"MultiAgentMode": {
- "description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
- "enum": [
- "none",
- "explicitRequestOnly",
- "proactive"
- ],
- "type": "string"
+ "description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.",
+ "oneOf": [
+ {
+ "enum": [
+ "explicitRequestOnly",
+ "proactive"
+ ],
+ "type": "string"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "custom": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "custom"
+ ],
+ "title": "CustomMultiAgentMode",
+ "type": "object"
+ }
+ ]
},
"NetworkAccess": {
"enum": [
@@ -2677,6 +2706,12 @@
"name": {
"type": "string"
},
+ "namespace": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"status": {
"type": [
"string",
@@ -3625,6 +3660,13 @@
"ephemeral": {
"type": "boolean"
},
+ "lastTurnId": {
+ "description": "Optional last turn id to fork through, inclusive.\n\nWhen specified, turns after `last_turn_id` are omitted from the fork. The referenced turn cannot be in progress.",
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"model": {
"description": "Configuration overrides for the forked thread, if any.",
"type": [
@@ -3741,6 +3783,13 @@
],
"type": "string"
},
+ "ThreadHistoryMode": {
+ "enum": [
+ "legacy",
+ "paginated"
+ ],
+ "type": "string"
+ },
"ThreadInjectItemsParams": {
"properties": {
"items": {
@@ -4168,6 +4217,7 @@
"type": "object"
},
"ThreadRollbackParams": {
+ "description": "DEPRECATED: `thread/rollback` will be removed soon.",
"properties": {
"numTurns": {
"description": "The number of turns to drop from the end of the thread. Must be >= 1.\n\nThis only modifies the thread's history and does not revert local file changes that have been made by the agent. Clients are responsible for reverting these changes.",
diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json
index a3d07c24a0..e7a533d76b 100644
--- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json
+++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json
@@ -2370,6 +2370,12 @@
},
"success": {
"type": "boolean"
+ },
+ "threadId": {
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
@@ -2378,6 +2384,12 @@
],
"type": "object"
},
+ "McpServerStartupFailureReason": {
+ "enum": [
+ "reauthenticationRequired"
+ ],
+ "type": "string"
+ },
"McpServerStartupState": {
"enum": [
"starting",
@@ -2395,6 +2407,16 @@
"null"
]
},
+ "failureReason": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/McpServerStartupFailureReason"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"name": {
"type": "string"
},
@@ -2416,6 +2438,18 @@
},
"McpToolCallAppContext": {
"properties": {
+ "actionName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "appName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"connectorId": {
"type": "string"
},
@@ -2430,6 +2464,12 @@
"string",
"null"
]
+ },
+ "templateId": {
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
@@ -2673,13 +2713,29 @@
"type": "object"
},
"MultiAgentMode": {
- "description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
- "enum": [
- "none",
- "explicitRequestOnly",
- "proactive"
- ],
- "type": "string"
+ "description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.",
+ "oneOf": [
+ {
+ "enum": [
+ "explicitRequestOnly",
+ "proactive"
+ ],
+ "type": "string"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "custom": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "custom"
+ ],
+ "title": "CustomMultiAgentMode",
+ "type": "object"
+ }
+ ]
},
"NetworkAccess": {
"enum": [
@@ -3877,6 +3933,13 @@
],
"type": "object"
},
+ "ThreadHistoryMode": {
+ "enum": [
+ "legacy",
+ "paginated"
+ ],
+ "type": "string"
+ },
"ThreadId": {
"type": "string"
},
diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
index 4d417ffb2f..0eedef903a 100644
--- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
+++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
@@ -8414,6 +8414,16 @@
"object",
"null"
]
+ },
+ "models": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/v2/ModelsRequirements"
+ },
+ {
+ "type": "null"
+ }
+ ]
}
},
"type": "object"
@@ -8678,6 +8688,13 @@
"ConsumeAccountRateLimitResetCreditParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
+ "creditId": {
+ "description": "Opaque reset-credit identifier to redeem. When omitted, the backend selects the next available credit.",
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"idempotencyKey": {
"description": "Identifies one logical reset attempt. A UUID is recommended; reuse the same value when retrying that attempt.",
"type": "string"
@@ -12026,6 +12043,12 @@
},
"success": {
"type": "boolean"
+ },
+ "threadId": {
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
@@ -12050,6 +12073,12 @@
"null"
]
},
+ "threadId": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"timeoutSecs": {
"format": "int64",
"type": [
@@ -12082,6 +12111,12 @@
"title": "McpServerRefreshResponse",
"type": "object"
},
+ "McpServerStartupFailureReason": {
+ "enum": [
+ "reauthenticationRequired"
+ ],
+ "type": "string"
+ },
"McpServerStartupState": {
"enum": [
"starting",
@@ -12153,6 +12188,16 @@
"null"
]
},
+ "failureReason": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/v2/McpServerStartupFailureReason"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"name": {
"type": "string"
},
@@ -12220,6 +12265,18 @@
},
"McpToolCallAppContext": {
"properties": {
+ "actionName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "appName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"connectorId": {
"type": "string"
},
@@ -12234,6 +12291,12 @@
"string",
"null"
]
+ },
+ "templateId": {
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
@@ -12788,14 +12851,45 @@
"title": "ModelVerificationNotification",
"type": "object"
},
+ "ModelsRequirements": {
+ "properties": {
+ "newThread": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/v2/NewThreadModelDefaults"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ },
+ "type": "object"
+ },
"MultiAgentMode": {
- "description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
- "enum": [
- "none",
- "explicitRequestOnly",
- "proactive"
- ],
- "type": "string"
+ "description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.",
+ "oneOf": [
+ {
+ "enum": [
+ "explicitRequestOnly",
+ "proactive"
+ ],
+ "type": "string"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "custom": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "custom"
+ ],
+ "title": "CustomMultiAgentMode",
+ "type": "object"
+ }
+ ]
},
"NetworkAccess": {
"enum": [
@@ -12935,6 +13029,33 @@
],
"type": "string"
},
+ "NewThreadModelDefaults": {
+ "properties": {
+ "model": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "modelReasoningEffort": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/v2/ReasoningEffort"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "serviceTier": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "type": "object"
+ },
"NonSteerableTurnKind": {
"enum": [
"review",
@@ -14109,6 +14230,40 @@
"title": "GitPluginSource",
"type": "object"
},
+ {
+ "properties": {
+ "package": {
+ "type": "string"
+ },
+ "registry": {
+ "description": "Optional HTTPS registry URL. Authentication stays in the user's npm config.",
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "type": {
+ "enum": [
+ "npm"
+ ],
+ "title": "NpmPluginSourceType",
+ "type": "string"
+ },
+ "version": {
+ "description": "Optional npm version or version range.",
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "required": [
+ "package",
+ "type"
+ ],
+ "title": "NpmPluginSource",
+ "type": "object"
+ },
{
"description": "The plugin is available in the remote catalog. Download metadata is kept server-side and is not exposed through the app-server API.",
"properties": {
@@ -14202,6 +14357,14 @@
},
"source": {
"$ref": "#/definitions/v2/PluginSource"
+ },
+ "version": {
+ "default": null,
+ "description": "Version advertised by the remote marketplace backend when available.",
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
@@ -14377,11 +14540,78 @@
],
"type": "string"
},
+ "RateLimitResetCredit": {
+ "properties": {
+ "description": {
+ "description": "Backend-provided display description for this credit, or `null` when unavailable.",
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "expiresAt": {
+ "description": "Unix timestamp in seconds when the credit expires, or `null` if it does not expire.",
+ "format": "int64",
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "grantedAt": {
+ "description": "Unix timestamp in seconds when the credit was granted.",
+ "format": "int64",
+ "type": "integer"
+ },
+ "id": {
+ "description": "Opaque backend identifier for this reset credit.",
+ "type": "string"
+ },
+ "resetType": {
+ "$ref": "#/definitions/v2/RateLimitResetType"
+ },
+ "status": {
+ "$ref": "#/definitions/v2/RateLimitResetCreditStatus"
+ },
+ "title": {
+ "description": "Backend-provided display title for this credit, or `null` when unavailable.",
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "required": [
+ "grantedAt",
+ "id",
+ "resetType",
+ "status"
+ ],
+ "type": "object"
+ },
+ "RateLimitResetCreditStatus": {
+ "enum": [
+ "available",
+ "redeeming",
+ "redeemed",
+ "unknown"
+ ],
+ "type": "string"
+ },
"RateLimitResetCreditsSummary": {
"properties": {
"availableCount": {
"format": "int64",
"type": "integer"
+ },
+ "credits": {
+ "description": "Detail rows for available reset credits, when the backend provides them.\n\n`null` means only `availableCount` is known, while an empty array means details were fetched and no available credits were returned. The backend may cap this list, so its length can be less than `availableCount`.",
+ "items": {
+ "$ref": "#/definitions/v2/RateLimitResetCredit"
+ },
+ "type": [
+ "array",
+ "null"
+ ]
}
},
"required": [
@@ -14389,6 +14619,13 @@
],
"type": "object"
},
+ "RateLimitResetType": {
+ "enum": [
+ "codexRateLimits",
+ "unknown"
+ ],
+ "type": "string"
+ },
"RateLimitSnapshot": {
"properties": {
"credits": {
@@ -15378,6 +15615,12 @@
"name": {
"type": "string"
},
+ "namespace": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"status": {
"type": [
"string",
@@ -17147,6 +17390,13 @@
"ephemeral": {
"type": "boolean"
},
+ "lastTurnId": {
+ "description": "Optional last turn id to fork through, inclusive.\n\nWhen specified, turns after `last_turn_id` are omitted from the fork. The referenced turn cannot be in progress.",
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"model": {
"description": "Configuration overrides for the forked thread, if any.",
"type": [
@@ -17466,6 +17716,13 @@
"title": "ThreadGoalUpdatedNotification",
"type": "object"
},
+ "ThreadHistoryMode": {
+ "enum": [
+ "legacy",
+ "paginated"
+ ],
+ "type": "string"
+ },
"ThreadId": {
"type": "string"
},
@@ -18985,6 +19242,7 @@
},
"ThreadRollbackParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
+ "description": "DEPRECATED: `thread/rollback` will be removed soon.",
"properties": {
"numTurns": {
"description": "The number of turns to drop from the end of the thread. Must be >= 1.\n\nThis only modifies the thread's history and does not revert local file changes that have been made by the agent. Clients are responsible for reverting these changes.",
diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json
index 0df923a0f4..b428f25258 100644
--- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json
+++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json
@@ -4654,6 +4654,16 @@
"object",
"null"
]
+ },
+ "models": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/ModelsRequirements"
+ },
+ {
+ "type": "null"
+ }
+ ]
}
},
"type": "object"
@@ -4918,6 +4928,13 @@
"ConsumeAccountRateLimitResetCreditParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
+ "creditId": {
+ "description": "Opaque reset-credit identifier to redeem. When omitted, the backend selects the next available credit.",
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"idempotencyKey": {
"description": "Identifies one logical reset attempt. A UUID is recommended; reuse the same value when retrying that attempt.",
"type": "string"
@@ -8430,6 +8447,12 @@
},
"success": {
"type": "boolean"
+ },
+ "threadId": {
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
@@ -8454,6 +8477,12 @@
"null"
]
},
+ "threadId": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"timeoutSecs": {
"format": "int64",
"type": [
@@ -8486,6 +8515,12 @@
"title": "McpServerRefreshResponse",
"type": "object"
},
+ "McpServerStartupFailureReason": {
+ "enum": [
+ "reauthenticationRequired"
+ ],
+ "type": "string"
+ },
"McpServerStartupState": {
"enum": [
"starting",
@@ -8557,6 +8592,16 @@
"null"
]
},
+ "failureReason": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/McpServerStartupFailureReason"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"name": {
"type": "string"
},
@@ -8624,6 +8669,18 @@
},
"McpToolCallAppContext": {
"properties": {
+ "actionName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "appName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"connectorId": {
"type": "string"
},
@@ -8638,6 +8695,12 @@
"string",
"null"
]
+ },
+ "templateId": {
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
@@ -9192,14 +9255,45 @@
"title": "ModelVerificationNotification",
"type": "object"
},
+ "ModelsRequirements": {
+ "properties": {
+ "newThread": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/NewThreadModelDefaults"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ },
+ "type": "object"
+ },
"MultiAgentMode": {
- "description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
- "enum": [
- "none",
- "explicitRequestOnly",
- "proactive"
- ],
- "type": "string"
+ "description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.",
+ "oneOf": [
+ {
+ "enum": [
+ "explicitRequestOnly",
+ "proactive"
+ ],
+ "type": "string"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "custom": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "custom"
+ ],
+ "title": "CustomMultiAgentMode",
+ "type": "object"
+ }
+ ]
},
"NetworkAccess": {
"enum": [
@@ -9339,6 +9433,33 @@
],
"type": "string"
},
+ "NewThreadModelDefaults": {
+ "properties": {
+ "model": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "modelReasoningEffort": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/ReasoningEffort"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "serviceTier": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "type": "object"
+ },
"NonSteerableTurnKind": {
"enum": [
"review",
@@ -10513,6 +10634,40 @@
"title": "GitPluginSource",
"type": "object"
},
+ {
+ "properties": {
+ "package": {
+ "type": "string"
+ },
+ "registry": {
+ "description": "Optional HTTPS registry URL. Authentication stays in the user's npm config.",
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "type": {
+ "enum": [
+ "npm"
+ ],
+ "title": "NpmPluginSourceType",
+ "type": "string"
+ },
+ "version": {
+ "description": "Optional npm version or version range.",
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "required": [
+ "package",
+ "type"
+ ],
+ "title": "NpmPluginSource",
+ "type": "object"
+ },
{
"description": "The plugin is available in the remote catalog. Download metadata is kept server-side and is not exposed through the app-server API.",
"properties": {
@@ -10606,6 +10761,14 @@
},
"source": {
"$ref": "#/definitions/PluginSource"
+ },
+ "version": {
+ "default": null,
+ "description": "Version advertised by the remote marketplace backend when available.",
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
@@ -10781,11 +10944,78 @@
],
"type": "string"
},
+ "RateLimitResetCredit": {
+ "properties": {
+ "description": {
+ "description": "Backend-provided display description for this credit, or `null` when unavailable.",
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "expiresAt": {
+ "description": "Unix timestamp in seconds when the credit expires, or `null` if it does not expire.",
+ "format": "int64",
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "grantedAt": {
+ "description": "Unix timestamp in seconds when the credit was granted.",
+ "format": "int64",
+ "type": "integer"
+ },
+ "id": {
+ "description": "Opaque backend identifier for this reset credit.",
+ "type": "string"
+ },
+ "resetType": {
+ "$ref": "#/definitions/RateLimitResetType"
+ },
+ "status": {
+ "$ref": "#/definitions/RateLimitResetCreditStatus"
+ },
+ "title": {
+ "description": "Backend-provided display title for this credit, or `null` when unavailable.",
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "required": [
+ "grantedAt",
+ "id",
+ "resetType",
+ "status"
+ ],
+ "type": "object"
+ },
+ "RateLimitResetCreditStatus": {
+ "enum": [
+ "available",
+ "redeeming",
+ "redeemed",
+ "unknown"
+ ],
+ "type": "string"
+ },
"RateLimitResetCreditsSummary": {
"properties": {
"availableCount": {
"format": "int64",
"type": "integer"
+ },
+ "credits": {
+ "description": "Detail rows for available reset credits, when the backend provides them.\n\n`null` means only `availableCount` is known, while an empty array means details were fetched and no available credits were returned. The backend may cap this list, so its length can be less than `availableCount`.",
+ "items": {
+ "$ref": "#/definitions/RateLimitResetCredit"
+ },
+ "type": [
+ "array",
+ "null"
+ ]
}
},
"required": [
@@ -10793,6 +11023,13 @@
],
"type": "object"
},
+ "RateLimitResetType": {
+ "enum": [
+ "codexRateLimits",
+ "unknown"
+ ],
+ "type": "string"
+ },
"RateLimitSnapshot": {
"properties": {
"credits": {
@@ -11782,6 +12019,12 @@
"name": {
"type": "string"
},
+ "namespace": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"status": {
"type": [
"string",
@@ -14926,6 +15169,13 @@
"ephemeral": {
"type": "boolean"
},
+ "lastTurnId": {
+ "description": "Optional last turn id to fork through, inclusive.\n\nWhen specified, turns after `last_turn_id` are omitted from the fork. The referenced turn cannot be in progress.",
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"model": {
"description": "Configuration overrides for the forked thread, if any.",
"type": [
@@ -15245,6 +15495,13 @@
"title": "ThreadGoalUpdatedNotification",
"type": "object"
},
+ "ThreadHistoryMode": {
+ "enum": [
+ "legacy",
+ "paginated"
+ ],
+ "type": "string"
+ },
"ThreadId": {
"type": "string"
},
@@ -16764,6 +17021,7 @@
},
"ThreadRollbackParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
+ "description": "DEPRECATED: `thread/rollback` will be removed soon.",
"properties": {
"numTurns": {
"description": "The number of turns to drop from the end of the thread. Must be >= 1.\n\nThis only modifies the thread's history and does not revert local file changes that have been made by the agent. Clients are responsible for reverting these changes.",
diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json
index 3fe6a89787..4e35f7aead 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json
@@ -169,6 +169,16 @@
"object",
"null"
]
+ },
+ "models": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/ModelsRequirements"
+ },
+ {
+ "type": "null"
+ }
+ ]
}
},
"type": "object"
@@ -362,6 +372,21 @@
],
"type": "object"
},
+ "ModelsRequirements": {
+ "properties": {
+ "newThread": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/NewThreadModelDefaults"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ },
+ "type": "object"
+ },
"NetworkDomainPermission": {
"enum": [
"allow",
@@ -484,6 +509,38 @@
],
"type": "string"
},
+ "NewThreadModelDefaults": {
+ "properties": {
+ "model": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "modelReasoningEffort": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/ReasoningEffort"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "serviceTier": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "type": "object"
+ },
+ "ReasoningEffort": {
+ "description": "A non-empty reasoning effort value advertised by the model.",
+ "minLength": 1,
+ "type": "string"
+ },
"ResidencyRequirement": {
"enum": [
"us"
diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConsumeAccountRateLimitResetCreditParams.json b/codex-rs/app-server-protocol/schema/json/v2/ConsumeAccountRateLimitResetCreditParams.json
index c9bf023a63..3d9d2c1e98 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/ConsumeAccountRateLimitResetCreditParams.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/ConsumeAccountRateLimitResetCreditParams.json
@@ -1,6 +1,13 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
+ "creditId": {
+ "description": "Opaque reset-credit identifier to redeem. When omitted, the backend selects the next available credit.",
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"idempotencyKey": {
"description": "Identifies one logical reset attempt. A UUID is recommended; reuse the same value when retrying that attempt.",
"type": "string"
diff --git a/codex-rs/app-server-protocol/schema/json/v2/GetAccountRateLimitsResponse.json b/codex-rs/app-server-protocol/schema/json/v2/GetAccountRateLimitsResponse.json
index 11b9712510..a2883a1293 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/GetAccountRateLimitsResponse.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/GetAccountRateLimitsResponse.json
@@ -49,11 +49,78 @@
],
"type": "string"
},
+ "RateLimitResetCredit": {
+ "properties": {
+ "description": {
+ "description": "Backend-provided display description for this credit, or `null` when unavailable.",
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "expiresAt": {
+ "description": "Unix timestamp in seconds when the credit expires, or `null` if it does not expire.",
+ "format": "int64",
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "grantedAt": {
+ "description": "Unix timestamp in seconds when the credit was granted.",
+ "format": "int64",
+ "type": "integer"
+ },
+ "id": {
+ "description": "Opaque backend identifier for this reset credit.",
+ "type": "string"
+ },
+ "resetType": {
+ "$ref": "#/definitions/RateLimitResetType"
+ },
+ "status": {
+ "$ref": "#/definitions/RateLimitResetCreditStatus"
+ },
+ "title": {
+ "description": "Backend-provided display title for this credit, or `null` when unavailable.",
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "required": [
+ "grantedAt",
+ "id",
+ "resetType",
+ "status"
+ ],
+ "type": "object"
+ },
+ "RateLimitResetCreditStatus": {
+ "enum": [
+ "available",
+ "redeeming",
+ "redeemed",
+ "unknown"
+ ],
+ "type": "string"
+ },
"RateLimitResetCreditsSummary": {
"properties": {
"availableCount": {
"format": "int64",
"type": "integer"
+ },
+ "credits": {
+ "description": "Detail rows for available reset credits, when the backend provides them.\n\n`null` means only `availableCount` is known, while an empty array means details were fetched and no available credits were returned. The backend may cap this list, so its length can be less than `availableCount`.",
+ "items": {
+ "$ref": "#/definitions/RateLimitResetCredit"
+ },
+ "type": [
+ "array",
+ "null"
+ ]
}
},
"required": [
@@ -61,6 +128,13 @@
],
"type": "object"
},
+ "RateLimitResetType": {
+ "enum": [
+ "codexRateLimits",
+ "unknown"
+ ],
+ "type": "string"
+ },
"RateLimitSnapshot": {
"properties": {
"credits": {
diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json
index 8f4b533d8f..dcf6312c7d 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json
@@ -299,6 +299,18 @@
},
"McpToolCallAppContext": {
"properties": {
+ "actionName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "appName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"connectorId": {
"type": "string"
},
@@ -313,6 +325,12 @@
"string",
"null"
]
+ },
+ "templateId": {
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json
index 4c930afde1..1933225ade 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json
@@ -299,6 +299,18 @@
},
"McpToolCallAppContext": {
"properties": {
+ "actionName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "appName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"connectorId": {
"type": "string"
},
@@ -313,6 +325,12 @@
"string",
"null"
]
+ },
+ "templateId": {
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
diff --git a/codex-rs/app-server-protocol/schema/json/v2/McpServerOauthLoginCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/McpServerOauthLoginCompletedNotification.json
index 35efd2baf2..6204ff672b 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/McpServerOauthLoginCompletedNotification.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/McpServerOauthLoginCompletedNotification.json
@@ -12,6 +12,12 @@
},
"success": {
"type": "boolean"
+ },
+ "threadId": {
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
diff --git a/codex-rs/app-server-protocol/schema/json/v2/McpServerOauthLoginParams.json b/codex-rs/app-server-protocol/schema/json/v2/McpServerOauthLoginParams.json
index 4370f444b9..de66dcefd6 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/McpServerOauthLoginParams.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/McpServerOauthLoginParams.json
@@ -13,6 +13,12 @@
"null"
]
},
+ "threadId": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"timeoutSecs": {
"format": "int64",
"type": [
diff --git a/codex-rs/app-server-protocol/schema/json/v2/McpServerStatusUpdatedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/McpServerStatusUpdatedNotification.json
index 87dab90cff..8efb36bdad 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/McpServerStatusUpdatedNotification.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/McpServerStatusUpdatedNotification.json
@@ -1,6 +1,12 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
+ "McpServerStartupFailureReason": {
+ "enum": [
+ "reauthenticationRequired"
+ ],
+ "type": "string"
+ },
"McpServerStartupState": {
"enum": [
"starting",
@@ -18,6 +24,16 @@
"null"
]
},
+ "failureReason": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/McpServerStartupFailureReason"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"name": {
"type": "string"
},
diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginInstalledResponse.json b/codex-rs/app-server-protocol/schema/json/v2/PluginInstalledResponse.json
index ce4ae5aa0b..55ad95be2e 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/PluginInstalledResponse.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/PluginInstalledResponse.json
@@ -413,6 +413,40 @@
"title": "GitPluginSource",
"type": "object"
},
+ {
+ "properties": {
+ "package": {
+ "type": "string"
+ },
+ "registry": {
+ "description": "Optional HTTPS registry URL. Authentication stays in the user's npm config.",
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "type": {
+ "enum": [
+ "npm"
+ ],
+ "title": "NpmPluginSourceType",
+ "type": "string"
+ },
+ "version": {
+ "description": "Optional npm version or version range.",
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "required": [
+ "package",
+ "type"
+ ],
+ "title": "NpmPluginSource",
+ "type": "object"
+ },
{
"description": "The plugin is available in the remote catalog. Download metadata is kept server-side and is not exposed through the app-server API.",
"properties": {
@@ -506,6 +540,14 @@
},
"source": {
"$ref": "#/definitions/PluginSource"
+ },
+ "version": {
+ "default": null,
+ "description": "Version advertised by the remote marketplace backend when available.",
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json
index 1002ad1a8d..25d12e50dd 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json
@@ -413,6 +413,40 @@
"title": "GitPluginSource",
"type": "object"
},
+ {
+ "properties": {
+ "package": {
+ "type": "string"
+ },
+ "registry": {
+ "description": "Optional HTTPS registry URL. Authentication stays in the user's npm config.",
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "type": {
+ "enum": [
+ "npm"
+ ],
+ "title": "NpmPluginSourceType",
+ "type": "string"
+ },
+ "version": {
+ "description": "Optional npm version or version range.",
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "required": [
+ "package",
+ "type"
+ ],
+ "title": "NpmPluginSource",
+ "type": "object"
+ },
{
"description": "The plugin is available in the remote catalog. Download metadata is kept server-side and is not exposed through the app-server API.",
"properties": {
@@ -506,6 +540,14 @@
},
"source": {
"$ref": "#/definitions/PluginSource"
+ },
+ "version": {
+ "default": null,
+ "description": "Version advertised by the remote marketplace backend when available.",
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json
index a2d2d442c5..b4b917602a 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json
@@ -553,6 +553,40 @@
"title": "GitPluginSource",
"type": "object"
},
+ {
+ "properties": {
+ "package": {
+ "type": "string"
+ },
+ "registry": {
+ "description": "Optional HTTPS registry URL. Authentication stays in the user's npm config.",
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "type": {
+ "enum": [
+ "npm"
+ ],
+ "title": "NpmPluginSourceType",
+ "type": "string"
+ },
+ "version": {
+ "description": "Optional npm version or version range.",
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "required": [
+ "package",
+ "type"
+ ],
+ "title": "NpmPluginSource",
+ "type": "object"
+ },
{
"description": "The plugin is available in the remote catalog. Download metadata is kept server-side and is not exposed through the app-server API.",
"properties": {
@@ -646,6 +680,14 @@
},
"source": {
"$ref": "#/definitions/PluginSource"
+ },
+ "version": {
+ "default": null,
+ "description": "Version advertised by the remote marketplace backend when available.",
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginShareListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/PluginShareListResponse.json
index be97e20b65..751a6c5e7f 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/PluginShareListResponse.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/PluginShareListResponse.json
@@ -369,6 +369,40 @@
"title": "GitPluginSource",
"type": "object"
},
+ {
+ "properties": {
+ "package": {
+ "type": "string"
+ },
+ "registry": {
+ "description": "Optional HTTPS registry URL. Authentication stays in the user's npm config.",
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "type": {
+ "enum": [
+ "npm"
+ ],
+ "title": "NpmPluginSourceType",
+ "type": "string"
+ },
+ "version": {
+ "description": "Optional npm version or version range.",
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "required": [
+ "package",
+ "type"
+ ],
+ "title": "NpmPluginSource",
+ "type": "object"
+ },
{
"description": "The plugin is available in the remote catalog. Download metadata is kept server-side and is not exposed through the app-server API.",
"properties": {
@@ -462,6 +496,14 @@
},
"source": {
"$ref": "#/definitions/PluginSource"
+ },
+ "version": {
+ "default": null,
+ "description": "Version advertised by the remote marketplace backend when available.",
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
diff --git a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json
index 01ac63c268..dad669b139 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json
@@ -751,6 +751,12 @@
"name": {
"type": "string"
},
+ "namespace": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"status": {
"type": [
"string",
diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json
index 16da9f205a..6851ddb487 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json
@@ -437,6 +437,18 @@
},
"McpToolCallAppContext": {
"properties": {
+ "actionName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "appName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"connectorId": {
"type": "string"
},
@@ -451,6 +463,12 @@
"string",
"null"
]
+ },
+ "templateId": {
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json
index ad0174cfe0..76278b106b 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json
@@ -126,6 +126,13 @@
"ephemeral": {
"type": "boolean"
},
+ "lastTurnId": {
+ "description": "Optional last turn id to fork through, inclusive.\n\nWhen specified, turns after `last_turn_id` are omitted from the fork. The referenced turn cannot be in progress.",
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"model": {
"description": "Configuration overrides for the forked thread, if any.",
"type": [
diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json
index 37a78533c2..59a14cc070 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json
@@ -541,6 +541,18 @@
},
"McpToolCallAppContext": {
"properties": {
+ "actionName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "appName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"connectorId": {
"type": "string"
},
@@ -555,6 +567,12 @@
"string",
"null"
]
+ },
+ "templateId": {
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
@@ -663,13 +681,29 @@
]
},
"MultiAgentMode": {
- "description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
- "enum": [
- "none",
- "explicitRequestOnly",
- "proactive"
- ],
- "type": "string"
+ "description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.",
+ "oneOf": [
+ {
+ "enum": [
+ "explicitRequestOnly",
+ "proactive"
+ ],
+ "type": "string"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "custom": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "custom"
+ ],
+ "title": "CustomMultiAgentMode",
+ "type": "object"
+ }
+ ]
},
"NetworkAccess": {
"enum": [
@@ -1170,6 +1204,13 @@
"description": "Extra app-server data for a thread.",
"type": "object"
},
+ "ThreadHistoryMode": {
+ "enum": [
+ "legacy",
+ "paginated"
+ ],
+ "type": "string"
+ },
"ThreadId": {
"type": "string"
},
diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json
index 67726c13a6..e5cbc2e606 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json
@@ -463,6 +463,18 @@
},
"McpToolCallAppContext": {
"properties": {
+ "actionName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "appName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"connectorId": {
"type": "string"
},
@@ -477,6 +489,12 @@
"string",
"null"
]
+ },
+ "templateId": {
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
@@ -977,6 +995,13 @@
"description": "Extra app-server data for a thread.",
"type": "object"
},
+ "ThreadHistoryMode": {
+ "enum": [
+ "legacy",
+ "paginated"
+ ],
+ "type": "string"
+ },
"ThreadId": {
"type": "string"
},
diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json
index cdd74a735a..cdd1d47a69 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json
@@ -463,6 +463,18 @@
},
"McpToolCallAppContext": {
"properties": {
+ "actionName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "appName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"connectorId": {
"type": "string"
},
@@ -477,6 +489,12 @@
"string",
"null"
]
+ },
+ "templateId": {
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
@@ -977,6 +995,13 @@
"description": "Extra app-server data for a thread.",
"type": "object"
},
+ "ThreadHistoryMode": {
+ "enum": [
+ "legacy",
+ "paginated"
+ ],
+ "type": "string"
+ },
"ThreadId": {
"type": "string"
},
diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json
index c3e388d1fd..613dd9648e 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json
@@ -463,6 +463,18 @@
},
"McpToolCallAppContext": {
"properties": {
+ "actionName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "appName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"connectorId": {
"type": "string"
},
@@ -477,6 +489,12 @@
"string",
"null"
]
+ },
+ "templateId": {
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
@@ -977,6 +995,13 @@
"description": "Extra app-server data for a thread.",
"type": "object"
},
+ "ThreadHistoryMode": {
+ "enum": [
+ "legacy",
+ "paginated"
+ ],
+ "type": "string"
+ },
"ThreadId": {
"type": "string"
},
diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json
index af078af68e..0cd7d9ae7b 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json
@@ -821,6 +821,12 @@
"name": {
"type": "string"
},
+ "namespace": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"status": {
"type": [
"string",
diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json
index ad44bf406b..80fbb183f8 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json
@@ -541,6 +541,18 @@
},
"McpToolCallAppContext": {
"properties": {
+ "actionName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "appName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"connectorId": {
"type": "string"
},
@@ -555,6 +567,12 @@
"string",
"null"
]
+ },
+ "templateId": {
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
@@ -663,13 +681,29 @@
]
},
"MultiAgentMode": {
- "description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
- "enum": [
- "none",
- "explicitRequestOnly",
- "proactive"
- ],
- "type": "string"
+ "description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.",
+ "oneOf": [
+ {
+ "enum": [
+ "explicitRequestOnly",
+ "proactive"
+ ],
+ "type": "string"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "custom": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "custom"
+ ],
+ "title": "CustomMultiAgentMode",
+ "type": "object"
+ }
+ ]
},
"NetworkAccess": {
"enum": [
@@ -1170,6 +1204,13 @@
"description": "Extra app-server data for a thread.",
"type": "object"
},
+ "ThreadHistoryMode": {
+ "enum": [
+ "legacy",
+ "paginated"
+ ],
+ "type": "string"
+ },
"ThreadId": {
"type": "string"
},
diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackParams.json
index cb3ba0db39..aa52fbd598 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackParams.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackParams.json
@@ -1,5 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
+ "description": "DEPRECATED: `thread/rollback` will be removed soon.",
"properties": {
"numTurns": {
"description": "The number of turns to drop from the end of the thread. Must be >= 1.\n\nThis only modifies the thread's history and does not revert local file changes that have been made by the agent. Clients are responsible for reverting these changes.",
diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json
index 5ae94d8d11..793f11c226 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json
@@ -463,6 +463,18 @@
},
"McpToolCallAppContext": {
"properties": {
+ "actionName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "appName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"connectorId": {
"type": "string"
},
@@ -477,6 +489,12 @@
"string",
"null"
]
+ },
+ "templateId": {
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
@@ -977,6 +995,13 @@
"description": "Extra app-server data for a thread.",
"type": "object"
},
+ "ThreadHistoryMode": {
+ "enum": [
+ "legacy",
+ "paginated"
+ ],
+ "type": "string"
+ },
"ThreadId": {
"type": "string"
},
diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadSettingsUpdatedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadSettingsUpdatedNotification.json
index 8d528246b5..f3296fa376 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/ThreadSettingsUpdatedNotification.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadSettingsUpdatedNotification.json
@@ -108,13 +108,29 @@
"type": "string"
},
"MultiAgentMode": {
- "description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
- "enum": [
- "none",
- "explicitRequestOnly",
- "proactive"
- ],
- "type": "string"
+ "description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.",
+ "oneOf": [
+ {
+ "enum": [
+ "explicitRequestOnly",
+ "proactive"
+ ],
+ "type": "string"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "custom": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "custom"
+ ],
+ "title": "CustomMultiAgentMode",
+ "type": "object"
+ }
+ ]
},
"NetworkAccess": {
"enum": [
diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json
index 50c0a9cf7f..3819f1f761 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json
@@ -195,13 +195,29 @@
"type": "string"
},
"MultiAgentMode": {
- "description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
- "enum": [
- "none",
- "explicitRequestOnly",
- "proactive"
- ],
- "type": "string"
+ "description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.",
+ "oneOf": [
+ {
+ "enum": [
+ "explicitRequestOnly",
+ "proactive"
+ ],
+ "type": "string"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "custom": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "custom"
+ ],
+ "title": "CustomMultiAgentMode",
+ "type": "object"
+ }
+ ]
},
"Personality": {
"enum": [
@@ -241,6 +257,13 @@
],
"type": "object"
},
+ "ThreadHistoryMode": {
+ "enum": [
+ "legacy",
+ "paginated"
+ ],
+ "type": "string"
+ },
"ThreadSource": {
"type": "string"
},
diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json
index df70c08c43..3ef1577038 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json
@@ -541,6 +541,18 @@
},
"McpToolCallAppContext": {
"properties": {
+ "actionName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "appName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"connectorId": {
"type": "string"
},
@@ -555,6 +567,12 @@
"string",
"null"
]
+ },
+ "templateId": {
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
@@ -663,13 +681,29 @@
]
},
"MultiAgentMode": {
- "description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
- "enum": [
- "none",
- "explicitRequestOnly",
- "proactive"
- ],
- "type": "string"
+ "description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.",
+ "oneOf": [
+ {
+ "enum": [
+ "explicitRequestOnly",
+ "proactive"
+ ],
+ "type": "string"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "custom": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "custom"
+ ],
+ "title": "CustomMultiAgentMode",
+ "type": "object"
+ }
+ ]
},
"NetworkAccess": {
"enum": [
@@ -1170,6 +1204,13 @@
"description": "Extra app-server data for a thread.",
"type": "object"
},
+ "ThreadHistoryMode": {
+ "enum": [
+ "legacy",
+ "paginated"
+ ],
+ "type": "string"
+ },
"ThreadId": {
"type": "string"
},
diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json
index 157f6193b4..a56d59c01d 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json
@@ -463,6 +463,18 @@
},
"McpToolCallAppContext": {
"properties": {
+ "actionName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "appName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"connectorId": {
"type": "string"
},
@@ -477,6 +489,12 @@
"string",
"null"
]
+ },
+ "templateId": {
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
@@ -977,6 +995,13 @@
"description": "Extra app-server data for a thread.",
"type": "object"
},
+ "ThreadHistoryMode": {
+ "enum": [
+ "legacy",
+ "paginated"
+ ],
+ "type": "string"
+ },
"ThreadId": {
"type": "string"
},
diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json
index 265578dcdd..6cc9b44cba 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json
@@ -463,6 +463,18 @@
},
"McpToolCallAppContext": {
"properties": {
+ "actionName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "appName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"connectorId": {
"type": "string"
},
@@ -477,6 +489,12 @@
"string",
"null"
]
+ },
+ "templateId": {
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
@@ -977,6 +995,13 @@
"description": "Extra app-server data for a thread.",
"type": "object"
},
+ "ThreadHistoryMode": {
+ "enum": [
+ "legacy",
+ "paginated"
+ ],
+ "type": "string"
+ },
"ThreadId": {
"type": "string"
},
diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json
index ae1b07cfc9..6ffb343595 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json
@@ -437,6 +437,18 @@
},
"McpToolCallAppContext": {
"properties": {
+ "actionName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "appName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"connectorId": {
"type": "string"
},
@@ -451,6 +463,12 @@
"string",
"null"
]
+ },
+ "templateId": {
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json
index 36b4d2a15d..cc4869e1d1 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json
@@ -141,13 +141,29 @@
"type": "string"
},
"MultiAgentMode": {
- "description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
- "enum": [
- "none",
- "explicitRequestOnly",
- "proactive"
- ],
- "type": "string"
+ "description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.",
+ "oneOf": [
+ {
+ "enum": [
+ "explicitRequestOnly",
+ "proactive"
+ ],
+ "type": "string"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "custom": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "custom"
+ ],
+ "title": "CustomMultiAgentMode",
+ "type": "object"
+ }
+ ]
},
"NetworkAccess": {
"enum": [
diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json
index 5853612dd9..805a5099a2 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json
@@ -437,6 +437,18 @@
},
"McpToolCallAppContext": {
"properties": {
+ "actionName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "appName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"connectorId": {
"type": "string"
},
@@ -451,6 +463,12 @@
"string",
"null"
]
+ },
+ "templateId": {
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json
index 7db58289ee..1f3f7c7036 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json
@@ -437,6 +437,18 @@
},
"McpToolCallAppContext": {
"properties": {
+ "actionName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "appName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"connectorId": {
"type": "string"
},
@@ -451,6 +463,12 @@
"string",
"null"
]
+ },
+ "templateId": {
+ "type": [
+ "string",
+ "null"
+ ]
}
},
"required": [
diff --git a/codex-rs/app-server-protocol/schema/typescript/MultiAgentMode.ts b/codex-rs/app-server-protocol/schema/typescript/MultiAgentMode.ts
index b59be94f8e..7784a6f5ca 100644
--- a/codex-rs/app-server-protocol/schema/typescript/MultiAgentMode.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/MultiAgentMode.ts
@@ -3,9 +3,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
- * Controls whether the model receives multi-agent delegation instructions and,
- * when it does, whether it should only spawn sub-agents after an explicit user
- * request or may delegate proactively when doing so would help. `none` leaves
- * the multi-agent tools available without injecting delegation instructions.
+ * Controls the effective multi-agent delegation instructions for a turn. `custom` means the
+ * configured mode hint defines the policy instead of a built-in policy.
*/
-export type MultiAgentMode = "none" | "explicitRequestOnly" | "proactive";
+export type MultiAgentMode = { "custom": string } | "explicitRequestOnly" | "proactive";
diff --git a/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts b/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts
index 62547d3e40..769b1b08fc 100644
--- a/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts
@@ -20,4 +20,4 @@ id?: string,
/**
* Set when using the Responses API.
*/
-call_id: string | null, status: LocalShellStatus, action: LocalShellAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call", id?: string, name: string, namespace?: string, arguments: string, call_id: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_call", id?: string, call_id: string | null, status?: string, execution: string, arguments: unknown, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call_output", id?: string, call_id: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call", id?: string, status?: string, call_id: string, name: string, input: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call_output", id?: string, call_id: string, name?: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_output", id?: string, call_id: string | null, status: string, execution: string, tools: unknown[], internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "web_search_call", id?: string, status?: string, action?: WebSearchAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "image_generation_call", id?: string, status: string, revised_prompt?: string, result: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction", id?: string, encrypted_content: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction_trigger", } | { "type": "context_compaction", id?: string, encrypted_content?: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "other" };
+call_id: string | null, status: LocalShellStatus, action: LocalShellAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call", id?: string, name: string, namespace?: string, arguments: string, call_id: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_call", id?: string, call_id: string | null, status?: string, execution: string, arguments: unknown, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call_output", id?: string, call_id: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call", id?: string, status?: string, call_id: string, name: string, namespace?: string, input: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call_output", id?: string, call_id: string, name?: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_output", id?: string, call_id: string | null, status: string, execution: string, tools: unknown[], internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "web_search_call", id?: string, status?: string, action?: WebSearchAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "image_generation_call", id?: string, status: string, revised_prompt?: string, result: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction", id?: string, encrypted_content: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction_trigger", } | { "type": "context_compaction", id?: string, encrypted_content?: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "other" };
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts
index 3eb9a65aa6..e2fd573a32 100644
--- a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts
@@ -4,8 +4,9 @@
import type { WebSearchMode } from "../WebSearchMode";
import type { AskForApproval } from "./AskForApproval";
import type { ComputerUseRequirements } from "./ComputerUseRequirements";
+import type { ModelsRequirements } from "./ModelsRequirements";
import type { ResidencyRequirement } from "./ResidencyRequirement";
import type { SandboxMode } from "./SandboxMode";
import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode";
-export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWindowsSandboxImplementations: Array | null, allowedPermissionProfiles: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, allowRemoteControl: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null};
+export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWindowsSandboxImplementations: Array | null, allowedPermissionProfiles: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, allowRemoteControl: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null, models: ModelsRequirements | null};
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ConsumeAccountRateLimitResetCreditParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConsumeAccountRateLimitResetCreditParams.ts
index c3cef64f36..f1c5bf351e 100644
--- a/codex-rs/app-server-protocol/schema/typescript/v2/ConsumeAccountRateLimitResetCreditParams.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConsumeAccountRateLimitResetCreditParams.ts
@@ -7,4 +7,9 @@ export type ConsumeAccountRateLimitResetCreditParams = {
* Identifies one logical reset attempt. A UUID is recommended; reuse the same value when
* retrying that attempt.
*/
-idempotencyKey: string, };
+idempotencyKey: string,
+/**
+ * Opaque reset-credit identifier to redeem. When omitted, the backend selects the next
+ * available credit.
+ */
+creditId?: string | null, };
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpServerOauthLoginCompletedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerOauthLoginCompletedNotification.ts
index 592860ae39..cfa6603094 100644
--- a/codex-rs/app-server-protocol/schema/typescript/v2/McpServerOauthLoginCompletedNotification.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerOauthLoginCompletedNotification.ts
@@ -2,4 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-export type McpServerOauthLoginCompletedNotification = { name: string, success: boolean, error?: string, };
+export type McpServerOauthLoginCompletedNotification = { name: string, threadId: string | null, success: boolean, error?: string, };
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpServerOauthLoginParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerOauthLoginParams.ts
index a61c304609..ff088b8d9f 100644
--- a/codex-rs/app-server-protocol/schema/typescript/v2/McpServerOauthLoginParams.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerOauthLoginParams.ts
@@ -2,4 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-export type McpServerOauthLoginParams = { name: string, scopes?: Array | null, timeoutSecs?: bigint | null, };
+export type McpServerOauthLoginParams = { name: string, threadId?: string | null, scopes?: Array | null, timeoutSecs?: bigint | null, };
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStartupFailureReason.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStartupFailureReason.ts
new file mode 100644
index 0000000000..0373e5444b
--- /dev/null
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStartupFailureReason.ts
@@ -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 McpServerStartupFailureReason = "reauthenticationRequired";
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStatusUpdatedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStatusUpdatedNotification.ts
index b5c0a9058c..fd192f22c6 100644
--- a/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStatusUpdatedNotification.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStatusUpdatedNotification.ts
@@ -1,6 +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 { McpServerStartupFailureReason } from "./McpServerStartupFailureReason";
import type { McpServerStartupState } from "./McpServerStartupState";
-export type McpServerStatusUpdatedNotification = { threadId: string | null, name: string, status: McpServerStartupState, error: string | null, };
+export type McpServerStatusUpdatedNotification = { threadId: string | null, name: string, status: McpServerStartupState, error: string | null, failureReason: McpServerStartupFailureReason | null, };
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpToolCallAppContext.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpToolCallAppContext.ts
index 0a8343428d..e4bc5a11d9 100644
--- a/codex-rs/app-server-protocol/schema/typescript/v2/McpToolCallAppContext.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpToolCallAppContext.ts
@@ -2,4 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-export type McpToolCallAppContext = { connectorId: string, linkId: string | null, resourceUri: string | null, };
+export type McpToolCallAppContext = { connectorId: string, linkId: string | null, resourceUri: string | null, appName: string | null, templateId: string | null, actionName: string | null, };
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ModelsRequirements.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ModelsRequirements.ts
new file mode 100644
index 0000000000..9041fff834
--- /dev/null
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/ModelsRequirements.ts
@@ -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 { NewThreadModelDefaults } from "./NewThreadModelDefaults";
+
+export type ModelsRequirements = { newThread: NewThreadModelDefaults | null, };
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/NewThreadModelDefaults.ts b/codex-rs/app-server-protocol/schema/typescript/v2/NewThreadModelDefaults.ts
new file mode 100644
index 0000000000..fed8b25eaa
--- /dev/null
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/NewThreadModelDefaults.ts
@@ -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 { ReasoningEffort } from "../ReasoningEffort";
+
+export type NewThreadModelDefaults = { model: string | null, modelReasoningEffort: ReasoningEffort | null, serviceTier: string | null, };
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PluginSource.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PluginSource.ts
index f6e867195d..c7ba3bcf1c 100644
--- a/codex-rs/app-server-protocol/schema/typescript/v2/PluginSource.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/PluginSource.ts
@@ -3,4 +3,12 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
-export type PluginSource = { "type": "local", path: AbsolutePathBuf, } | { "type": "git", url: string, path: string | null, refName: string | null, sha: string | null, } | { "type": "remote" };
+export type PluginSource = { "type": "local", path: AbsolutePathBuf, } | { "type": "git", url: string, path: string | null, refName: string | null, sha: string | null, } | { "type": "npm", package: string,
+/**
+ * Optional npm version or version range.
+ */
+version: string | null,
+/**
+ * Optional HTTPS registry URL. Authentication stays in the user's npm config.
+ */
+registry: string | null, } | { "type": "remote" };
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PluginSummary.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PluginSummary.ts
index 268349cb9b..d12f55b3a3 100644
--- a/codex-rs/app-server-protocol/schema/typescript/v2/PluginSummary.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/PluginSummary.ts
@@ -13,6 +13,10 @@ export type PluginSummary = { id: string,
* Backend remote plugin identifier when available.
*/
remotePluginId: string | null,
+/**
+ * Version advertised by the remote marketplace backend when available.
+ */
+version: string | null,
/**
* Version of the locally materialized plugin package when available.
*/
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCredit.ts b/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCredit.ts
new file mode 100644
index 0000000000..514c1558a2
--- /dev/null
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCredit.ts
@@ -0,0 +1,27 @@
+// 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 { RateLimitResetCreditStatus } from "./RateLimitResetCreditStatus";
+import type { RateLimitResetType } from "./RateLimitResetType";
+
+export type RateLimitResetCredit = {
+/**
+ * Opaque backend identifier for this reset credit.
+ */
+id: string, resetType: RateLimitResetType, status: RateLimitResetCreditStatus,
+/**
+ * Unix timestamp in seconds when the credit was granted.
+ */
+grantedAt: number,
+/**
+ * Unix timestamp in seconds when the credit expires, or `null` if it does not expire.
+ */
+expiresAt: number | null,
+/**
+ * Backend-provided display title for this credit, or `null` when unavailable.
+ */
+title: string | null,
+/**
+ * Backend-provided display description for this credit, or `null` when unavailable.
+ */
+description: string | null, };
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCreditStatus.ts b/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCreditStatus.ts
new file mode 100644
index 0000000000..fa15861b3a
--- /dev/null
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCreditStatus.ts
@@ -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 RateLimitResetCreditStatus = "available" | "redeeming" | "redeemed" | "unknown";
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCreditsSummary.ts b/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCreditsSummary.ts
index e42dd8a703..46a8eee2e7 100644
--- a/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCreditsSummary.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCreditsSummary.ts
@@ -1,5 +1,14 @@
// 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 { RateLimitResetCredit } from "./RateLimitResetCredit";
-export type RateLimitResetCreditsSummary = { availableCount: bigint, };
+export type RateLimitResetCreditsSummary = { availableCount: bigint,
+/**
+ * Detail rows for available reset credits, when the backend provides them.
+ *
+ * `null` means only `availableCount` is known, while an empty array means details were fetched
+ * and no available credits were returned. The backend may cap this list, so its length can be
+ * less than `availableCount`.
+ */
+credits: Array | null, };
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetType.ts b/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetType.ts
new file mode 100644
index 0000000000..718145bf47
--- /dev/null
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetType.ts
@@ -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 RateLimitResetType = "codexRateLimits" | "unknown";
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts
index c5109b2c7d..3ace4d4417 100644
--- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts
@@ -18,6 +18,12 @@ import type { ThreadSource } from "./ThreadSource";
* Prefer using thread_id whenever possible.
*/
export type ThreadForkParams = {threadId: string, /**
+ * Optional last turn id to fork through, inclusive.
+ *
+ * When specified, turns after `last_turn_id` are omitted from the fork.
+ * The referenced turn cannot be in progress.
+ */
+lastTurnId?: string | null, /**
* Configuration overrides for the forked thread, if any.
*/
model?: string | null, modelProvider?: string | null, serviceTier?: string | null | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, /**
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadHistoryMode.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadHistoryMode.ts
new file mode 100644
index 0000000000..db0f2d824e
--- /dev/null
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadHistoryMode.ts
@@ -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 ThreadHistoryMode = "legacy" | "paginated";
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRollbackParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRollbackParams.ts
index 1c938e3bfd..af416d1872 100644
--- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRollbackParams.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRollbackParams.ts
@@ -2,6 +2,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+/**
+ * DEPRECATED: `thread/rollback` will be removed soon.
+ */
export type ThreadRollbackParams = { threadId: string,
/**
* The number of turns to drop from the end of the thread. Must be >= 1.
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts
index ff7df9e9ec..c335f144ec 100644
--- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts
@@ -238,6 +238,7 @@ export type { McpServerOauthLoginCompletedNotification } from "./McpServerOauthL
export type { McpServerOauthLoginParams } from "./McpServerOauthLoginParams";
export type { McpServerOauthLoginResponse } from "./McpServerOauthLoginResponse";
export type { McpServerRefreshResponse } from "./McpServerRefreshResponse";
+export type { McpServerStartupFailureReason } from "./McpServerStartupFailureReason";
export type { McpServerStartupState } from "./McpServerStartupState";
export type { McpServerStatus } from "./McpServerStatus";
export type { McpServerStatusDetail } from "./McpServerStatusDetail";
@@ -266,6 +267,7 @@ export type { ModelServiceTier } from "./ModelServiceTier";
export type { ModelUpgradeInfo } from "./ModelUpgradeInfo";
export type { ModelVerification } from "./ModelVerification";
export type { ModelVerificationNotification } from "./ModelVerificationNotification";
+export type { ModelsRequirements } from "./ModelsRequirements";
export type { NetworkAccess } from "./NetworkAccess";
export type { NetworkApprovalContext } from "./NetworkApprovalContext";
export type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol";
@@ -274,6 +276,7 @@ export type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment";
export type { NetworkPolicyRuleAction } from "./NetworkPolicyRuleAction";
export type { NetworkRequirements } from "./NetworkRequirements";
export type { NetworkUnixSocketPermission } from "./NetworkUnixSocketPermission";
+export type { NewThreadModelDefaults } from "./NewThreadModelDefaults";
export type { NonSteerableTurnKind } from "./NonSteerableTurnKind";
export type { OverriddenMetadata } from "./OverriddenMetadata";
export type { PatchApplyStatus } from "./PatchApplyStatus";
@@ -332,7 +335,10 @@ export type { ProcessOutputDeltaNotification } from "./ProcessOutputDeltaNotific
export type { ProcessOutputStream } from "./ProcessOutputStream";
export type { ProcessTerminalSize } from "./ProcessTerminalSize";
export type { RateLimitReachedType } from "./RateLimitReachedType";
+export type { RateLimitResetCredit } from "./RateLimitResetCredit";
+export type { RateLimitResetCreditStatus } from "./RateLimitResetCreditStatus";
export type { RateLimitResetCreditsSummary } from "./RateLimitResetCreditsSummary";
+export type { RateLimitResetType } from "./RateLimitResetType";
export type { RateLimitSnapshot } from "./RateLimitSnapshot";
export type { RateLimitWindow } from "./RateLimitWindow";
export type { RawResponseItemCompletedNotification } from "./RawResponseItemCompletedNotification";
@@ -409,6 +415,7 @@ export type { ThreadGoalSetParams } from "./ThreadGoalSetParams";
export type { ThreadGoalSetResponse } from "./ThreadGoalSetResponse";
export type { ThreadGoalStatus } from "./ThreadGoalStatus";
export type { ThreadGoalUpdatedNotification } from "./ThreadGoalUpdatedNotification";
+export type { ThreadHistoryMode } from "./ThreadHistoryMode";
export type { ThreadInjectItemsParams } from "./ThreadInjectItemsParams";
export type { ThreadInjectItemsResponse } from "./ThreadInjectItemsResponse";
export type { ThreadItem } from "./ThreadItem";
diff --git a/codex-rs/app-server-protocol/src/export.rs b/codex-rs/app-server-protocol/src/export.rs
index 91eca690f1..9f27dff7fa 100644
--- a/codex-rs/app-server-protocol/src/export.rs
+++ b/codex-rs/app-server-protocol/src/export.rs
@@ -43,6 +43,8 @@ pub(crate) const GENERATED_TS_HEADER: &str = "// GENERATED CODE! DO NOT MODIFY B
const IGNORED_DEFINITIONS: &[&str] = &["Option<()>"];
const JSON_V1_ALLOWLIST: &[&str] = &["InitializeParams", "InitializeResponse"];
const EXPERIMENTAL_CLIENT_METHOD_DEPENDENCY_TYPES: &[&str] = &[
+ "EnvironmentShellInfo",
+ "PathUri",
"RemoteControlClient",
"RemoteControlClientsListOrder",
"ThreadBackgroundTerminal",
diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs
index 51a37fbaa9..c4b94d8b3f 100644
--- a/codex-rs/app-server-protocol/src/protocol/common.rs
+++ b/codex-rs/app-server-protocol/src/protocol/common.rs
@@ -943,6 +943,13 @@ client_request_definitions! {
serialization: global("environment"),
response: v2::EnvironmentAddResponse,
},
+ #[experimental("environment/info")]
+ /// Reads information from a configured execution environment.
+ EnvironmentInfo => "environment/info" {
+ params: v2::EnvironmentInfoParams,
+ serialization: global_shared_read("environment"),
+ response: v2::EnvironmentInfoResponse,
+ },
McpServerOauthLogin => "mcpServer/oauth/login" {
params: v2::McpServerOauthLoginParams,
@@ -1913,6 +1920,7 @@ mod tests {
request_id: request_id(),
params: v2::McpServerOauthLoginParams {
name: "server-a".to_string(),
+ thread_id: None,
scopes: None,
timeout_secs: None,
},
@@ -2579,6 +2587,7 @@ mod tests {
parent_thread_id: None,
preview: "first prompt".to_string(),
ephemeral: true,
+ history_mode: Default::default(),
model_provider: "openai".to_string(),
created_at: 1,
updated_at: 2,
@@ -2629,6 +2638,7 @@ mod tests {
"parentThreadId": null,
"preview": "first prompt",
"ephemeral": true,
+ "historyMode": "legacy",
"modelProvider": "openai",
"createdAt": 1,
"updatedAt": 2,
@@ -3185,6 +3195,7 @@ mod tests {
request_id: RequestId::Integer(9),
params: v2::ThreadRealtimeStartParams {
client_managed_handoffs: Some(true),
+ flush_transcript_tail_on_session_end: Some(true),
codex_responses_as_items: None,
codex_response_item_prefix: None,
codex_response_handoff_prefix: Some("silent context".to_string()),
@@ -3206,6 +3217,7 @@ mod tests {
"params": {
"threadId": "thr_123",
"clientManagedHandoffs": true,
+ "flushTranscriptTailOnSessionEnd": true,
"codexResponsesAsItems": null,
"codexResponseItemPrefix": null,
"codexResponseHandoffPrefix": "silent context",
@@ -3230,6 +3242,7 @@ mod tests {
request_id: RequestId::Integer(9),
params: v2::ThreadRealtimeStartParams {
client_managed_handoffs: None,
+ flush_transcript_tail_on_session_end: None,
codex_responses_as_items: None,
codex_response_item_prefix: None,
codex_response_handoff_prefix: None,
@@ -3251,6 +3264,7 @@ mod tests {
"params": {
"threadId": "thr_123",
"clientManagedHandoffs": null,
+ "flushTranscriptTailOnSessionEnd": null,
"codexResponsesAsItems": null,
"codexResponseItemPrefix": null,
"codexResponseHandoffPrefix": null,
@@ -3270,6 +3284,7 @@ mod tests {
request_id: RequestId::Integer(9),
params: v2::ThreadRealtimeStartParams {
client_managed_handoffs: None,
+ flush_transcript_tail_on_session_end: None,
codex_responses_as_items: None,
codex_response_item_prefix: None,
codex_response_handoff_prefix: None,
@@ -3291,6 +3306,7 @@ mod tests {
"params": {
"threadId": "thr_123",
"clientManagedHandoffs": null,
+ "flushTranscriptTailOnSessionEnd": null,
"codexResponsesAsItems": null,
"codexResponseItemPrefix": null,
"codexResponseHandoffPrefix": null,
@@ -3508,6 +3524,7 @@ mod tests {
request_id: RequestId::Integer(1),
params: v2::ThreadRealtimeStartParams {
client_managed_handoffs: None,
+ flush_transcript_tail_on_session_end: None,
codex_responses_as_items: None,
codex_response_item_prefix: None,
codex_response_handoff_prefix: None,
diff --git a/codex-rs/app-server-protocol/src/protocol/item_builders.rs b/codex-rs/app-server-protocol/src/protocol/item_builders.rs
index 66019980a3..c041fe9621 100644
--- a/codex-rs/app-server-protocol/src/protocol/item_builders.rs
+++ b/codex-rs/app-server-protocol/src/protocol/item_builders.rs
@@ -128,7 +128,10 @@ pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> Thread
}
}
-fn command_actions_for_path_uri(parsed_cmd: &[ParsedCommand], cwd: &PathUri) -> Vec {
+pub(crate) fn command_actions_for_path_uri(
+ parsed_cmd: &[ParsedCommand],
+ cwd: &PathUri,
+) -> Vec {
// TODO(anp): Carry PathUri into CommandAction so foreign Read actions retain resolved paths.
// Until then, omit those actions rather than project a foreign cwd onto the host.
let native_cwd = if cwd.infer_path_convention() == Some(PathConvention::native()) {
diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history.rs b/codex-rs/app-server-protocol/src/protocol/thread_history.rs
index 98702267ab..2d9c6b6a1a 100644
--- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs
+++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs
@@ -574,52 +574,25 @@ impl ThreadHistoryBuilder {
}
fn handle_item_started(&mut self, payload: &ItemStartedEvent) {
- match &payload.item {
- codex_protocol::items::TurnItem::Plan(plan) => {
- if plan.text.is_empty() {
- return;
- }
- self.upsert_item_in_turn_id(
- &payload.turn_id,
- ThreadItem::from(payload.item.clone()),
- );
- }
- codex_protocol::items::TurnItem::Sleep(_) => {
- self.upsert_item_in_turn_id(
- &payload.turn_id,
- ThreadItem::from(payload.item.clone()),
- );
- }
- codex_protocol::items::TurnItem::UserMessage(_)
- | codex_protocol::items::TurnItem::HookPrompt(_)
- | codex_protocol::items::TurnItem::AgentMessage(_)
- | codex_protocol::items::TurnItem::Reasoning(_)
- | codex_protocol::items::TurnItem::WebSearch(_)
- | codex_protocol::items::TurnItem::ImageView(_)
- | codex_protocol::items::TurnItem::ImageGeneration(_)
- | codex_protocol::items::TurnItem::FileChange(_)
- | codex_protocol::items::TurnItem::McpToolCall(_)
- | codex_protocol::items::TurnItem::ContextCompaction(_) => {}
- }
+ self.handle_materialized_item_lifecycle(&payload.turn_id, &payload.item);
}
fn handle_item_completed(&mut self, payload: &ItemCompletedEvent) {
- match &payload.item {
- codex_protocol::items::TurnItem::Plan(plan) => {
- if plan.text.is_empty() {
- return;
- }
- self.upsert_item_in_turn_id(
- &payload.turn_id,
- ThreadItem::from(payload.item.clone()),
- );
- }
- codex_protocol::items::TurnItem::Sleep(_) => {
- self.upsert_item_in_turn_id(
- &payload.turn_id,
- ThreadItem::from(payload.item.clone()),
- );
- }
+ self.handle_materialized_item_lifecycle(&payload.turn_id, &payload.item);
+ }
+
+ fn handle_materialized_item_lifecycle(
+ &mut self,
+ turn_id: &str,
+ item: &codex_protocol::items::TurnItem,
+ ) {
+ let should_upsert = match item {
+ codex_protocol::items::TurnItem::Plan(plan) => !plan.text.is_empty(),
+ codex_protocol::items::TurnItem::Sleep(_)
+ | codex_protocol::items::TurnItem::CommandExecution(_)
+ | codex_protocol::items::TurnItem::DynamicToolCall(_)
+ | codex_protocol::items::TurnItem::CollabAgentToolCall(_)
+ | codex_protocol::items::TurnItem::SubAgentActivity(_) => true,
codex_protocol::items::TurnItem::UserMessage(_)
| codex_protocol::items::TurnItem::HookPrompt(_)
| codex_protocol::items::TurnItem::AgentMessage(_)
@@ -629,7 +602,11 @@ impl ThreadHistoryBuilder {
| codex_protocol::items::TurnItem::ImageGeneration(_)
| codex_protocol::items::TurnItem::FileChange(_)
| codex_protocol::items::TurnItem::McpToolCall(_)
- | codex_protocol::items::TurnItem::ContextCompaction(_) => {}
+ | codex_protocol::items::TurnItem::ContextCompaction(_) => false,
+ };
+
+ if should_upsert {
+ self.upsert_item_in_turn_id(turn_id, ThreadItem::from(item.clone()));
}
}
@@ -775,6 +752,9 @@ impl ThreadHistoryBuilder {
connector_id,
link_id: payload.link_id.clone(),
resource_uri: payload.mcp_app_resource_uri.clone(),
+ app_name: payload.app_name.clone(),
+ template_id: payload.template_id.clone(),
+ action_name: payload.action_name.clone(),
}),
mcp_app_resource_uri: payload.mcp_app_resource_uri.clone(),
plugin_id: payload.plugin_id.clone(),
@@ -825,6 +805,9 @@ impl ThreadHistoryBuilder {
connector_id,
link_id: payload.link_id.clone(),
resource_uri: payload.mcp_app_resource_uri.clone(),
+ app_name: payload.app_name.clone(),
+ template_id: payload.template_id.clone(),
+ action_name: payload.action_name.clone(),
}),
mcp_app_resource_uri: payload.mcp_app_resource_uri.clone(),
plugin_id: payload.plugin_id.clone(),
@@ -1567,6 +1550,8 @@ mod tests {
use crate::protocol::v2::CommandExecutionSource;
use codex_protocol::ThreadId;
use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem;
+ use codex_protocol::items::CommandExecutionItem as CoreCommandExecutionItem;
+ use codex_protocol::items::CommandExecutionStatus as CoreCommandExecutionStatus;
use codex_protocol::items::HookPromptFragment as CoreHookPromptFragment;
use codex_protocol::items::SleepItem as CoreSleepItem;
use codex_protocol::items::TurnItem as CoreTurnItem;
@@ -1861,6 +1846,77 @@ mod tests {
);
}
+ #[test]
+ fn rebuilds_command_execution_item_from_persisted_completion() {
+ let turn_id = "turn-1";
+ let thread_id = ThreadId::new();
+ let command_item = CoreTurnItem::CommandExecution(CoreCommandExecutionItem {
+ id: "exec-1".to_string(),
+ process_id: Some("pid-1".to_string()),
+ command: vec!["echo".to_string(), "hello world".to_string()],
+ cwd: test_path_buf("/tmp").abs().into(),
+ parsed_cmd: vec![ParsedCommand::Unknown {
+ cmd: "echo hello world".to_string(),
+ }],
+ source: ExecCommandSource::Agent,
+ interaction_input: None,
+ status: CoreCommandExecutionStatus::Completed,
+ stdout: Some("hello world\n".to_string()),
+ stderr: Some(String::new()),
+ aggregated_output: Some("hello world\n".to_string()),
+ exit_code: Some(0),
+ duration: Some(Duration::from_millis(12)),
+ formatted_output: Some("hello world\n".to_string()),
+ });
+ let events = vec![
+ EventMsg::TurnStarted(TurnStartedEvent {
+ turn_id: turn_id.to_string(),
+ trace_id: None,
+ started_at: None,
+ model_context_window: None,
+ collaboration_mode_kind: Default::default(),
+ }),
+ EventMsg::ItemCompleted(ItemCompletedEvent {
+ thread_id,
+ turn_id: turn_id.to_string(),
+ item: command_item,
+ completed_at_ms: 1_000,
+ }),
+ EventMsg::TurnComplete(TurnCompleteEvent {
+ turn_id: turn_id.to_string(),
+ last_agent_message: None,
+ completed_at: None,
+ duration_ms: None,
+ time_to_first_token_ms: None,
+ }),
+ ];
+
+ let items = events
+ .into_iter()
+ .map(RolloutItem::EventMsg)
+ .collect::>();
+ let turns = build_turns_from_rollout_items(&items);
+
+ assert_eq!(turns.len(), 1);
+ assert_eq!(
+ turns[0].items,
+ vec![ThreadItem::CommandExecution {
+ id: "exec-1".to_string(),
+ command: "echo 'hello world'".to_string(),
+ cwd: test_path_buf("/tmp").abs().into(),
+ process_id: Some("pid-1".to_string()),
+ source: CommandExecutionSource::Agent,
+ status: CommandExecutionStatus::Completed,
+ command_actions: vec![CommandAction::Unknown {
+ command: "echo hello world".to_string(),
+ }],
+ aggregated_output: Some("hello world\n".to_string()),
+ exit_code: Some(0),
+ duration_ms: Some(12),
+ }]
+ );
+ }
+
#[test]
fn preserves_user_message_client_id_from_legacy_event() {
let turn_id = "turn-1";
@@ -2418,6 +2474,9 @@ mod tests {
connector_id: None,
mcp_app_resource_uri: None,
link_id: None,
+ app_name: None,
+ template_id: None,
+ action_name: None,
plugin_id: None,
duration: Duration::from_millis(8),
result: Err("boom".into()),
@@ -2499,6 +2558,9 @@ mod tests {
connector_id: Some("calendar".into()),
mcp_app_resource_uri: Some("ui://widget/lookup.html".into()),
link_id: Some("link_calendar".into()),
+ app_name: Some("Calendar".into()),
+ template_id: Some("calendar_template".into()),
+ action_name: Some("lookup".into()),
plugin_id: Some("sample@test".into()),
duration: Duration::from_millis(8),
result: Ok(CallToolResult {
@@ -2533,6 +2595,9 @@ mod tests {
connector_id: "calendar".into(),
link_id: Some("link_calendar".into()),
resource_uri: Some("ui://widget/lookup.html".into()),
+ app_name: Some("Calendar".into()),
+ template_id: Some("calendar_template".into()),
+ action_name: Some("lookup".into()),
}),
mcp_app_resource_uri: Some("ui://widget/lookup.html".into()),
plugin_id: Some("sample@test".into()),
diff --git a/codex-rs/app-server-protocol/src/protocol/v2/account.rs b/codex-rs/app-server-protocol/src/protocol/v2/account.rs
index 635d001bba..788c8f7aaf 100644
--- a/codex-rs/app-server-protocol/src/protocol/v2/account.rs
+++ b/codex-rs/app-server-protocol/src/protocol/v2/account.rs
@@ -284,6 +284,52 @@ pub struct GetAccountRateLimitsResponse {
#[ts(export_to = "v2/")]
pub struct RateLimitResetCreditsSummary {
pub available_count: i64,
+ /// Detail rows for available reset credits, when the backend provides them.
+ ///
+ /// `null` means only `availableCount` is known, while an empty array means details were fetched
+ /// and no available credits were returned. The backend may cap this list, so its length can be
+ /// less than `availableCount`.
+ pub credits: Option>,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export_to = "v2/")]
+pub struct RateLimitResetCredit {
+ /// Opaque backend identifier for this reset credit.
+ pub id: String,
+ pub reset_type: RateLimitResetType,
+ pub status: RateLimitResetCreditStatus,
+ /// Unix timestamp in seconds when the credit was granted.
+ #[ts(type = "number")]
+ pub granted_at: i64,
+ /// Unix timestamp in seconds when the credit expires, or `null` if it does not expire.
+ #[ts(type = "number | null")]
+ pub expires_at: Option,
+ /// Backend-provided display title for this credit, or `null` when unavailable.
+ pub title: Option,
+ /// Backend-provided display description for this credit, or `null` when unavailable.
+ pub description: Option,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export_to = "v2/", rename_all = "camelCase")]
+pub enum RateLimitResetType {
+ CodexRateLimits,
+ #[serde(other)]
+ Unknown,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export_to = "v2/", rename_all = "camelCase")]
+pub enum RateLimitResetCreditStatus {
+ Available,
+ Redeeming,
+ Redeemed,
+ #[serde(other)]
+ Unknown,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
@@ -293,6 +339,10 @@ pub struct ConsumeAccountRateLimitResetCreditParams {
/// Identifies one logical reset attempt. A UUID is recommended; reuse the same value when
/// retrying that attempt.
pub idempotency_key: String,
+ /// Opaque reset-credit identifier to redeem. When omitted, the backend selects the next
+ /// available credit.
+ #[ts(optional = nullable)]
+ pub credit_id: Option,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
diff --git a/codex-rs/app-server-protocol/src/protocol/v2/config.rs b/codex-rs/app-server-protocol/src/protocol/v2/config.rs
index 4c9e355b47..f615b25d44 100644
--- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs
+++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs
@@ -390,6 +390,23 @@ pub struct ConfigRequirements {
pub enforce_residency: Option,
#[experimental("configRequirements/read.network")]
pub network: Option,
+ pub models: Option,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export_to = "v2/")]
+pub struct ModelsRequirements {
+ pub new_thread: Option,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export_to = "v2/")]
+pub struct NewThreadModelDefaults {
+ pub model: Option,
+ pub model_reasoning_effort: Option,
+ pub service_tier: Option,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
diff --git a/codex-rs/app-server-protocol/src/protocol/v2/environment.rs b/codex-rs/app-server-protocol/src/protocol/v2/environment.rs
index ccffd5813b..320e2af894 100644
--- a/codex-rs/app-server-protocol/src/protocol/v2/environment.rs
+++ b/codex-rs/app-server-protocol/src/protocol/v2/environment.rs
@@ -1,3 +1,4 @@
+use codex_utils_path_uri::PathUri;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
@@ -19,3 +20,29 @@ pub struct EnvironmentAddParams {
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct EnvironmentAddResponse {}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export_to = "v2/")]
+pub struct EnvironmentInfoParams {
+ pub environment_id: String,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export_to = "v2/")]
+pub struct EnvironmentInfoResponse {
+ pub shell: EnvironmentShellInfo,
+ /// Default working directory reported by the environment, as a canonical file URI.
+ pub cwd: Option,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export_to = "v2/")]
+pub struct EnvironmentShellInfo {
+ /// Stable shell name, for example `zsh`, `bash`, `powershell`, `sh`, or `cmd`.
+ pub name: String,
+ /// Target-native shell executable path or command name.
+ pub path: String,
+}
diff --git a/codex-rs/app-server-protocol/src/protocol/v2/item.rs b/codex-rs/app-server-protocol/src/protocol/v2/item.rs
index e2a0d45129..cd04752449 100644
--- a/codex-rs/app-server-protocol/src/protocol/v2/item.rs
+++ b/codex-rs/app-server-protocol/src/protocol/v2/item.rs
@@ -8,12 +8,17 @@ use super::NetworkPolicyAmendment;
use super::RequestPermissionProfile;
use super::UserInput;
use super::shared::v2_enum_from_core;
+use crate::protocol::item_builders::command_actions_for_path_uri;
use crate::protocol::item_builders::convert_patch_changes;
use codex_experimental_api_macros::ExperimentalApi;
use codex_protocol::approvals::GuardianAssessmentAction as CoreGuardianAssessmentAction;
use codex_protocol::approvals::GuardianAssessmentDecisionSource as CoreGuardianAssessmentDecisionSource;
use codex_protocol::approvals::GuardianCommandSource as CoreGuardianCommandSource;
use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent;
+use codex_protocol::items::CollabAgentTool as CoreCollabAgentTool;
+use codex_protocol::items::CollabAgentToolCallStatus as CoreCollabAgentToolCallStatus;
+use codex_protocol::items::CommandExecutionStatus as CoreCommandExecutionStatus;
+use codex_protocol::items::DynamicToolCallStatus as CoreDynamicToolCallStatus;
use codex_protocol::items::McpToolCallStatus as CoreMcpToolCallStatus;
use codex_protocol::items::TurnItem as CoreTurnItem;
use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation;
@@ -30,6 +35,7 @@ use codex_protocol::protocol::GuardianUserAuthorization as CoreGuardianUserAutho
use codex_protocol::protocol::PatchApplyStatus as CorePatchApplyStatus;
use codex_protocol::protocol::ReviewDecision as CoreReviewDecision;
use codex_protocol::protocol::SubAgentActivityKind as CoreSubAgentActivityKind;
+use codex_shell_command::parse_command::shlex_join;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::LegacyAppPathString;
use schemars::JsonSchema;
@@ -396,6 +402,9 @@ pub struct McpToolCallAppContext {
pub connector_id: String,
pub link_id: Option,
pub resource_uri: Option,
+ pub app_name: Option,
+ pub template_id: Option,
+ pub action_name: Option,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
@@ -851,6 +860,64 @@ impl From for ThreadItem {
summary: reasoning.summary_text,
content: reasoning.raw_content,
},
+ CoreTurnItem::CommandExecution(command) => ThreadItem::CommandExecution {
+ id: command.id,
+ command: shlex_join(&command.command),
+ cwd: command.cwd.clone().into(),
+ process_id: command.process_id,
+ source: command.source.into(),
+ status: command.status.into(),
+ command_actions: command_actions_for_path_uri(&command.parsed_cmd, &command.cwd),
+ aggregated_output: command
+ .aggregated_output
+ .filter(|output| !output.is_empty()),
+ exit_code: command.exit_code,
+ duration_ms: command
+ .duration
+ .and_then(|duration| i64::try_from(duration.as_millis()).ok()),
+ },
+ CoreTurnItem::DynamicToolCall(call) => ThreadItem::DynamicToolCall {
+ id: call.id,
+ namespace: call.namespace,
+ tool: call.tool,
+ arguments: call.arguments,
+ status: call.status.into(),
+ content_items: call.content_items.map(|items| {
+ items
+ .into_iter()
+ .map(DynamicToolCallOutputContentItem::from)
+ .collect()
+ }),
+ success: call.success,
+ duration_ms: call
+ .duration
+ .and_then(|duration| i64::try_from(duration.as_millis()).ok()),
+ },
+ CoreTurnItem::CollabAgentToolCall(call) => ThreadItem::CollabAgentToolCall {
+ id: call.id,
+ tool: call.tool.into(),
+ status: call.status.into(),
+ sender_thread_id: call.sender_thread_id.to_string(),
+ receiver_thread_ids: call
+ .receiver_thread_ids
+ .into_iter()
+ .map(String::from)
+ .collect(),
+ prompt: call.prompt,
+ model: call.model,
+ reasoning_effort: call.reasoning_effort,
+ agents_states: call
+ .agents_states
+ .into_iter()
+ .map(|(thread_id, status)| (thread_id.to_string(), status.into()))
+ .collect(),
+ },
+ CoreTurnItem::SubAgentActivity(activity) => ThreadItem::SubAgentActivity {
+ id: activity.id,
+ kind: activity.kind.into(),
+ agent_thread_id: activity.agent_thread_id.to_string(),
+ agent_path: String::from(activity.agent_path),
+ },
CoreTurnItem::WebSearch(search) => ThreadItem::WebSearch {
id: search.id,
query: search.query,
@@ -895,6 +962,9 @@ impl From for ThreadItem {
connector_id,
link_id: mcp.link_id,
resource_uri: mcp.mcp_app_resource_uri.clone(),
+ app_name: mcp.app_name,
+ template_id: mcp.template_id,
+ action_name: mcp.action_name,
}),
mcp_app_resource_uri: mcp.mcp_app_resource_uri,
plugin_id: mcp.plugin_id,
@@ -935,6 +1005,17 @@ impl From for CommandExecutionStatus {
}
}
+impl From for CommandExecutionStatus {
+ fn from(value: CoreCommandExecutionStatus) -> Self {
+ match value {
+ CoreCommandExecutionStatus::InProgress => Self::InProgress,
+ CoreCommandExecutionStatus::Completed => Self::Completed,
+ CoreCommandExecutionStatus::Failed => Self::Failed,
+ CoreCommandExecutionStatus::Declined => Self::Declined,
+ }
+ }
+}
+
impl From<&CoreExecCommandStatus> for CommandExecutionStatus {
fn from(value: &CoreExecCommandStatus) -> Self {
match value {
@@ -1022,6 +1103,16 @@ impl From for McpToolCallStatus {
}
}
+impl From for DynamicToolCallStatus {
+ fn from(value: CoreDynamicToolCallStatus) -> Self {
+ match value {
+ CoreDynamicToolCallStatus::InProgress => Self::InProgress,
+ CoreDynamicToolCallStatus::Completed => Self::Completed,
+ CoreDynamicToolCallStatus::Failed => Self::Failed,
+ }
+ }
+}
+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -1049,6 +1140,28 @@ pub enum CollabAgentToolCallStatus {
Failed,
}
+impl From for CollabAgentTool {
+ fn from(value: CoreCollabAgentTool) -> Self {
+ match value {
+ CoreCollabAgentTool::SpawnAgent => Self::SpawnAgent,
+ CoreCollabAgentTool::SendInput => Self::SendInput,
+ CoreCollabAgentTool::ResumeAgent => Self::ResumeAgent,
+ CoreCollabAgentTool::Wait => Self::Wait,
+ CoreCollabAgentTool::CloseAgent => Self::CloseAgent,
+ }
+ }
+}
+
+impl From for CollabAgentToolCallStatus {
+ fn from(value: CoreCollabAgentToolCallStatus) -> Self {
+ match value {
+ CoreCollabAgentToolCallStatus::InProgress => Self::InProgress,
+ CoreCollabAgentToolCallStatus::Completed => Self::Completed,
+ CoreCollabAgentToolCallStatus::Failed => Self::Failed,
+ }
+ }
+}
+
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -1456,6 +1569,21 @@ pub enum DynamicToolCallOutputContentItem {
InputImage { image_url: String },
}
+impl From
+ for DynamicToolCallOutputContentItem
+{
+ fn from(item: codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem) -> Self {
+ match item {
+ codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem::InputText { text } => {
+ Self::InputText { text }
+ }
+ codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem::InputImage {
+ image_url,
+ } => Self::InputImage { image_url },
+ }
+ }
+}
+
impl From
for codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem
{
diff --git a/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs b/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs
index cdfeb524f5..d24da0c879 100644
--- a/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs
+++ b/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs
@@ -23,6 +23,12 @@ v2_enum_from_core!(
}
);
+v2_enum_from_core!(
+ pub enum McpServerStartupFailureReason from codex_protocol::protocol::McpStartupFailureReason {
+ ReauthenticationRequired
+ }
+);
+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -186,6 +192,8 @@ pub struct McpServerRefreshResponse {}
#[ts(export_to = "v2/")]
pub struct McpServerOauthLoginParams {
pub name: String,
+ #[ts(optional = nullable)]
+ pub thread_id: Option,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub scopes: Option>,
@@ -215,6 +223,7 @@ pub struct McpToolCallProgressNotification {
#[ts(export_to = "v2/")]
pub struct McpServerOauthLoginCompletedNotification {
pub name: String,
+ pub thread_id: Option,
pub success: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
@@ -239,6 +248,7 @@ pub struct McpServerStatusUpdatedNotification {
pub name: String,
pub status: McpServerStartupState,
pub error: Option,
+ pub failure_reason: Option,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
diff --git a/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs b/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs
index dbe0b3d55d..19a4ccd889 100644
--- a/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs
+++ b/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs
@@ -600,6 +600,9 @@ pub struct PluginSummary {
pub id: String,
/// Backend remote plugin identifier when available.
pub remote_plugin_id: Option,
+ /// Version advertised by the remote marketplace backend when available.
+ #[serde(default)]
+ pub version: Option,
/// Version of the locally materialized plugin package when available.
#[serde(default)]
pub local_version: Option,
@@ -744,6 +747,15 @@ pub enum PluginSource {
ref_name: Option,
sha: Option,
},
+ #[serde(rename_all = "camelCase")]
+ #[ts(rename_all = "camelCase")]
+ Npm {
+ package: String,
+ /// Optional npm version or version range.
+ version: Option,
+ /// Optional HTTPS registry URL. Authentication stays in the user's npm config.
+ registry: Option,
+ },
/// The plugin is available in the remote catalog. Download metadata is
/// kept server-side and is not exposed through the app-server API.
Remote,
diff --git a/codex-rs/app-server-protocol/src/protocol/v2/realtime.rs b/codex-rs/app-server-protocol/src/protocol/v2/realtime.rs
index 4b8b2056ea..b78f390bec 100644
--- a/codex-rs/app-server-protocol/src/protocol/v2/realtime.rs
+++ b/codex-rs/app-server-protocol/src/protocol/v2/realtime.rs
@@ -70,6 +70,10 @@ pub struct ThreadRealtimeStartParams {
/// them automatically. Defaults to false.
#[ts(optional = nullable)]
pub client_managed_handoffs: Option,
+ /// Routes any transcript tail remaining at session end through Codex. Defaults to false.
+ /// TODO: Remove this rollout knob once transcript-tail flushing is always enabled.
+ #[ts(optional = nullable)]
+ pub flush_transcript_tail_on_session_end: Option,
/// Sends automatic Codex responses as realtime conversation items instead of handoff appends.
#[ts(optional = nullable)]
pub codex_responses_as_items: Option,
diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs
index 7c78fee4c8..77d8a1439b 100644
--- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs
+++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs
@@ -1,13 +1,22 @@
use super::*;
+use crate::ServerNotification;
use codex_protocol::approvals::ElicitationRequest as CoreElicitationRequest;
use codex_protocol::config_types::MultiAgentMode;
use codex_protocol::items::AgentMessageContent;
use codex_protocol::items::AgentMessageItem;
+use codex_protocol::items::CollabAgentTool as CoreCollabAgentTool;
+use codex_protocol::items::CollabAgentToolCallItem;
+use codex_protocol::items::CollabAgentToolCallStatus as CoreCollabAgentToolCallStatus;
+use codex_protocol::items::CommandExecutionItem;
+use codex_protocol::items::CommandExecutionStatus as CoreCommandExecutionStatus;
+use codex_protocol::items::DynamicToolCallItem;
+use codex_protocol::items::DynamicToolCallStatus as CoreDynamicToolCallStatus;
use codex_protocol::items::FileChangeItem;
use codex_protocol::items::ImageViewItem;
use codex_protocol::items::McpToolCallItem;
use codex_protocol::items::McpToolCallStatus as CoreMcpToolCallStatus;
use codex_protocol::items::ReasoningItem;
+use codex_protocol::items::SubAgentActivityItem;
use codex_protocol::items::TurnItem;
use codex_protocol::items::UserMessageItem;
use codex_protocol::items::WebSearchItem;
@@ -29,8 +38,10 @@ use codex_protocol::permissions::FileSystemSpecialPath as CoreFileSystemSpecialP
use codex_protocol::protocol::AgentStatus as CoreAgentStatus;
use codex_protocol::protocol::AskForApproval as CoreAskForApproval;
use codex_protocol::protocol::ConversationTextRole;
+use codex_protocol::protocol::ExecCommandSource as CoreExecCommandSource;
use codex_protocol::protocol::GranularApprovalConfig as CoreGranularApprovalConfig;
use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess;
+use codex_protocol::protocol::SubAgentActivityKind as CoreSubAgentActivityKind;
use codex_protocol::request_permissions::RequestPermissionProfile as CoreRequestPermissionProfile;
use codex_protocol::user_input::UserInput as CoreUserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -174,6 +185,7 @@ fn thread_resume_response_round_trips_initial_turns_page() {
parent_thread_id: None,
preview: String::new(),
ephemeral: false,
+ history_mode: Default::default(),
model_provider: "openai".to_string(),
created_at: 1,
updated_at: 1,
@@ -837,6 +849,30 @@ fn thread_path_params_deserialize_empty_path_as_none() {
);
}
+#[test]
+fn thread_fork_last_turn_id_round_trips() {
+ let params: ThreadForkParams = serde_json::from_value(json!({
+ "threadId": "thread-1",
+ "lastTurnId": "turn-2",
+ }))
+ .expect("thread/fork params deserialize");
+
+ assert_eq!(params.last_turn_id, Some("turn-2".to_string()));
+ let serialized = serde_json::to_value(params).expect("thread/fork params serialize");
+ assert_eq!(serialized["lastTurnId"], json!("turn-2"));
+
+ let omitted = serde_json::to_value(ThreadForkParams {
+ thread_id: "thread-1".to_string(),
+ ..Default::default()
+ })
+ .expect("thread/fork params without last turn id serialize");
+ assert_eq!(
+ omitted["lastTurnId"],
+ serde_json::Value::Null,
+ "optional lastTurnId should serialize as null when omitted"
+ );
+}
+
#[test]
fn fs_get_metadata_response_round_trips_minimal_fields() {
let response = FsGetMetadataResponse {
@@ -1751,6 +1787,7 @@ fn config_requirements_granular_allowed_approval_policy_is_marked_experimental()
hooks: None,
enforce_residency: None,
network: None,
+ models: None,
});
assert_eq!(reason, Some("askForApproval.granular"));
@@ -2155,6 +2192,7 @@ fn mcp_server_status_updated_accepts_missing_thread_id() {
name: "optional_broken".to_string(),
status: McpServerStartupState::Failed,
error: Some("handshake failed".to_string()),
+ failure_reason: None,
};
assert_eq!(notification, expected);
assert_eq!(
@@ -2164,6 +2202,33 @@ fn mcp_server_status_updated_accepts_missing_thread_id() {
"name": "optional_broken",
"status": "failed",
"error": "handshake failed",
+ "failureReason": null,
+ })
+ );
+}
+
+#[test]
+fn mcp_server_status_updated_serializes_failure_reason() {
+ let notification =
+ ServerNotification::McpServerStatusUpdated(McpServerStatusUpdatedNotification {
+ thread_id: Some("thread-1".to_string()),
+ name: "expired-oauth".to_string(),
+ status: McpServerStartupState::Failed,
+ error: Some("OAuth credentials expired".to_string()),
+ failure_reason: Some(McpServerStartupFailureReason::ReauthenticationRequired),
+ });
+
+ assert_eq!(
+ serde_json::to_value(notification).expect("notification should serialize"),
+ json!({
+ "method": "mcpServer/startupStatus/updated",
+ "params": {
+ "threadId": "thread-1",
+ "name": "expired-oauth",
+ "status": "failed",
+ "error": "OAuth credentials expired",
+ "failureReason": "reauthenticationRequired",
+ },
})
);
}
@@ -2577,6 +2642,134 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
}
);
+ let command_item = TurnItem::CommandExecution(CommandExecutionItem {
+ id: "exec-1".to_string(),
+ process_id: Some("pid-1".to_string()),
+ command: vec!["echo".to_string(), "done".to_string()],
+ cwd: PathUri::from_abs_path(&test_path_buf("/tmp").abs()),
+ parsed_cmd: vec![codex_protocol::parse_command::ParsedCommand::Unknown {
+ cmd: "echo done".to_string(),
+ }],
+ source: CoreExecCommandSource::Agent,
+ interaction_input: None,
+ status: CoreCommandExecutionStatus::Completed,
+ stdout: Some("done\n".to_string()),
+ stderr: Some(String::new()),
+ aggregated_output: Some("done\n".to_string()),
+ exit_code: Some(0),
+ duration: Some(Duration::from_millis(5)),
+ formatted_output: Some("done\n".to_string()),
+ });
+
+ assert_eq!(
+ ThreadItem::from(command_item),
+ ThreadItem::CommandExecution {
+ id: "exec-1".to_string(),
+ command: "echo done".to_string(),
+ cwd: LegacyAppPathString::from_abs_path(&test_path_buf("/tmp").abs()),
+ process_id: Some("pid-1".to_string()),
+ source: CommandExecutionSource::Agent,
+ status: CommandExecutionStatus::Completed,
+ command_actions: vec![CommandAction::Unknown {
+ command: "echo done".to_string(),
+ }],
+ aggregated_output: Some("done\n".to_string()),
+ exit_code: Some(0),
+ duration_ms: Some(5),
+ }
+ );
+
+ let dynamic_tool_call_item = TurnItem::DynamicToolCall(DynamicToolCallItem {
+ id: "dynamic-1".to_string(),
+ namespace: Some("apps".to_string()),
+ tool: "lookup".to_string(),
+ arguments: json!({"id": "123"}),
+ status: CoreDynamicToolCallStatus::Completed,
+ content_items: Some(vec![
+ codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem::InputText {
+ text: "ok".to_string(),
+ },
+ ]),
+ success: Some(true),
+ error: None,
+ duration: Some(Duration::from_millis(5)),
+ });
+
+ assert_eq!(
+ ThreadItem::from(dynamic_tool_call_item),
+ ThreadItem::DynamicToolCall {
+ id: "dynamic-1".to_string(),
+ namespace: Some("apps".to_string()),
+ tool: "lookup".to_string(),
+ arguments: json!({"id": "123"}),
+ status: DynamicToolCallStatus::Completed,
+ content_items: Some(vec![DynamicToolCallOutputContentItem::InputText {
+ text: "ok".to_string(),
+ }]),
+ success: Some(true),
+ duration_ms: Some(5),
+ }
+ );
+
+ let sender_thread_id = codex_protocol::ThreadId::default();
+ let receiver_thread_id = codex_protocol::ThreadId::default();
+ let collab_item = TurnItem::CollabAgentToolCall(CollabAgentToolCallItem {
+ id: "collab-1".to_string(),
+ tool: CoreCollabAgentTool::SendInput,
+ status: CoreCollabAgentToolCallStatus::Completed,
+ sender_thread_id,
+ receiver_thread_ids: vec![receiver_thread_id],
+ receiver_agents: Vec::new(),
+ prompt: Some("continue".to_string()),
+ model: None,
+ reasoning_effort: None,
+ agents_states: [(receiver_thread_id, CoreAgentStatus::Completed(None))]
+ .into_iter()
+ .collect(),
+ });
+
+ assert_eq!(
+ ThreadItem::from(collab_item),
+ ThreadItem::CollabAgentToolCall {
+ id: "collab-1".to_string(),
+ tool: CollabAgentTool::SendInput,
+ status: CollabAgentToolCallStatus::Completed,
+ sender_thread_id: sender_thread_id.to_string(),
+ receiver_thread_ids: vec![receiver_thread_id.to_string()],
+ prompt: Some("continue".to_string()),
+ model: None,
+ reasoning_effort: None,
+ agents_states: [(
+ receiver_thread_id.to_string(),
+ CollabAgentState {
+ status: CollabAgentStatus::Completed,
+ message: None,
+ },
+ )]
+ .into_iter()
+ .collect(),
+ }
+ );
+
+ let sub_agent_activity_item = TurnItem::SubAgentActivity(SubAgentActivityItem {
+ id: "activity-1".to_string(),
+ kind: CoreSubAgentActivityKind::Interrupted,
+ agent_thread_id: receiver_thread_id,
+ agent_path: codex_protocol::AgentPath::root()
+ .join("worker")
+ .expect("worker path"),
+ });
+
+ assert_eq!(
+ ThreadItem::from(sub_agent_activity_item),
+ ThreadItem::SubAgentActivity {
+ id: "activity-1".to_string(),
+ kind: SubAgentActivityKind::Interrupted,
+ agent_thread_id: receiver_thread_id.to_string(),
+ agent_path: "/root/worker".to_string(),
+ }
+ );
+
let search_item = TurnItem::WebSearch(WebSearchItem {
id: "search-1".to_string(),
query: "docs".to_string(),
@@ -2648,6 +2841,9 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
connector_id: Some("calendar".to_string()),
mcp_app_resource_uri: Some("app://connector".to_string()),
link_id: Some("link_calendar".to_string()),
+ app_name: Some("Calendar".to_string()),
+ template_id: Some("calendar_template".to_string()),
+ action_name: Some("create_event".to_string()),
plugin_id: Some("sample@test".to_string()),
status: CoreMcpToolCallStatus::InProgress,
result: None,
@@ -2667,6 +2863,9 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
connector_id: "calendar".to_string(),
link_id: Some("link_calendar".to_string()),
resource_uri: Some("app://connector".to_string()),
+ app_name: Some("Calendar".to_string()),
+ template_id: Some("calendar_template".to_string()),
+ action_name: Some("create_event".to_string()),
}),
mcp_app_resource_uri: Some("app://connector".to_string()),
plugin_id: Some("sample@test".to_string()),
@@ -2684,6 +2883,9 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
connector_id: None,
mcp_app_resource_uri: None,
link_id: None,
+ app_name: None,
+ template_id: None,
+ action_name: None,
plugin_id: None,
status: CoreMcpToolCallStatus::Completed,
result: Some(CallToolResult {
@@ -2730,6 +2932,9 @@ fn mcp_tool_call_app_context_serializes_connector_id() {
connector_id: "calendar".to_string(),
link_id: Some("link_calendar".to_string()),
resource_uri: Some("app://connector".to_string()),
+ app_name: Some("Calendar".to_string()),
+ template_id: Some("calendar_template".to_string()),
+ action_name: Some("create_event".to_string()),
}),
mcp_app_resource_uri: Some("app://connector".to_string()),
plugin_id: None,
@@ -2751,6 +2956,9 @@ fn mcp_tool_call_app_context_serializes_connector_id() {
"connectorId": "calendar",
"linkId": "link_calendar",
"resourceUri": "app://connector",
+ "appName": "Calendar",
+ "templateId": "calendar_template",
+ "actionName": "create_event",
},
"mcpAppResourceUri": "app://connector",
"pluginId": null,
@@ -2761,6 +2969,29 @@ fn mcp_tool_call_app_context_serializes_connector_id() {
);
}
+#[test]
+fn mcp_tool_call_app_context_serializes_missing_mixed_version_fields_as_null() {
+ assert_eq!(
+ serde_json::to_value(McpToolCallAppContext {
+ connector_id: "calendar".to_string(),
+ link_id: None,
+ resource_uri: None,
+ app_name: None,
+ template_id: None,
+ action_name: None,
+ })
+ .expect("MCP tool call app context should serialize"),
+ json!({
+ "connectorId": "calendar",
+ "linkId": null,
+ "resourceUri": null,
+ "appName": null,
+ "templateId": null,
+ "actionName": null,
+ })
+ );
+}
+
#[test]
fn user_input_into_core_preserves_image_detail() {
assert_eq!(
@@ -2834,7 +3065,7 @@ fn skills_extra_roots_set_params_rejects_relative_roots() {
}
#[test]
-fn plugin_source_serializes_local_git_and_remote_variants() {
+fn plugin_source_serializes_local_git_npm_and_remote_variants() {
let local_path = if cfg!(windows) {
r"C:\plugins\linear"
} else {
@@ -2868,6 +3099,21 @@ fn plugin_source_serializes_local_git_and_remote_variants() {
}),
);
+ assert_eq!(
+ serde_json::to_value(PluginSource::Npm {
+ package: "@acme/plugin".to_string(),
+ version: Some("^1.2.0".to_string()),
+ registry: Some("https://npm.example.com".to_string()),
+ })
+ .unwrap(),
+ json!({
+ "type": "npm",
+ "package": "@acme/plugin",
+ "version": "^1.2.0",
+ "registry": "https://npm.example.com",
+ }),
+ );
+
assert_eq!(
serde_json::to_value(PluginSource::Remote).unwrap(),
json!({
@@ -3396,6 +3642,7 @@ fn plugin_share_list_response_serializes_share_items() {
remote_plugin_id: Some(
"plugins~Plugin_00000000000000000000000000000000".to_string(),
),
+ version: None,
local_version: None,
name: "gmail".to_string(),
share_context: None,
@@ -3417,6 +3664,7 @@ fn plugin_share_list_response_serializes_share_items() {
"plugin": {
"id": "gmail@openai-curated-remote",
"remotePluginId": "plugins~Plugin_00000000000000000000000000000000",
+ "version": null,
"localVersion": null,
"name": "gmail",
"shareContext": null,
diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs
index c330c7eefd..367a4a64c8 100644
--- a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs
+++ b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs
@@ -4,6 +4,7 @@ use super::AskForApproval;
use super::SandboxMode;
use super::SandboxPolicy;
use super::Thread;
+use super::ThreadHistoryMode;
use super::ThreadItem;
use super::ThreadSource;
use super::Turn;
@@ -57,6 +58,11 @@ pub struct ThreadStartParams {
pub model: Option,
#[ts(optional = nullable)]
pub model_provider: Option,
+ /// Allow a provider with an authoritative static model catalog to replace an unavailable
+ /// requested model with its default.
+ #[experimental("thread/start.allowProviderModelFallback")]
+ #[serde(default, skip_serializing_if = "std::ops::Not::not")]
+ pub allow_provider_model_fallback: bool,
#[serde(
default,
deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option",
@@ -100,6 +106,10 @@ pub struct ThreadStartParams {
pub multi_agent_mode: Option,
#[ts(optional = nullable)]
pub ephemeral: Option,
+ /// Persisted thread history contract to use for this new thread.
+ #[experimental("thread/start.historyMode")]
+ #[ts(optional = nullable)]
+ pub history_mode: Option,
#[ts(optional = nullable)]
pub session_start_source: Option,
/// Optional client-supplied analytics source classification for this thread.
@@ -484,6 +494,13 @@ impl From for TurnsPage {
pub struct ThreadForkParams {
pub thread_id: String,
+ /// Optional last turn id to fork through, inclusive.
+ ///
+ /// When specified, turns after `last_turn_id` are omitted from the fork.
+ /// The referenced turn cannot be in progress.
+ #[ts(optional = nullable)]
+ pub last_turn_id: Option,
+
/// [UNSTABLE] Specify the rollout path to fork from.
/// If specified, the thread_id param will be ignored.
#[experimental("thread/fork.path")]
@@ -668,7 +685,7 @@ pub struct ThreadIncrementElicitationParams {
#[ts(export_to = "v2/")]
pub struct ThreadIncrementElicitationResponse {
/// Current out-of-band elicitation count after the increment.
- pub count: u64,
+ pub count: i64,
/// Whether timeout accounting is paused after applying the increment.
pub paused: bool,
}
@@ -688,7 +705,7 @@ pub struct ThreadDecrementElicitationParams {
#[ts(export_to = "v2/")]
pub struct ThreadDecrementElicitationResponse {
/// Current out-of-band elicitation count after the decrement.
- pub count: u64,
+ pub count: i64,
/// Whether timeout accounting remains paused after applying the decrement.
pub paused: bool,
}
@@ -1025,6 +1042,7 @@ pub struct ThreadBackgroundTerminalsTerminateResponse {
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
+/// DEPRECATED: `thread/rollback` will be removed soon.
pub struct ThreadRollbackParams {
pub thread_id: String,
/// The number of turns to drop from the end of the thread. Must be >= 1.
diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs
index 3338513df7..2393efb25c 100644
--- a/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs
+++ b/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs
@@ -5,6 +5,7 @@ use super::TurnStatus;
use codex_experimental_api_macros::ExperimentalApi;
use codex_protocol::protocol::SessionSource as CoreSessionSource;
use codex_protocol::protocol::SubAgentSource as CoreSubAgentSource;
+use codex_protocol::protocol::ThreadHistoryMode as CoreThreadHistoryMode;
use codex_protocol::protocol::ThreadSource as CoreThreadSource;
use codex_utils_absolute_path::AbsolutePathBuf;
use schemars::JsonSchema;
@@ -64,6 +65,33 @@ impl From for CoreSessionSource {
}
}
+#[derive(Default, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
+#[serde(rename_all = "lowercase")]
+#[ts(rename_all = "lowercase", export_to = "v2/")]
+pub enum ThreadHistoryMode {
+ #[default]
+ Legacy,
+ Paginated,
+}
+
+impl From for ThreadHistoryMode {
+ fn from(value: CoreThreadHistoryMode) -> Self {
+ match value {
+ CoreThreadHistoryMode::Legacy => Self::Legacy,
+ CoreThreadHistoryMode::Paginated => Self::Paginated,
+ }
+ }
+}
+
+impl From for CoreThreadHistoryMode {
+ fn from(value: ThreadHistoryMode) -> Self {
+ match value {
+ ThreadHistoryMode::Legacy => Self::Legacy,
+ ThreadHistoryMode::Paginated => Self::Paginated,
+ }
+ }
+}
+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, TS)]
#[serde(try_from = "String", into = "String")]
#[ts(type = "string")]
@@ -155,6 +183,10 @@ pub struct Thread {
pub preview: String,
/// Whether the thread is ephemeral and should not be materialized on disk.
pub ephemeral: bool,
+ /// Persisted thread history contract selected when this thread was created.
+ #[experimental("thread.historyMode")]
+ #[serde(default)]
+ pub history_mode: ThreadHistoryMode,
/// Model provider used for this thread (for example, 'openai').
pub model_provider: String,
/// Unix timestamp (in seconds) when the thread was created.
diff --git a/codex-rs/app-server-test-client/src/plugin_analytics_mutation_smoke.rs b/codex-rs/app-server-test-client/src/plugin_analytics_mutation_smoke.rs
index 70aa13ab9c..c9f9653155 100644
--- a/codex-rs/app-server-test-client/src/plugin_analytics_mutation_smoke.rs
+++ b/codex-rs/app-server-test-client/src/plugin_analytics_mutation_smoke.rs
@@ -124,7 +124,6 @@ pub(super) fn run_cleanup(
overrides.extend([
"analytics.enabled=false".to_string(),
"features.plugins=true".to_string(),
- "features.remote_plugin=true".to_string(),
]);
let mut client = CodexClient::spawn_stdio(codex_bin, &overrides)?;
client.initialize()?;
@@ -199,7 +198,6 @@ fn spawn_client(
overrides.extend([
"analytics.enabled=true".to_string(),
"features.plugins=true".to_string(),
- "features.remote_plugin=true".to_string(),
]);
let environment = vec![(
OsString::from(ANALYTICS_CAPTURE_ENV_VAR),
diff --git a/codex-rs/app-server-test-client/src/plugin_analytics_smoke.rs b/codex-rs/app-server-test-client/src/plugin_analytics_smoke.rs
index 65704eeb65..aaf54dd2da 100644
--- a/codex-rs/app-server-test-client/src/plugin_analytics_smoke.rs
+++ b/codex-rs/app-server-test-client/src/plugin_analytics_smoke.rs
@@ -263,7 +263,6 @@ fn smoke_config_overrides(responses_base_url: &str) -> Result> {
Ok(vec![
"analytics.enabled=true".to_string(),
"features.plugins=true".to_string(),
- "features.remote_plugin=true".to_string(),
format!("model={}", quoted(MOCK_MODEL_SLUG)?),
format!("model_provider={}", quoted(MOCK_PROVIDER_ID)?),
format!(
diff --git a/codex-rs/app-server-transport/Cargo.toml b/codex-rs/app-server-transport/Cargo.toml
index d4c0a83739..af1f0e9603 100644
--- a/codex-rs/app-server-transport/Cargo.toml
+++ b/codex-rs/app-server-transport/Cargo.toml
@@ -35,8 +35,10 @@ constant_time_eq = { workspace = true }
futures = { workspace = true }
gethostname = { workspace = true }
hmac = { workspace = true }
+httpdate = { workspace = true }
jsonwebtoken = { workspace = true }
owo-colors = { workspace = true, features = ["supports-colors"] }
+rand = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sha2 = { workspace = true }
diff --git a/codex-rs/app-server-transport/src/transport/remote_control/enroll.rs b/codex-rs/app-server-transport/src/transport/remote_control/enroll.rs
index e327503a93..be64072476 100644
--- a/codex-rs/app-server-transport/src/transport/remote_control/enroll.rs
+++ b/codex-rs/app-server-transport/src/transport/remote_control/enroll.rs
@@ -1,8 +1,4 @@
-use super::auth::RemoteControlConnectionAuth;
use super::pairing_unavailable_error;
-use super::protocol::EnrollRemoteServerRequest;
-use super::protocol::EnrollRemoteServerResponse;
-use super::protocol::RefreshRemoteServerRequest;
use super::protocol::RemoteControlPairingStatusRequest;
use super::protocol::RemoteControlPairingStatusResponse as BackendRemoteControlPairingStatusResponse;
use super::protocol::RemoteControlTarget;
@@ -14,8 +10,6 @@ use codex_app_server_protocol::RemoteControlPairingStatusResponse;
use codex_login::default_client::build_reqwest_client;
use codex_state::RemoteControlEnrollmentRecord;
use codex_state::StateRuntime;
-use serde::Serialize;
-use serde::de::DeserializeOwned;
use std::io;
use std::io::ErrorKind;
use time::OffsetDateTime;
@@ -23,15 +17,13 @@ use time::format_description::well_known::Rfc3339;
use tracing::info;
use tracing::warn;
-const REMOTE_CONTROL_ENROLL_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
const REMOTE_CONTROL_PAIRING_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
const REMOTE_CONTROL_RESPONSE_BODY_MAX_BYTES: usize = 4096;
-const REMOTE_CONTROL_SERVER_TOKEN_REFRESH_SKEW_SECS: i64 = 30;
+const REMOTE_CONTROL_SERVER_TOKEN_REFRESH_SKEW_SECS: i64 = 5 * 60;
const REQUEST_ID_HEADER: &str = "x-request-id";
const OAI_REQUEST_ID_HEADER: &str = "x-oai-request-id";
const CF_RAY_HEADER: &str = "cf-ray";
-pub(super) const REMOTE_CONTROL_INSTALLATION_ID_HEADER: &str = "x-codex-installation-id";
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct RemoteControlEnrollment {
@@ -42,6 +34,14 @@ pub(super) struct RemoteControlEnrollment {
pub(super) server_name: String,
pub(super) remote_control_token: Option,
pub(super) expires_at: Option,
+ pub(super) next_refresh_at: Option,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub(super) enum RemoteControlServerTokenRefreshRequirement {
+ Required,
+ Proactive,
+ NotNeeded,
}
impl RemoteControlEnrollment {
@@ -49,7 +49,9 @@ impl RemoteControlEnrollment {
&self,
request: StartRemoteControlPairingRequest,
) -> io::Result {
- if self.should_refresh_server_token() {
+ if self.server_token_refresh_requirement()
+ == RemoteControlServerTokenRefreshRequirement::Required
+ {
return Err(pairing_unavailable_error());
}
let remote_control_token = self
@@ -142,7 +144,9 @@ impl RemoteControlEnrollment {
&self,
request: RemoteControlPairingStatusRequest,
) -> io::Result {
- if self.should_refresh_server_token() {
+ if self.server_token_refresh_requirement()
+ == RemoteControlServerTokenRefreshRequirement::Required
+ {
return Err(pairing_unavailable_error());
}
let remote_control_token = self
@@ -201,13 +205,35 @@ impl RemoteControlEnrollment {
})
}
+ pub(super) fn server_token_refresh_requirement(
+ &self,
+ ) -> RemoteControlServerTokenRefreshRequirement {
+ self.server_token_refresh_requirement_at(OffsetDateTime::now_utc())
+ }
+
pub(super) fn should_refresh_server_token(&self) -> bool {
- self.remote_control_token.is_none()
- || self.expires_at.is_none_or(|expires_at| {
- expires_at.unix_timestamp()
- <= OffsetDateTime::now_utc().unix_timestamp()
- + REMOTE_CONTROL_SERVER_TOKEN_REFRESH_SKEW_SECS
- })
+ self.server_token_refresh_requirement()
+ != RemoteControlServerTokenRefreshRequirement::NotNeeded
+ }
+
+ pub(super) fn server_token_refresh_requirement_at(
+ &self,
+ now: OffsetDateTime,
+ ) -> RemoteControlServerTokenRefreshRequirement {
+ let Some(expires_at) = self.remote_control_token.as_ref().and(self.expires_at) else {
+ return RemoteControlServerTokenRefreshRequirement::Required;
+ };
+ if expires_at <= now {
+ return RemoteControlServerTokenRefreshRequirement::Required;
+ }
+ if expires_at > now + time::Duration::seconds(REMOTE_CONTROL_SERVER_TOKEN_REFRESH_SKEW_SECS)
+ || self
+ .next_refresh_at
+ .is_some_and(|next_refresh_at| next_refresh_at > now)
+ {
+ return RemoteControlServerTokenRefreshRequirement::NotNeeded;
+ }
+ RemoteControlServerTokenRefreshRequirement::Proactive
}
pub(super) fn clear_server_token(&mut self) {
@@ -267,6 +293,7 @@ pub(super) async fn load_persisted_remote_control_enrollment(
server_name: enrollment.server_name,
remote_control_token: None,
expires_at: None,
+ next_refresh_at: None,
}))
}
None => {
@@ -398,164 +425,12 @@ pub(crate) fn format_headers(headers: &HeaderMap) -> String {
format!("request-id: {request_id_str}, cf-ray: {cf_ray_str}")
}
-pub(super) async fn enroll_remote_control_server(
- remote_control_target: &RemoteControlTarget,
- auth: &RemoteControlConnectionAuth,
- installation_id: &str,
- server_name: &str,
-) -> io::Result {
- let enroll_url = &remote_control_target.enroll_url;
- let request = EnrollRemoteServerRequest {
- name: server_name.to_string(),
- os: std::env::consts::OS,
- arch: std::env::consts::ARCH,
- app_server_version: env!("CARGO_PKG_VERSION"),
- installation_id: installation_id.to_string(),
- };
- let enrollment_response = send_remote_control_server_request::<_, EnrollRemoteServerResponse>(
- enroll_url,
- auth,
- installation_id,
- &request,
- "enroll",
- "server enrollment",
- )
- .await?;
- let mut enrollment = RemoteControlEnrollment {
- remote_control_target: remote_control_target.clone(),
- account_id: auth.account_id.clone(),
- environment_id: enrollment_response.environment_id,
- server_id: enrollment_response.server_id,
- server_name: server_name.to_string(),
- remote_control_token: None,
- expires_at: None,
- };
- update_remote_control_server_token(
- &mut enrollment,
- enroll_url,
- enrollment_response.remote_control_token,
- enrollment_response.expires_at,
- )?;
- Ok(enrollment)
-}
-
-pub(super) async fn refresh_remote_control_server(
- auth: &RemoteControlConnectionAuth,
- installation_id: &str,
- enrollment: &mut RemoteControlEnrollment,
-) -> io::Result<()> {
- let refresh_url = enrollment.remote_control_target.refresh_url.clone();
- let request = RefreshRemoteServerRequest {
- server_id: enrollment.server_id.clone(),
- installation_id: installation_id.to_string(),
- };
- let refreshed = send_remote_control_server_request::<_, EnrollRemoteServerResponse>(
- &refresh_url,
- auth,
- installation_id,
- &request,
- "refresh",
- "server refresh",
- )
- .await?;
- if refreshed.server_id != enrollment.server_id
- || refreshed.environment_id != enrollment.environment_id
- {
- return Err(io::Error::other(format!(
- "remote control server refresh returned mismatched enrollment: expected server_id={}, environment_id={}; got server_id={}, environment_id={}",
- enrollment.server_id,
- enrollment.environment_id,
- refreshed.server_id,
- refreshed.environment_id
- )));
- }
-
- update_remote_control_server_token(
- enrollment,
- &refresh_url,
- refreshed.remote_control_token,
- refreshed.expires_at,
- )
-}
-
-async fn send_remote_control_server_request(
- url: &str,
- auth: &RemoteControlConnectionAuth,
- installation_id: &str,
- request: &Request,
- action: &str,
- response_kind: &str,
-) -> io::Result
-where
- Request: Serialize,
- Response: DeserializeOwned,
-{
- let client = build_reqwest_client();
- let auth_headers = auth.request_headers()?;
- let response = client
- .post(url)
- .timeout(REMOTE_CONTROL_ENROLL_TIMEOUT)
- .headers(auth_headers)
- .header(REMOTE_CONTROL_INSTALLATION_ID_HEADER, installation_id)
- .json(request)
- .send()
- .await
- .map_err(|err| {
- io::Error::other(format!(
- "failed to {action} remote control server at `{url}`: {err}"
- ))
- })?;
- let headers = response.headers().clone();
- let status = response.status();
- let body = response.bytes().await.map_err(|err| {
- io::Error::other(format!(
- "failed to read remote control {response_kind} response from `{url}`: {err}"
- ))
- })?;
- let body_preview = preview_remote_control_response_body(&body);
- if !status.is_success() {
- let headers_str = format_headers(&headers);
- let error_kind = match status.as_u16() {
- 401 | 403 => ErrorKind::PermissionDenied,
- 404 => ErrorKind::NotFound,
- _ => ErrorKind::Other,
- };
- return Err(io::Error::new(
- error_kind,
- format!(
- "remote control {response_kind} failed at `{url}`: HTTP {status}, {headers_str}, body: {body_preview}"
- ),
- ));
- }
-
- serde_json::from_slice::(&body).map_err(|err| {
- let headers_str = format_headers(&headers);
- io::Error::other(format!(
- "failed to parse remote control {response_kind} response from `{url}`: HTTP {status}, {headers_str}, body: {body_preview}, decode error: {err}"
- ))
- })
-}
-
-fn update_remote_control_server_token(
- enrollment: &mut RemoteControlEnrollment,
- url: &str,
- token: String,
- expires_at: String,
-) -> io::Result<()> {
- let expires_at = OffsetDateTime::parse(&expires_at, &Rfc3339).map_err(|err| {
- io::Error::other(format!(
- "failed to parse remote control server token expiry from `{url}`: {err}"
- ))
- })?;
- enrollment.remote_control_token = Some(token);
- enrollment.expires_at = Some(expires_at);
- Ok(())
-}
-
#[cfg(test)]
mod tests {
use super::*;
+ use crate::transport::remote_control::auth::RemoteControlConnectionAuth;
use crate::transport::remote_control::protocol::normalize_remote_control_url;
+ use crate::transport::remote_control::server_api::enroll_remote_control_server;
use codex_state::StateRuntime;
use pretty_assertions::assert_eq;
use serde_json::json;
@@ -575,28 +450,6 @@ mod tests {
.expect("state runtime should initialize")
}
- #[test]
- fn remote_control_enrollment_refreshes_server_token_before_expiry() {
- let expires_soon = RemoteControlEnrollment {
- remote_control_target: normalize_remote_control_url("http://localhost/backend-api/")
- .expect("target should normalize"),
- account_id: "account-a".to_string(),
- environment_id: "env_first".to_string(),
- server_id: "srv_e_first".to_string(),
- server_name: "first-server".to_string(),
- remote_control_token: Some("expires-soon".to_string()),
- expires_at: Some(OffsetDateTime::now_utc() + time::Duration::seconds(29)),
- };
- let expires_later = RemoteControlEnrollment {
- expires_at: Some(OffsetDateTime::now_utc() + time::Duration::seconds(31)),
- remote_control_token: Some("expires-later".to_string()),
- ..expires_soon.clone()
- };
-
- assert!(expires_soon.should_refresh_server_token());
- assert!(!expires_later.should_refresh_server_token());
- }
-
#[test]
fn preview_remote_control_response_body_redacts_server_token() {
assert_eq!(
@@ -630,6 +483,7 @@ mod tests {
server_name: "first-server".to_string(),
remote_control_token: None,
expires_at: None,
+ next_refresh_at: None,
};
let second_enrollment = RemoteControlEnrollment {
remote_control_target: second_target.clone(),
@@ -639,6 +493,7 @@ mod tests {
server_name: "second-server".to_string(),
remote_control_token: None,
expires_at: None,
+ next_refresh_at: None,
};
update_persisted_remote_control_enrollment(
@@ -714,6 +569,7 @@ mod tests {
server_name: "first-server".to_string(),
remote_control_token: None,
expires_at: None,
+ next_refresh_at: None,
};
let second_enrollment = RemoteControlEnrollment {
remote_control_target: second_target.clone(),
@@ -723,6 +579,7 @@ mod tests {
server_name: "second-server".to_string(),
remote_control_token: None,
expires_at: None,
+ next_refresh_at: None,
};
update_persisted_remote_control_enrollment(
diff --git a/codex-rs/app-server-transport/src/transport/remote_control/mod.rs b/codex-rs/app-server-transport/src/transport/remote_control/mod.rs
index d0e934d90b..7422c92c42 100644
--- a/codex-rs/app-server-transport/src/transport/remote_control/mod.rs
+++ b/codex-rs/app-server-transport/src/transport/remote_control/mod.rs
@@ -5,6 +5,7 @@ mod desired_state;
mod enroll;
mod protocol;
mod segment;
+mod server_api;
mod websocket;
use self::auth::load_remote_control_auth;
@@ -12,10 +13,10 @@ use self::auth::recover_remote_control_auth;
use self::desired_state::RemoteControlDesiredState;
use self::desired_state::acquire_persistence_lock;
use self::enroll::RemoteControlEnrollment;
-use self::enroll::enroll_remote_control_server;
use self::enroll::load_persisted_remote_control_enrollment;
-use self::enroll::refresh_remote_control_server;
use self::enroll::update_persisted_remote_control_enrollment;
+use self::server_api::enroll_remote_control_server;
+use self::server_api::refresh_remote_control_server;
use crate::transport::remote_control::websocket::RemoteControlChannels;
use crate::transport::remote_control::websocket::RemoteControlStatusPublisher;
use crate::transport::remote_control::websocket::RemoteControlWebsocket;
@@ -825,27 +826,39 @@ async fn refresh_pairing_enrollment(
installation_id: &str,
enrollment: &mut RemoteControlEnrollment,
) -> io::Result<()> {
- if let Err(err) = refresh_remote_control_server(auth, installation_id, enrollment).await {
- if err.kind() != io::ErrorKind::PermissionDenied {
- return Err(err);
- }
+ let mut refresh_result = refresh_remote_control_server(auth, installation_id, enrollment).await;
+ if refresh_result
+ .as_ref()
+ .is_err_and(|err| err.kind() == io::ErrorKind::PermissionDenied)
+ {
let mut auth_recovery = auth_manager.unauthorized_recovery();
let mut auth_change_rx = auth_manager.auth_change_receiver();
- if !recover_remote_control_auth(&mut auth_recovery, &mut auth_change_rx).await {
- return Err(err);
+ if recover_remote_control_auth(&mut auth_recovery, &mut auth_change_rx).await {
+ match load_remote_control_auth(auth_manager).await {
+ Ok(recovered_auth) if recovered_auth.account_id == enrollment.account_id => {
+ *auth = recovered_auth;
+ refresh_result =
+ refresh_remote_control_server(auth, installation_id, enrollment).await;
+ }
+ Ok(_) | Err(_) => {
+ enrollment.clear_server_token();
+ refresh_result = Err(pairing_unavailable_error());
+ }
+ }
+ } else {
+ enrollment.clear_server_token();
}
- *auth = load_remote_control_auth(auth_manager)
- .await
- .map_err(|_| pairing_unavailable_error())?;
- if auth.account_id != enrollment.account_id {
- return Err(pairing_unavailable_error());
- }
- refresh_remote_control_server(auth, installation_id, enrollment).await?
}
- if replace_current_enrollment(current_enrollment, enrollment) {
- Ok(())
- } else {
+ if refresh_result
+ .as_ref()
+ .is_err_and(|err| err.kind() == io::ErrorKind::PermissionDenied)
+ {
+ enrollment.clear_server_token();
+ }
+ if !replace_current_enrollment(current_enrollment, enrollment) {
Err(pairing_unavailable_error())
+ } else {
+ refresh_result
}
}
diff --git a/codex-rs/app-server-transport/src/transport/remote_control/server_api.rs b/codex-rs/app-server-transport/src/transport/remote_control/server_api.rs
new file mode 100644
index 0000000000..def38907c7
--- /dev/null
+++ b/codex-rs/app-server-transport/src/transport/remote_control/server_api.rs
@@ -0,0 +1,339 @@
+use super::auth::RemoteControlConnectionAuth;
+use super::enroll::RemoteControlEnrollment;
+use super::enroll::RemoteControlServerTokenRefreshRequirement;
+use super::enroll::format_headers;
+use super::enroll::preview_remote_control_response_body;
+use super::protocol::EnrollRemoteServerRequest;
+use super::protocol::EnrollRemoteServerResponse;
+use super::protocol::RefreshRemoteServerRequest;
+use super::protocol::RemoteControlTarget;
+use axum::http::HeaderMap;
+use axum::http::StatusCode;
+use codex_login::default_client::build_reqwest_client;
+use rand::Rng;
+use serde::Serialize;
+use serde::de::DeserializeOwned;
+use std::fmt;
+use std::io;
+use std::io::ErrorKind;
+use std::time::Duration;
+use time::OffsetDateTime;
+use time::format_description::well_known::Rfc3339;
+use tracing::warn;
+
+const REMOTE_CONTROL_ENROLL_TIMEOUT: Duration = Duration::from_secs(30);
+const REMOTE_CONTROL_SERVER_TOKEN_REFRESH_BACKOFF_MIN_SECS: u64 = 24;
+const REMOTE_CONTROL_SERVER_TOKEN_REFRESH_BACKOFF_MAX_SECS: u64 = 36;
+
+pub(super) const REMOTE_CONTROL_INSTALLATION_ID_HEADER: &str = "x-codex-installation-id";
+
+#[derive(Debug)]
+struct RemoteControlServerRequestError {
+ message: String,
+ status: Option,
+ retry_at: Option,
+}
+
+impl RemoteControlServerRequestError {
+ fn io_error(
+ message: String,
+ status: Option,
+ retry_at: Option,
+ timed_out: bool,
+ ) -> io::Error {
+ let kind = match status {
+ Some(StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN) => ErrorKind::PermissionDenied,
+ Some(StatusCode::NOT_FOUND) => ErrorKind::NotFound,
+ Some(status) if timed_out && !status.is_client_error() => ErrorKind::TimedOut,
+ None if timed_out => ErrorKind::TimedOut,
+ Some(_) | None => ErrorKind::Other,
+ };
+ io::Error::new(
+ kind,
+ Self {
+ message,
+ status,
+ retry_at,
+ },
+ )
+ }
+
+ fn is_transient(&self, kind: ErrorKind) -> bool {
+ kind == ErrorKind::TimedOut
+ || self.status.is_none()
+ || self.status.is_some_and(|status| {
+ status == StatusCode::TOO_MANY_REQUESTS || status.is_server_error()
+ })
+ }
+}
+
+impl fmt::Display for RemoteControlServerRequestError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str(&self.message)
+ }
+}
+
+impl std::error::Error for RemoteControlServerRequestError {}
+
+pub(super) async fn enroll_remote_control_server(
+ remote_control_target: &RemoteControlTarget,
+ auth: &RemoteControlConnectionAuth,
+ installation_id: &str,
+ server_name: &str,
+) -> io::Result {
+ let enroll_url = &remote_control_target.enroll_url;
+ let request = EnrollRemoteServerRequest {
+ name: server_name.to_string(),
+ os: std::env::consts::OS,
+ arch: std::env::consts::ARCH,
+ app_server_version: env!("CARGO_PKG_VERSION"),
+ installation_id: installation_id.to_string(),
+ };
+ let enrollment_response = send_remote_control_server_request::<_, EnrollRemoteServerResponse>(
+ enroll_url,
+ auth,
+ installation_id,
+ &request,
+ "enroll",
+ "server enrollment",
+ REMOTE_CONTROL_ENROLL_TIMEOUT,
+ )
+ .await?;
+ let mut enrollment = RemoteControlEnrollment {
+ remote_control_target: remote_control_target.clone(),
+ account_id: auth.account_id.clone(),
+ environment_id: enrollment_response.environment_id,
+ server_id: enrollment_response.server_id,
+ server_name: server_name.to_string(),
+ remote_control_token: None,
+ expires_at: None,
+ next_refresh_at: None,
+ };
+ update_remote_control_server_token(
+ &mut enrollment,
+ enroll_url,
+ enrollment_response.remote_control_token,
+ enrollment_response.expires_at,
+ )?;
+ Ok(enrollment)
+}
+
+pub(super) async fn refresh_remote_control_server(
+ auth: &RemoteControlConnectionAuth,
+ installation_id: &str,
+ enrollment: &mut RemoteControlEnrollment,
+) -> io::Result<()> {
+ let now = OffsetDateTime::now_utc();
+ let refresh_requirement = enrollment.server_token_refresh_requirement_at(now);
+ if refresh_requirement == RemoteControlServerTokenRefreshRequirement::NotNeeded {
+ return Ok(());
+ }
+ if refresh_requirement == RemoteControlServerTokenRefreshRequirement::Required
+ && let Some(next_refresh_at) = enrollment.next_refresh_at
+ && next_refresh_at > now
+ {
+ return Err(io::Error::new(
+ ErrorKind::WouldBlock,
+ format!("remote control server token refresh deferred until {next_refresh_at}"),
+ ));
+ }
+ let refresh_url = enrollment.remote_control_target.refresh_url.clone();
+ let request = RefreshRemoteServerRequest {
+ server_id: enrollment.server_id.clone(),
+ installation_id: installation_id.to_string(),
+ };
+ let refreshed = match send_remote_control_server_request::<_, EnrollRemoteServerResponse>(
+ &refresh_url,
+ auth,
+ installation_id,
+ &request,
+ "refresh",
+ "server refresh",
+ REMOTE_CONTROL_ENROLL_TIMEOUT,
+ )
+ .await
+ {
+ Ok(refreshed) => refreshed,
+ Err(err) => {
+ let Some(refresh_error) = remote_control_server_request_error(&err) else {
+ return Err(err);
+ };
+ if !refresh_error.is_transient(err.kind()) {
+ return Err(err);
+ }
+ let now = OffsetDateTime::now_utc();
+ let refresh_is_required = enrollment.server_token_refresh_requirement_at(now)
+ == RemoteControlServerTokenRefreshRequirement::Required;
+ let (refresh_delay, next_refresh_at) = refresh_deferral(refresh_error.retry_at, now);
+ enrollment.next_refresh_at = Some(next_refresh_at);
+ if refresh_is_required {
+ warn!(
+ refresh_url,
+ server_id = %enrollment.server_id,
+ environment_id = %enrollment.environment_id,
+ error = %err,
+ ?refresh_delay,
+ %next_refresh_at,
+ "required remote control server token refresh failed; deferring next attempt"
+ );
+ return Err(err);
+ }
+ warn!(
+ refresh_url,
+ server_id = %enrollment.server_id,
+ environment_id = %enrollment.environment_id,
+ error = %err,
+ ?refresh_delay,
+ %next_refresh_at,
+ "proactive remote control server token refresh failed; continuing with valid token"
+ );
+ return Ok(());
+ }
+ };
+ if refreshed.server_id != enrollment.server_id
+ || refreshed.environment_id != enrollment.environment_id
+ {
+ return Err(io::Error::other(format!(
+ "remote control server refresh returned mismatched enrollment: expected server_id={}, environment_id={}; got server_id={}, environment_id={}",
+ enrollment.server_id,
+ enrollment.environment_id,
+ refreshed.server_id,
+ refreshed.environment_id
+ )));
+ }
+
+ update_remote_control_server_token(
+ enrollment,
+ &refresh_url,
+ refreshed.remote_control_token,
+ refreshed.expires_at,
+ )
+}
+
+async fn send_remote_control_server_request(
+ url: &str,
+ auth: &RemoteControlConnectionAuth,
+ installation_id: &str,
+ request: &Request,
+ action: &str,
+ response_kind: &str,
+ timeout: Duration,
+) -> io::Result
+where
+ Request: Serialize,
+ Response: DeserializeOwned,
+{
+ let client = build_reqwest_client();
+ let auth_headers = auth.request_headers()?;
+ let response = client
+ .post(url)
+ .timeout(timeout)
+ .headers(auth_headers)
+ .header(REMOTE_CONTROL_INSTALLATION_ID_HEADER, installation_id)
+ .json(request)
+ .send()
+ .await
+ .map_err(|err| {
+ let timed_out = err.is_timeout();
+ RemoteControlServerRequestError::io_error(
+ format!("failed to {action} remote control server at `{url}`: {err}"),
+ /*status*/ None,
+ /*retry_at*/ None,
+ timed_out,
+ )
+ })?;
+ let headers = response.headers().clone();
+ let status = response.status();
+ let retry_at = parse_retry_after(&headers, OffsetDateTime::now_utc());
+ let body = response.bytes().await.map_err(|err| {
+ let timed_out = err.is_timeout();
+ RemoteControlServerRequestError::io_error(
+ format!("failed to read remote control {response_kind} response from `{url}`: {err}"),
+ Some(status),
+ retry_at,
+ timed_out,
+ )
+ })?;
+ let body_preview = preview_remote_control_response_body(&body);
+ if !status.is_success() {
+ let headers_str = format_headers(&headers);
+ return Err(RemoteControlServerRequestError::io_error(
+ format!(
+ "remote control {response_kind} failed at `{url}`: HTTP {status}, {headers_str}, body: {body_preview}"
+ ),
+ Some(status),
+ retry_at,
+ /*timed_out*/ false,
+ ));
+ }
+
+ serde_json::from_slice::(&body).map_err(|err| {
+ let headers_str = format_headers(&headers);
+ io::Error::other(format!(
+ "failed to parse remote control {response_kind} response from `{url}`: HTTP {status}, {headers_str}, body: {body_preview}, decode error: {err}"
+ ))
+ })
+}
+
+fn update_remote_control_server_token(
+ enrollment: &mut RemoteControlEnrollment,
+ url: &str,
+ token: String,
+ expires_at: String,
+) -> io::Result<()> {
+ let expires_at = OffsetDateTime::parse(&expires_at, &Rfc3339).map_err(|err| {
+ io::Error::other(format!(
+ "failed to parse remote control server token expiry from `{url}`: {err}"
+ ))
+ })?;
+ enrollment.remote_control_token = Some(token);
+ enrollment.expires_at = Some(expires_at);
+ enrollment.next_refresh_at = None;
+ Ok(())
+}
+
+fn remote_control_server_request_error(
+ err: &io::Error,
+) -> Option<&RemoteControlServerRequestError> {
+ err.get_ref()?.downcast_ref()
+}
+
+fn parse_retry_after(headers: &HeaderMap, received_at: OffsetDateTime) -> Option {
+ let retry_after = headers
+ .get(axum::http::header::RETRY_AFTER)?
+ .to_str()
+ .ok()?;
+ let retry_at = if let Ok(seconds) = retry_after.parse::() {
+ let seconds = i64::try_from(seconds).ok()?;
+ received_at.checked_add(time::Duration::seconds(seconds))?
+ } else {
+ OffsetDateTime::from(httpdate::parse_http_date(retry_after).ok()?)
+ };
+ (retry_at > received_at).then_some(retry_at)
+}
+
+fn refresh_deferral(
+ retry_at: Option,
+ now: OffsetDateTime,
+) -> (Duration, OffsetDateTime) {
+ if let Some(retry_at) = retry_at
+ && let Ok(delay) = Duration::try_from(retry_at - now)
+ && !delay.is_zero()
+ {
+ return (delay, retry_at);
+ }
+ let delay = remote_control_server_token_refresh_backoff();
+ let next_refresh_at = now + time::Duration::seconds(delay.as_secs() as i64);
+ (delay, next_refresh_at)
+}
+
+fn remote_control_server_token_refresh_backoff() -> Duration {
+ Duration::from_secs(rand::rng().random_range(
+ REMOTE_CONTROL_SERVER_TOKEN_REFRESH_BACKOFF_MIN_SECS
+ ..=REMOTE_CONTROL_SERVER_TOKEN_REFRESH_BACKOFF_MAX_SECS,
+ ))
+}
+
+#[cfg(test)]
+#[path = "server_api_tests.rs"]
+mod tests;
diff --git a/codex-rs/app-server-transport/src/transport/remote_control/server_api_tests.rs b/codex-rs/app-server-transport/src/transport/remote_control/server_api_tests.rs
new file mode 100644
index 0000000000..786dc65a90
--- /dev/null
+++ b/codex-rs/app-server-transport/src/transport/remote_control/server_api_tests.rs
@@ -0,0 +1,284 @@
+use super::*;
+use crate::transport::remote_control::protocol::normalize_remote_control_url;
+use pretty_assertions::assert_eq;
+use serde_json::json;
+use std::time::SystemTime;
+use tokio::io::AsyncWriteExt;
+use tokio::net::TcpListener;
+use tokio::sync::oneshot;
+
+const TEST_REQUEST_TIMEOUT: Duration = Duration::from_millis(100);
+
+fn auth() -> RemoteControlConnectionAuth {
+ RemoteControlConnectionAuth {
+ auth_provider: codex_model_provider::unauthenticated_auth_provider(),
+ account_id: "account-a".to_string(),
+ }
+}
+
+fn assert_transient_timeout(err: &io::Error, expected_status: Option) {
+ let request_error = remote_control_server_request_error(err)
+ .expect("request error should preserve refresh metadata");
+ assert_eq!(
+ (
+ err.kind(),
+ request_error.status,
+ request_error.is_transient(err.kind()),
+ ),
+ (ErrorKind::TimedOut, expected_status, true)
+ );
+}
+
+async fn timed_out_request(partial_response: Option<&'static [u8]>) -> io::Error {
+ let listener = TcpListener::bind("127.0.0.1:0")
+ .await
+ .expect("listener should bind");
+ let url = format!(
+ "http://{}/backend-api/wham/remote/control/server/refresh",
+ listener
+ .local_addr()
+ .expect("listener should have a local address")
+ );
+ let (request_done_tx, request_done_rx) = oneshot::channel();
+ let server_task = tokio::spawn(async move {
+ let (mut stream, _) = listener.accept().await.expect("request should connect");
+ if let Some(partial_response) = partial_response {
+ stream
+ .write_all(partial_response)
+ .await
+ .expect("partial response should write");
+ }
+ request_done_rx
+ .await
+ .expect("test should report request completion");
+ });
+
+ let err = send_remote_control_server_request::<_, serde_json::Value>(
+ &url,
+ &auth(),
+ "installation-id",
+ &json!({"server_id": "server-id"}),
+ "refresh",
+ "server refresh",
+ TEST_REQUEST_TIMEOUT,
+ )
+ .await
+ .expect_err("incomplete response should time out");
+ request_done_tx
+ .send(())
+ .expect("server should wait for request completion");
+ server_task.await.expect("server task should finish");
+ err
+}
+
+fn enrollment(now: OffsetDateTime) -> RemoteControlEnrollment {
+ RemoteControlEnrollment {
+ remote_control_target: normalize_remote_control_url("http://localhost/backend-api/")
+ .expect("target should normalize"),
+ account_id: "account-a".to_string(),
+ environment_id: "env_first".to_string(),
+ server_id: "srv_e_first".to_string(),
+ server_name: "first-server".to_string(),
+ remote_control_token: Some("token".to_string()),
+ expires_at: Some(now + time::Duration::seconds(300)),
+ next_refresh_at: None,
+ }
+}
+
+#[test]
+fn remote_control_enrollment_classifies_server_token_refresh_requirement() {
+ let now =
+ OffsetDateTime::from_unix_timestamp(1_700_000_000).expect("test timestamp should parse");
+ let enrollment = enrollment(now);
+ let cases = [
+ (
+ enrollment.clone(),
+ RemoteControlServerTokenRefreshRequirement::Proactive,
+ ),
+ (
+ RemoteControlEnrollment {
+ expires_at: Some(now + time::Duration::seconds(301)),
+ ..enrollment.clone()
+ },
+ RemoteControlServerTokenRefreshRequirement::NotNeeded,
+ ),
+ (
+ RemoteControlEnrollment {
+ next_refresh_at: Some(now + time::Duration::seconds(30)),
+ ..enrollment.clone()
+ },
+ RemoteControlServerTokenRefreshRequirement::NotNeeded,
+ ),
+ (
+ RemoteControlEnrollment {
+ next_refresh_at: Some(now),
+ ..enrollment.clone()
+ },
+ RemoteControlServerTokenRefreshRequirement::Proactive,
+ ),
+ (
+ RemoteControlEnrollment {
+ remote_control_token: None,
+ ..enrollment.clone()
+ },
+ RemoteControlServerTokenRefreshRequirement::Required,
+ ),
+ (
+ RemoteControlEnrollment {
+ expires_at: None,
+ ..enrollment.clone()
+ },
+ RemoteControlServerTokenRefreshRequirement::Required,
+ ),
+ (
+ RemoteControlEnrollment {
+ expires_at: Some(now),
+ next_refresh_at: Some(now + time::Duration::hours(1)),
+ ..enrollment
+ },
+ RemoteControlServerTokenRefreshRequirement::Required,
+ ),
+ ];
+
+ for (enrollment, expected) in cases {
+ assert_eq!(
+ enrollment.server_token_refresh_requirement_at(now),
+ expected
+ );
+ }
+}
+
+#[test]
+fn remote_control_server_request_error_classifies_status_before_timeout() {
+ let cases = [
+ (None, true, ErrorKind::TimedOut, true),
+ (Some(StatusCode::OK), true, ErrorKind::TimedOut, true),
+ (
+ Some(StatusCode::TOO_MANY_REQUESTS),
+ false,
+ ErrorKind::Other,
+ true,
+ ),
+ (Some(StatusCode::BAD_GATEWAY), false, ErrorKind::Other, true),
+ (
+ Some(StatusCode::UNAUTHORIZED),
+ true,
+ ErrorKind::PermissionDenied,
+ false,
+ ),
+ (
+ Some(StatusCode::FORBIDDEN),
+ true,
+ ErrorKind::PermissionDenied,
+ false,
+ ),
+ (
+ Some(StatusCode::NOT_FOUND),
+ true,
+ ErrorKind::NotFound,
+ false,
+ ),
+ (Some(StatusCode::BAD_REQUEST), true, ErrorKind::Other, false),
+ (None, false, ErrorKind::Other, true),
+ ];
+
+ for (status, timed_out, expected_kind, expected_transient) in cases {
+ let err = RemoteControlServerRequestError::io_error(
+ String::new(),
+ status,
+ /*retry_at*/ None,
+ timed_out,
+ );
+ let request_error = remote_control_server_request_error(&err)
+ .expect("request error should preserve refresh metadata");
+ assert_eq!(
+ (err.kind(), request_error.is_transient(err.kind())),
+ (expected_kind, expected_transient)
+ );
+ }
+}
+
+#[tokio::test]
+async fn request_timeout_before_response_headers_is_transient() {
+ let err = timed_out_request(/*partial_response*/ None).await;
+ assert_transient_timeout(&err, /*expected_status*/ None);
+}
+
+#[tokio::test]
+async fn response_body_timeout_is_transient() {
+ let err = timed_out_request(Some(b"HTTP/1.1 200 OK\r\nContent-Length: 20\r\n\r\n{")).await;
+ assert_transient_timeout(&err, Some(StatusCode::OK));
+}
+
+#[test]
+fn retry_after_supports_delta_seconds_and_http_dates() {
+ let now =
+ OffsetDateTime::from_unix_timestamp(1_700_000_000).expect("test timestamp should parse");
+ let mut headers = HeaderMap::new();
+ headers.insert(
+ axum::http::header::RETRY_AFTER,
+ axum::http::HeaderValue::from_static("120"),
+ );
+ assert_eq!(
+ parse_retry_after(&headers, now),
+ Some(now + time::Duration::seconds(120))
+ );
+
+ let retry_at = now + time::Duration::seconds(90);
+ let retry_at_system = SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_090);
+ headers.insert(
+ axum::http::header::RETRY_AFTER,
+ httpdate::fmt_http_date(retry_at_system)
+ .parse()
+ .expect("HTTP date should be a valid header value"),
+ );
+ assert_eq!(parse_retry_after(&headers, now), Some(retry_at));
+}
+
+#[test]
+fn invalid_or_expired_retry_after_uses_bounded_fallback() {
+ let now =
+ OffsetDateTime::from_unix_timestamp(1_700_000_000).expect("test timestamp should parse");
+ let mut headers = HeaderMap::new();
+ headers.insert(
+ axum::http::header::RETRY_AFTER,
+ axum::http::HeaderValue::from_static("invalid"),
+ );
+ assert_eq!(parse_retry_after(&headers, now), None);
+
+ headers.insert(
+ axum::http::header::RETRY_AFTER,
+ httpdate::fmt_http_date(SystemTime::UNIX_EPOCH + Duration::from_secs(1_699_999_999))
+ .parse()
+ .expect("HTTP date should be a valid header value"),
+ );
+ assert_eq!(parse_retry_after(&headers, now), None);
+
+ let expired_while_reading_body = Some(now + time::Duration::seconds(1));
+ for retry_at in [None, expired_while_reading_body] {
+ let deferred_at = now + time::Duration::seconds(2);
+ let (delay, next_refresh_at) = refresh_deferral(retry_at, deferred_at);
+ assert!(
+ (Duration::from_secs(REMOTE_CONTROL_SERVER_TOKEN_REFRESH_BACKOFF_MIN_SECS)
+ ..=Duration::from_secs(REMOTE_CONTROL_SERVER_TOKEN_REFRESH_BACKOFF_MAX_SECS,))
+ .contains(&delay)
+ );
+ assert_eq!(
+ next_refresh_at,
+ deferred_at + time::Duration::seconds(delay.as_secs() as i64)
+ );
+ }
+}
+
+#[test]
+fn http_date_retry_after_preserves_absolute_deadline() {
+ let received_at =
+ OffsetDateTime::from_unix_timestamp(1_700_000_000).expect("test timestamp should parse");
+ let retry_at = received_at + time::Duration::seconds(120);
+ let body_read_at = received_at + time::Duration::seconds(30);
+
+ assert_eq!(
+ refresh_deferral(Some(retry_at), body_read_at),
+ (Duration::from_secs(90), retry_at)
+ );
+}
diff --git a/codex-rs/app-server-transport/src/transport/remote_control/tests.rs b/codex-rs/app-server-transport/src/transport/remote_control/tests.rs
index 3a2d844654..465c6b13ec 100644
--- a/codex-rs/app-server-transport/src/transport/remote_control/tests.rs
+++ b/codex-rs/app-server-transport/src/transport/remote_control/tests.rs
@@ -1,5 +1,4 @@
use super::auth::REMOTE_CONTROL_ACCOUNT_ID_HEADER;
-use super::enroll::REMOTE_CONTROL_INSTALLATION_ID_HEADER;
use super::enroll::RemoteControlEnrollment;
use super::enroll::load_persisted_remote_control_enrollment;
use super::enroll::update_persisted_remote_control_enrollment;
@@ -8,6 +7,7 @@ use super::protocol::ClientEvent;
use super::protocol::ClientId;
use super::protocol::StreamId;
use super::protocol::normalize_remote_control_url;
+use super::server_api::REMOTE_CONTROL_INSTALLATION_ID_HEADER;
use super::websocket::REMOTE_CONTROL_PROTOCOL_VERSION;
use super::websocket::RemoteControlWebsocket;
use super::websocket::RemoteControlWebsocketConfig;
@@ -372,7 +372,7 @@ fn test_server_name() -> String {
gethostname().to_string_lossy().trim().to_string()
}
-fn remote_control_handle_with_current_enrollment(
+pub(super) fn remote_control_handle_with_current_enrollment(
remote_control_url: &str,
auth_manager: Arc,
) -> RemoteControlHandle {
@@ -400,6 +400,7 @@ fn remote_control_handle_with_current_enrollment(
OffsetDateTime::from_unix_timestamp(33_336_362_096)
.expect("future timestamp should parse"),
),
+ next_refresh_at: None,
},
)));
RemoteControlHandle {
@@ -1682,6 +1683,7 @@ async fn remote_control_http_mode_refreshes_persisted_enrollment_before_connecti
server_name: "persisted-server".to_string(),
remote_control_token: None,
expires_at: None,
+ next_refresh_at: None,
};
update_persisted_remote_control_enrollment(
Some(state_db.as_ref()),
@@ -1802,6 +1804,7 @@ async fn remote_control_stdio_mode_waits_for_client_name_before_connecting() {
server_name: "persisted-server".to_string(),
remote_control_token: None,
expires_at: None,
+ next_refresh_at: None,
};
update_persisted_remote_control_enrollment(
Some(state_db.as_ref()),
@@ -1899,6 +1902,7 @@ async fn remote_control_waits_for_account_id_before_enrolling() {
server_name: expected_server_name,
remote_control_token: None,
expires_at: None,
+ next_refresh_at: None,
};
let (transport_event_tx, _transport_event_rx) =
@@ -1995,6 +1999,7 @@ async fn persisted_enable_does_not_follow_auth_to_an_account_without_a_preferenc
server_name: "server-a".to_string(),
remote_control_token: None,
expires_at: None,
+ next_refresh_at: None,
};
update_persisted_remote_control_enrollment(
Some(state_db.as_ref()),
@@ -2102,6 +2107,7 @@ async fn remote_control_http_mode_reenrolls_when_refresh_reports_stale_enrollmen
server_name: "stale-server".to_string(),
remote_control_token: None,
expires_at: None,
+ next_refresh_at: None,
};
let refreshed_enrollment = RemoteControlEnrollment {
remote_control_target: remote_control_target.clone(),
@@ -2111,6 +2117,7 @@ async fn remote_control_http_mode_reenrolls_when_refresh_reports_stale_enrollmen
server_name: expected_server_name,
remote_control_token: None,
expires_at: None,
+ next_refresh_at: None,
};
update_persisted_remote_control_enrollment(
Some(state_db.as_ref()),
@@ -2225,6 +2232,7 @@ async fn remote_control_http_mode_reenrolls_after_explicit_missing_server_404()
server_name: "stale-server".to_string(),
remote_control_token: None,
expires_at: None,
+ next_refresh_at: None,
};
let refreshed_enrollment = RemoteControlEnrollment {
remote_control_target: remote_control_target.clone(),
@@ -2234,6 +2242,7 @@ async fn remote_control_http_mode_reenrolls_after_explicit_missing_server_404()
server_name: expected_server_name,
remote_control_token: None,
expires_at: None,
+ next_refresh_at: None,
};
update_persisted_remote_control_enrollment(
Some(state_db.as_ref()),
@@ -2371,6 +2380,7 @@ async fn remote_control_http_mode_preserves_stale_enrollment_when_reenrollment_f
server_name: test_server_name(),
remote_control_token: None,
expires_at: None,
+ next_refresh_at: None,
};
update_persisted_remote_control_enrollment(
Some(state_db.as_ref()),
@@ -2421,6 +2431,7 @@ async fn remote_control_http_mode_preserves_stale_enrollment_when_reenrollment_f
retry_refresh_request.request_line,
"POST /backend-api/wham/remote/control/server/refresh HTTP/1.1"
);
+ let refresh_failed_at = OffsetDateTime::now_utc();
respond_with_status(
retry_refresh_request.stream,
"500 Internal Server Error",
@@ -2428,9 +2439,26 @@ async fn remote_control_http_mode_preserves_stale_enrollment_when_reenrollment_f
)
.await;
+ let current_enrollment = remote_handle
+ .current_enrollment
+ .lock()
+ .await
+ .clone()
+ .expect("stale enrollment should remain available");
+ let next_refresh_at = current_enrollment
+ .next_refresh_at
+ .expect("required refresh failure should set a retry deadline");
+ assert!(
+ (refresh_failed_at + time::Duration::seconds(24)
+ ..=OffsetDateTime::now_utc() + time::Duration::seconds(36))
+ .contains(&next_refresh_at)
+ );
assert_eq!(
- *remote_handle.current_enrollment.lock().await,
- Some(stale_enrollment.clone())
+ current_enrollment,
+ RemoteControlEnrollment {
+ next_refresh_at: Some(next_refresh_at),
+ ..stale_enrollment.clone()
+ }
);
assert_eq!(
state_db
@@ -2474,6 +2502,7 @@ async fn remote_control_http_mode_preserves_enrollment_after_generic_websocket_4
server_name: "stale-server".to_string(),
remote_control_token: None,
expires_at: None,
+ next_refresh_at: None,
};
update_persisted_remote_control_enrollment(
Some(state_db.as_ref()),
diff --git a/codex-rs/app-server-transport/src/transport/remote_control/tests/pairing_tests.rs b/codex-rs/app-server-transport/src/transport/remote_control/tests/pairing_tests.rs
index 380f13984e..6f8b9878ea 100644
--- a/codex-rs/app-server-transport/src/transport/remote_control/tests/pairing_tests.rs
+++ b/codex-rs/app-server-transport/src/transport/remote_control/tests/pairing_tests.rs
@@ -21,6 +21,69 @@ fn remote_control_enrollment(
OffsetDateTime::from_unix_timestamp(33_336_362_096)
.expect("future timestamp should parse"),
),
+ next_refresh_at: None,
+ }
+}
+
+async fn auth_manager_with_replacement(
+ codex_home: &TempDir,
+ replacement_account_id: &str,
+) -> Arc {
+ let mut stale_auth = remote_control_auth_dot_json(Some("account_id"));
+ stale_auth
+ .tokens
+ .as_mut()
+ .expect("stale auth should include tokens")
+ .access_token = "stale-token".to_string();
+ save_auth(
+ codex_home.path(),
+ &stale_auth,
+ AuthCredentialsStoreMode::File,
+ AuthKeyringBackendKind::default(),
+ )
+ .expect("stale auth should save");
+ let auth_manager = AuthManager::shared(
+ codex_home.path().to_path_buf(),
+ /*enable_codex_api_key_env*/ false,
+ AuthCredentialsStoreMode::File,
+ /*forced_chatgpt_workspace_id*/ None,
+ /*chatgpt_base_url*/ None,
+ AuthKeyringBackendKind::default(),
+ /*auth_route_config*/ None,
+ )
+ .await;
+ let mut replacement_auth = remote_control_auth_dot_json(Some(replacement_account_id));
+ replacement_auth
+ .tokens
+ .as_mut()
+ .expect("replacement auth should include tokens")
+ .access_token = "fresh-token".to_string();
+ save_auth(
+ codex_home.path(),
+ &replacement_auth,
+ AuthCredentialsStoreMode::File,
+ AuthKeyringBackendKind::default(),
+ )
+ .expect("replacement auth should save");
+ auth_manager
+}
+
+fn pairing_response_json(server_id: &str, environment_id: &str) -> serde_json::Value {
+ json!({
+ "pairing_code": "pairing-code",
+ "manual_pairing_code": "ABCD-EFGH",
+ "server_id": server_id,
+ "environment_id": environment_id,
+ "expires_at": "3026-05-22T12:34:56Z",
+ })
+}
+
+fn pairing_response(environment_id: &str) -> RemoteControlPairingStartResponse {
+ RemoteControlPairingStartResponse {
+ pairing_code: "pairing-code".to_string(),
+ manual_pairing_code: Some("ABCD-EFGH".to_string()),
+ environment_id: environment_id.to_string(),
+ expires_at: 33_336_362_096,
}
}
@@ -147,13 +210,7 @@ async fn remote_control_handle_starts_pairing_before_websocket_connects() {
);
respond_with_json(
pairing_request.stream,
- json!({
- "pairing_code": "pairing-code",
- "manual_pairing_code": "ABCD-EFGH",
- "server_id": "srv_e_test",
- "environment_id": "env_test",
- "expires_at": "3026-05-22T12:34:56Z",
- }),
+ pairing_response_json("srv_e_test", "env_test"),
)
.await;
});
@@ -178,15 +235,139 @@ async fn remote_control_handle_starts_pairing_before_websocket_connects() {
.expect("pairing should use the current server before websocket connect");
server_task.await.expect("server task should finish");
- assert_eq!(
- response,
- RemoteControlPairingStartResponse {
- pairing_code: "pairing-code".to_string(),
- manual_pairing_code: Some("ABCD-EFGH".to_string()),
- environment_id: "env_test".to_string(),
- expires_at: 33_336_362_096,
- }
+ assert_eq!(response, pairing_response("env_test"));
+}
+
+#[tokio::test]
+async fn proactive_refresh_rate_limit_uses_valid_token_for_pairing() {
+ let listener = TcpListener::bind("127.0.0.1:0")
+ .await
+ .expect("listener should bind");
+ let remote_control_url = remote_control_url_for_listener(&listener);
+ let server_task = tokio::spawn(async move {
+ let refresh_request = accept_http_request(&listener).await;
+ assert_eq!(
+ refresh_request.request_line,
+ "POST /backend-api/wham/remote/control/server/refresh HTTP/1.1"
+ );
+ respond_with_status_and_headers(
+ refresh_request.stream,
+ "429 Too Many Requests",
+ &[],
+ "rate limited",
+ )
+ .await;
+
+ let pairing_request = accept_http_request(&listener).await;
+ assert_eq!(
+ pairing_request.request_line,
+ "POST /backend-api/wham/remote/control/server/pair HTTP/1.1"
+ );
+ assert_eq!(
+ pairing_request.headers.get("authorization"),
+ Some(&format!("Bearer {TEST_REMOTE_CONTROL_SERVER_TOKEN}"))
+ );
+ respond_with_json(
+ pairing_request.stream,
+ pairing_response_json("srv_e_test", "env_test"),
+ )
+ .await;
+ });
+ let remote_handle = remote_control_handle_with_current_enrollment(
+ &remote_control_url,
+ remote_control_auth_manager(),
);
+ remote_handle
+ .current_enrollment
+ .lock()
+ .await
+ .as_mut()
+ .expect("current enrollment should exist")
+ .expires_at = Some(OffsetDateTime::now_utc() + time::Duration::minutes(4));
+
+ let response = remote_handle
+ .start_pairing(
+ RemoteControlPairingStartParams::default(),
+ /*app_server_client_name*/ None,
+ )
+ .await
+ .expect("valid token should allow pairing after proactive refresh failure");
+ server_task.await.expect("server task should finish");
+
+ assert_eq!(response, pairing_response("env_test"));
+ assert!(
+ remote_handle
+ .current_enrollment
+ .snapshot()
+ .and_then(|enrollment| enrollment.next_refresh_at)
+ .is_some()
+ );
+}
+
+#[tokio::test]
+async fn required_refresh_deadline_blocks_pairing_without_request() {
+ let listener = TcpListener::bind("127.0.0.1:0")
+ .await
+ .expect("listener should bind");
+ let remote_control_url = remote_control_url_for_listener(&listener);
+ let server_task = tokio::spawn(async move {
+ let refresh_request = accept_http_request(&listener).await;
+ assert_eq!(
+ refresh_request.request_line,
+ "POST /backend-api/wham/remote/control/server/refresh HTTP/1.1"
+ );
+ respond_with_status_and_headers(
+ refresh_request.stream,
+ "502 Bad Gateway",
+ &[("retry-after", "120")],
+ "upstream unavailable",
+ )
+ .await;
+ listener
+ });
+ let remote_handle = remote_control_handle_with_current_enrollment(
+ &remote_control_url,
+ remote_control_auth_manager(),
+ );
+ remote_handle
+ .current_enrollment
+ .lock()
+ .await
+ .as_mut()
+ .expect("current enrollment should exist")
+ .expires_at = Some(OffsetDateTime::now_utc() - time::Duration::seconds(1));
+
+ let refresh_err = remote_handle
+ .start_pairing(
+ RemoteControlPairingStartParams::default(),
+ /*app_server_client_name*/ None,
+ )
+ .await
+ .expect_err("required refresh failure should block pairing");
+ let listener = server_task.await.expect("server task should finish");
+ let next_refresh_at = remote_handle
+ .current_enrollment
+ .snapshot()
+ .and_then(|enrollment| enrollment.next_refresh_at)
+ .expect("required pairing refresh should preserve the retry deadline");
+ let deferred_err = remote_handle
+ .start_pairing(
+ RemoteControlPairingStartParams::default(),
+ /*app_server_client_name*/ None,
+ )
+ .await
+ .expect_err("required refresh deadline should block pairing");
+
+ assert!(refresh_err.to_string().contains("HTTP 502 Bad Gateway"));
+ assert_eq!(deferred_err.kind(), io::ErrorKind::WouldBlock);
+ assert!(
+ deferred_err
+ .to_string()
+ .contains(&next_refresh_at.to_string())
+ );
+ timeout(Duration::from_millis(100), listener.accept())
+ .await
+ .expect_err("pairing should not issue a request before the refresh deadline");
}
#[tokio::test]
@@ -423,13 +604,7 @@ async fn remote_control_handle_refreshes_after_pairing_auth_failure() {
);
respond_with_json(
refreshed_pairing_request.stream,
- json!({
- "pairing_code": "pairing-code",
- "manual_pairing_code": "ABCD-EFGH",
- "server_id": "srv_e_test",
- "environment_id": "env_test",
- "expires_at": "3026-05-22T12:34:56Z",
- }),
+ pairing_response_json("srv_e_test", "env_test"),
)
.await;
});
@@ -447,14 +622,54 @@ async fn remote_control_handle_refreshes_after_pairing_auth_failure() {
.expect("pairing should refresh after server token auth failure");
server_task.await.expect("server task should finish");
+ assert_eq!(response, pairing_response("env_test"));
+}
+
+#[tokio::test]
+async fn pairing_auth_failure_preserves_refresh_deadline() {
+ let listener = TcpListener::bind("127.0.0.1:0")
+ .await
+ .expect("listener should bind");
+ let remote_control_url = remote_control_url_for_listener(&listener);
+ let server_task = tokio::spawn(async move {
+ let pairing_request = accept_http_request(&listener).await;
+ assert_eq!(
+ pairing_request.request_line,
+ "POST /backend-api/wham/remote/control/server/pair HTTP/1.1"
+ );
+ respond_with_status(pairing_request.stream, "401 Unauthorized", "").await;
+ });
+ let remote_handle = remote_control_handle_with_current_enrollment(
+ &remote_control_url,
+ remote_control_auth_manager(),
+ );
+ let next_refresh_at = OffsetDateTime::now_utc() + time::Duration::minutes(2);
+ remote_handle
+ .current_enrollment
+ .lock()
+ .await
+ .as_mut()
+ .expect("current enrollment should exist")
+ .next_refresh_at = Some(next_refresh_at);
+ let mut expected_enrollment = remote_handle
+ .current_enrollment
+ .snapshot()
+ .expect("current enrollment should exist");
+ expected_enrollment.clear_server_token();
+
+ let err = remote_handle
+ .start_pairing(
+ RemoteControlPairingStartParams::default(),
+ /*app_server_client_name*/ None,
+ )
+ .await
+ .expect_err("refresh deadline should throttle recovery after token rejection");
+ server_task.await.expect("server task should finish");
+
+ assert_eq!(err.kind(), io::ErrorKind::WouldBlock);
assert_eq!(
- response,
- RemoteControlPairingStartResponse {
- pairing_code: "pairing-code".to_string(),
- manual_pairing_code: Some("ABCD-EFGH".to_string()),
- environment_id: "env_test".to_string(),
- expires_at: 33_336_362_096,
- }
+ remote_handle.current_enrollment.snapshot(),
+ Some(expected_enrollment)
);
}
@@ -508,53 +723,12 @@ async fn remote_control_handle_recovers_auth_before_refreshing_pairing() {
);
respond_with_json(
pairing_request.stream,
- json!({
- "pairing_code": "pairing-code",
- "manual_pairing_code": "ABCD-EFGH",
- "server_id": "srv_e_test",
- "environment_id": "env_test",
- "expires_at": "3026-05-22T12:34:56Z",
- }),
+ pairing_response_json("srv_e_test", "env_test"),
)
.await;
});
let codex_home = TempDir::new().expect("temp dir should create");
- let mut stale_auth = remote_control_auth_dot_json(Some("account_id"));
- stale_auth
- .tokens
- .as_mut()
- .expect("stale auth should include tokens")
- .access_token = "stale-token".to_string();
- save_auth(
- codex_home.path(),
- &stale_auth,
- AuthCredentialsStoreMode::File,
- AuthKeyringBackendKind::default(),
- )
- .expect("stale auth should save");
- let auth_manager = AuthManager::shared(
- codex_home.path().to_path_buf(),
- /*enable_codex_api_key_env*/ false,
- AuthCredentialsStoreMode::File,
- /*forced_chatgpt_workspace_id*/ None,
- /*chatgpt_base_url*/ None,
- AuthKeyringBackendKind::default(),
- /*auth_route_config*/ None,
- )
- .await;
- let mut fresh_auth = remote_control_auth_dot_json(Some("account_id"));
- fresh_auth
- .tokens
- .as_mut()
- .expect("fresh auth should include tokens")
- .access_token = "fresh-token".to_string();
- save_auth(
- codex_home.path(),
- &fresh_auth,
- AuthCredentialsStoreMode::File,
- AuthKeyringBackendKind::default(),
- )
- .expect("fresh auth should save");
+ let auth_manager = auth_manager_with_replacement(&codex_home, "account_id").await;
let remote_handle =
remote_control_handle_with_current_enrollment(&remote_control_url, auth_manager);
remote_handle
@@ -574,14 +748,128 @@ async fn remote_control_handle_recovers_auth_before_refreshing_pairing() {
.expect("pairing should refresh after auth recovery");
server_task.await.expect("server task should finish");
+ assert_eq!(response, pairing_response("env_test"));
+}
+
+#[tokio::test]
+async fn pairing_publishes_refresh_deferral_after_auth_recovery() {
+ let listener = TcpListener::bind("127.0.0.1:0")
+ .await
+ .expect("listener should bind");
+ let remote_control_url = remote_control_url_for_listener(&listener);
+ let server_task = tokio::spawn(async move {
+ let stale_refresh_request = accept_http_request(&listener).await;
+ assert_eq!(
+ stale_refresh_request.headers.get("authorization"),
+ Some(&"Bearer stale-token".to_string())
+ );
+ respond_with_status(stale_refresh_request.stream, "401 Unauthorized", "").await;
+
+ let recovered_refresh_request = accept_http_request(&listener).await;
+ assert_eq!(
+ recovered_refresh_request.headers.get("authorization"),
+ Some(&"Bearer fresh-token".to_string())
+ );
+ respond_with_status_and_headers(
+ recovered_refresh_request.stream,
+ "502 Bad Gateway",
+ &[("retry-after", "120")],
+ "upstream unavailable",
+ )
+ .await;
+ });
+ let codex_home = TempDir::new().expect("temp dir should create");
+ let auth_manager = auth_manager_with_replacement(&codex_home, "account_id").await;
+ let remote_handle =
+ remote_control_handle_with_current_enrollment(&remote_control_url, auth_manager);
+ remote_handle
+ .current_enrollment
+ .lock()
+ .await
+ .as_mut()
+ .expect("current enrollment should exist")
+ .expires_at = Some(OffsetDateTime::now_utc() - time::Duration::seconds(1));
+
+ let refresh_started_at = OffsetDateTime::now_utc();
+ let refresh_err = remote_handle
+ .start_pairing(
+ RemoteControlPairingStartParams::default(),
+ /*app_server_client_name*/ None,
+ )
+ .await
+ .expect_err("required refresh should remain strict after auth recovery");
+ let refresh_completed_at = OffsetDateTime::now_utc();
+ let deferred_err = remote_handle
+ .start_pairing(
+ RemoteControlPairingStartParams::default(),
+ /*app_server_client_name*/ None,
+ )
+ .await
+ .expect_err("published deadline should throttle the next pairing refresh");
+ server_task.await.expect("server task should finish");
+
+ assert!(refresh_err.to_string().contains("HTTP 502 Bad Gateway"));
+ assert_eq!(deferred_err.kind(), io::ErrorKind::WouldBlock);
+ let next_refresh_at = remote_handle
+ .current_enrollment
+ .snapshot()
+ .and_then(|enrollment| enrollment.next_refresh_at)
+ .expect("required refresh failure should publish its retry deadline");
+ assert!(
+ (refresh_started_at + time::Duration::seconds(120)
+ ..=refresh_completed_at + time::Duration::seconds(120))
+ .contains(&next_refresh_at)
+ );
+}
+
+#[tokio::test]
+async fn pairing_auth_recovery_failure_publishes_cleared_server_token() {
+ let listener = TcpListener::bind("127.0.0.1:0")
+ .await
+ .expect("listener should bind");
+ let remote_control_url = remote_control_url_for_listener(&listener);
+ let server_task = tokio::spawn(async move {
+ let stale_refresh_request = accept_http_request(&listener).await;
+ assert_eq!(
+ stale_refresh_request.request_line,
+ "POST /backend-api/wham/remote/control/server/refresh HTTP/1.1"
+ );
+ assert_eq!(
+ stale_refresh_request.headers.get("authorization"),
+ Some(&"Bearer stale-token".to_string())
+ );
+ respond_with_status(stale_refresh_request.stream, "401 Unauthorized", "").await;
+ });
+ let codex_home = TempDir::new().expect("temp dir should create");
+ let auth_manager = auth_manager_with_replacement(&codex_home, "different_account_id").await;
+ let remote_handle =
+ remote_control_handle_with_current_enrollment(&remote_control_url, auth_manager);
+ remote_handle
+ .current_enrollment
+ .lock()
+ .await
+ .as_mut()
+ .expect("current enrollment should exist")
+ .expires_at = Some(OffsetDateTime::now_utc() + time::Duration::seconds(29));
+ let mut expected_enrollment = remote_handle
+ .current_enrollment
+ .snapshot()
+ .expect("current enrollment should exist");
+ expected_enrollment.clear_server_token();
+
+ let err = remote_handle
+ .start_pairing(
+ RemoteControlPairingStartParams::default(),
+ /*app_server_client_name*/ None,
+ )
+ .await
+ .expect_err("pairing should fail after auth changes account");
+ server_task.await.expect("server task should finish");
+
+ assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
assert_eq!(
- response,
- RemoteControlPairingStartResponse {
- pairing_code: "pairing-code".to_string(),
- manual_pairing_code: Some("ABCD-EFGH".to_string()),
- environment_id: "env_test".to_string(),
- expires_at: 33_336_362_096,
- }
+ remote_handle.current_enrollment.snapshot(),
+ Some(expected_enrollment)
);
}
@@ -688,6 +976,7 @@ async fn remote_control_handle_reenrolls_after_stale_pairing_enrollment() {
server_name: test_server_name(),
remote_control_token: None,
expires_at: None,
+ next_refresh_at: None,
};
update_persisted_remote_control_enrollment(
Some(state_db.as_ref()),
@@ -745,13 +1034,10 @@ async fn remote_control_handle_reenrolls_after_stale_pairing_enrollment() {
);
respond_with_json(
refreshed_pairing_request.stream,
- json!({
- "pairing_code": "pairing-code",
- "manual_pairing_code": "ABCD-EFGH",
- "server_id": server_refreshed_enrollment.server_id,
- "environment_id": server_refreshed_enrollment.environment_id,
- "expires_at": "3026-05-22T12:34:56Z",
- }),
+ pairing_response_json(
+ &server_refreshed_enrollment.server_id,
+ &server_refreshed_enrollment.environment_id,
+ ),
)
.await;
});
@@ -764,15 +1050,7 @@ async fn remote_control_handle_reenrolls_after_stale_pairing_enrollment() {
.expect("pairing should re-enroll after stale enrollment");
server_task.await.expect("server task should finish");
- assert_eq!(
- response,
- RemoteControlPairingStartResponse {
- pairing_code: "pairing-code".to_string(),
- manual_pairing_code: Some("ABCD-EFGH".to_string()),
- environment_id: "env_refreshed".to_string(),
- expires_at: 33_336_362_096,
- }
- );
+ assert_eq!(response, pairing_response("env_refreshed"));
assert_eq!(
state_db
.get_remote_control_enrollment(
diff --git a/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs b/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs
index e34cd34091..7434454499 100644
--- a/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs
+++ b/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs
@@ -23,12 +23,12 @@ use crate::transport::remote_control::auth::recover_remote_control_auth;
use crate::transport::remote_control::client_tracker::ClientTracker;
use crate::transport::remote_control::client_tracker::REMOTE_CONTROL_IDLE_SWEEP_INTERVAL;
use crate::transport::remote_control::enroll::RemoteControlEnrollment;
-use crate::transport::remote_control::enroll::enroll_remote_control_server;
use crate::transport::remote_control::enroll::format_headers;
use crate::transport::remote_control::enroll::load_persisted_remote_control_enrollment;
use crate::transport::remote_control::enroll::preview_remote_control_response_body;
-use crate::transport::remote_control::enroll::refresh_remote_control_server;
use crate::transport::remote_control::enroll::update_persisted_remote_control_enrollment;
+use crate::transport::remote_control::server_api::enroll_remote_control_server;
+use crate::transport::remote_control::server_api::refresh_remote_control_server;
use axum::http::HeaderValue;
use base64::Engine;
use codex_app_server_protocol::RemoteControlConnectionStatus;
@@ -1566,17 +1566,19 @@ async fn prepare_remote_control_enrollment(
)
.await?;
}
- Err(err)
- if err.kind() == ErrorKind::PermissionDenied
- && recover_remote_control_auth(
- auth_context.auth_recovery,
- auth_context.auth_change_rx,
- )
- .await =>
- {
- return Err(io::Error::other(format!(
- "{err}; retrying after auth recovery"
- )));
+ Err(err) if err.kind() == ErrorKind::PermissionDenied => {
+ if recover_remote_control_auth(
+ auth_context.auth_recovery,
+ auth_context.auth_change_rx,
+ )
+ .await
+ {
+ return Err(io::Error::other(format!(
+ "{err}; retrying after auth recovery"
+ )));
+ }
+ enrollment_ref.clear_server_token();
+ return Err(err);
}
Err(err) => return Err(err),
}
@@ -1681,13 +1683,15 @@ async fn clear_remote_control_server_token_if_matches(
enrollment: &RemoteControlEnrollment,
) -> io::Result<()> {
let mut current_enrollment = current_enrollment.lock().await;
- current_enrollment
+ let current_enrollment = current_enrollment
.as_mut()
.filter(|current| same_remote_control_enrollment(current, enrollment))
.ok_or_else(|| {
io::Error::other("missing remote control enrollment after websocket auth failure")
- })?
- .clear_server_token();
+ })?;
+ if current_enrollment.remote_control_token == enrollment.remote_control_token {
+ current_enrollment.clear_server_token();
+ }
Ok(())
}
@@ -1804,6 +1808,10 @@ fn format_remote_control_websocket_connect_error(
message
}
+#[cfg(test)]
+#[path = "websocket_refresh_tests.rs"]
+mod refresh_tests;
+
#[cfg(test)]
mod tests {
use super::*;
@@ -1844,13 +1852,15 @@ mod tests {
// Windows Bazel CI can take longer than a few seconds for the websocket
// client connection attempt to reach the local test listener.
#[cfg(windows)]
- const TEST_HTTP_ACCEPT_TIMEOUT: Duration = Duration::from_secs(30);
+ pub(super) const TEST_HTTP_ACCEPT_TIMEOUT: Duration = Duration::from_secs(30);
#[cfg(not(windows))]
- const TEST_HTTP_ACCEPT_TIMEOUT: Duration = Duration::from_secs(5);
- const TEST_INSTALLATION_ID: &str = "11111111-1111-4111-8111-111111111111";
- const TEST_REMOTE_CONTROL_SERVER_TOKEN: &str = "Remote Control Token";
+ pub(super) const TEST_HTTP_ACCEPT_TIMEOUT: Duration = Duration::from_secs(5);
+ pub(super) const TEST_INSTALLATION_ID: &str = "11111111-1111-4111-8111-111111111111";
+ pub(super) const TEST_REMOTE_CONTROL_SERVER_TOKEN: &str = "Remote Control Token";
- fn remote_control_enrollment(remote_control_token: Option<&str>) -> RemoteControlEnrollment {
+ pub(super) fn remote_control_enrollment(
+ remote_control_token: Option<&str>,
+ ) -> RemoteControlEnrollment {
RemoteControlEnrollment {
remote_control_target: normalize_remote_control_url("http://localhost/backend-api/")
.expect("target should normalize"),
@@ -1861,10 +1871,11 @@ mod tests {
remote_control_token: remote_control_token.map(str::to_string),
expires_at: remote_control_token
.map(|_| time::OffsetDateTime::now_utc() + time::Duration::hours(1)),
+ next_refresh_at: None,
}
}
- fn test_current_enrollment(
+ pub(super) fn test_current_enrollment(
enrollment: Option,
) -> CurrentRemoteControlEnrollment {
Arc::new(RemoteControlEnrollmentState::new(enrollment))
@@ -1930,7 +1941,7 @@ mod tests {
));
}
- fn remote_control_status_channel() -> (
+ pub(super) fn remote_control_status_channel() -> (
RemoteControlStatusPublisher,
watch::Receiver,
) {
@@ -1943,7 +1954,7 @@ mod tests {
(RemoteControlStatusPublisher::new(status_tx), status_rx)
}
- fn enabled_desired_state_sender() -> watch::Sender {
+ pub(super) fn enabled_desired_state_sender() -> watch::Sender {
watch::channel(RemoteControlDesiredState::Enabled {
persistence_preference: None,
})
@@ -1981,17 +1992,17 @@ mod tests {
);
}
- async fn remote_control_state_runtime(codex_home: &TempDir) -> Arc {
+ pub(super) async fn remote_control_state_runtime(codex_home: &TempDir) -> Arc {
StateRuntime::init(codex_home.path().to_path_buf(), "test-provider".to_string())
.await
.expect("state runtime should initialize")
}
- fn remote_control_auth_manager() -> Arc {
+ pub(super) fn remote_control_auth_manager() -> Arc {
auth_manager_from_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
}
- fn remote_control_url_for_listener(listener: &TcpListener) -> String {
+ pub(super) fn remote_control_url_for_listener(listener: &TcpListener) -> String {
let addr = listener
.local_addr()
.expect("listener should have a local addr");
@@ -2126,9 +2137,10 @@ mod tests {
let auth_manager = remote_control_auth_manager();
let mut auth_recovery = auth_manager.unauthorized_recovery();
let mut auth_change_rx = auth_manager.auth_change_receiver();
- let current_enrollment = test_current_enrollment(Some(remote_control_enrollment(Some(
- TEST_REMOTE_CONTROL_SERVER_TOKEN,
- ))));
+ let next_refresh_at = time::OffsetDateTime::now_utc() + time::Duration::minutes(2);
+ let mut enrollment = remote_control_enrollment(Some(TEST_REMOTE_CONTROL_SERVER_TOKEN));
+ enrollment.next_refresh_at = Some(next_refresh_at);
+ let current_enrollment = test_current_enrollment(Some(enrollment));
let (status_publisher, status_rx) = remote_control_status_channel();
let server_task = tokio::spawn(async move {
@@ -2178,6 +2190,7 @@ mod tests {
);
let mut expected_enrollment = remote_control_enrollment(/*remote_control_token*/ None);
expected_enrollment.remote_control_target = remote_control_target;
+ expected_enrollment.next_refresh_at = Some(next_refresh_at);
assert_eq!(*current_enrollment.lock().await, Some(expected_enrollment));
}
@@ -2318,9 +2331,12 @@ mod tests {
.await;
let mut auth_recovery = auth_manager.unauthorized_recovery();
let mut auth_change_rx = auth_manager.auth_change_receiver();
- let current_enrollment = test_current_enrollment(Some(remote_control_enrollment(
- /*remote_control_token*/ None,
- )));
+ let mut expected_enrollment =
+ remote_control_enrollment(Some(TEST_REMOTE_CONTROL_SERVER_TOKEN));
+ expected_enrollment.remote_control_target = remote_control_target.clone();
+ expected_enrollment.expires_at =
+ Some(time::OffsetDateTime::now_utc() + time::Duration::minutes(4));
+ let current_enrollment = test_current_enrollment(Some(expected_enrollment.clone()));
let (status_publisher, status_rx) = remote_control_status_channel();
save_auth(
codex_home.path(),
@@ -2377,6 +2393,7 @@ mod tests {
.expect("token should be readable"),
"fresh-token"
);
+ assert_eq!(current_enrollment.snapshot(), Some(expected_enrollment));
assert!(
!auth_change_rx
.has_changed()
@@ -3411,7 +3428,7 @@ mod tests {
state.observe_client_message(envelope, wire_size_bytes)
}
- async fn accept_http_request(listener: &TcpListener) -> (TcpStream, String) {
+ pub(super) async fn accept_http_request(listener: &TcpListener) -> (TcpStream, String) {
let (stream, _) = timeout(TEST_HTTP_ACCEPT_TIMEOUT, listener.accept())
.await
.expect("HTTP request should arrive in time")
@@ -3482,7 +3499,7 @@ mod tests {
serde_json::from_str(text.as_ref()).expect("server event should deserialize")
}
- async fn respond_with_status_and_headers(
+ pub(super) async fn respond_with_status_and_headers(
mut stream: TcpStream,
status: &str,
headers: &[(&str, &str)],
diff --git a/codex-rs/app-server-transport/src/transport/remote_control/websocket_refresh_tests.rs b/codex-rs/app-server-transport/src/transport/remote_control/websocket_refresh_tests.rs
new file mode 100644
index 0000000000..ee4ea15f83
--- /dev/null
+++ b/codex-rs/app-server-transport/src/transport/remote_control/websocket_refresh_tests.rs
@@ -0,0 +1,467 @@
+use super::tests::TEST_HTTP_ACCEPT_TIMEOUT;
+use super::tests::TEST_INSTALLATION_ID;
+use super::tests::TEST_REMOTE_CONTROL_SERVER_TOKEN;
+use super::tests::accept_http_request;
+use super::tests::enabled_desired_state_sender;
+use super::tests::remote_control_auth_manager;
+use super::tests::remote_control_enrollment;
+use super::tests::remote_control_state_runtime;
+use super::tests::remote_control_status_channel;
+use super::tests::remote_control_url_for_listener;
+use super::tests::respond_with_status_and_headers;
+use super::tests::test_current_enrollment;
+use super::*;
+use crate::transport::remote_control::protocol::normalize_remote_control_url;
+use crate::transport::remote_control::tests::remote_control_handle_with_current_enrollment;
+use codex_app_server_protocol::RemoteControlPairingStartParams;
+use codex_app_server_protocol::RemoteControlPairingStartResponse;
+use pretty_assertions::assert_eq;
+use tempfile::TempDir;
+use tokio::net::TcpListener;
+use tokio::net::TcpStream;
+use tokio::time::Duration;
+use tokio::time::timeout;
+use tokio_tungstenite::WebSocketStream;
+use tokio_tungstenite::accept_async;
+
+async fn connect_test_websocket(
+ remote_control_target: &RemoteControlTarget,
+ state_db: &StateRuntime,
+ auth_manager: &Arc,
+ current_enrollment: &CurrentRemoteControlEnrollment,
+) -> io::Result<()> {
+ let mut auth_recovery = auth_manager.unauthorized_recovery();
+ let mut auth_change_rx = auth_manager.auth_change_receiver();
+ let (status_publisher, _) = remote_control_status_channel();
+ let desired_state_tx = enabled_desired_state_sender();
+ let desired_state_persistence_lock = Semaphore::new(1);
+ connect_remote_control_websocket(
+ remote_control_target,
+ Some(state_db),
+ RemoteControlAuthContext {
+ auth_manager,
+ auth_recovery: &mut auth_recovery,
+ auth_change_rx: &mut auth_change_rx,
+ },
+ current_enrollment,
+ RemoteControlConnectOptions {
+ installation_id: TEST_INSTALLATION_ID,
+ server_name: "test-server",
+ subscribe_cursor: None,
+ app_server_client_name: None,
+ desired_state_tx: &desired_state_tx,
+ desired_state_persistence_lock: &desired_state_persistence_lock,
+ },
+ &status_publisher,
+ )
+ .await
+ .map(|_| ())
+}
+
+#[tokio::test]
+async fn proactive_refresh_failure_uses_valid_token_for_websocket_connect() {
+ let listener = TcpListener::bind("127.0.0.1:0")
+ .await
+ .expect("listener should bind");
+ let remote_control_url = remote_control_url_for_listener(&listener);
+ let remote_control_target =
+ normalize_remote_control_url(&remote_control_url).expect("target should parse");
+ let server_task = tokio::spawn(async move {
+ let (stream, request_line) = accept_http_request(&listener).await;
+ assert_eq!(
+ request_line,
+ "POST /backend-api/wham/remote/control/server/refresh HTTP/1.1"
+ );
+ respond_with_status_and_headers(stream, "502 Bad Gateway", &[], "upstream unavailable")
+ .await;
+ accept_test_websocket(&listener).await
+ });
+ let codex_home = TempDir::new().expect("temp dir should create");
+ let state_db = remote_control_state_runtime(&codex_home).await;
+ let auth_manager = remote_control_auth_manager();
+ let mut enrollment = remote_control_enrollment(Some(TEST_REMOTE_CONTROL_SERVER_TOKEN));
+ enrollment.expires_at = Some(time::OffsetDateTime::now_utc() + time::Duration::minutes(4));
+ let current_enrollment = test_current_enrollment(Some(enrollment));
+
+ let refresh_started_at = time::OffsetDateTime::now_utc();
+ connect_test_websocket(
+ &remote_control_target,
+ state_db.as_ref(),
+ &auth_manager,
+ ¤t_enrollment,
+ )
+ .await
+ .expect("valid token should allow websocket connect after proactive refresh failure");
+ let refresh_completed_at = time::OffsetDateTime::now_utc();
+ let server_websocket = server_task.await.expect("server task should succeed");
+
+ let enrollment = current_enrollment
+ .lock()
+ .await
+ .clone()
+ .expect("enrollment should remain available");
+ assert_eq!(
+ enrollment.remote_control_token.as_deref(),
+ Some(TEST_REMOTE_CONTROL_SERVER_TOKEN)
+ );
+ let next_refresh_at = enrollment
+ .next_refresh_at
+ .expect("transient refresh should set a retry deadline");
+ assert!(
+ (refresh_started_at + time::Duration::seconds(24)
+ ..=refresh_completed_at + time::Duration::seconds(36))
+ .contains(&next_refresh_at)
+ );
+ drop(server_websocket);
+}
+
+#[tokio::test]
+async fn proactive_refresh_connection_failure_uses_valid_token_for_websocket_connect() {
+ let listener = TcpListener::bind("127.0.0.1:0")
+ .await
+ .expect("listener should bind");
+ let remote_control_url = remote_control_url_for_listener(&listener);
+ let remote_control_target =
+ normalize_remote_control_url(&remote_control_url).expect("target should parse");
+ let server_task = tokio::spawn(async move {
+ let (stream, request_line) = accept_http_request(&listener).await;
+ assert_eq!(
+ request_line,
+ "POST /backend-api/wham/remote/control/server/refresh HTTP/1.1"
+ );
+ drop(stream);
+ accept_test_websocket(&listener).await
+ });
+ let codex_home = TempDir::new().expect("temp dir should create");
+ let state_db = remote_control_state_runtime(&codex_home).await;
+ let auth_manager = remote_control_auth_manager();
+ let mut enrollment = remote_control_enrollment(Some(TEST_REMOTE_CONTROL_SERVER_TOKEN));
+ enrollment.expires_at = Some(time::OffsetDateTime::now_utc() + time::Duration::minutes(4));
+ let current_enrollment = test_current_enrollment(Some(enrollment));
+
+ connect_test_websocket(
+ &remote_control_target,
+ state_db.as_ref(),
+ &auth_manager,
+ ¤t_enrollment,
+ )
+ .await
+ .expect("valid token should allow websocket connect after refresh connection failure");
+ let server_websocket = server_task.await.expect("server task should succeed");
+
+ assert!(
+ current_enrollment
+ .snapshot()
+ .and_then(|enrollment| enrollment.next_refresh_at)
+ .is_some(),
+ "connection failure should set a retry deadline"
+ );
+ drop(server_websocket);
+}
+
+#[tokio::test]
+async fn websocket_retry_after_throttles_pairing_refresh() {
+ let listener = TcpListener::bind("127.0.0.1:0")
+ .await
+ .expect("listener should bind");
+ let remote_control_url = remote_control_url_for_listener(&listener);
+ let remote_control_target =
+ normalize_remote_control_url(&remote_control_url).expect("target should parse");
+ let server_task = tokio::spawn(async move {
+ let (stream, request_line) = accept_http_request(&listener).await;
+ assert_eq!(
+ request_line,
+ "POST /backend-api/wham/remote/control/server/refresh HTTP/1.1"
+ );
+ respond_with_status_and_headers(
+ stream,
+ "502 Bad Gateway",
+ &[("retry-after", "120")],
+ "upstream unavailable",
+ )
+ .await;
+ let first_websocket = accept_test_websocket(&listener).await;
+ let (pairing_stream, request_line) = accept_http_request(&listener).await;
+ assert_eq!(
+ request_line,
+ "POST /backend-api/wham/remote/control/server/pair HTTP/1.1"
+ );
+ respond_with_status_and_headers(
+ pairing_stream,
+ "200 OK",
+ &[],
+ r#"{"pairing_code":"pairing-code","manual_pairing_code":"ABCD-EFGH","server_id":"srv_e_test","environment_id":"env_test","expires_at":"3026-05-22T12:34:56Z"}"#,
+ )
+ .await;
+ first_websocket
+ });
+ let codex_home = TempDir::new().expect("temp dir should create");
+ let state_db = remote_control_state_runtime(&codex_home).await;
+ let auth_manager = remote_control_auth_manager();
+ let mut remote_handle =
+ remote_control_handle_with_current_enrollment(&remote_control_url, auth_manager.clone());
+ remote_handle.state_db = Some(state_db.clone());
+ remote_handle
+ .current_enrollment
+ .lock()
+ .await
+ .as_mut()
+ .expect("current enrollment should exist")
+ .expires_at = Some(time::OffsetDateTime::now_utc() + time::Duration::minutes(4));
+ let current_enrollment = remote_handle.current_enrollment.clone();
+ let refresh_started_at = time::OffsetDateTime::now_utc();
+ connect_test_websocket(
+ &remote_control_target,
+ state_db.as_ref(),
+ &auth_manager,
+ ¤t_enrollment,
+ )
+ .await
+ .expect("first websocket should connect after deferred refresh");
+ let refresh_completed_at = time::OffsetDateTime::now_utc();
+ let next_refresh_at = current_enrollment
+ .snapshot()
+ .and_then(|enrollment| enrollment.next_refresh_at)
+ .expect("Retry-After should set a retry deadline");
+ assert!(
+ (refresh_started_at + time::Duration::seconds(120)
+ ..=refresh_completed_at + time::Duration::seconds(120))
+ .contains(&next_refresh_at)
+ );
+
+ let pairing_response = remote_handle
+ .start_pairing(
+ RemoteControlPairingStartParams::default(),
+ /*app_server_client_name*/ None,
+ )
+ .await
+ .expect("websocket Retry-After should throttle pairing refresh");
+ let first_server_websocket = server_task.await.expect("server task should succeed");
+
+ assert_eq!(
+ pairing_response,
+ RemoteControlPairingStartResponse {
+ pairing_code: "pairing-code".to_string(),
+ manual_pairing_code: Some("ABCD-EFGH".to_string()),
+ environment_id: "env_test".to_string(),
+ expires_at: 33_336_362_096,
+ }
+ );
+ drop(first_server_websocket);
+}
+
+#[tokio::test]
+async fn pairing_http_date_retry_after_throttles_websocket_refresh() {
+ let listener = TcpListener::bind("127.0.0.1:0")
+ .await
+ .expect("listener should bind");
+ let remote_control_url = remote_control_url_for_listener(&listener);
+ let remote_control_target =
+ normalize_remote_control_url(&remote_control_url).expect("target should parse");
+ let retry_after =
+ httpdate::fmt_http_date(std::time::SystemTime::now() + Duration::from_secs(120));
+ let expected_next_refresh_at = time::OffsetDateTime::from(
+ httpdate::parse_http_date(&retry_after).expect("Retry-After date should parse"),
+ );
+ let server_task = tokio::spawn(async move {
+ let (refresh_stream, request_line) = accept_http_request(&listener).await;
+ assert_eq!(
+ request_line,
+ "POST /backend-api/wham/remote/control/server/refresh HTTP/1.1"
+ );
+ respond_with_status_and_headers(
+ refresh_stream,
+ "502 Bad Gateway",
+ &[("retry-after", &retry_after)],
+ "upstream unavailable",
+ )
+ .await;
+ let (pairing_stream, request_line) = accept_http_request(&listener).await;
+ assert_eq!(
+ request_line,
+ "POST /backend-api/wham/remote/control/server/pair HTTP/1.1"
+ );
+ respond_with_status_and_headers(
+ pairing_stream,
+ "200 OK",
+ &[],
+ r#"{"pairing_code":"pairing-code","manual_pairing_code":"ABCD-EFGH","server_id":"srv_e_test","environment_id":"env_test","expires_at":"3026-05-22T12:34:56Z"}"#,
+ )
+ .await;
+ accept_test_websocket(&listener).await
+ });
+ let codex_home = TempDir::new().expect("temp dir should create");
+ let state_db = remote_control_state_runtime(&codex_home).await;
+ let auth_manager = remote_control_auth_manager();
+ let mut remote_handle =
+ remote_control_handle_with_current_enrollment(&remote_control_url, auth_manager.clone());
+ remote_handle.state_db = Some(state_db.clone());
+ remote_handle
+ .current_enrollment
+ .lock()
+ .await
+ .as_mut()
+ .expect("current enrollment should exist")
+ .expires_at = Some(time::OffsetDateTime::now_utc() + time::Duration::minutes(4));
+ let current_enrollment = remote_handle.current_enrollment.clone();
+
+ let pairing_response = remote_handle
+ .start_pairing(
+ RemoteControlPairingStartParams::default(),
+ /*app_server_client_name*/ None,
+ )
+ .await
+ .expect("pairing should continue after proactive refresh failure");
+ assert_eq!(
+ current_enrollment
+ .snapshot()
+ .and_then(|enrollment| enrollment.next_refresh_at),
+ Some(expected_next_refresh_at)
+ );
+ connect_test_websocket(
+ &remote_control_target,
+ state_db.as_ref(),
+ &auth_manager,
+ ¤t_enrollment,
+ )
+ .await
+ .expect("pairing Retry-After should throttle websocket refresh");
+ let server_websocket = server_task.await.expect("server task should succeed");
+
+ assert_eq!(
+ pairing_response,
+ RemoteControlPairingStartResponse {
+ pairing_code: "pairing-code".to_string(),
+ manual_pairing_code: Some("ABCD-EFGH".to_string()),
+ environment_id: "env_test".to_string(),
+ expires_at: 33_336_362_096,
+ }
+ );
+ drop(server_websocket);
+}
+
+async fn assert_refresh_failure_blocks_websocket(
+ expires_in: time::Duration,
+ response_delay: Duration,
+) {
+ let listener = TcpListener::bind("127.0.0.1:0")
+ .await
+ .expect("listener should bind");
+ let remote_control_url = remote_control_url_for_listener(&listener);
+ let remote_control_target =
+ normalize_remote_control_url(&remote_control_url).expect("target should parse");
+ let (connects_done_tx, connects_done_rx) = oneshot::channel();
+ let server_task = tokio::spawn(async move {
+ let (stream, request_line) = accept_http_request(&listener).await;
+ assert_eq!(
+ request_line,
+ "POST /backend-api/wham/remote/control/server/refresh HTTP/1.1"
+ );
+ tokio::time::sleep(response_delay).await;
+ respond_with_status_and_headers(
+ stream,
+ "502 Bad Gateway",
+ &[("retry-after", "120")],
+ "upstream unavailable",
+ )
+ .await;
+ assert_no_connection_until_connect_finishes(&listener, connects_done_rx).await;
+ });
+ let codex_home = TempDir::new().expect("temp dir should create");
+ let state_db = remote_control_state_runtime(&codex_home).await;
+ let auth_manager = remote_control_auth_manager();
+ let mut enrollment = remote_control_enrollment(Some(TEST_REMOTE_CONTROL_SERVER_TOKEN));
+ enrollment.expires_at = Some(time::OffsetDateTime::now_utc() + expires_in);
+ let current_enrollment = test_current_enrollment(Some(enrollment));
+
+ let refresh_started_at = time::OffsetDateTime::now_utc();
+ let refresh_err = connect_test_websocket(
+ &remote_control_target,
+ state_db.as_ref(),
+ &auth_manager,
+ ¤t_enrollment,
+ )
+ .await
+ .expect_err("required refresh failure should block websocket connect");
+ let refresh_completed_at = time::OffsetDateTime::now_utc();
+ let deferred_err = connect_test_websocket(
+ &remote_control_target,
+ state_db.as_ref(),
+ &auth_manager,
+ ¤t_enrollment,
+ )
+ .await
+ .expect_err("required refresh deadline should block websocket reconnect");
+ connects_done_tx
+ .send(())
+ .expect("server should wait for connect attempts to finish");
+
+ server_task.await.expect("server task should succeed");
+ assert!(refresh_err.to_string().contains("HTTP 502 Bad Gateway"));
+ assert_eq!(deferred_err.kind(), io::ErrorKind::WouldBlock);
+ assert!(deferred_err.to_string().contains("refresh deferred until"));
+ let next_refresh_at = current_enrollment
+ .snapshot()
+ .and_then(|enrollment| enrollment.next_refresh_at)
+ .expect("required refresh failure should set a retry deadline");
+ assert!(
+ (refresh_started_at + time::Duration::seconds(120)
+ ..=refresh_completed_at + time::Duration::seconds(120))
+ .contains(&next_refresh_at)
+ );
+}
+
+#[tokio::test]
+async fn expired_token_refresh_failure_throttles_reconnect_without_websocket() {
+ assert_refresh_failure_blocks_websocket(-time::Duration::seconds(1), Duration::ZERO).await;
+}
+
+#[tokio::test]
+async fn token_expiring_during_refresh_failure_throttles_reconnect_without_websocket() {
+ assert_refresh_failure_blocks_websocket(
+ time::Duration::seconds(1),
+ Duration::from_millis(1_200),
+ )
+ .await;
+}
+
+#[tokio::test]
+async fn websocket_auth_failure_does_not_clear_rotated_server_token() {
+ let attempted_enrollment = remote_control_enrollment(Some("old-token"));
+ let mut rotated_enrollment = attempted_enrollment.clone();
+ rotated_enrollment.remote_control_token = Some("new-token".to_string());
+ rotated_enrollment.expires_at =
+ Some(time::OffsetDateTime::now_utc() + time::Duration::hours(1));
+ let current_enrollment = test_current_enrollment(Some(rotated_enrollment.clone()));
+
+ clear_remote_control_server_token_if_matches(¤t_enrollment, &attempted_enrollment)
+ .await
+ .expect("matching enrollment identity should remain available");
+
+ assert_eq!(current_enrollment.snapshot(), Some(rotated_enrollment));
+}
+
+async fn accept_test_websocket(listener: &TcpListener) -> WebSocketStream {
+ let (stream, _) = timeout(TEST_HTTP_ACCEPT_TIMEOUT, listener.accept())
+ .await
+ .expect("websocket request should arrive in time")
+ .expect("listener accept should succeed");
+ accept_async(stream)
+ .await
+ .expect("websocket handshake should succeed")
+}
+
+async fn assert_no_connection_until_connect_finishes(
+ listener: &TcpListener,
+ mut connect_done_rx: oneshot::Receiver<()>,
+) {
+ tokio::select! {
+ accepted = listener.accept() => {
+ accepted.expect("unexpected websocket connection should be accepted");
+ panic!("required refresh failure must not proceed to websocket connect");
+ }
+ connect_done = &mut connect_done_rx => {
+ connect_done.expect("connect completion should be reported");
+ }
+ }
+}
diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md
index 246b2bb0c0..a7986753ee 100644
--- a/codex-rs/app-server/README.md
+++ b/codex-rs/app-server/README.md
@@ -137,9 +137,9 @@ Example with notification opt-out:
## API Overview
-- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; paths must be absolute. For permissions, prefer experimental `permissions` profile selection by id; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Deprecated experimental `multiAgentMode` is ignored; use Ultra reasoning effort for proactive multi-agent behavior. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd`. Experimental `selectedCapabilityRoots` selects environment-owned plugin or standalone-skill roots using environment-native absolute paths. Skills found below those roots are listed and read through the owning environment. Stdio MCP servers declared by selected plugins are also started in that environment; HTTP MCP declarations remain inactive.
+- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. Experimental `allowProviderModelFallback` lets providers backed by an authoritative static model catalog replace an unavailable requested `model` with the catalog default; dynamic or cached catalogs preserve the requested model. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; paths must be absolute. For permissions, prefer experimental `permissions` profile selection by id; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Deprecated experimental `multiAgentMode` is ignored; use Ultra reasoning effort for proactive multi-agent behavior. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd`. Experimental `selectedCapabilityRoots` selects environment-owned plugin or standalone-skill roots using environment-native absolute paths. Skills found below those roots are listed and read through the owning environment. Stdio MCP servers declared by selected plugins are started in that environment, and HTTP MCP connections use that environment's HTTP client.
- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`.
-- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Experimental clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately. Accepts the same permission override rules as `thread/start`.
+- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; pass an optional `lastTurnId` to copy history only through that turn, inclusive, and drop later turns from the fork. An in-progress `lastTurnId` is rejected. If `lastTurnId` is null while the source thread is mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Experimental clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately. Accepts the same permission override rules as `thread/start`.
- `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. `instructionSources` lists loaded instruction files using each source environment's native absolute path syntax, including files loaded from remote environments. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known. Their deprecated experimental `multiAgentMode` field, and the corresponding thread setting, always report `explicitRequestOnly`; Ultra reasoning effort is the source of proactive multi-agent behavior.
- `thread/list` — page through stored threads; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Experimental clients can use `parentThreadId` for direct spawned children or `ancestorThreadId` for spawned descendants at any depth; the two filters are mutually exclusive. Review and Guardian threads are not included because they do not participate in that spawn-edge lifecycle. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. Subagent threads also include `parentThreadId` when the immediate parent is known.
- `thread/loaded/list` — list the thread ids currently loaded in memory.
@@ -167,7 +167,7 @@ Example with notification opt-out:
- `thread/backgroundTerminals/clean` — terminate all running background terminals for a thread (experimental; requires `capabilities.experimentalApi`); returns `{}` when the cleanup request is accepted.
- `thread/backgroundTerminals/list` — list running background terminals for a loaded thread (experimental; requires `capabilities.experimentalApi`); returns `data` with the running terminal ids.
- `thread/backgroundTerminals/terminate` — terminate one running background terminal by app-server `processId` (experimental; requires `capabilities.experimentalApi`); returns whether a process was terminated.
-- `thread/rollback` — drop the last N turns from the agent’s in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success.
+- `thread/rollback` — deprecated and will be removed soon. Drop the last N turns from the agent’s in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success.
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; paths must be absolute. Prefer experimental `permissions` profile selection by id for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissions`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". Deprecated experimental `multiAgentMode` is ignored; Ultra reasoning effort selects proactive behavior.
- `thread/inject_items` — append raw Responses API items to a loaded thread’s model-visible history without starting a user turn; returns `{}` on success.
- `turn/steer` — add user input to an already in-flight regular turn without starting a new turn; returns the active `turnId` that accepted the input. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Review and manual compaction turns reject `turn/steer`.
@@ -205,6 +205,7 @@ Example with notification opt-out:
- `permissionProfile/list` — beta; list available permission profile ids with optional display `description` text and an `allowed` flag reflecting effective requirements, using cursor pagination. Pass `cwd` when the caller needs project-local `[permissions.]` entries to be included in the current catalog view.
- `experimentalFeature/enablement/set` — patch the in-memory process-wide runtime feature enablement for currently supported feature keys. For each feature, precedence is: cloud requirements > --enable > config.toml > experimentalFeature/enablement/set (new) > code default. Invalid keys will be ignored.
- `environment/add` — experimental; add or replace a named remote environment by `environmentId` and `execServerUrl` for later selection by `thread/start` or `turn/start`; optional `connectTimeoutMs` overrides the WebSocket connection timeout; returns `{}` and does not change the default environment.
+- `environment/info` — experimental; connect to a configured environment by `environmentId` and return its detected `shell` plus its default `cwd` as a canonical environment-native `file:` URI. Connection failures are returned as request errors.
- `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). Built-in presets do not select a model; the Plan preset selects medium reasoning effort. This response omits built-in developer instructions; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly.
- `skills/list` — list skills for one or more `cwd` values (optional `forceReload`).
- `skills/extraRoots/set` — replace the app-server process runtime extra standalone skill roots. The roots are not persisted; missing directories are accepted and simply load no skills.
@@ -212,7 +213,7 @@ Example with notification opt-out:
- `marketplace/add` — add a remote plugin marketplace from an HTTP(S) Git URL, SSH Git URL, or GitHub `owner/repo` shorthand, then persist it into the user marketplace config. Returns the installed root path plus whether the marketplace was already present.
- `marketplace/remove` — remove a configured marketplace by name from the user marketplace config, and delete its installed marketplace root when one exists.
- `marketplace/upgrade` — upgrade all configured Git plugin marketplaces, or one named marketplace when `marketplaceName` is provided. Returns selected marketplace names, upgraded roots, and per-marketplace errors.
-- `plugin/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata, plugin `availability` (`AVAILABLE` by default or `DISABLED_BY_ADMIN` for remote plugins blocked upstream), fail-open `marketplaceLoadErrors` entries for marketplace files that could not be parsed or loaded, and best-effort `featuredPluginIds` for the official curated marketplace. Clients can explicitly request the remote `workspace-directory`, `shared-with-me`, or `created-by-me-remote` marketplace kinds. `interface.category` uses the marketplace category when present; otherwise it falls back to the plugin manifest category (**under development; do not call from production clients yet**).
+- `plugin/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata, the remote marketplace `version` and locally materialized `localVersion` when available, plugin `availability` (`AVAILABLE` by default or `DISABLED_BY_ADMIN` for remote plugins blocked upstream), fail-open `marketplaceLoadErrors` entries for marketplace files that could not be parsed or loaded, and best-effort `featuredPluginIds` for the official curated marketplace. Clients can explicitly request the remote `workspace-directory`, `shared-with-me`, or `created-by-me-remote` marketplace kinds. `interface.category` uses the marketplace category when present; otherwise it falls back to the plugin manifest category (**under development; do not call from production clients yet**).
- `plugin/installed` — list installed plugin rows plus any explicitly requested local install-suggestion plugin names, without fetching the broader remote catalog. Mention surfaces can use this narrower view when they need plugin mention payloads rather than plugin-page discovery data (**under development; do not call from production clients yet**).
- `plugin/read` — read one plugin by `marketplacePath` plus `pluginName`, returning marketplace info, a list-style `summary`, manifest descriptions/interface metadata, and bundled skills/hooks/apps/MCP server names. Remote plugin details expose the canonical `shareUrl` supplied by the remote catalog when available; it is `null` for local plugins or when the catalog omits it. This field is separate from `summary.shareContext`, which continues to describe user and workspace sharing state. Returned plugin skills include their current `enabled` state after local config filtering; bundled hooks are returned as lightweight declaration summaries keyed for correlation with `hooks/list`. Use `plugin/install`'s `appsNeedingAuth` to drive post-install authentication and `app/list`'s `isAccessible` to determine current connector accessibility (**under development; do not call from production clients yet**).
- `plugin/skill/read` — read remote plugin skill markdown on demand by `remoteMarketplaceName`, `remotePluginId`, and `skillName`. This lets clients preview uninstalled remote plugin skills without downloading the plugin bundle.
@@ -229,7 +230,7 @@ Example with notification opt-out:
- `skills/config/write` — write user-level skill config by name or absolute path.
- `plugin/install` — install a plugin from a discovered marketplace entry, rejecting marketplace entries marked unavailable for install, install MCPs if any, and return the effective plugin auth policy plus any apps that still need auth (**under development; do not call from production clients yet**).
- `plugin/uninstall` — uninstall a local plugin by `pluginId` in `@` form by removing its cached files and clearing its user-level config entry, or uninstall a remote ChatGPT plugin by backend `pluginId` by forwarding the uninstall to the ChatGPT plugin backend and removing any downloaded remote-plugin cache (**under development; do not call from production clients yet**).
-- `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; returns an `authorization_url` and later emits `mcpServer/oauthLogin/completed` once the browser flow finishes.
+- `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; pass `threadId` to resolve servers from that thread's selected plugins and executor, and receive an `authorization_url` followed by `mcpServer/oauthLogin/completed` once the browser flow finishes.
- `tool/requestUserInput` — prompt the user with 1–3 short questions for a tool call and return their answers (experimental).
- `config/mcpServer/reload` — reload MCP server config from disk and queue a refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` without restarting the server.
- `mcpServerStatus/list` — enumerate configured MCP servers with their tools, auth status, server info, plus resources/resource templates for `full` detail; supports optional `threadId` and cursor+limit pagination. If `threadId` is omitted, the server reads from the latest global config directly. If `detail` is omitted, the server defaults to `full`.
@@ -242,7 +243,7 @@ Example with notification opt-out:
- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any `details` returned by detect. Callers may pass `source` to identify the product that initiated the import; omitted or `null` means unspecified. The response acknowledges the synchronous import phase with an `importId`. Expected migration failures are reported as per-item failures rather than JSON-RPC errors, so the server still returns that `importId` and emits `externalAgentConfig/import/completed` with the same ID once all synchronous and background work finishes. The completion notification contains type-level `itemTypeResults` with successes and failures, including raw failure messages for the client to report separately.
- `config/value/write` — write a single config key/value to the user's config.toml on disk; dotted paths such as `desktop.someKey` use the same generic write surface.
- `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads, including multiple `desktop.*` edits.
-- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), remote-control policy (`allowRemoteControl`; `false` force-disables remote control while `true` or `null` preserves existing behavior), computer use policy (`computerUse`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`.
+- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), remote-control policy (`allowRemoteControl`; `false` force-disables remote control while `true` or `null` preserves existing behavior), computer use policy (`computerUse`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, managed new-thread defaults (`models.newThread.model`, `models.newThread.modelReasoningEffort`, and `models.newThread.serviceTier`), and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`.
### Example: Start or resume a thread
@@ -1300,7 +1301,7 @@ Event notifications are the server-initiated event stream for thread lifecycles,
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`.
-Recoverable configuration and initialization warnings use the existing `configWarning` notification: `{ summary, details?, path?, range? }`. App-server may emit it during initialization for config parsing and related setup diagnostics.
+Recoverable configuration and initialization warnings use the existing `configWarning` notification: `{ summary, details?, path?, range? }`. App-server may emit it during initialization for config parsing and related setup diagnostics, or to the requesting connection during `thread/start` when that thread's exec-policy rules fail to parse.
Generic runtime warnings use the `warning` notification: `{ threadId?, message }`. App-server emits this for non-fatal warnings from the core event stream, including cases where not all enabled skills are included in the model-visible skills list for a session.
@@ -1345,7 +1346,7 @@ Because audio is intentionally separate from `ThreadItem`, clients can opt out o
### MCP server startup events
-- `mcpServer/startupStatus/updated` — `{ threadId, name, status, error }` when app-server observes an MCP server startup transition. `threadId` identifies the owning thread when startup is thread-scoped and is `null` when startup is app-scoped. `status` is one of `starting`, `ready`, `failed`, or `cancelled`. `error` is `null` except for `failed`.
+- `mcpServer/startupStatus/updated` — `{ threadId, name, status, error, failureReason }` when app-server observes an MCP server startup transition. `threadId` identifies the owning thread when startup is thread-scoped and is `null` when startup is app-scoped. `status` is one of `starting`, `ready`, `failed`, or `cancelled`. `error` and `failureReason` are `null` except for `failed`; `failureReason` is `reauthenticationRequired` when stored OAuth credentials have expired and cannot be refreshed, so clients can prompt the user to reconnect the named server.
### Turn events
@@ -1372,7 +1373,7 @@ Today both notifications carry an empty `items` array even when item events were
- `reasoning` — `{id, summary, content}` where `summary` holds streamed reasoning summaries (applicable for most OpenAI models) and `content` holds raw reasoning blocks (applicable for e.g. open source models).
- `commandExecution` — `{id, command, cwd, status, commandActions, aggregatedOutput?, exitCode?, durationMs?}` for sandboxed commands; `status` is `inProgress`, `completed`, `failed`, or `declined`.
- `fileChange` — `{id, changes, status}` describing proposed edits; `changes` list `{path, kind, diff}` and `status` is `inProgress`, `completed`, `failed`, or `declined`.
-- `mcpToolCall` — `{id, server, tool, status, arguments, appContext, mcpAppResourceUri?, pluginId, result?, error?}` describing MCP calls; `appContext` is `{connectorId, linkId, resourceUri}` for calls through a trusted MCP app, where `connectorId` identifies the connector that owns the tool, `linkId` identifies the app link, and `resourceUri` points to the widget template. The top-level `mcpAppResourceUri` is deprecated and temporarily duplicated for client migration. `tool` identifies the invoked action. `status` is `inProgress`, `completed`, or `failed`.
+- `mcpToolCall` — `{id, server, tool, status, arguments, appContext, mcpAppResourceUri?, pluginId, result?, error?}` describing MCP calls; `appContext` is `{connectorId, linkId, resourceUri, appName, templateId, actionName}` for calls through a trusted MCP app, where `connectorId` identifies the connector that owns the tool, `linkId` identifies the app link, `resourceUri` points to the widget template, `appName` is the connector's display name, `templateId` identifies the app template, and `actionName` is the stable connector `Action.name`. `appName`, `templateId`, and `actionName` may be null for older rollout entries. The top-level `mcpAppResourceUri` is deprecated and temporarily duplicated for client migration. `tool` identifies the raw MCP tool. `status` is `inProgress`, `completed`, or `failed`.
- `collabToolCall` — `{id, tool, status, senderThreadId, receiverThreadId?, newThreadId?, prompt?, agentStatus?}` describing collab tool calls (`spawn_agent`, `send_input`, `resume_agent`, `wait`, `close_agent`); `status` is `inProgress`, `completed`, or `failed`.
- `webSearch` — `{id, query, action?}` for a web search request issued by the agent; `action` mirrors the Responses API web_search action payload (`search`, `open_page`, `find_in_page`) and may be omitted until completion.
- `imageView` — `{id, path}` emitted when the agent invokes the image viewer tool.
@@ -1928,14 +1929,14 @@ Codex supports these authentication modes. The current mode is surfaced in `acco
- `account/login/cancel` — cancel a pending managed ChatGPT login by `loginId`.
- `account/logout` — sign out; triggers `account/updated`.
- `account/updated` (notify) — emitted whenever auth mode changes (`authMode`: `apikey`, `chatgpt`, `personalAccessToken`, or `null`) and includes the current ChatGPT `planType` when available.
-- `account/rateLimits/read` — fetch ChatGPT rate limits, an optional effective monthly credit limit, and the number of earned rate-limit resets currently available. Rate-limit updates arrive via `account/rateLimits/updated` (notify); the reset count is snapshot-only.
-- `account/rateLimitResetCredit/consume` — consume one earned reset using a caller-provided idempotency key.
+- `account/rateLimits/read` — fetch ChatGPT rate limits, an optional effective monthly credit limit, and the earned rate-limit resets currently available, including expiry details when provided by the backend. Rate-limit updates arrive via `account/rateLimits/updated` (notify); reset-credit data is snapshot-only.
+- `account/rateLimitResetCredit/consume` — consume one earned reset using a caller-provided idempotency key, optionally selecting a reset-credit ID returned by `account/rateLimits/read`.
- `account/usage/read` — fetch ChatGPT account token-activity summary and daily buckets.
- `account/workspaceMessages/read` — fetch active workspace messages, including workspace notification headlines when available.
- `account/rateLimits/updated` (notify) — emitted whenever a user's ChatGPT rate limits change. This is a sparse rolling update; merge available values into the most recent `account/rateLimits/read` response or refetch that snapshot.
- `account/sendAddCreditsNudgeEmail` — ask ChatGPT to email the workspace owner about depleted credits or a reached usage limit.
-- `mcpServer/oauthLogin/completed` (notify) — emitted after a `mcpServer/oauth/login` flow finishes for a server; payload includes `{ name, success, error? }`.
-- `mcpServer/startupStatus/updated` (notify) — emitted when a configured MCP server's startup status changes; payload includes `{ threadId, name, status, error }`, where `threadId` is the owning thread when startup is thread-scoped and `null` when it is app-scoped, and `status` is `starting`, `ready`, `failed`, or `cancelled`.
+- `mcpServer/oauthLogin/completed` (notify) — emitted after a `mcpServer/oauth/login` flow finishes for a server; payload includes `{ name, threadId, success, error? }`.
+- `mcpServer/startupStatus/updated` (notify) — emitted when a configured MCP server's startup status changes; payload includes `{ threadId, name, status, error, failureReason }`, where `threadId` is the owning thread when startup is thread-scoped and `null` when it is app-scoped, and `status` is `starting`, `ready`, `failed`, or `cancelled`. `failureReason` is `reauthenticationRequired` when stored OAuth credentials have expired and cannot be refreshed, so clients can prompt the user to reconnect the named server.
### 1) Check auth state
@@ -2031,7 +2032,30 @@ Field notes:
```json
{ "method": "account/rateLimits/read", "id": 7 }
-{ "id": 7, "result": { "rateLimits": { "primary": { "usedPercent": 25, "windowDurationMins": 15, "resetsAt": 1730947200 }, "secondary": null, "rateLimitReachedType": null }, "rateLimitResetCredits": { "availableCount": 2 } } }
+{
+ "id": 7,
+ "result": {
+ "rateLimits": {
+ "primary": { "usedPercent": 25, "windowDurationMins": 15, "resetsAt": 1730947200 },
+ "secondary": null,
+ "rateLimitReachedType": null
+ },
+ "rateLimitResetCredits": {
+ "availableCount": 2,
+ "credits": [
+ {
+ "id": "RateLimitResetCredit_1",
+ "resetType": "codexRateLimits",
+ "status": "available",
+ "grantedAt": 1781654400,
+ "expiresAt": 1784246400,
+ "title": "Full reset (Weekly + 5 hr)",
+ "description": "Ready to redeem"
+ }
+ ]
+ }
+ }
+}
{ "method": "account/rateLimits/updated", "params": { "rateLimits": { … } } }
```
@@ -2042,23 +2066,27 @@ Field notes:
- `resetsAt` is a Unix timestamp (seconds) for the next reset.
- `rateLimitReachedType` identifies the backend-classified limit state when one has been reached.
- `individualLimit` describes the effective monthly credit limit when available. In an `account/rateLimits/read` response, `null` means no monthly limit is available. In a sparse `account/rateLimits/updated` notification, nullable account metadata may be unavailable and does not clear a previously observed value.
-- `rateLimitResetCredits` contains the available earned-reset count when the backend provides it; otherwise it is `null`. Refetch `account/rateLimits/read` after consuming a reset.
+- `rateLimitResetCredits` contains the available earned-reset count when the backend provides it; otherwise it is `null`.
+- `rateLimitResetCredits.credits` is `null` when only the count is available. An empty array means details were fetched and no available credits were returned.
+- The backend may cap `rateLimitResetCredits.credits`, so `availableCount` is the authoritative total and can be greater than the number of detail rows.
+- Refetch `account/rateLimits/read` after consuming a reset.
### 8) Earned rate-limit resets (ChatGPT)
```json
-{ "method": "account/rateLimitResetCredit/consume", "id": 8, "params": { "idempotencyKey": "8ae96ff3-3425-4f4c-8772-b6fd61502868" } }
+{ "method": "account/rateLimitResetCredit/consume", "id": 8, "params": { "idempotencyKey": "8ae96ff3-3425-4f4c-8772-b6fd61502868", "creditId": "RateLimitResetCredit_1" } }
{ "id": 8, "result": { "outcome": "reset" } }
```
Field notes:
- `idempotencyKey` must be non-empty. A UUID is recommended for each logical redemption attempt; reuse the same value when retrying that attempt.
+- `creditId` is optional. When provided, it must be a non-empty opaque ID returned by `account/rateLimits/read`; when omitted, the backend selects the next available credit.
- `reset` means a credit was consumed.
- `alreadyRedeemed` means the same redemption completed previously. Treat it as an idempotent success and refresh account limits.
- `nothingToReset` means there is no eligible rate-limit window to reset.
- `noCredit` means the account has no earned reset credits available.
-- Refetch `account/rateLimits/read` after consuming a reset instead of inferring updated windows from this response.
+- Refetch `account/rateLimits/read` after consuming a reset instead of inferring updated state from this response.
### 9) Workspace messages (ChatGPT)
diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs
index 36dbdd9d3b..c63103161f 100644
--- a/codex-rs/app-server/src/bespoke_event_handling.rs
+++ b/codex-rs/app-server/src/bespoke_event_handling.rs
@@ -200,18 +200,20 @@ pub(crate) async fn apply_bespoke_event_handling(
.await;
}
EventMsg::McpStartupUpdate(update) => {
- let (status, error) = match update.status {
+ let (status, error, failure_reason) = match update.status {
codex_protocol::protocol::McpStartupStatus::Starting => {
- (McpServerStartupState::Starting, None)
+ (McpServerStartupState::Starting, None, None)
}
codex_protocol::protocol::McpStartupStatus::Ready => {
- (McpServerStartupState::Ready, None)
- }
- codex_protocol::protocol::McpStartupStatus::Failed { error } => {
- (McpServerStartupState::Failed, Some(error))
+ (McpServerStartupState::Ready, None, None)
}
+ codex_protocol::protocol::McpStartupStatus::Failed { error, reason } => (
+ McpServerStartupState::Failed,
+ Some(error),
+ reason.map(Into::into),
+ ),
codex_protocol::protocol::McpStartupStatus::Cancelled => {
- (McpServerStartupState::Cancelled, None)
+ (McpServerStartupState::Cancelled, None, None)
}
};
let notification = McpServerStatusUpdatedNotification {
@@ -219,6 +221,7 @@ pub(crate) async fn apply_bespoke_event_handling(
name: update.server,
status,
error,
+ failure_reason,
};
outgoing
.send_server_notification(ServerNotification::McpServerStatusUpdated(notification))
@@ -2284,6 +2287,7 @@ mod tests {
cwd: test_path_buf("/tmp").abs().into(),
cli_version: "0.0.0".to_string(),
source: SessionSource::Cli,
+ history_mode: Default::default(),
thread_source: None,
agent_nickname: None,
agent_role: None,
diff --git a/codex-rs/app-server/src/current_time.rs b/codex-rs/app-server/src/current_time.rs
index 205068be2b..57be618af3 100644
--- a/codex-rs/app-server/src/current_time.rs
+++ b/codex-rs/app-server/src/current_time.rs
@@ -22,7 +22,8 @@ use crate::outgoing_message::ConnectionId;
use crate::outgoing_message::OutgoingMessageSender;
use crate::thread_state::ThreadStateManager;
-const CURRENT_TIME_REQUEST_TIMEOUT: Duration = Duration::from_secs(5);
+const CURRENT_TIME_REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
+const CURRENT_TIME_POLL_INTERVAL: Duration = Duration::from_secs(1);
pub(crate) fn app_server_time_provider(
outgoing: Arc,
@@ -51,10 +52,32 @@ impl TimeProvider for AppServerTimeProvider {
})
}
- fn sleep(&self, _thread_id: ThreadId, duration: Duration) -> SleepFuture<'_> {
+ fn sleep(&self, thread_id: ThreadId, duration: Duration) -> SleepFuture<'_> {
+ let outgoing = self.outgoing.clone();
+ let thread_state_manager = self.thread_state_manager.clone();
Box::pin(async move {
- tokio::time::sleep(duration).await;
- Ok(())
+ let outgoing = outgoing
+ .upgrade()
+ .context("app-server current-time provider is unavailable")?;
+ let started_at =
+ request_current_time(outgoing.clone(), thread_state_manager.clone(), thread_id)
+ .await?;
+ let wake_at = started_at
+ .checked_add_signed(
+ chrono::Duration::from_std(duration)
+ .context("external sleep duration is outside the supported range")?,
+ )
+ .context("external sleep deadline is outside the supported range")?;
+
+ loop {
+ tokio::time::sleep(CURRENT_TIME_POLL_INTERVAL).await;
+ if request_current_time(outgoing.clone(), thread_state_manager.clone(), thread_id)
+ .await?
+ >= wake_at
+ {
+ return Ok(());
+ }
+ }
})
}
}
diff --git a/codex-rs/app-server/src/in_process.rs b/codex-rs/app-server/src/in_process.rs
index 938d539a9f..d8a2bc2276 100644
--- a/codex-rs/app-server/src/in_process.rs
+++ b/codex-rs/app-server/src/in_process.rs
@@ -80,6 +80,7 @@ use codex_arg0::Arg0DispatchPaths;
use codex_config::CloudConfigBundleLoader;
use codex_config::LoaderOverrides;
use codex_config::ThreadConfigLoader;
+use codex_core::check_execpolicy_for_warnings;
use codex_core::config::Config;
use codex_core::resolve_installation_id;
use codex_exec_server::EnvironmentManager;
@@ -349,7 +350,16 @@ impl InProcessClientHandle {
/// This function sends `initialize` followed by `initialized` before returning
/// the handle, so callers receive a ready-to-use runtime. If initialize fails,
/// the runtime is shut down and an `InvalidData` error is returned.
-pub async fn start(args: InProcessStartArgs) -> IoResult {
+pub async fn start(mut args: InProcessStartArgs) -> IoResult {
+ if let Ok(Some(err)) = check_execpolicy_for_warnings(&args.config.config_layer_stack).await {
+ let (path, range) = crate::exec_policy_warning_location(&err);
+ args.config_warnings.push(ConfigWarningNotification {
+ summary: "Error parsing rules; custom rules not applied.".to_string(),
+ details: Some(err.to_string()),
+ path,
+ range,
+ });
+ }
let initialize = args.initialize.clone();
let client = start_uninitialized(args).await?;
diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs
index beed35848b..fa6b6cd811 100644
--- a/codex-rs/app-server/src/lib.rs
+++ b/codex-rs/app-server/src/lib.rs
@@ -320,6 +320,16 @@ fn exec_policy_warning_location(err: &ExecPolicyError) -> (Option, Optio
}
}
+fn exec_policy_config_warning(err: &ExecPolicyError) -> ConfigWarningNotification {
+ let (path, range) = exec_policy_warning_location(err);
+ ConfigWarningNotification {
+ summary: "Error parsing rules; custom rules not applied.".to_string(),
+ details: Some(err.to_string()),
+ path,
+ range,
+ }
+}
+
fn app_text_range(range: &CoreTextRange) -> AppTextRange {
AppTextRange {
start: AppTextPosition {
@@ -612,14 +622,7 @@ pub async fn run_main_with_transport_options(
}
if let Ok(Some(err)) = check_execpolicy_for_warnings(&config.config_layer_stack).await {
- let (path, range) = exec_policy_warning_location(&err);
- let message = ConfigWarningNotification {
- summary: "Error parsing rules; custom rules not applied.".to_string(),
- details: Some(err.to_string()),
- path,
- range,
- };
- config_warnings.push(message);
+ config_warnings.push(exec_policy_config_warning(&err));
}
if let Some(warning) = project_config_warning(&config) {
@@ -912,7 +915,7 @@ pub async fn run_main_with_transport_options(
async move {
let mut listen_for_threads = true;
let mut shutdown_state = ShutdownState::default();
- loop {
+ let exit_reason = loop {
let running_turn_count = {
let running_turn_count = running_turn_count_rx.borrow();
*running_turn_count
@@ -925,7 +928,7 @@ pub async fn run_main_with_transport_options(
let _ = outbound_control_tx
.send(OutboundControlEvent::DisconnectAll)
.await;
- break;
+ break "shutdown_requested";
}
tokio::select! {
@@ -947,7 +950,7 @@ pub async fn run_main_with_transport_options(
}
event = transport_event_rx.recv() => {
let Some(event) = event else {
- break;
+ break "transport_channel_closed";
};
match event {
TransportEvent::ConnectionOpened {
@@ -977,7 +980,7 @@ pub async fn run_main_with_transport_options(
.await
.is_err()
{
- break;
+ break "outbound_router_closed";
}
connections.insert(
connection_id,
@@ -1005,10 +1008,10 @@ pub async fn run_main_with_transport_options(
.await;
});
if !outbound_closed {
- break;
+ break "outbound_router_closed";
}
if shutdown_when_no_connections && connections.is_empty() {
- break;
+ break "last_connection_closed";
}
}
TransportEvent::IncomingMessage { connection_id, message } => {
@@ -1147,7 +1150,7 @@ pub async fn run_main_with_transport_options(
}
}
}
- }
+ };
if !shutdown_state.forced() {
futures::future::join_all(
@@ -1162,7 +1165,12 @@ pub async fn run_main_with_transport_options(
} else {
connection_cleanup_tasks.abort();
}
- info!("processor task exited (channel closed)");
+ info!(
+ exit_reason,
+ remaining_connection_count = connections.len(),
+ shutdown_forced = shutdown_state.forced(),
+ "processor task exited"
+ );
}
});
diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs
index 41ecbfe561..6ae73784ff 100644
--- a/codex-rs/app-server/src/message_processor.rs
+++ b/codex-rs/app-server/src/message_processor.rs
@@ -447,7 +447,7 @@ impl MessageProcessor {
outgoing.clone(),
analytics_events_client.clone(),
Arc::clone(&config),
- config_warnings,
+ config_warnings.clone(),
rpc_transport,
);
let marketplace_processor = MarketplaceRequestProcessor::new(
@@ -495,6 +495,7 @@ impl MessageProcessor {
state_db.clone(),
log_db,
Arc::clone(&skills_watcher),
+ config_warnings,
);
let turn_processor = TurnRequestProcessor::new(
auth_manager.clone(),
@@ -1037,6 +1038,9 @@ impl MessageProcessor {
ClientRequest::EnvironmentAdd { params, .. } => {
self.environment_processor.environment_add(params).await
}
+ ClientRequest::EnvironmentInfo { params, .. } => {
+ self.environment_processor.environment_info(params).await
+ }
ClientRequest::FsReadFile { params, .. } => self
.fs_processor
.read_file(params)
@@ -1205,7 +1209,7 @@ impl MessageProcessor {
}
ClientRequest::ThreadRollback { params, .. } => {
self.thread_processor
- .thread_rollback(&request_id, params)
+ .thread_rollback(&request_id, params, app_server_client_name.as_deref())
.await
}
ClientRequest::ThreadList { params, .. } => {
diff --git a/codex-rs/app-server/src/outgoing_message.rs b/codex-rs/app-server/src/outgoing_message.rs
index 9ce815bafe..16d5c2ff9d 100644
--- a/codex-rs/app-server/src/outgoing_message.rs
+++ b/codex-rs/app-server/src/outgoing_message.rs
@@ -504,13 +504,36 @@ impl OutgoingMessageSender {
where
T: Into,
{
- self.send_response_as(request_id, response.into()).await;
+ self.send_response_as_inner(request_id, response.into(), /*thread_originator*/ None)
+ .await;
+ }
+
+ pub(crate) async fn send_response_with_thread_originator(
+ &self,
+ request_id: ConnectionRequestId,
+ response: T,
+ thread_originator: String,
+ ) where
+ T: Into,
+ {
+ self.send_response_as_inner(request_id, response.into(), Some(thread_originator))
+ .await;
}
pub(crate) async fn send_response_as(
&self,
request_id: ConnectionRequestId,
response: ClientResponsePayload,
+ ) {
+ self.send_response_as_inner(request_id, response, /*thread_originator*/ None)
+ .await;
+ }
+
+ async fn send_response_as_inner(
+ &self,
+ request_id: ConnectionRequestId,
+ response: ClientResponsePayload,
+ thread_originator: Option,
) {
let connection_id = request_id.connection_id;
let request_id_for_analytics = request_id.request_id.clone();
@@ -518,11 +541,24 @@ impl OutgoingMessageSender {
.into_jsonrpc_parts_and_payload(request_id.request_id.clone())
.map(|(id, result, response)| {
if let Some(response) = response {
- self.analytics_events_client.track_response(
- connection_id.0,
- request_id_for_analytics,
- response,
- );
+ match thread_originator {
+ Some(thread_originator) => {
+ self.analytics_events_client
+ .track_response_with_thread_originator(
+ connection_id.0,
+ request_id_for_analytics,
+ response,
+ thread_originator,
+ );
+ }
+ None => {
+ self.analytics_events_client.track_response(
+ connection_id.0,
+ request_id_for_analytics,
+ response,
+ );
+ }
+ }
}
(id, result)
});
diff --git a/codex-rs/app-server/src/request_processors.rs b/codex-rs/app-server/src/request_processors.rs
index d73a41706c..f03351468f 100644
--- a/codex-rs/app-server/src/request_processors.rs
+++ b/codex-rs/app-server/src/request_processors.rs
@@ -56,11 +56,15 @@ use codex_app_server_protocol::ConsumeAccountRateLimitResetCreditParams;
use codex_app_server_protocol::ConsumeAccountRateLimitResetCreditResponse;
use codex_app_server_protocol::ConversationGitInfo;
use codex_app_server_protocol::ConversationSummary;
+use codex_app_server_protocol::DeprecationNoticeNotification;
use codex_app_server_protocol::DynamicToolFunctionSpec;
use codex_app_server_protocol::DynamicToolNamespaceTool;
use codex_app_server_protocol::DynamicToolSpec;
use codex_app_server_protocol::EnvironmentAddParams;
use codex_app_server_protocol::EnvironmentAddResponse;
+use codex_app_server_protocol::EnvironmentInfoParams;
+use codex_app_server_protocol::EnvironmentInfoResponse;
+use codex_app_server_protocol::EnvironmentShellInfo;
use codex_app_server_protocol::ExperimentalFeature as ApiExperimentalFeature;
use codex_app_server_protocol::ExperimentalFeatureListParams;
use codex_app_server_protocol::ExperimentalFeatureListResponse;
@@ -152,7 +156,10 @@ use codex_app_server_protocol::PluginSource;
use codex_app_server_protocol::PluginSummary;
use codex_app_server_protocol::PluginUninstallParams;
use codex_app_server_protocol::PluginUninstallResponse;
+use codex_app_server_protocol::RateLimitResetCredit;
+use codex_app_server_protocol::RateLimitResetCreditStatus;
use codex_app_server_protocol::RateLimitResetCreditsSummary;
+use codex_app_server_protocol::RateLimitResetType;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ReviewDelivery as ApiReviewDelivery;
use codex_app_server_protocol::ReviewStartParams;
@@ -205,6 +212,8 @@ use codex_app_server_protocol::ThreadGoalSetResponse;
use codex_app_server_protocol::ThreadGoalStatus;
use codex_app_server_protocol::ThreadGoalUpdatedNotification;
use codex_app_server_protocol::ThreadHistoryBuilder;
+#[cfg(test)]
+use codex_app_server_protocol::ThreadHistoryMode;
use codex_app_server_protocol::ThreadIncrementElicitationParams;
use codex_app_server_protocol::ThreadIncrementElicitationResponse;
use codex_app_server_protocol::ThreadInjectItemsParams;
@@ -292,6 +301,8 @@ use codex_backend_client::CodexWorkspaceMessage as BackendWorkspaceMessage;
use codex_backend_client::CodexWorkspaceMessageType as BackendWorkspaceMessageType;
use codex_backend_client::CodexWorkspaceMessagesResponse as BackendWorkspaceMessagesResponse;
use codex_backend_client::ConsumeRateLimitResetCreditCode as BackendConsumeRateLimitResetCreditCode;
+use codex_backend_client::RateLimitResetCreditDetails as BackendRateLimitResetCreditDetails;
+use codex_backend_client::RateLimitResetCreditsDetails as BackendRateLimitResetCreditsDetails;
use codex_backend_client::RequestError as BackendRequestError;
use codex_backend_client::TokenUsageProfile;
use codex_chatgpt::connectors;
@@ -327,6 +338,7 @@ use codex_core::path_utils;
#[cfg(test)]
use codex_core::read_head_for_summary;
use codex_core::sandboxing::SandboxPermissions;
+use codex_core::truncate_rollout_after_turn_id;
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
use codex_core::windows_sandbox::WindowsSandboxSetupMode as CoreWindowsSandboxSetupMode;
use codex_core::windows_sandbox::WindowsSandboxSetupRequest;
@@ -380,7 +392,7 @@ use codex_mcp::McpRuntimeContext;
use codex_mcp::McpServerStatusSnapshot;
use codex_mcp::McpSnapshotDetail;
use codex_mcp::collect_mcp_server_status_snapshot_with_detail;
-use codex_mcp::discover_supported_scopes;
+use codex_mcp::discover_supported_scopes_with_http_client;
use codex_mcp::read_mcp_resource as read_mcp_resource_without_thread;
use codex_mcp::resolve_oauth_scopes;
use codex_memories_write::clear_memory_roots_contents;
@@ -428,7 +440,7 @@ use codex_protocol::protocol::USER_MESSAGE_BEGIN;
use codex_protocol::protocol::W3cTraceContext;
use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS;
use codex_protocol::user_input::UserInput as CoreInputItem;
-use codex_rmcp_client::perform_oauth_login_return_url;
+use codex_rmcp_client::perform_oauth_login_return_url_with_http_client;
use codex_rollout::is_persisted_rollout_item;
use codex_rollout::state_db::StateDbHandle;
use codex_rollout::state_db::reconcile_rollout;
diff --git a/codex-rs/app-server/src/request_processors/account_processor.rs b/codex-rs/app-server/src/request_processors/account_processor.rs
index 4691e22204..9592563bec 100644
--- a/codex-rs/app-server/src/request_processors/account_processor.rs
+++ b/codex-rs/app-server/src/request_processors/account_processor.rs
@@ -891,9 +891,11 @@ impl AccountRequestProcessor {
let client = BackendClient::from_auth(self.config.chatgpt_base_url.clone(), &auth)
.map_err(|err| internal_error(format!("failed to construct backend client: {err}")))?;
- let response = client
- .get_rate_limits_with_reset_credits()
- .await
+ let (response, detailed_rate_limit_reset_credits) = tokio::join!(
+ client.get_rate_limits_with_reset_credits(),
+ Self::detailed_rate_limit_reset_credits(&client),
+ );
+ let response = response
.map_err(|err| internal_error(format!("failed to fetch codex rate limits: {err}")))?;
if response.rate_limits.is_empty() {
return Err(internal_error(
@@ -920,6 +922,15 @@ impl AccountRequestProcessor {
.cloned()
.unwrap_or_else(|| response.rate_limits[0].clone());
+ let rate_limit_reset_credits = detailed_rate_limit_reset_credits.or_else(|| {
+ response
+ .rate_limit_reset_credits
+ .map(|summary| RateLimitResetCreditsSummary {
+ available_count: summary.available_count,
+ credits: None,
+ })
+ });
+
Ok(GetAccountRateLimitsResponse {
rate_limits: rate_limits.into(),
rate_limits_by_limit_id: Some(
@@ -928,11 +939,7 @@ impl AccountRequestProcessor {
.map(|(limit_id, snapshot)| (limit_id, snapshot.into()))
.collect(),
),
- rate_limit_reset_credits: response.rate_limit_reset_credits.map(|summary| {
- RateLimitResetCreditsSummary {
- available_count: summary.available_count,
- }
- }),
+ rate_limit_reset_credits,
})
}
diff --git a/codex-rs/app-server/src/request_processors/account_processor/rate_limit_resets.rs b/codex-rs/app-server/src/request_processors/account_processor/rate_limit_resets.rs
index c8a397d3a7..919d30d770 100644
--- a/codex-rs/app-server/src/request_processors/account_processor/rate_limit_resets.rs
+++ b/codex-rs/app-server/src/request_processors/account_processor/rate_limit_resets.rs
@@ -1,11 +1,47 @@
use super::*;
const RATE_LIMIT_RESET_REQUEST_TIMEOUT: Duration = Duration::from_secs(/*secs*/ 10);
+const RATE_LIMIT_RESET_DETAILS_REQUEST_TIMEOUT: Duration = Duration::from_secs(/*secs*/ 5);
#[cfg(debug_assertions)]
const RATE_LIMIT_RESET_REQUEST_TIMEOUT_ENV_VAR: &str =
"CODEX_TEST_RATE_LIMIT_RESET_REQUEST_TIMEOUT_MS";
impl AccountRequestProcessor {
+ pub(super) async fn detailed_rate_limit_reset_credits(
+ client: &BackendClient,
+ ) -> Option {
+ let details = match tokio::time::timeout(
+ RATE_LIMIT_RESET_DETAILS_REQUEST_TIMEOUT,
+ client.list_rate_limit_reset_credits(),
+ )
+ .await
+ {
+ Ok(Ok(details)) => details,
+ Ok(Err(err)) => {
+ tracing::warn!(
+ "failed to fetch rate limit reset credit details; falling back to the usage response: {err}"
+ );
+ return None;
+ }
+ Err(_) => {
+ tracing::warn!(
+ "rate limit reset credit detail request timed out; falling back to the usage response"
+ );
+ return None;
+ }
+ };
+
+ match rate_limit_reset_credits_from_backend(details) {
+ Ok(summary) => Some(summary),
+ Err(err) => {
+ tracing::warn!(
+ "failed to parse rate limit reset credit details; falling back to the usage response: {err}"
+ );
+ None
+ }
+ }
+ }
+
pub(crate) async fn consume_account_rate_limit_reset_credit(
&self,
params: ConsumeAccountRateLimitResetCreditParams,
@@ -13,6 +49,9 @@ impl AccountRequestProcessor {
if params.idempotency_key.is_empty() {
return Err(invalid_request("idempotencyKey must not be empty"));
}
+ if params.credit_id.as_deref().is_some_and(str::is_empty) {
+ return Err(invalid_request("creditId must not be empty"));
+ }
let client = self.rate_limit_reset_backend_client().await?;
let request_timeout = RATE_LIMIT_RESET_REQUEST_TIMEOUT;
@@ -22,10 +61,20 @@ impl AccountRequestProcessor {
.and_then(|value| value.parse::().ok())
.map(Duration::from_millis)
.unwrap_or(request_timeout);
- let response = tokio::time::timeout(
- request_timeout,
- client.consume_rate_limit_reset_credit(¶ms.idempotency_key),
- )
+ let response = tokio::time::timeout(request_timeout, async {
+ match params.credit_id.as_deref() {
+ Some(credit_id) => {
+ client
+ .consume_rate_limit_reset_credit_by_id(¶ms.idempotency_key, credit_id)
+ .await
+ }
+ None => {
+ client
+ .consume_rate_limit_reset_credit(¶ms.idempotency_key)
+ .await
+ }
+ }
+ })
.await
.map_err(|_| internal_error("rate limit reset consume timed out"))?
.map_err(|err| internal_error(format!("failed to consume rate limit reset: {err}")))?;
@@ -64,3 +113,56 @@ impl AccountRequestProcessor {
.map_err(|err| internal_error(format!("failed to construct backend client: {err}")))
}
}
+
+fn rate_limit_reset_credits_from_backend(
+ details: BackendRateLimitResetCreditsDetails,
+) -> Result {
+ let credits = details
+ .credits
+ .into_iter()
+ .map(rate_limit_reset_credit_from_backend)
+ .collect::, _>>()?;
+ Ok(RateLimitResetCreditsSummary {
+ available_count: details.available_count,
+ credits: Some(credits),
+ })
+}
+
+fn rate_limit_reset_credit_from_backend(
+ credit: BackendRateLimitResetCreditDetails,
+) -> Result {
+ let reset_type = match credit.reset_type.as_str() {
+ "codex_rate_limits" => RateLimitResetType::CodexRateLimits,
+ _ => RateLimitResetType::Unknown,
+ };
+ let status = match credit.status.as_str() {
+ "available" => RateLimitResetCreditStatus::Available,
+ "redeeming" => RateLimitResetCreditStatus::Redeeming,
+ "redeemed" => RateLimitResetCreditStatus::Redeemed,
+ _ => RateLimitResetCreditStatus::Unknown,
+ };
+ let granted_at = rate_limit_reset_credit_timestamp(&credit.granted_at)
+ .map_err(|err| format!("invalid granted_at for credit `{}`: {err}", credit.id))?;
+ let expires_at = credit
+ .expires_at
+ .as_deref()
+ .map(rate_limit_reset_credit_timestamp)
+ .transpose()
+ .map_err(|err| format!("invalid expires_at for credit `{}`: {err}", credit.id))?;
+
+ Ok(RateLimitResetCredit {
+ id: credit.id,
+ reset_type,
+ status,
+ granted_at,
+ expires_at,
+ title: credit.title,
+ description: credit.description,
+ })
+}
+
+fn rate_limit_reset_credit_timestamp(timestamp: &str) -> Result {
+ DateTime::parse_from_rfc3339(timestamp)
+ .map(|timestamp| timestamp.timestamp())
+ .map_err(|err| format!("failed to parse timestamp `{timestamp}`: {err}"))
+}
diff --git a/codex-rs/app-server/src/request_processors/config_processor.rs b/codex-rs/app-server/src/request_processors/config_processor.rs
index 50bba54009..b6dbb30b08 100644
--- a/codex-rs/app-server/src/request_processors/config_processor.rs
+++ b/codex-rs/app-server/src/request_processors/config_processor.rs
@@ -24,9 +24,11 @@ use codex_app_server_protocol::ExperimentalFeatureEnablementSetResponse;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::ManagedHooksRequirements;
use codex_app_server_protocol::ModelProviderCapabilitiesReadResponse;
+use codex_app_server_protocol::ModelsRequirements;
use codex_app_server_protocol::NetworkDomainPermission;
use codex_app_server_protocol::NetworkRequirements;
use codex_app_server_protocol::NetworkUnixSocketPermission;
+use codex_app_server_protocol::NewThreadModelDefaults;
use codex_app_server_protocol::SandboxMode;
use codex_app_server_protocol::WindowsSandboxSetupMode;
use codex_config::ConfigRequirementsToml;
@@ -375,6 +377,13 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR
.enforce_residency
.map(map_residency_requirement_to_api),
network: requirements.network.map(map_network_requirements_to_api),
+ models: requirements.models.map(|models| ModelsRequirements {
+ new_thread: models.new_thread.map(|new_thread| NewThreadModelDefaults {
+ model: new_thread.model,
+ model_reasoning_effort: new_thread.model_reasoning_effort,
+ service_tier: new_thread.service_tier,
+ }),
+ }),
}
}
@@ -568,7 +577,10 @@ mod tests {
use codex_app_server_protocol::WindowsSandboxSetupMode;
use codex_config::ComputerUseRequirementsToml;
use codex_config::ConfigRequirementsToml;
+ use codex_config::ModelsRequirementsToml;
+ use codex_config::NewThreadModelDefaultsToml;
use codex_config::WindowsRequirementsToml;
+ use codex_protocol::openai_models::ReasoningEffort;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
@@ -628,6 +640,31 @@ mod tests {
assert_eq!(mapped.allow_remote_control, Some(false));
}
+ #[test]
+ fn requirements_api_includes_new_thread_model_defaults() {
+ let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
+ models: Some(ModelsRequirementsToml {
+ new_thread: Some(NewThreadModelDefaultsToml {
+ model: Some("gpt-managed".to_string()),
+ model_reasoning_effort: Some(ReasoningEffort::Medium),
+ service_tier: Some("fast".to_string()),
+ }),
+ }),
+ ..ConfigRequirementsToml::default()
+ });
+
+ let defaults = mapped
+ .models
+ .and_then(|models| models.new_thread)
+ .expect("new-thread defaults");
+ assert_eq!(defaults.model.as_deref(), Some("gpt-managed"));
+ assert_eq!(
+ defaults.model_reasoning_effort,
+ Some(ReasoningEffort::Medium)
+ );
+ assert_eq!(defaults.service_tier.as_deref(), Some("fast"));
+ }
+
#[test]
fn requirements_api_includes_computer_use_requirements() {
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
diff --git a/codex-rs/app-server/src/request_processors/environment_processor.rs b/codex-rs/app-server/src/request_processors/environment_processor.rs
index 36533aa231..635e1732ec 100644
--- a/codex-rs/app-server/src/request_processors/environment_processor.rs
+++ b/codex-rs/app-server/src/request_processors/environment_processor.rs
@@ -26,4 +26,30 @@ impl EnvironmentRequestProcessor {
.map_err(|err| invalid_request(err.to_string()))?;
Ok(Some(EnvironmentAddResponse {}.into()))
}
+
+ pub(crate) async fn environment_info(
+ &self,
+ params: EnvironmentInfoParams,
+ ) -> Result