mirror of
https://github.com/openai/codex.git
synced 2026-11-08 20:40:33 +00:00
Merge remote-tracking branch 'origin/main' into etraut/goal-capacity-retry
This commit is contained in:
@@ -190,7 +190,7 @@ common:ci-windows-cross --strategy=TestRunner=local
|
||||
# V8 embeds IsolateData offsets in snapshot builtins; Windows snapshots must be
|
||||
# generated by a Windows mksnapshot binary rather than the Linux RBE host tool.
|
||||
common:ci-windows-cross --strategy=V8Mksnapshot=local
|
||||
common:ci-windows-cross --local_test_jobs=4
|
||||
common:ci-windows-cross --local_test_jobs=8
|
||||
common:ci-windows-cross --test_env=RUST_TEST_THREADS=1
|
||||
# Native Windows CI still covers the PowerShell parser-process tests. The
|
||||
# cross-built gnullvm binaries currently hang in those tests when run on the
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
version = 1
|
||||
name = "codex"
|
||||
|
||||
# TODO(anp) make it optional to specify this field
|
||||
[setup]
|
||||
script = "python ./.codex/environments/setup.py"
|
||||
script = ""
|
||||
|
||||
[[actions]]
|
||||
name = "Run"
|
||||
|
||||
@@ -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()
|
||||
@@ -7,10 +7,6 @@ inputs:
|
||||
cache-scope:
|
||||
description: Logical namespace used to keep concurrent Bazel jobs from reserving the same repository cache key.
|
||||
required: true
|
||||
install-test-prereqs:
|
||||
description: Install DotSlash for Bazel-backed test jobs.
|
||||
required: false
|
||||
default: "false"
|
||||
outputs:
|
||||
repository-cache-path:
|
||||
description: Filesystem path used for the Bazel repository cache.
|
||||
@@ -30,7 +26,6 @@ runs:
|
||||
uses: ./.github/actions/setup-bazel-ci
|
||||
with:
|
||||
target: ${{ inputs.target }}
|
||||
install-test-prereqs: ${{ inputs.install-test-prereqs }}
|
||||
|
||||
- name: Compute bazel repository cache key
|
||||
id: cache_bazel_repository_key
|
||||
|
||||
@@ -16,7 +16,6 @@ runs:
|
||||
- uses: ./.github/actions/setup-bazel-ci
|
||||
with:
|
||||
target: ${{ inputs.target }}
|
||||
install-test-prereqs: true
|
||||
|
||||
- name: Install Linux sandbox build dependencies
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
name: setup-bazel-ci
|
||||
description: Prepare a Bazel CI runner with shared caches and optional test prerequisites.
|
||||
description: Prepare a Bazel CI runner with shared caches.
|
||||
inputs:
|
||||
target:
|
||||
description: Target triple used for cache namespacing.
|
||||
required: true
|
||||
install-test-prereqs:
|
||||
description: Install DotSlash for Bazel-backed test jobs.
|
||||
required: false
|
||||
default: "false"
|
||||
outputs:
|
||||
repository-cache-path:
|
||||
description: Filesystem path used for the Bazel repository cache.
|
||||
@@ -16,21 +12,7 @@ outputs:
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
# Some integration tests rely on DotSlash being installed.
|
||||
# See https://github.com/openai/codex/pull/7617.
|
||||
- name: Install DotSlash
|
||||
if: inputs.install-test-prereqs == 'true'
|
||||
uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
|
||||
|
||||
- name: Make DotSlash available in PATH (Unix)
|
||||
if: inputs.install-test-prereqs == 'true' && runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: cp "$(which dotslash)" /usr/local/bin
|
||||
|
||||
- name: Make DotSlash available in PATH (Windows)
|
||||
if: inputs.install-test-prereqs == 'true' && runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: Copy-Item (Get-Command dotslash).Source -Destination "$env:LOCALAPPDATA\Microsoft\WindowsApps\dotslash.exe"
|
||||
- uses: ./.github/actions/setup-ci
|
||||
|
||||
- name: Set up Bazel
|
||||
uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0
|
||||
@@ -124,8 +106,3 @@ runs:
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: ./.github/scripts/compute-bazel-windows-path.ps1
|
||||
|
||||
- name: Enable Git long paths (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: git config --global core.longpaths true
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
name: setup-ci
|
||||
description: Prepare common tools and environment shared by CI jobs.
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
# TODO(anp): Move this under CI_BUILD_ROOT once all consumers use the
|
||||
# shared build-path contract.
|
||||
- name: Configure Cargo target directory
|
||||
shell: bash
|
||||
run: echo "CARGO_TARGET_DIR=${CARGO_TARGET_DIR:-$GITHUB_WORKSPACE/codex-rs/target}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Prefer the Git CLI for Cargo git dependencies
|
||||
shell: bash
|
||||
run: echo "CARGO_NET_GIT_FETCH_WITH_CLI=true" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install DotSlash
|
||||
uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
|
||||
|
||||
- name: Install just
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
with:
|
||||
tool: just@1.51.0
|
||||
|
||||
# Some integration tests spawn DotSlash from a stable system path rather
|
||||
# than inheriting the action-local PATH entry.
|
||||
- name: Make DotSlash available in PATH (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ -w /usr/local/bin ]]; then
|
||||
cp "$(which dotslash)" /usr/local/bin
|
||||
else
|
||||
sudo cp "$(which dotslash)" /usr/local/bin
|
||||
fi
|
||||
|
||||
- name: Make DotSlash available in PATH (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: Copy-Item (Get-Command dotslash).Source -Destination "$env:LOCALAPPDATA\Microsoft\WindowsApps\dotslash.exe"
|
||||
|
||||
- name: Enable Git long paths (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: git config --global core.longpaths true
|
||||
@@ -48,7 +48,7 @@ runs:
|
||||
{
|
||||
echo "files<<EOF"
|
||||
for binary in ${BINARIES}; do
|
||||
echo "${GITHUB_WORKSPACE}/codex-rs/target/${TARGET}/release/${binary}.exe"
|
||||
echo "${CARGO_TARGET_DIR}/${TARGET}/release/${binary}.exe"
|
||||
done
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -9,6 +9,7 @@ Usage: build-codex-package-archive.sh \
|
||||
--entrypoint-dir <dir> \
|
||||
--archive-dir <dir> \
|
||||
[--bwrap-bin <path>] \
|
||||
[--code-mode-host-bin <path>] \
|
||||
[--zsh-manifest <path>] \
|
||||
[--codex-command-runner-bin <path>] \
|
||||
[--codex-windows-sandbox-setup-bin <path>] \
|
||||
@@ -23,6 +24,7 @@ archive_dir=""
|
||||
target_suffixed_entrypoint="false"
|
||||
resource_args=()
|
||||
bwrap_bin_provided="false"
|
||||
code_mode_host_bin_provided="false"
|
||||
command_runner_bin_provided="false"
|
||||
sandbox_setup_bin_provided="false"
|
||||
|
||||
@@ -49,6 +51,11 @@ while [[ $# -gt 0 ]]; do
|
||||
bwrap_bin_provided="true"
|
||||
shift 2
|
||||
;;
|
||||
--code-mode-host-bin)
|
||||
resource_args+=(--code-mode-host-bin "${2:?--code-mode-host-bin requires a value}")
|
||||
code_mode_host_bin_provided="true"
|
||||
shift 2
|
||||
;;
|
||||
--zsh-manifest)
|
||||
resource_args+=(--zsh-manifest "${2:?--zsh-manifest requires a value}")
|
||||
shift 2
|
||||
@@ -114,6 +121,11 @@ case "$target" in
|
||||
;;
|
||||
esac
|
||||
|
||||
code_mode_host_bin="${entrypoint_dir%/}/codex-code-mode-host${exe_suffix}"
|
||||
if [[ "$code_mode_host_bin_provided" == "false" && -f "$code_mode_host_bin" ]]; then
|
||||
resource_args+=(--code-mode-host-bin "$code_mode_host_bin")
|
||||
fi
|
||||
|
||||
entrypoint_name="$entrypoint"
|
||||
if [[ "$target_suffixed_entrypoint" == "true" ]]; then
|
||||
entrypoint_name="${entrypoint_name}-${target}"
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -4,5 +4,7 @@
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -258,21 +258,27 @@ def stage_artifacts(
|
||||
print(staged_checksums)
|
||||
|
||||
|
||||
def upstream_release_pair_paths(source_root: Path, target: str) -> tuple[Path, Path]:
|
||||
def upstream_release_pair_paths(
|
||||
target: str,
|
||||
target_dir: Path,
|
||||
) -> tuple[Path, Path]:
|
||||
lib_name = (
|
||||
"rusty_v8.lib" if target.endswith("-pc-windows-msvc") else "librusty_v8.a"
|
||||
)
|
||||
gn_out = source_root / "target" / target / "release" / "gn_out"
|
||||
gn_out = target_dir / target / "release" / "gn_out"
|
||||
return gn_out / "obj" / lib_name, gn_out / "src_binding.rs"
|
||||
|
||||
|
||||
def stage_upstream_release_pair(
|
||||
source_root: Path,
|
||||
target: str,
|
||||
output_dir: Path,
|
||||
target_dir: Path,
|
||||
sandbox: bool = False,
|
||||
) -> None:
|
||||
lib_path, binding_path = upstream_release_pair_paths(source_root, target)
|
||||
lib_path, binding_path = upstream_release_pair_paths(
|
||||
target,
|
||||
target_dir,
|
||||
)
|
||||
stage_artifacts(target, lib_path, binding_path, output_dir, sandbox)
|
||||
|
||||
|
||||
@@ -330,7 +336,7 @@ def parse_args() -> argparse.Namespace:
|
||||
"stage-upstream-release-pair"
|
||||
)
|
||||
stage_upstream_release_pair_parser.add_argument(
|
||||
"--source-root", type=Path, required=True
|
||||
"--target-dir", type=Path, required=True
|
||||
)
|
||||
stage_upstream_release_pair_parser.add_argument("--target", required=True)
|
||||
stage_upstream_release_pair_parser.add_argument("--output-dir", required=True)
|
||||
@@ -373,9 +379,9 @@ def main() -> int:
|
||||
return 0
|
||||
if args.command == "stage-upstream-release-pair":
|
||||
stage_upstream_release_pair(
|
||||
source_root=args.source_root,
|
||||
target=args.target,
|
||||
output_dir=Path(args.output_dir),
|
||||
target_dir=args.target_dir,
|
||||
sandbox=args.sandbox,
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import plistlib
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SIGNING_DIR = ROOT / ".github" / "scripts" / "macos-signing"
|
||||
ALLOW_JIT = "com.apple.security.cs.allow-jit"
|
||||
ALLOW_UNSIGNED_EXECUTABLE_MEMORY = (
|
||||
"com.apple.security.cs.allow-unsigned-executable-memory"
|
||||
)
|
||||
|
||||
|
||||
class MacosSigningEntitlementsTest(unittest.TestCase):
|
||||
def load(self, binary: str) -> dict[str, bool]:
|
||||
path = SIGNING_DIR / f"{binary}.entitlements.plist"
|
||||
with path.open("rb") as file:
|
||||
return plistlib.load(file)
|
||||
|
||||
def test_v8_binaries_allow_unsigned_executable_memory(self) -> None:
|
||||
expected = {
|
||||
ALLOW_JIT: True,
|
||||
ALLOW_UNSIGNED_EXECUTABLE_MEMORY: True,
|
||||
}
|
||||
for binary in ["codex", "codex-app-server", "codex-code-mode-host"]:
|
||||
with self.subTest(binary=binary):
|
||||
self.assertEqual(self.load(binary), expected)
|
||||
|
||||
def test_responses_proxy_keeps_existing_entitlements(self) -> None:
|
||||
self.assertEqual(
|
||||
self.load("codex-responses-api-proxy"),
|
||||
{ALLOW_JIT: True},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -205,8 +205,8 @@ class RustyV8BazelTest(unittest.TestCase):
|
||||
),
|
||||
),
|
||||
rusty_v8_bazel.upstream_release_pair_paths(
|
||||
Path("/tmp/rusty_v8"),
|
||||
"x86_64-apple-darwin",
|
||||
Path("/tmp/rusty_v8/target"),
|
||||
),
|
||||
)
|
||||
self.assertEqual(
|
||||
@@ -221,25 +221,30 @@ class RustyV8BazelTest(unittest.TestCase):
|
||||
),
|
||||
),
|
||||
rusty_v8_bazel.upstream_release_pair_paths(
|
||||
Path("/tmp/rusty_v8"),
|
||||
"x86_64-pc-windows-msvc",
|
||||
Path("/tmp/rusty_v8/target"),
|
||||
),
|
||||
)
|
||||
|
||||
def test_stage_upstream_release_pair(self) -> None:
|
||||
with TemporaryDirectory() as source_dir, TemporaryDirectory() as output_dir:
|
||||
source_root = Path(source_dir)
|
||||
with (
|
||||
TemporaryDirectory() as target_dir,
|
||||
TemporaryDirectory() as output_dir,
|
||||
):
|
||||
gn_out = (
|
||||
source_root / "target" / "x86_64-pc-windows-msvc" / "release" / "gn_out"
|
||||
Path(target_dir)
|
||||
/ "x86_64-pc-windows-msvc"
|
||||
/ "release"
|
||||
/ "gn_out"
|
||||
)
|
||||
(gn_out / "obj").mkdir(parents=True)
|
||||
(gn_out / "obj" / "rusty_v8.lib").write_bytes(b"archive")
|
||||
(gn_out / "src_binding.rs").write_text("binding")
|
||||
|
||||
rusty_v8_bazel.stage_upstream_release_pair(
|
||||
source_root,
|
||||
"x86_64-pc-windows-msvc",
|
||||
Path(output_dir),
|
||||
Path(target_dir),
|
||||
sandbox=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from v8_canary_changes import changed_files
|
||||
from v8_canary_changes import canary_required
|
||||
from v8_canary_changes import merge_base
|
||||
from v8_canary_changes import resolved_v8_version
|
||||
from v8_canary_changes import windows_source_required
|
||||
@@ -46,6 +47,14 @@ version = "149.2.0"
|
||||
)
|
||||
)
|
||||
|
||||
def test_setup_ci_change_requires_canary_and_source_build(self) -> None:
|
||||
changed_files = {".github/actions/setup-ci/action.yml"}
|
||||
|
||||
self.assertTrue(canary_required(changed_files, "149.2.0", "149.2.0"))
|
||||
self.assertTrue(
|
||||
windows_source_required(changed_files, "149.2.0", "149.2.0")
|
||||
)
|
||||
|
||||
def test_manual_dispatch_requires_source_build(self) -> None:
|
||||
self.assertTrue(
|
||||
windows_source_required(
|
||||
|
||||
@@ -21,6 +21,7 @@ ROOT = Path(__file__).resolve().parents[2]
|
||||
CANARY_PATH_PATTERNS = {
|
||||
".bazelrc",
|
||||
".github/actions/setup-bazel-ci/**",
|
||||
".github/actions/setup-ci/**",
|
||||
".github/scripts/run_bazel_with_buildbuddy.py",
|
||||
".github/scripts/rusty_v8_bazel.py",
|
||||
".github/scripts/rusty_v8_module_bazel.py",
|
||||
@@ -40,6 +41,7 @@ CANARY_PATH_PATTERNS = {
|
||||
# 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/actions/setup-ci/**",
|
||||
".github/scripts/rusty_v8_bazel.py",
|
||||
".github/scripts/rusty_v8_module_bazel.py",
|
||||
".github/scripts/v8_canary_changes.py",
|
||||
@@ -72,6 +74,15 @@ def canary_required(
|
||||
)
|
||||
|
||||
|
||||
def matching_windows_source_paths(changed_files: set[str]) -> set[str]:
|
||||
"""Return changed paths that require Windows rusty_v8 source builds."""
|
||||
return {
|
||||
path
|
||||
for path in changed_files
|
||||
if any(fnmatchcase(path, pattern) for pattern in WINDOWS_SOURCE_BUILD_PATHS)
|
||||
}
|
||||
|
||||
|
||||
def resolved_v8_version(cargo_lock: bytes) -> str:
|
||||
versions = sorted(
|
||||
{
|
||||
@@ -96,7 +107,7 @@ def windows_source_required(
|
||||
return (
|
||||
force
|
||||
or base_v8_version != head_v8_version
|
||||
or not changed_files.isdisjoint(WINDOWS_SOURCE_BUILD_PATHS)
|
||||
or bool(matching_windows_source_paths(changed_files))
|
||||
)
|
||||
|
||||
|
||||
@@ -165,7 +176,7 @@ def main() -> None:
|
||||
if matched_canary_paths
|
||||
else "no relevant changes"
|
||||
)
|
||||
matched_windows_paths = sorted(files & WINDOWS_SOURCE_BUILD_PATHS)
|
||||
matched_windows_paths = sorted(matching_windows_source_paths(files))
|
||||
windows_source_reason = (
|
||||
", ".join(matched_windows_paths)
|
||||
if matched_windows_paths
|
||||
|
||||
@@ -57,10 +57,12 @@ jobs:
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
if: matrix.os == 'ubuntu-24.04' && matrix.target == 'x86_64-unknown-linux-gnu'
|
||||
- name: Prepare Bazel CI
|
||||
id: prepare_bazel
|
||||
uses: ./.github/actions/prepare-bazel-ci
|
||||
with:
|
||||
tool: just
|
||||
target: ${{ matrix.target }}
|
||||
cache-scope: bazel-${{ github.job }}
|
||||
|
||||
- name: Check rusty_v8 MODULE.bazel checksums
|
||||
if: matrix.os == 'ubuntu-24.04' && matrix.target == 'x86_64-unknown-linux-gnu'
|
||||
@@ -69,13 +71,6 @@ jobs:
|
||||
python3 .github/scripts/rusty_v8_bazel.py check-module-bazel
|
||||
just test-github-scripts
|
||||
|
||||
- name: Prepare Bazel CI
|
||||
id: prepare_bazel
|
||||
uses: ./.github/actions/prepare-bazel-ci
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
cache-scope: bazel-${{ github.job }}
|
||||
install-test-prereqs: "true"
|
||||
- name: Check MODULE.bazel.lock is up to date
|
||||
if: matrix.os == 'ubuntu-24.04' && matrix.target == 'x86_64-unknown-linux-gnu'
|
||||
shell: bash
|
||||
@@ -176,7 +171,6 @@ jobs:
|
||||
# not save it from every shard below; duplicate uploads would sit on
|
||||
# the PR-blocking critical path after the useful test work is done.
|
||||
cache-scope: bazel-test
|
||||
install-test-prereqs: "true"
|
||||
|
||||
- name: bazel test shard
|
||||
env:
|
||||
@@ -287,7 +281,6 @@ jobs:
|
||||
with:
|
||||
target: x86_64-pc-windows-gnullvm
|
||||
cache-scope: bazel-${{ github.job }}
|
||||
install-test-prereqs: "true"
|
||||
|
||||
- name: bazel test //...
|
||||
env:
|
||||
|
||||
@@ -3,11 +3,6 @@ name: cargo-deny
|
||||
on:
|
||||
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.
|
||||
env:
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
|
||||
|
||||
jobs:
|
||||
cargo-deny:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -21,6 +16,8 @@ jobs:
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- uses: ./.github/actions/setup-ci
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ jobs:
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- uses: ./.github/actions/setup-ci
|
||||
|
||||
- name: Verify codex-rs Cargo manifests inherit workspace settings
|
||||
run: python3 .github/scripts/verify_cargo_workspace_manifests.py
|
||||
|
||||
@@ -76,15 +78,10 @@ jobs:
|
||||
- name: Check root README ToC
|
||||
run: python3 scripts/readme_toc.py README.md
|
||||
|
||||
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
with:
|
||||
tool: just@1.51.0
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
with:
|
||||
version: "0.11.3"
|
||||
- name: Install DotSlash
|
||||
uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
|
||||
- name: Check formatting (run `just fmt` to fix)
|
||||
run: just fmt-check
|
||||
|
||||
|
||||
@@ -48,11 +48,6 @@ on:
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
# Caller workflow-level env does not flow through workflow_call, so keep the
|
||||
# Cargo git transport hardening on the archive and shard jobs directly here.
|
||||
env:
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
|
||||
|
||||
jobs:
|
||||
archive:
|
||||
name: Build nextest archive
|
||||
@@ -81,6 +76,8 @@ jobs:
|
||||
shell: pwsh
|
||||
run: ../.github/scripts/setup-dev-drive.ps1
|
||||
|
||||
- uses: ./.github/actions/setup-ci
|
||||
|
||||
- name: Install Linux build dependencies
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
shell: bash
|
||||
@@ -91,9 +88,6 @@ jobs:
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev bubblewrap
|
||||
fi
|
||||
|
||||
- name: Install DotSlash
|
||||
uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
|
||||
|
||||
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
|
||||
with:
|
||||
targets: ${{ inputs.target }}
|
||||
@@ -212,7 +206,7 @@ jobs:
|
||||
--profile ${{ inputs.profile }} \
|
||||
-p codex-linux-sandbox \
|
||||
--bin codex-linux-sandbox
|
||||
cp "target/${{ inputs.target }}/${{ inputs.profile }}/codex-linux-sandbox" "${helper_dir}/"
|
||||
cp "${CARGO_TARGET_DIR}/${{ inputs.target }}/${{ inputs.profile }}/codex-linux-sandbox" "${helper_dir}/"
|
||||
else
|
||||
cargo build \
|
||||
--target ${{ inputs.target }} \
|
||||
@@ -220,8 +214,8 @@ jobs:
|
||||
-p codex-windows-sandbox \
|
||||
--bin codex-windows-sandbox-setup \
|
||||
--bin codex-command-runner
|
||||
cp "target/${{ inputs.target }}/${{ inputs.profile }}/codex-windows-sandbox-setup.exe" "${helper_dir}/"
|
||||
cp "target/${{ inputs.target }}/${{ inputs.profile }}/codex-command-runner.exe" "${helper_dir}/"
|
||||
cp "${CARGO_TARGET_DIR}/${{ inputs.target }}/${{ inputs.profile }}/codex-windows-sandbox-setup.exe" "${helper_dir}/"
|
||||
cp "${CARGO_TARGET_DIR}/${{ inputs.target }}/${{ inputs.profile }}/codex-command-runner.exe" "${helper_dir}/"
|
||||
fi
|
||||
|
||||
- name: Upload Cargo timings (nextest)
|
||||
@@ -229,7 +223,7 @@ jobs:
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: cargo-timings-rust-ci-nextest-${{ inputs.target }}-${{ inputs.profile }}
|
||||
path: codex-rs/target/**/cargo-timings/cargo-timing.html
|
||||
path: ${{ env.CARGO_TARGET_DIR }}/**/cargo-timings/cargo-timing.html
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Upload nextest archive
|
||||
@@ -306,6 +300,8 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: ./.github/actions/setup-ci
|
||||
|
||||
- name: Install Linux build dependencies
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
shell: bash
|
||||
@@ -316,9 +312,6 @@ jobs:
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev bubblewrap
|
||||
fi
|
||||
|
||||
- name: Install DotSlash
|
||||
uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
|
||||
|
||||
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
|
||||
with:
|
||||
targets: ${{ inputs.target }}
|
||||
@@ -376,13 +369,13 @@ jobs:
|
||||
|
||||
if [[ "${RUNNER_OS}" == "Linux" ]]; then
|
||||
helper_dir="${RUNNER_TEMP}/${TEST_HELPERS_ARTIFACT}"
|
||||
helper_target_dir="$(pwd)/target/${{ inputs.target }}/${{ inputs.profile }}"
|
||||
helper_target_dir="${CARGO_TARGET_DIR}/${{ inputs.target }}/${{ inputs.profile }}"
|
||||
mkdir -p "${helper_target_dir}"
|
||||
cp "${helper_dir}/codex-linux-sandbox" "${helper_target_dir}/"
|
||||
chmod +x "${helper_target_dir}/codex-linux-sandbox"
|
||||
elif [[ "${RUNNER_OS}" == "Windows" ]]; then
|
||||
helper_dir="${RUNNER_TEMP}/${TEST_HELPERS_ARTIFACT}"
|
||||
helper_target_dir="$(pwd)/target/${{ inputs.target }}/${{ inputs.profile }}"
|
||||
helper_target_dir="${CARGO_TARGET_DIR}/${{ inputs.target }}/${{ inputs.profile }}"
|
||||
mkdir -p "${helper_target_dir}"
|
||||
cp "${helper_dir}/codex-windows-sandbox-setup.exe" "${helper_target_dir}/"
|
||||
cp "${helper_dir}/codex-command-runner.exe" "${helper_target_dir}/"
|
||||
@@ -430,7 +423,7 @@ jobs:
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: nextest-junit-rust-ci-${{ inputs.artifact_id }}-shard-${{ matrix.shard }}
|
||||
path: codex-rs/target/nextest/default/junit.xml
|
||||
path: ${{ env.CARGO_TARGET_DIR }}/nextest/default/junit.xml
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Tear down remote test env
|
||||
|
||||
@@ -8,13 +8,6 @@ on:
|
||||
- "**full-ci**"
|
||||
workflow_dispatch:
|
||||
|
||||
# CI builds in debug (dev) for faster signal.
|
||||
env:
|
||||
# Cargo's libgit2 transport has been flaky on macOS when fetching git
|
||||
# dependencies with nested submodules. Use the system git CLI, which has
|
||||
# better network/proxy behavior and matches Cargo's own suggested fallback.
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
|
||||
|
||||
jobs:
|
||||
# --- CI that doesn't need specific targets ---------------------------------
|
||||
general:
|
||||
@@ -27,12 +20,10 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: ./.github/actions/setup-ci
|
||||
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
|
||||
with:
|
||||
components: rustfmt
|
||||
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
with:
|
||||
tool: just
|
||||
- name: cargo fmt
|
||||
run: cargo fmt -- --config imports_granularity=Item --check
|
||||
- name: Rust benchmark smoke test
|
||||
@@ -48,6 +39,7 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: ./.github/actions/setup-ci
|
||||
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
|
||||
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
with:
|
||||
@@ -65,6 +57,7 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: ./.github/actions/setup-ci
|
||||
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
|
||||
with:
|
||||
toolchain: nightly-2025-09-18
|
||||
@@ -123,7 +116,6 @@ jobs:
|
||||
- uses: ./.github/actions/setup-bazel-ci
|
||||
with:
|
||||
target: ${{ runner.os }}
|
||||
install-test-prereqs: true
|
||||
- name: Install Linux sandbox build dependencies
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
shell: bash
|
||||
@@ -256,6 +248,7 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: ./.github/actions/setup-ci
|
||||
- name: Install Linux build dependencies
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
shell: bash
|
||||
@@ -410,7 +403,7 @@ jobs:
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: cargo-timings-rust-ci-clippy-${{ matrix.target }}-${{ matrix.profile }}
|
||||
path: codex-rs/target/**/cargo-timings/cargo-timing.html
|
||||
path: ${{ env.CARGO_TARGET_DIR }}/**/cargo-timings/cargo-timing.html
|
||||
if-no-files-found: warn
|
||||
|
||||
# Save caches explicitly; make non-fatal so cache packaging
|
||||
|
||||
@@ -3,11 +3,6 @@ on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
||||
# Cargo's libgit2 transport has been flaky when fetching git dependencies with
|
||||
# nested submodules. Prefer the system git CLI across every Cargo invocation.
|
||||
env:
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
|
||||
|
||||
jobs:
|
||||
# --- Detect what changed so the fast PR workflow only runs relevant jobs ----
|
||||
changed:
|
||||
@@ -76,12 +71,10 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
persist-credentials: false
|
||||
- uses: ./.github/actions/setup-ci
|
||||
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
|
||||
with:
|
||||
components: rustfmt
|
||||
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
with:
|
||||
tool: just
|
||||
- name: cargo fmt
|
||||
run: cargo fmt -- --config imports_granularity=Item --check
|
||||
- name: Rust benchmark smoke test
|
||||
@@ -104,6 +97,7 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
persist-credentials: false
|
||||
- uses: ./.github/actions/setup-ci
|
||||
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
|
||||
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
with:
|
||||
@@ -128,6 +122,7 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
persist-credentials: false
|
||||
- uses: ./.github/actions/setup-ci
|
||||
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
|
||||
- name: Install nightly argument-comment-lint toolchain
|
||||
shell: bash
|
||||
|
||||
@@ -3,11 +3,8 @@ name: rust-release-windows
|
||||
on:
|
||||
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.
|
||||
env:
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
|
||||
WINDOWS_BINARIES: "codex codex-responses-api-proxy codex-windows-sandbox-setup codex-command-runner codex-app-server"
|
||||
WINDOWS_BINARIES: "codex codex-code-mode-host codex-responses-api-proxy codex-windows-sandbox-setup codex-command-runner codex-app-server"
|
||||
|
||||
jobs:
|
||||
build-windows-binaries:
|
||||
@@ -28,14 +25,14 @@ jobs:
|
||||
- runner: windows-x64
|
||||
target: x86_64-pc-windows-msvc
|
||||
bundle: primary
|
||||
binaries: "codex codex-responses-api-proxy"
|
||||
binaries: "codex codex-code-mode-host codex-responses-api-proxy"
|
||||
runs_on:
|
||||
group: ${{ github.event.repository.name }}-runners
|
||||
labels: ${{ github.event.repository.name }}-windows-x64
|
||||
- runner: windows-arm64
|
||||
target: aarch64-pc-windows-msvc
|
||||
bundle: primary
|
||||
binaries: "codex codex-responses-api-proxy"
|
||||
binaries: "codex codex-code-mode-host codex-responses-api-proxy"
|
||||
runs_on:
|
||||
group: ${{ github.event.repository.name }}-runners
|
||||
labels: ${{ github.event.repository.name }}-windows-arm64
|
||||
@@ -56,14 +53,14 @@ jobs:
|
||||
- runner: windows-x64
|
||||
target: x86_64-pc-windows-msvc
|
||||
bundle: app-server
|
||||
binaries: "codex-app-server"
|
||||
binaries: "codex-app-server codex-code-mode-host"
|
||||
runs_on:
|
||||
group: ${{ github.event.repository.name }}-runners
|
||||
labels: ${{ github.event.repository.name }}-windows-x64
|
||||
- runner: windows-arm64
|
||||
target: aarch64-pc-windows-msvc
|
||||
bundle: app-server
|
||||
binaries: "codex-app-server"
|
||||
binaries: "codex-app-server codex-code-mode-host"
|
||||
runs_on:
|
||||
group: ${{ github.event.repository.name }}-runners
|
||||
labels: ${{ github.event.repository.name }}-windows-arm64
|
||||
@@ -72,6 +69,7 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: ./.github/actions/setup-ci
|
||||
- name: Print runner specs (Windows)
|
||||
shell: powershell
|
||||
run: |
|
||||
@@ -112,13 +110,13 @@ jobs:
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: cargo-timings-rust-release-windows-${{ matrix.target }}-${{ matrix.bundle }}
|
||||
path: codex-rs/target/**/cargo-timings/cargo-timing.html
|
||||
path: ${{ env.CARGO_TARGET_DIR }}/**/cargo-timings/cargo-timing.html
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Stage Windows binaries
|
||||
shell: bash
|
||||
run: |
|
||||
release_dir="target/${{ matrix.target }}/release"
|
||||
release_dir="$CARGO_TARGET_DIR/${{ matrix.target }}/release"
|
||||
output_dir="$release_dir/staged-${{ matrix.bundle }}"
|
||||
mkdir -p "$output_dir"
|
||||
for binary in ${{ matrix.binaries }}; do
|
||||
@@ -141,7 +139,7 @@ jobs:
|
||||
with:
|
||||
name: windows-binaries-${{ matrix.target }}-${{ matrix.bundle }}
|
||||
path: |
|
||||
codex-rs/target/${{ matrix.target }}/release/staged-${{ matrix.bundle }}/*
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/release/staged-${{ matrix.bundle }}/*
|
||||
|
||||
build-windows-symbols:
|
||||
needs:
|
||||
@@ -164,19 +162,20 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: ./.github/actions/setup-ci
|
||||
- name: Download prebuilt Windows binaries
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: windows-binaries-${{ matrix.target }}-*
|
||||
merge-multiple: true
|
||||
path: codex-rs/target/${{ matrix.target }}/release
|
||||
path: ${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/release
|
||||
- name: Build symbols archive
|
||||
shell: bash
|
||||
run: |
|
||||
bash "${GITHUB_WORKSPACE}/.github/scripts/archive-release-symbols-and-strip-binaries.sh" \
|
||||
--target "${{ matrix.target }}" \
|
||||
--artifact-name "${{ matrix.target }}" \
|
||||
--release-dir "target/${{ matrix.target }}/release" \
|
||||
--release-dir "${CARGO_TARGET_DIR}/${{ matrix.target }}/release" \
|
||||
--archive-dir "symbols-dist/${{ matrix.target }}" \
|
||||
--binaries "${WINDOWS_BINARIES}"
|
||||
- name: Upload symbols archive
|
||||
@@ -221,30 +220,32 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: ./.github/actions/setup-ci
|
||||
|
||||
- name: Download prebuilt Windows primary binaries
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: windows-binaries-${{ matrix.target }}-primary
|
||||
path: codex-rs/target/${{ matrix.target }}/release
|
||||
path: ${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/release
|
||||
|
||||
- name: Download prebuilt Windows helper binaries
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: windows-binaries-${{ matrix.target }}-helpers
|
||||
path: codex-rs/target/${{ matrix.target }}/release
|
||||
path: ${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/release
|
||||
|
||||
- name: Download prebuilt Windows app-server binary
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: windows-binaries-${{ matrix.target }}-app-server
|
||||
path: codex-rs/target/${{ matrix.target }}/release
|
||||
path: ${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/release
|
||||
|
||||
- name: Verify binaries
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for binary in ${WINDOWS_BINARIES}; do
|
||||
ls -lh "target/${{ matrix.target }}/release/${binary}.exe"
|
||||
ls -lh "$CARGO_TARGET_DIR/${{ matrix.target }}/release/${binary}.exe"
|
||||
done
|
||||
|
||||
- name: Sign Windows binaries with Azure Trusted Signing
|
||||
@@ -266,13 +267,10 @@ jobs:
|
||||
mkdir -p "$dest"
|
||||
|
||||
for binary in ${WINDOWS_BINARIES}; do
|
||||
cp "target/${{ matrix.target }}/release/${binary}.exe" \
|
||||
cp "$CARGO_TARGET_DIR/${{ matrix.target }}/release/${binary}.exe" \
|
||||
"$dest/${binary}-${{ matrix.target }}.exe"
|
||||
done
|
||||
|
||||
- name: Install DotSlash
|
||||
uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
|
||||
|
||||
- name: Build Codex package archives
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -293,7 +291,7 @@ jobs:
|
||||
bash "$archive_script" \
|
||||
--target "$target" \
|
||||
--bundle "{}" \
|
||||
--entrypoint-dir "target/$target/release" \
|
||||
--entrypoint-dir "$CARGO_TARGET_DIR/$target/release" \
|
||||
--archive-dir "dist/$target"
|
||||
|
||||
- name: Build Python runtime wheel
|
||||
|
||||
@@ -83,50 +83,50 @@ jobs:
|
||||
target: aarch64-apple-darwin
|
||||
bundle: primary
|
||||
artifact_name: aarch64-apple-darwin
|
||||
binaries: "codex codex-responses-api-proxy"
|
||||
binaries: "codex codex-code-mode-host codex-responses-api-proxy"
|
||||
build_dmg: "true"
|
||||
- runner: macos-15-xlarge
|
||||
target: aarch64-apple-darwin
|
||||
bundle: app-server
|
||||
artifact_name: aarch64-apple-darwin-app-server
|
||||
binaries: "codex-app-server"
|
||||
binaries: "codex-app-server codex-code-mode-host"
|
||||
build_dmg: "false"
|
||||
- runner: macos-15-xlarge
|
||||
target: x86_64-apple-darwin
|
||||
bundle: primary
|
||||
artifact_name: x86_64-apple-darwin
|
||||
binaries: "codex codex-responses-api-proxy"
|
||||
binaries: "codex codex-code-mode-host codex-responses-api-proxy"
|
||||
build_dmg: "true"
|
||||
- runner: macos-15-xlarge
|
||||
target: x86_64-apple-darwin
|
||||
bundle: app-server
|
||||
artifact_name: x86_64-apple-darwin-app-server
|
||||
binaries: "codex-app-server"
|
||||
binaries: "codex-app-server codex-code-mode-host"
|
||||
build_dmg: "false"
|
||||
# Release artifacts intentionally ship MUSL-linked Linux binaries.
|
||||
- runner: ${{ github.event.repository.name }}-linux-x64-xl
|
||||
target: x86_64-unknown-linux-musl
|
||||
bundle: primary
|
||||
artifact_name: x86_64-unknown-linux-musl
|
||||
binaries: "codex codex-responses-api-proxy bwrap"
|
||||
binaries: "codex codex-code-mode-host codex-responses-api-proxy bwrap"
|
||||
build_dmg: "false"
|
||||
- runner: ${{ github.event.repository.name }}-linux-x64-xl
|
||||
target: x86_64-unknown-linux-musl
|
||||
bundle: app-server
|
||||
artifact_name: x86_64-unknown-linux-musl-app-server
|
||||
binaries: "codex-app-server"
|
||||
binaries: "codex-app-server codex-code-mode-host"
|
||||
build_dmg: "false"
|
||||
- runner: ${{ github.event.repository.name }}-linux-arm64
|
||||
target: aarch64-unknown-linux-musl
|
||||
bundle: primary
|
||||
artifact_name: aarch64-unknown-linux-musl
|
||||
binaries: "codex codex-responses-api-proxy bwrap"
|
||||
binaries: "codex codex-code-mode-host codex-responses-api-proxy bwrap"
|
||||
build_dmg: "false"
|
||||
- runner: ${{ github.event.repository.name }}-linux-arm64
|
||||
target: aarch64-unknown-linux-musl
|
||||
bundle: app-server
|
||||
artifact_name: aarch64-unknown-linux-musl-app-server
|
||||
binaries: "codex-app-server"
|
||||
binaries: "codex-app-server codex-code-mode-host"
|
||||
build_dmg: "false"
|
||||
|
||||
steps:
|
||||
@@ -335,6 +335,11 @@ jobs:
|
||||
mkdir -p "$dest"
|
||||
|
||||
for binary in ${{ matrix.binaries }}; do
|
||||
# Both variants package the host, but only the primary bundle publishes
|
||||
# standalone binary archives to avoid duplicate release asset names.
|
||||
if [[ "${{ matrix.bundle }}" == "app-server" && "$binary" == "codex-code-mode-host" ]]; then
|
||||
continue
|
||||
fi
|
||||
cp "target/${{ matrix.target }}/release/${binary}" "$dest/${binary}-${{ matrix.target }}"
|
||||
if [[ "${{ matrix.target }}" == *linux* ]]; then
|
||||
cp "target/${{ matrix.target }}/release/${binary}.sigstore" \
|
||||
@@ -347,9 +352,13 @@ jobs:
|
||||
rm -rf "$bundle_root"
|
||||
mkdir -p "$bundle_root/codex-resources"
|
||||
cp "$dest/codex-${{ matrix.target }}" "$bundle_root/codex"
|
||||
cp "$dest/codex-code-mode-host-${{ matrix.target }}" "$bundle_root/codex-code-mode-host"
|
||||
cp "$dest/bwrap-${{ matrix.target }}" "$bundle_root/codex-resources/bwrap"
|
||||
chmod 0755 "$bundle_root/codex" "$bundle_root/codex-resources/bwrap"
|
||||
tar -C "$bundle_root" -cf - codex codex-resources/bwrap |
|
||||
chmod 0755 \
|
||||
"$bundle_root/codex" \
|
||||
"$bundle_root/codex-code-mode-host" \
|
||||
"$bundle_root/codex-resources/bwrap"
|
||||
tar -C "$bundle_root" -cf - codex codex-code-mode-host codex-resources/bwrap |
|
||||
zstd -T0 -19 -o "$dest/codex-${{ matrix.target }}-bundle.tar.zst"
|
||||
fi
|
||||
|
||||
@@ -497,19 +506,19 @@ jobs:
|
||||
- target: aarch64-apple-darwin
|
||||
bundle: primary
|
||||
artifact_name: aarch64-apple-darwin
|
||||
binaries: "codex codex-responses-api-proxy"
|
||||
binaries: "codex codex-code-mode-host codex-responses-api-proxy"
|
||||
- target: aarch64-apple-darwin
|
||||
bundle: app-server
|
||||
artifact_name: aarch64-apple-darwin-app-server
|
||||
binaries: "codex-app-server"
|
||||
binaries: "codex-app-server codex-code-mode-host"
|
||||
- target: x86_64-apple-darwin
|
||||
bundle: primary
|
||||
artifact_name: x86_64-apple-darwin
|
||||
binaries: "codex codex-responses-api-proxy"
|
||||
binaries: "codex codex-code-mode-host codex-responses-api-proxy"
|
||||
- target: x86_64-apple-darwin
|
||||
bundle: app-server
|
||||
artifact_name: x86_64-apple-darwin-app-server
|
||||
binaries: "codex-app-server"
|
||||
binaries: "codex-app-server codex-code-mode-host"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -564,6 +573,12 @@ jobs:
|
||||
zstd -d --stdout "$unsigned_path" >"$signed_path"
|
||||
chmod 0755 "$signed_path"
|
||||
|
||||
entitlements="${GITHUB_WORKSPACE}/.github/scripts/macos-signing/${binary}.entitlements.plist"
|
||||
if [[ ! -f "$entitlements" ]]; then
|
||||
echo "Entitlements file $entitlements not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
.github/scripts/macos-signing/sign_macos_code.sh \
|
||||
--target "$signed_path" \
|
||||
--identity unused \
|
||||
@@ -571,7 +586,7 @@ jobs:
|
||||
--identifier "$binary" \
|
||||
--options runtime \
|
||||
--timestamp true \
|
||||
--entitlements .github/scripts/macos-signing/codex.entitlements.plist
|
||||
--entitlements "$entitlements"
|
||||
|
||||
mkdir -p "${report_dir}/${binary}"
|
||||
rcodesign print-signature-info "$signed_path" \
|
||||
@@ -615,22 +630,22 @@ jobs:
|
||||
- target: aarch64-apple-darwin
|
||||
bundle: primary
|
||||
artifact_name: aarch64-apple-darwin
|
||||
binaries: "codex codex-responses-api-proxy"
|
||||
binaries: "codex codex-code-mode-host codex-responses-api-proxy"
|
||||
build_dmg: "true"
|
||||
- target: aarch64-apple-darwin
|
||||
bundle: app-server
|
||||
artifact_name: aarch64-apple-darwin-app-server
|
||||
binaries: "codex-app-server"
|
||||
binaries: "codex-app-server codex-code-mode-host"
|
||||
build_dmg: "false"
|
||||
- target: x86_64-apple-darwin
|
||||
bundle: primary
|
||||
artifact_name: x86_64-apple-darwin
|
||||
binaries: "codex codex-responses-api-proxy"
|
||||
binaries: "codex codex-code-mode-host codex-responses-api-proxy"
|
||||
build_dmg: "true"
|
||||
- target: x86_64-apple-darwin
|
||||
bundle: app-server
|
||||
artifact_name: x86_64-apple-darwin-app-server
|
||||
binaries: "codex-app-server"
|
||||
binaries: "codex-app-server codex-code-mode-host"
|
||||
build_dmg: "false"
|
||||
|
||||
steps:
|
||||
@@ -707,6 +722,11 @@ jobs:
|
||||
mkdir -p "$dest"
|
||||
|
||||
for binary in ${{ matrix.binaries }}; do
|
||||
# Both variants package the host, but only the primary bundle publishes
|
||||
# standalone binary archives to avoid duplicate release asset names.
|
||||
if [[ "${{ matrix.bundle }}" == "app-server" && "$binary" == "codex-code-mode-host" ]]; then
|
||||
continue
|
||||
fi
|
||||
cp "target/${{ matrix.target }}/release/${binary}" "$dest/${binary}-${{ matrix.target }}"
|
||||
done
|
||||
|
||||
@@ -911,22 +931,22 @@ jobs:
|
||||
- target: aarch64-apple-darwin
|
||||
bundle: primary
|
||||
artifact_name: aarch64-apple-darwin
|
||||
binaries: "codex codex-responses-api-proxy"
|
||||
binaries: "codex codex-code-mode-host codex-responses-api-proxy"
|
||||
verify_dmg: "true"
|
||||
- target: aarch64-apple-darwin
|
||||
bundle: app-server
|
||||
artifact_name: aarch64-apple-darwin-app-server
|
||||
binaries: "codex-app-server"
|
||||
binaries: "codex-app-server codex-code-mode-host"
|
||||
verify_dmg: "false"
|
||||
- target: x86_64-apple-darwin
|
||||
bundle: primary
|
||||
artifact_name: x86_64-apple-darwin
|
||||
binaries: "codex codex-responses-api-proxy"
|
||||
binaries: "codex codex-code-mode-host codex-responses-api-proxy"
|
||||
verify_dmg: "true"
|
||||
- target: x86_64-apple-darwin
|
||||
bundle: app-server
|
||||
artifact_name: x86_64-apple-darwin-app-server
|
||||
binaries: "codex-app-server"
|
||||
binaries: "codex-app-server codex-code-mode-host"
|
||||
verify_dmg: "false"
|
||||
|
||||
steps:
|
||||
@@ -960,15 +980,29 @@ jobs:
|
||||
|
||||
target="${{ matrix.target }}"
|
||||
packaged_dir="dist/${target}"
|
||||
expected_entitlements="${GITHUB_WORKSPACE}/.github/scripts/macos-signing/codex.entitlements.plist"
|
||||
case "$target" in
|
||||
aarch64-apple-darwin) expected_arch="arm64" ;;
|
||||
x86_64-apple-darwin) expected_arch="x86_64" ;;
|
||||
*)
|
||||
echo "Unexpected macOS target: $target"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
verify_signed_binary() {
|
||||
local path="$1"
|
||||
local actual_entitlements normalized_actual normalized_expected
|
||||
local binary="$2"
|
||||
local actual_entitlements expected_entitlements normalized_actual normalized_expected
|
||||
|
||||
chmod 0755 "$path"
|
||||
lipo "$path" -verify_arch "$expected_arch"
|
||||
codesign --verify --strict --verbose=2 "$path"
|
||||
|
||||
expected_entitlements="${GITHUB_WORKSPACE}/.github/scripts/macos-signing/${binary}.entitlements.plist"
|
||||
if [[ ! -f "$expected_entitlements" ]]; then
|
||||
echo "Expected entitlements file $expected_entitlements not found"
|
||||
exit 1
|
||||
fi
|
||||
actual_entitlements="$(mktemp)"
|
||||
normalized_actual="$(mktemp)"
|
||||
normalized_expected="$(mktemp)"
|
||||
@@ -981,17 +1015,23 @@ jobs:
|
||||
|
||||
for binary in ${{ matrix.binaries }}; do
|
||||
binary_path="${RUNNER_TEMP}/signed-binaries/${binary}"
|
||||
verify_signed_binary "$binary_path"
|
||||
verify_signed_binary "$binary_path" "$binary"
|
||||
|
||||
# The app-server package contains the host, but its standalone archives
|
||||
# are omitted above to avoid duplicate release asset names.
|
||||
if [[ "${{ matrix.bundle }}" == "app-server" && "$binary" == "codex-code-mode-host" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
direct_archive_dir="${RUNNER_TEMP}/direct-archive-${binary}-${target}"
|
||||
rm -rf "$direct_archive_dir"
|
||||
mkdir -p "$direct_archive_dir"
|
||||
tar -xzf "${packaged_dir}/${binary}-${target}.tar.gz" -C "$direct_archive_dir"
|
||||
verify_signed_binary "${direct_archive_dir}/${binary}-${target}"
|
||||
verify_signed_binary "${direct_archive_dir}/${binary}-${target}" "$binary"
|
||||
|
||||
direct_zstd_path="${RUNNER_TEMP}/${binary}-${target}-from-zstd"
|
||||
zstd -d --stdout "${packaged_dir}/${binary}-${target}.zst" >"$direct_zstd_path"
|
||||
verify_signed_binary "$direct_zstd_path"
|
||||
verify_signed_binary "$direct_zstd_path" "$binary"
|
||||
done
|
||||
|
||||
case "${{ matrix.bundle }}" in
|
||||
@@ -1013,7 +1053,8 @@ jobs:
|
||||
rm -rf "$package_dir"
|
||||
mkdir -p "$package_dir"
|
||||
tar -xzf "${packaged_dir}/${package_stem}-${target}.tar.gz" -C "$package_dir"
|
||||
verify_signed_binary "${package_dir}/bin/${package_entrypoint}"
|
||||
verify_signed_binary "${package_dir}/bin/${package_entrypoint}" "$package_entrypoint"
|
||||
verify_signed_binary "${package_dir}/bin/codex-code-mode-host" "codex-code-mode-host"
|
||||
|
||||
if [[ "${{ matrix.verify_dmg }}" != "true" ]]; then
|
||||
exit 0
|
||||
@@ -1039,7 +1080,7 @@ jobs:
|
||||
trap cleanup_mount EXIT
|
||||
|
||||
for binary in ${{ matrix.binaries }}; do
|
||||
verify_signed_binary "${mount_dir}/${binary}"
|
||||
verify_signed_binary "${mount_dir}/${binary}" "$binary"
|
||||
done
|
||||
|
||||
cleanup_mount
|
||||
|
||||
@@ -392,7 +392,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 .github/scripts/rusty_v8_bazel.py stage-upstream-release-pair \
|
||||
--source-root upstream-rusty-v8 \
|
||||
--target-dir upstream-rusty-v8/target \
|
||||
--target "${TARGET}" \
|
||||
--output-dir "dist/${TARGET}" \
|
||||
--sandbox
|
||||
|
||||
@@ -9,11 +9,6 @@ on:
|
||||
pull_request: {}
|
||||
workflow_dispatch:
|
||||
|
||||
# Cargo's libgit2 transport has been flaky when fetching git dependencies with
|
||||
# nested submodules. Prefer the system git CLI for Cargo builds and smoke tests.
|
||||
env:
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}::${{ github.event.pull_request.number > 0 && format('pr-{0}', github.event.pull_request.number) || github.ref_name }}
|
||||
cancel-in-progress: ${{ github.ref_name != 'main' }}
|
||||
@@ -313,18 +308,22 @@ jobs:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
CARGO_TARGET_DIR: ${{ github.workspace }}/upstream-rusty-v8/target
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: windows-2022
|
||||
- runner: windows-2025
|
||||
target: x86_64-pc-windows-msvc
|
||||
- runner: windows-2022
|
||||
- runner: windows-2025
|
||||
target: aarch64-pc-windows-msvc
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- uses: ./.github/actions/setup-ci
|
||||
|
||||
- name: Configure git for upstream checkout
|
||||
shell: bash
|
||||
run: git config --global core.symlinks true
|
||||
@@ -367,17 +366,17 @@ jobs:
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
with:
|
||||
path: |
|
||||
upstream-rusty-v8/target/sccache
|
||||
upstream-rusty-v8/target/${{ matrix.target }}/release/gn_out
|
||||
key: rusty-v8-source-${{ matrix.target }}-sandbox-${{ hashFiles('upstream-rusty-v8/Cargo.lock', 'upstream-rusty-v8/build.rs', 'upstream-rusty-v8/git_submodule_status.txt') }}
|
||||
${{ env.CARGO_TARGET_DIR }}/sccache
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/release/gn_out
|
||||
key: rusty-v8-source-${{ matrix.target }}-${{ matrix.runner }}-sandbox-${{ hashFiles('upstream-rusty-v8/Cargo.lock', 'upstream-rusty-v8/build.rs', 'upstream-rusty-v8/git_submodule_status.txt') }}
|
||||
restore-keys: |
|
||||
rusty-v8-source-${{ matrix.target }}-sandbox-
|
||||
rusty-v8-source-${{ matrix.target }}-${{ matrix.runner }}-sandbox-
|
||||
|
||||
- name: Install and start sccache
|
||||
shell: pwsh
|
||||
env:
|
||||
SCCACHE_CACHE_SIZE: 256M
|
||||
SCCACHE_DIR: ${{ github.workspace }}/upstream-rusty-v8/target/sccache
|
||||
SCCACHE_DIR: ${{ env.CARGO_TARGET_DIR }}/sccache
|
||||
SCCACHE_IDLE_TIMEOUT: 0
|
||||
run: |
|
||||
$version = "v0.8.2"
|
||||
@@ -412,7 +411,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 .github/scripts/rusty_v8_bazel.py stage-upstream-release-pair \
|
||||
--source-root upstream-rusty-v8 \
|
||||
--target-dir "${CARGO_TARGET_DIR}" \
|
||||
--target "${TARGET}" \
|
||||
--output-dir "dist/${TARGET}" \
|
||||
--sandbox
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
user.bazelrc
|
||||
Generated
+1
-1
@@ -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\":{}}",
|
||||
|
||||
Generated
+61
-29
@@ -412,6 +412,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shlex",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"uuid",
|
||||
"wiremock",
|
||||
@@ -1936,6 +1937,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"chrono",
|
||||
"codex-client",
|
||||
"codex-http-client",
|
||||
"codex-protocol",
|
||||
"codex-utils-rustls-provider",
|
||||
"eventsource-stream",
|
||||
@@ -2106,6 +2108,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"codex-experimental-api-macros",
|
||||
"codex-extension-items",
|
||||
"codex-protocol",
|
||||
"codex-shell-command",
|
||||
"codex-utils-absolute-path",
|
||||
@@ -2262,7 +2265,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"codex-api",
|
||||
"codex-backend-openapi-models",
|
||||
"codex-client",
|
||||
"codex-http-client",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-protocol",
|
||||
@@ -2392,33 +2395,12 @@ dependencies = [
|
||||
name = "codex-client"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"codex-utils-cargo-bin",
|
||||
"codex-utils-rustls-provider",
|
||||
"codex-http-client",
|
||||
"eventsource-stream",
|
||||
"futures",
|
||||
"http 1.4.0",
|
||||
"opentelemetry",
|
||||
"opentelemetry_sdk",
|
||||
"pretty_assertions",
|
||||
"rand 0.9.3",
|
||||
"rcgen",
|
||||
"reqwest 0.12.28",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
"system-configuration",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-opentelemetry",
|
||||
"tracing-subscriber",
|
||||
"windows-sys 0.52.0",
|
||||
"zstd 0.13.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2452,11 +2434,11 @@ dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"clap",
|
||||
"codex-client",
|
||||
"codex-cloud-tasks-client",
|
||||
"codex-cloud-tasks-mock-client",
|
||||
"codex-core",
|
||||
"codex-git-utils",
|
||||
"codex-http-client",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-tui",
|
||||
@@ -2526,6 +2508,7 @@ dependencies = [
|
||||
"codex-utils-cargo-bin",
|
||||
"pretty_assertions",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
@@ -2660,12 +2643,14 @@ dependencies = [
|
||||
"codex-exec-server",
|
||||
"codex-execpolicy",
|
||||
"codex-extension-api",
|
||||
"codex-extension-items",
|
||||
"codex-features",
|
||||
"codex-feedback",
|
||||
"codex-file-system",
|
||||
"codex-git-utils",
|
||||
"codex-home",
|
||||
"codex-hooks",
|
||||
"codex-http-client",
|
||||
"codex-image-generation-extension",
|
||||
"codex-install-context",
|
||||
"codex-login",
|
||||
@@ -2915,9 +2900,9 @@ dependencies = [
|
||||
"bytes",
|
||||
"clatter",
|
||||
"codex-api",
|
||||
"codex-client",
|
||||
"codex-exec-server-protocol",
|
||||
"codex-file-system",
|
||||
"codex-http-client",
|
||||
"codex-network-proxy",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
@@ -3031,6 +3016,18 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-extension-items"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-utils-absolute-path",
|
||||
"pretty_assertions",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"ts-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-external-agent-migration"
|
||||
version = "0.0.0"
|
||||
@@ -3213,6 +3210,37 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-http-client"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"codex-utils-cargo-bin",
|
||||
"codex-utils-rustls-provider",
|
||||
"futures",
|
||||
"http 1.4.0",
|
||||
"opentelemetry",
|
||||
"opentelemetry_sdk",
|
||||
"pretty_assertions",
|
||||
"rcgen",
|
||||
"reqwest 0.12.28",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
"system-configuration",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-opentelemetry",
|
||||
"tracing-subscriber",
|
||||
"windows-sys 0.52.0",
|
||||
"zstd 0.13.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-image-generation-extension"
|
||||
version = "0.0.0"
|
||||
@@ -3222,6 +3250,7 @@ dependencies = [
|
||||
"codex-core",
|
||||
"codex-exec-server",
|
||||
"codex-extension-api",
|
||||
"codex-extension-items",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-model-provider-info",
|
||||
@@ -3264,6 +3293,7 @@ dependencies = [
|
||||
"clap",
|
||||
"codex-core",
|
||||
"codex-install-context",
|
||||
"codex-network-proxy",
|
||||
"codex-process-hardening",
|
||||
"codex-protocol",
|
||||
"codex-sandboxing",
|
||||
@@ -3303,8 +3333,8 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"codex-agent-identity",
|
||||
"codex-client",
|
||||
"codex-config",
|
||||
"codex-http-client",
|
||||
"codex-keyring-store",
|
||||
"codex-model-provider-info",
|
||||
"codex-otel",
|
||||
@@ -3518,8 +3548,8 @@ dependencies = [
|
||||
"codex-agent-identity",
|
||||
"codex-api",
|
||||
"codex-aws-auth",
|
||||
"codex-client",
|
||||
"codex-feedback",
|
||||
"codex-http-client",
|
||||
"codex-login",
|
||||
"codex-model-provider-info",
|
||||
"codex-models-manager",
|
||||
@@ -3700,6 +3730,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"codex-async-utils",
|
||||
"codex-execpolicy",
|
||||
"codex-extension-items",
|
||||
"codex-network-proxy",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-image",
|
||||
@@ -4057,6 +4088,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-code-mode",
|
||||
"codex-connectors",
|
||||
"codex-extension-items",
|
||||
"codex-features",
|
||||
"codex-file-system",
|
||||
"codex-protocol",
|
||||
@@ -4821,9 +4853,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",
|
||||
]
|
||||
|
||||
@@ -41,6 +41,7 @@ members = [
|
||||
"core-plugins",
|
||||
"core-skills",
|
||||
"hooks",
|
||||
"http-client",
|
||||
"secrets",
|
||||
"exec",
|
||||
"file-system",
|
||||
@@ -53,6 +54,7 @@ members = [
|
||||
"ext/goal",
|
||||
"ext/guardian",
|
||||
"ext/image-generation",
|
||||
"ext/items",
|
||||
"ext/memories",
|
||||
"ext/mcp",
|
||||
"ext/skills",
|
||||
@@ -164,6 +166,7 @@ codex-cloud-tasks-mock-client = { path = "cloud-tasks-mock-client" }
|
||||
codex-code-mode = { path = "code-mode" }
|
||||
codex-code-mode-protocol = { path = "code-mode-protocol" }
|
||||
codex-home = { path = "codex-home" }
|
||||
codex-http-client = { path = "http-client" }
|
||||
codex-config = { path = "config" }
|
||||
codex-connectors = { path = "connectors" }
|
||||
codex-connectors-extension = { path = "ext/connectors" }
|
||||
@@ -178,6 +181,7 @@ codex-exec-server-protocol = { path = "exec-server-protocol" }
|
||||
codex-exec-server = { path = "exec-server" }
|
||||
codex-execpolicy = { path = "execpolicy" }
|
||||
codex-extension-api = { path = "ext/extension-api" }
|
||||
codex-extension-items = { path = "ext/items" }
|
||||
codex-goal-extension = { path = "ext/goal" }
|
||||
codex-guardian = { path = "ext/guardian" }
|
||||
codex-image-generation-extension = { path = "ext/image-generation" }
|
||||
|
||||
@@ -103,6 +103,7 @@ use codex_app_server_protocol::GuardianApprovalReview;
|
||||
use codex_app_server_protocol::GuardianApprovalReviewAction;
|
||||
use codex_app_server_protocol::GuardianApprovalReviewStatus;
|
||||
use codex_app_server_protocol::GuardianCommandSource as AppServerGuardianCommandSource;
|
||||
use codex_app_server_protocol::ImageGenerationItem;
|
||||
use codex_app_server_protocol::InitializeCapabilities;
|
||||
use codex_app_server_protocol::InitializeParams;
|
||||
use codex_app_server_protocol::ItemCompletedNotification;
|
||||
@@ -4405,13 +4406,13 @@ async fn turn_event_counts_completed_tool_items() {
|
||||
query: "codex".to_string(),
|
||||
action: None,
|
||||
},
|
||||
ThreadItem::ImageGeneration {
|
||||
ThreadItem::ImageGeneration(ImageGenerationItem {
|
||||
id: "image-1".to_string(),
|
||||
status: "completed".to_string(),
|
||||
revised_prompt: None,
|
||||
result: "ok".to_string(),
|
||||
saved_path: None,
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
for item in completed_tool_items {
|
||||
|
||||
@@ -403,7 +403,7 @@ impl TurnToolCounts {
|
||||
self.subagent_tool_call += 1;
|
||||
}
|
||||
ThreadItem::WebSearch { .. } => self.web_search += 1,
|
||||
ThreadItem::ImageGeneration { .. } => self.image_generation += 1,
|
||||
ThreadItem::ImageGeneration(_) => self.image_generation += 1,
|
||||
ThreadItem::UserMessage { .. }
|
||||
| ThreadItem::HookPrompt { .. }
|
||||
| ThreadItem::AgentMessage { .. }
|
||||
@@ -1733,8 +1733,8 @@ fn tracked_tool_item_id(item: &ThreadItem) -> Option<&str> {
|
||||
| ThreadItem::McpToolCall { id, .. }
|
||||
| ThreadItem::DynamicToolCall { id, .. }
|
||||
| ThreadItem::CollabAgentToolCall { id, .. }
|
||||
| ThreadItem::WebSearch { id, .. }
|
||||
| ThreadItem::ImageGeneration { id, .. } => Some(id),
|
||||
| ThreadItem::WebSearch { id, .. } => Some(id),
|
||||
ThreadItem::ImageGeneration(item) => Some(&item.id),
|
||||
ThreadItem::UserMessage { .. }
|
||||
| ThreadItem::HookPrompt { .. }
|
||||
| ThreadItem::AgentMessage { .. }
|
||||
@@ -2057,18 +2057,12 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
|
||||
},
|
||||
}))
|
||||
}
|
||||
ThreadItem::ImageGeneration {
|
||||
id,
|
||||
status,
|
||||
revised_prompt,
|
||||
saved_path,
|
||||
..
|
||||
} => {
|
||||
let (terminal_status, failure_kind) = image_generation_outcome(status.as_str());
|
||||
ThreadItem::ImageGeneration(item) => {
|
||||
let (terminal_status, failure_kind) = image_generation_outcome(item.status.as_str());
|
||||
let base = tool_item_base(
|
||||
thread_id,
|
||||
turn_id,
|
||||
id.clone(),
|
||||
item.id.clone(),
|
||||
"image_generation".to_string(),
|
||||
ToolItemOutcome {
|
||||
terminal_status,
|
||||
@@ -2089,8 +2083,8 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
|
||||
event_type: "codex_image_generation_event",
|
||||
event_params: CodexImageGenerationEventParams {
|
||||
base,
|
||||
revised_prompt_present: revised_prompt.is_some(),
|
||||
saved_path_present: saved_path.is_some(),
|
||||
revised_prompt_present: item.revised_prompt.is_some(),
|
||||
saved_path_present: item.saved_path.is_some(),
|
||||
},
|
||||
},
|
||||
))
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ workspace = true
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
codex-experimental-api-macros = { workspace = true }
|
||||
codex-extension-items = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-shell-command = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
|
||||
@@ -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"
|
||||
@@ -1456,6 +1463,17 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"appBrand": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/LoginAppBrand"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"codexStreamlinedLogin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -1465,6 +1483,9 @@
|
||||
],
|
||||
"title": "ChatgptLoginAccountParamsType",
|
||||
"type": "string"
|
||||
},
|
||||
"useHostedLoginSuccessPage": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -1525,6 +1546,13 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"LoginAppBrand": {
|
||||
"enum": [
|
||||
"codex",
|
||||
"chatgpt"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"MarketplaceAddParams": {
|
||||
"properties": {
|
||||
"refName": {
|
||||
|
||||
@@ -536,6 +536,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Backend auth supplied as request headers.",
|
||||
"enum": [
|
||||
"headers"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Programmatic Codex auth backed by a registered Agent Identity.",
|
||||
"enum": [
|
||||
|
||||
+127
@@ -6728,6 +6728,7 @@
|
||||
"enum": [
|
||||
"auto",
|
||||
"prompt",
|
||||
"writes",
|
||||
"approve"
|
||||
],
|
||||
"type": "string"
|
||||
@@ -6950,6 +6951,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Backend auth supplied as request headers.",
|
||||
"enum": [
|
||||
"headers"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Programmatic Codex auth backed by a registered Agent Identity.",
|
||||
"enum": [
|
||||
@@ -8688,6 +8696,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"
|
||||
@@ -11496,6 +11511,17 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"appBrand": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/LoginAppBrand"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"codexStreamlinedLogin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -11505,6 +11531,9 @@
|
||||
],
|
||||
"title": "Chatgptv2::LoginAccountParamsType",
|
||||
"type": "string"
|
||||
},
|
||||
"useHostedLoginSuccessPage": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -11659,6 +11688,13 @@
|
||||
],
|
||||
"title": "LoginAccountResponse"
|
||||
},
|
||||
"LoginAppBrand": {
|
||||
"enum": [
|
||||
"codex",
|
||||
"chatgpt"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"LogoutAccountResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "LogoutAccountResponse",
|
||||
@@ -13416,6 +13452,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginInstallPolicySource": {
|
||||
"enum": [
|
||||
"WORKSPACE_SETTING",
|
||||
"IMPLICIT_CANONICAL_APP"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginInstallResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -14299,6 +14342,16 @@
|
||||
"installPolicy": {
|
||||
"$ref": "#/definitions/v2/PluginInstallPolicy"
|
||||
},
|
||||
"installPolicySource": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/PluginInstallPolicySource"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"installed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -14533,11 +14586,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": [
|
||||
@@ -14545,6 +14665,13 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RateLimitResetType": {
|
||||
"enum": [
|
||||
"codexRateLimits",
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"RateLimitSnapshot": {
|
||||
"properties": {
|
||||
"credits": {
|
||||
|
||||
+127
@@ -854,6 +854,7 @@
|
||||
"enum": [
|
||||
"auto",
|
||||
"prompt",
|
||||
"writes",
|
||||
"approve"
|
||||
],
|
||||
"type": "string"
|
||||
@@ -1076,6 +1077,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Backend auth supplied as request headers.",
|
||||
"enum": [
|
||||
"headers"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Programmatic Codex auth backed by a registered Agent Identity.",
|
||||
"enum": [
|
||||
@@ -4928,6 +4936,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"
|
||||
@@ -7900,6 +7915,17 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"appBrand": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/LoginAppBrand"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"codexStreamlinedLogin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -7909,6 +7935,9 @@
|
||||
],
|
||||
"title": "Chatgptv2::LoginAccountParamsType",
|
||||
"type": "string"
|
||||
},
|
||||
"useHostedLoginSuccessPage": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -8063,6 +8092,13 @@
|
||||
],
|
||||
"title": "LoginAccountResponse"
|
||||
},
|
||||
"LoginAppBrand": {
|
||||
"enum": [
|
||||
"codex",
|
||||
"chatgpt"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"LogoutAccountResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "LogoutAccountResponse",
|
||||
@@ -9820,6 +9856,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginInstallPolicySource": {
|
||||
"enum": [
|
||||
"WORKSPACE_SETTING",
|
||||
"IMPLICIT_CANONICAL_APP"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginInstallResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -10703,6 +10746,16 @@
|
||||
"installPolicy": {
|
||||
"$ref": "#/definitions/PluginInstallPolicy"
|
||||
},
|
||||
"installPolicySource": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PluginInstallPolicySource"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"installed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -10937,11 +10990,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": [
|
||||
@@ -10949,6 +11069,13 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RateLimitResetType": {
|
||||
"enum": [
|
||||
"codexRateLimits",
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"RateLimitSnapshot": {
|
||||
"properties": {
|
||||
"credits": {
|
||||
|
||||
@@ -25,6 +25,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Backend auth supplied as request headers.",
|
||||
"enum": [
|
||||
"headers"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Programmatic Codex auth backed by a registered Agent Identity.",
|
||||
"enum": [
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
"enum": [
|
||||
"auto",
|
||||
"prompt",
|
||||
"writes",
|
||||
"approve"
|
||||
],
|
||||
"type": "string"
|
||||
|
||||
+7
@@ -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"
|
||||
|
||||
+74
@@ -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": {
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"LoginAppBrand": {
|
||||
"enum": [
|
||||
"codex",
|
||||
"chatgpt"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
@@ -23,6 +32,17 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"appBrand": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/LoginAppBrand"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"codexStreamlinedLogin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -32,6 +52,9 @@
|
||||
],
|
||||
"title": "Chatgptv2::LoginAccountParamsType",
|
||||
"type": "string"
|
||||
},
|
||||
"useHostedLoginSuccessPage": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -63,6 +63,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginInstallPolicySource": {
|
||||
"enum": [
|
||||
"WORKSPACE_SETTING",
|
||||
"IMPLICIT_CANONICAL_APP"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginInterface": {
|
||||
"properties": {
|
||||
"brandColor": {
|
||||
@@ -489,6 +496,16 @@
|
||||
"installPolicy": {
|
||||
"$ref": "#/definitions/PluginInstallPolicy"
|
||||
},
|
||||
"installPolicySource": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PluginInstallPolicySource"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"installed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -63,6 +63,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginInstallPolicySource": {
|
||||
"enum": [
|
||||
"WORKSPACE_SETTING",
|
||||
"IMPLICIT_CANONICAL_APP"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginInterface": {
|
||||
"properties": {
|
||||
"brandColor": {
|
||||
@@ -489,6 +496,16 @@
|
||||
"installPolicy": {
|
||||
"$ref": "#/definitions/PluginInstallPolicy"
|
||||
},
|
||||
"installPolicySource": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PluginInstallPolicySource"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"installed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -242,6 +242,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginInstallPolicySource": {
|
||||
"enum": [
|
||||
"WORKSPACE_SETTING",
|
||||
"IMPLICIT_CANONICAL_APP"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginInterface": {
|
||||
"properties": {
|
||||
"brandColor": {
|
||||
@@ -629,6 +636,16 @@
|
||||
"installPolicy": {
|
||||
"$ref": "#/definitions/PluginInstallPolicy"
|
||||
},
|
||||
"installPolicySource": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PluginInstallPolicySource"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"installed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -37,6 +37,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginInstallPolicySource": {
|
||||
"enum": [
|
||||
"WORKSPACE_SETTING",
|
||||
"IMPLICIT_CANONICAL_APP"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginInterface": {
|
||||
"properties": {
|
||||
"brandColor": {
|
||||
@@ -445,6 +452,16 @@
|
||||
"installPolicy": {
|
||||
"$ref": "#/definitions/PluginInstallPolicy"
|
||||
},
|
||||
"installPolicySource": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PluginInstallPolicySource"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"installed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
+1
-1
@@ -5,4 +5,4 @@
|
||||
/**
|
||||
* Authentication mode for OpenAI-backed providers.
|
||||
*/
|
||||
export type AuthMode = "apikey" | "chatgpt" | "chatgptAuthTokens" | "agentIdentity" | "personalAccessToken" | "bedrockApiKey";
|
||||
export type AuthMode = "apikey" | "chatgpt" | "chatgptAuthTokens" | "headers" | "agentIdentity" | "personalAccessToken" | "bedrockApiKey";
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AbsolutePathBuf } from "./AbsolutePathBuf";
|
||||
|
||||
export type ImageGenerationItem = { id: string, status: string, revisedPrompt: string | null, result: string, savedPath?: AbsolutePathBuf, };
|
||||
@@ -37,6 +37,7 @@ export type { GitDiffToRemoteParams } from "./GitDiffToRemoteParams";
|
||||
export type { GitDiffToRemoteResponse } from "./GitDiffToRemoteResponse";
|
||||
export type { GitSha } from "./GitSha";
|
||||
export type { ImageDetail } from "./ImageDetail";
|
||||
export type { ImageGenerationItem } from "./ImageGenerationItem";
|
||||
export type { InitializeCapabilities } from "./InitializeCapabilities";
|
||||
export type { InitializeParams } from "./InitializeParams";
|
||||
export type { InitializeResponse } from "./InitializeResponse";
|
||||
|
||||
@@ -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 AppToolApproval = "auto" | "prompt" | "approve";
|
||||
export type AppToolApproval = "auto" | "prompt" | "writes" | "approve";
|
||||
|
||||
Generated
+6
-1
@@ -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, };
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { LoginAppBrand } from "./LoginAppBrand";
|
||||
|
||||
export type LoginAccountParams = { "type": "apiKey", apiKey: string, } | { "type": "chatgpt", codexStreamlinedLogin?: boolean, } | { "type": "chatgptDeviceCode" } | { "type": "chatgptAuthTokens",
|
||||
export type LoginAccountParams = { "type": "apiKey", apiKey: string, } | { "type": "chatgpt", codexStreamlinedLogin?: boolean, useHostedLoginSuccessPage?: boolean, appBrand?: LoginAppBrand | null, } | { "type": "chatgptDeviceCode" } | { "type": "chatgptAuthTokens",
|
||||
/**
|
||||
* Access token (JWT) supplied by the client.
|
||||
* This token is used for backend API requests and email extraction.
|
||||
|
||||
@@ -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 LoginAppBrand = "codex" | "chatgpt";
|
||||
+5
@@ -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 PluginInstallPolicySource = "WORKSPACE_SETTING" | "IMPLICIT_CANONICAL_APP";
|
||||
@@ -4,6 +4,7 @@
|
||||
import type { PluginAuthPolicy } from "./PluginAuthPolicy";
|
||||
import type { PluginAvailability } from "./PluginAvailability";
|
||||
import type { PluginInstallPolicy } from "./PluginInstallPolicy";
|
||||
import type { PluginInstallPolicySource } from "./PluginInstallPolicySource";
|
||||
import type { PluginInterface } from "./PluginInterface";
|
||||
import type { PluginShareContext } from "./PluginShareContext";
|
||||
import type { PluginSource } from "./PluginSource";
|
||||
@@ -24,7 +25,7 @@ localVersion: string | null, name: string,
|
||||
/**
|
||||
* Remote sharing context associated with this plugin when available.
|
||||
*/
|
||||
shareContext: PluginShareContext | null, source: PluginSource, installed: boolean, enabled: boolean, installPolicy: PluginInstallPolicy, authPolicy: PluginAuthPolicy,
|
||||
shareContext: PluginShareContext | null, source: PluginSource, installed: boolean, enabled: boolean, installPolicy: PluginInstallPolicy, installPolicySource: PluginInstallPolicySource | null, authPolicy: PluginAuthPolicy,
|
||||
/**
|
||||
* Availability state for installing and using the plugin.
|
||||
*/
|
||||
|
||||
@@ -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, };
|
||||
+5
@@ -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";
|
||||
+10
-1
@@ -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<RateLimitResetCredit> | null, };
|
||||
|
||||
@@ -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";
|
||||
@@ -1,7 +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 { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
import type { ImageGenerationItem } from "../ImageGenerationItem";
|
||||
import type { LegacyAppPathString } from "../LegacyAppPathString";
|
||||
import type { MessagePhase } from "../MessagePhase";
|
||||
import type { ReasoningEffort } from "../ReasoningEffort";
|
||||
@@ -105,4 +105,4 @@ reasoningEffort: ReasoningEffort | null,
|
||||
/**
|
||||
* Last known status of the target agents, when available.
|
||||
*/
|
||||
agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "subAgentActivity", id: string, kind: SubAgentActivityKind, agentThreadId: string, agentPath: string, } | { "type": "webSearch", id: string, query: string, action: WebSearchAction | null, } | { "type": "imageView", id: string, path: LegacyAppPathString, } | { "type": "sleep", id: string, durationMs: number, } | { "type": "imageGeneration", id: string, status: string, revisedPrompt: string | null, result: string, savedPath?: AbsolutePathBuf, } | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, };
|
||||
agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "subAgentActivity", id: string, kind: SubAgentActivityKind, agentThreadId: string, agentPath: string, } | { "type": "webSearch", id: string, query: string, action: WebSearchAction | null, } | { "type": "imageView", id: string, path: LegacyAppPathString, } | { "type": "sleep", id: string, durationMs: number, } | { "type": "imageGeneration" } & ImageGenerationItem | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, };
|
||||
|
||||
@@ -194,6 +194,7 @@ export type { ListMcpServerStatusParams } from "./ListMcpServerStatusParams";
|
||||
export type { ListMcpServerStatusResponse } from "./ListMcpServerStatusResponse";
|
||||
export type { LoginAccountParams } from "./LoginAccountParams";
|
||||
export type { LoginAccountResponse } from "./LoginAccountResponse";
|
||||
export type { LoginAppBrand } from "./LoginAppBrand";
|
||||
export type { LogoutAccountResponse } from "./LogoutAccountResponse";
|
||||
export type { ManagedHooksRequirements } from "./ManagedHooksRequirements";
|
||||
export type { MarketplaceAddParams } from "./MarketplaceAddParams";
|
||||
@@ -294,6 +295,7 @@ export type { PluginDetail } from "./PluginDetail";
|
||||
export type { PluginHookSummary } from "./PluginHookSummary";
|
||||
export type { PluginInstallParams } from "./PluginInstallParams";
|
||||
export type { PluginInstallPolicy } from "./PluginInstallPolicy";
|
||||
export type { PluginInstallPolicySource } from "./PluginInstallPolicySource";
|
||||
export type { PluginInstallResponse } from "./PluginInstallResponse";
|
||||
export type { PluginInstalledParams } from "./PluginInstalledParams";
|
||||
export type { PluginInstalledResponse } from "./PluginInstalledResponse";
|
||||
@@ -335,7 +337,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";
|
||||
|
||||
@@ -31,6 +31,11 @@ pub enum AuthMode {
|
||||
#[ts(rename = "chatgptAuthTokens")]
|
||||
#[strum(serialize = "chatgptAuthTokens")]
|
||||
ChatgptAuthTokens,
|
||||
/// Backend auth supplied as request headers.
|
||||
#[serde(rename = "headers")]
|
||||
#[ts(rename = "headers")]
|
||||
#[strum(serialize = "headers")]
|
||||
Headers,
|
||||
/// Programmatic Codex auth backed by a registered Agent Identity.
|
||||
#[serde(rename = "agentIdentity")]
|
||||
#[ts(rename = "agentIdentity")]
|
||||
@@ -53,7 +58,7 @@ impl AuthMode {
|
||||
pub fn has_chatgpt_account(self) -> bool {
|
||||
match self {
|
||||
Self::Chatgpt | Self::ChatgptAuthTokens | Self::PersonalAccessToken => true,
|
||||
Self::ApiKey | Self::AgentIdentity | Self::BedrockApiKey => false,
|
||||
Self::ApiKey | Self::Headers | Self::AgentIdentity | Self::BedrockApiKey => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +67,7 @@ impl AuthMode {
|
||||
match self {
|
||||
Self::Chatgpt
|
||||
| Self::ChatgptAuthTokens
|
||||
| Self::Headers
|
||||
| Self::AgentIdentity
|
||||
| Self::PersonalAccessToken => true,
|
||||
Self::ApiKey | Self::BedrockApiKey => false,
|
||||
@@ -2721,7 +2727,9 @@ mod tests {
|
||||
let request = ClientRequest::LoginAccount {
|
||||
request_id: RequestId::Integer(3),
|
||||
params: v2::LoginAccountParams::Chatgpt {
|
||||
app_brand: None,
|
||||
codex_streamlined_login: false,
|
||||
use_hosted_login_success_page: false,
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -2729,7 +2737,8 @@ mod tests {
|
||||
"method": "account/login/start",
|
||||
"id": 3,
|
||||
"params": {
|
||||
"type": "chatgpt"
|
||||
"type": "chatgpt",
|
||||
"appBrand": null
|
||||
}
|
||||
}),
|
||||
serde_json::to_value(&request)?,
|
||||
@@ -2742,7 +2751,9 @@ mod tests {
|
||||
let request = ClientRequest::LoginAccount {
|
||||
request_id: RequestId::Integer(3),
|
||||
params: v2::LoginAccountParams::Chatgpt {
|
||||
app_brand: None,
|
||||
codex_streamlined_login: true,
|
||||
use_hosted_login_success_page: false,
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -2751,6 +2762,7 @@ mod tests {
|
||||
"id": 3,
|
||||
"params": {
|
||||
"type": "chatgpt",
|
||||
"appBrand": null,
|
||||
"codexStreamlinedLogin": true
|
||||
}
|
||||
}),
|
||||
@@ -2759,6 +2771,32 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_account_login_chatgpt_with_hosted_success_page() -> Result<()> {
|
||||
let request = ClientRequest::LoginAccount {
|
||||
request_id: RequestId::Integer(3),
|
||||
params: v2::LoginAccountParams::Chatgpt {
|
||||
app_brand: Some(v2::LoginAppBrand::Chatgpt),
|
||||
codex_streamlined_login: true,
|
||||
use_hosted_login_success_page: true,
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
json!({
|
||||
"method": "account/login/start",
|
||||
"id": 3,
|
||||
"params": {
|
||||
"type": "chatgpt",
|
||||
"appBrand": "chatgpt",
|
||||
"codexStreamlinedLogin": true,
|
||||
"useHostedLoginSuccessPage": true
|
||||
}
|
||||
}),
|
||||
serde_json::to_value(&request)?,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_account_login_chatgpt_device_code() -> Result<()> {
|
||||
let request = ClientRequest::LoginAccount {
|
||||
@@ -3195,6 +3233,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()),
|
||||
@@ -3216,6 +3255,7 @@ mod tests {
|
||||
"params": {
|
||||
"threadId": "thr_123",
|
||||
"clientManagedHandoffs": true,
|
||||
"flushTranscriptTailOnSessionEnd": true,
|
||||
"codexResponsesAsItems": null,
|
||||
"codexResponseItemPrefix": null,
|
||||
"codexResponseHandoffPrefix": "silent context",
|
||||
@@ -3240,6 +3280,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,
|
||||
@@ -3261,6 +3302,7 @@ mod tests {
|
||||
"params": {
|
||||
"threadId": "thr_123",
|
||||
"clientManagedHandoffs": null,
|
||||
"flushTranscriptTailOnSessionEnd": null,
|
||||
"codexResponsesAsItems": null,
|
||||
"codexResponseItemPrefix": null,
|
||||
"codexResponseHandoffPrefix": null,
|
||||
@@ -3280,6 +3322,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,
|
||||
@@ -3301,6 +3344,7 @@ mod tests {
|
||||
"params": {
|
||||
"threadId": "thr_123",
|
||||
"clientManagedHandoffs": null,
|
||||
"flushTranscriptTailOnSessionEnd": null,
|
||||
"codexResponsesAsItems": null,
|
||||
"codexResponseItemPrefix": null,
|
||||
"codexResponseHandoffPrefix": null,
|
||||
@@ -3518,6 +3562,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,
|
||||
|
||||
@@ -22,6 +22,7 @@ use crate::protocol::v2::TurnItemsView;
|
||||
use crate::protocol::v2::TurnStatus;
|
||||
use crate::protocol::v2::UserInput;
|
||||
use crate::protocol::v2::WebSearchAction;
|
||||
use codex_extension_items::image_generation::ImageGenerationItem;
|
||||
use codex_protocol::items::parse_hook_prompt_message;
|
||||
use codex_protocol::models::MessagePhase;
|
||||
use codex_protocol::protocol::AgentReasoningEvent;
|
||||
@@ -592,7 +593,8 @@ impl ThreadHistoryBuilder {
|
||||
| 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::SubAgentActivity(_)
|
||||
| codex_protocol::items::TurnItem::Extension(_) => true,
|
||||
codex_protocol::items::TurnItem::UserMessage(_)
|
||||
| codex_protocol::items::TurnItem::HookPrompt(_)
|
||||
| codex_protocol::items::TurnItem::AgentMessage(_)
|
||||
@@ -827,24 +829,24 @@ impl ThreadHistoryBuilder {
|
||||
}
|
||||
|
||||
fn handle_image_generation_begin(&mut self, payload: &ImageGenerationBeginEvent) {
|
||||
let item = ThreadItem::ImageGeneration {
|
||||
let item = ThreadItem::ImageGeneration(ImageGenerationItem {
|
||||
id: payload.call_id.clone(),
|
||||
status: String::new(),
|
||||
revised_prompt: None,
|
||||
result: String::new(),
|
||||
saved_path: None,
|
||||
};
|
||||
});
|
||||
self.upsert_item_in_current_turn(item);
|
||||
}
|
||||
|
||||
fn handle_image_generation_end(&mut self, payload: &ImageGenerationEndEvent) {
|
||||
let item = ThreadItem::ImageGeneration {
|
||||
let item = ThreadItem::ImageGeneration(ImageGenerationItem {
|
||||
id: payload.call_id.clone(),
|
||||
status: payload.status.clone(),
|
||||
revised_prompt: payload.revised_prompt.clone(),
|
||||
result: payload.result.clone(),
|
||||
saved_path: payload.saved_path.clone(),
|
||||
};
|
||||
});
|
||||
self.upsert_item_in_current_turn(item);
|
||||
}
|
||||
|
||||
@@ -1548,6 +1550,7 @@ impl From<&PendingTurn> for Turn {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::protocol::v2::CommandExecutionSource;
|
||||
use codex_extension_items::ExtensionItem as CoreExtensionItem;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem;
|
||||
use codex_protocol::items::CommandExecutionItem as CoreCommandExecutionItem;
|
||||
@@ -1846,6 +1849,60 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuilds_extension_image_generation_item_from_persisted_completion() {
|
||||
let turn_id = "turn-1";
|
||||
let thread_id = ThreadId::new();
|
||||
let saved_path = test_path_buf("/tmp/image-1.png").abs();
|
||||
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: CoreTurnItem::Extension(CoreExtensionItem::ImageGeneration(
|
||||
ImageGenerationItem {
|
||||
id: "image-1".to_string(),
|
||||
status: "completed".to_string(),
|
||||
revised_prompt: Some("A blue square".to_string()),
|
||||
result: "cG5n".to_string(),
|
||||
saved_path: Some(saved_path.clone()),
|
||||
},
|
||||
)),
|
||||
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::<Vec<_>>();
|
||||
|
||||
let turns = build_turns_from_rollout_items(&items);
|
||||
|
||||
assert_eq!(
|
||||
turns[0].items,
|
||||
vec![ThreadItem::ImageGeneration(ImageGenerationItem {
|
||||
id: "image-1".to_string(),
|
||||
status: "completed".to_string(),
|
||||
revised_prompt: Some("A blue square".to_string()),
|
||||
result: "cG5n".to_string(),
|
||||
saved_path: Some(saved_path),
|
||||
})]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuilds_command_execution_item_from_persisted_completion() {
|
||||
let turn_id = "turn-1";
|
||||
@@ -2058,13 +2115,13 @@ mod tests {
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
},
|
||||
ThreadItem::ImageGeneration {
|
||||
ThreadItem::ImageGeneration(ImageGenerationItem {
|
||||
id: "ig_123".into(),
|
||||
status: "completed".into(),
|
||||
revised_prompt: Some("final prompt".into()),
|
||||
result: "Zm9v".into(),
|
||||
saved_path: Some(test_path_buf("/tmp/ig_123.png").abs()),
|
||||
},
|
||||
}),
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
@@ -78,6 +78,11 @@ pub enum LoginAccountParams {
|
||||
Chatgpt {
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
codex_streamlined_login: bool,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
use_hosted_login_success_page: bool,
|
||||
#[serde(default)]
|
||||
#[ts(optional = nullable)]
|
||||
app_brand: Option<LoginAppBrand>,
|
||||
},
|
||||
#[serde(rename = "chatgptDeviceCode")]
|
||||
#[ts(rename = "chatgptDeviceCode")]
|
||||
@@ -102,6 +107,16 @@ pub enum LoginAccountParams {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(rename_all = "lowercase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum LoginAppBrand {
|
||||
#[default]
|
||||
Codex,
|
||||
Chatgpt,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type")]
|
||||
@@ -284,6 +299,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<Vec<RateLimitResetCredit>>,
|
||||
}
|
||||
|
||||
#[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<i64>,
|
||||
/// Backend-provided display title for this credit, or `null` when unavailable.
|
||||
pub title: Option<String>,
|
||||
/// Backend-provided display description for this credit, or `null` when unavailable.
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[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 +354,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<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
|
||||
@@ -163,6 +163,7 @@ pub struct AnalyticsConfig {
|
||||
pub enum AppToolApproval {
|
||||
Auto,
|
||||
Prompt,
|
||||
Writes,
|
||||
Approve,
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ 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_extension_items::ExtensionItem;
|
||||
pub use codex_extension_items::image_generation::ImageGenerationItem;
|
||||
use codex_protocol::approvals::GuardianAssessmentAction as CoreGuardianAssessmentAction;
|
||||
use codex_protocol::approvals::GuardianAssessmentDecisionSource as CoreGuardianAssessmentDecisionSource;
|
||||
use codex_protocol::approvals::GuardianCommandSource as CoreGuardianCommandSource;
|
||||
@@ -246,7 +248,10 @@ pub enum ThreadItem {
|
||||
#[ts(rename_all = "camelCase")]
|
||||
/// EXPERIMENTAL - proposed plan item content. The completed plan item is
|
||||
/// authoritative and may not match the concatenation of `PlanDelta` text.
|
||||
Plan { id: String, text: String },
|
||||
Plan {
|
||||
id: String,
|
||||
text: String,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
Reasoning {
|
||||
@@ -373,26 +378,24 @@ pub enum ThreadItem {
|
||||
#[ts(type = "number")]
|
||||
duration_ms: u64,
|
||||
},
|
||||
ImageGeneration(ImageGenerationItem),
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
ImageGeneration {
|
||||
EnteredReviewMode {
|
||||
id: String,
|
||||
status: String,
|
||||
revised_prompt: Option<String>,
|
||||
result: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
saved_path: Option<AbsolutePathBuf>,
|
||||
review: String,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
EnteredReviewMode { id: String, review: String },
|
||||
ExitedReviewMode {
|
||||
id: String,
|
||||
review: String,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
ExitedReviewMode { id: String, review: String },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
ContextCompaction { id: String },
|
||||
ContextCompaction {
|
||||
id: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
@@ -432,10 +435,10 @@ impl ThreadItem {
|
||||
| ThreadItem::WebSearch { id, .. }
|
||||
| ThreadItem::ImageView { id, .. }
|
||||
| ThreadItem::Sleep { id, .. }
|
||||
| ThreadItem::ImageGeneration { id, .. }
|
||||
| ThreadItem::EnteredReviewMode { id, .. }
|
||||
| ThreadItem::ExitedReviewMode { id, .. }
|
||||
| ThreadItem::ContextCompaction { id, .. } => id,
|
||||
ThreadItem::ImageGeneration(item) => &item.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -931,13 +934,18 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
id: sleep.id,
|
||||
duration_ms: sleep.duration_ms,
|
||||
},
|
||||
CoreTurnItem::ImageGeneration(image) => ThreadItem::ImageGeneration {
|
||||
id: image.id,
|
||||
status: image.status,
|
||||
revised_prompt: image.revised_prompt,
|
||||
result: image.result,
|
||||
saved_path: image.saved_path,
|
||||
CoreTurnItem::Extension(extension) => match extension {
|
||||
ExtensionItem::ImageGeneration(item) => ThreadItem::ImageGeneration(item),
|
||||
},
|
||||
CoreTurnItem::ImageGeneration(image) => {
|
||||
ThreadItem::ImageGeneration(ImageGenerationItem {
|
||||
id: image.id,
|
||||
status: image.status,
|
||||
revised_prompt: image.revised_prompt,
|
||||
result: image.result,
|
||||
saved_path: image.saved_path,
|
||||
})
|
||||
}
|
||||
CoreTurnItem::FileChange(file_change) => ThreadItem::FileChange {
|
||||
id: file_change.id,
|
||||
changes: convert_patch_changes(&file_change.changes),
|
||||
|
||||
@@ -567,6 +567,17 @@ pub enum PluginInstallPolicy {
|
||||
InstalledByDefault,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum PluginInstallPolicySource {
|
||||
#[serde(rename = "WORKSPACE_SETTING")]
|
||||
#[ts(rename = "WORKSPACE_SETTING")]
|
||||
WorkspaceSetting,
|
||||
#[serde(rename = "IMPLICIT_CANONICAL_APP")]
|
||||
#[ts(rename = "IMPLICIT_CANONICAL_APP")]
|
||||
ImplicitCanonicalApp,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum PluginAuthPolicy {
|
||||
@@ -613,6 +624,7 @@ pub struct PluginSummary {
|
||||
pub installed: bool,
|
||||
pub enabled: bool,
|
||||
pub install_policy: PluginInstallPolicy,
|
||||
pub install_policy_source: Option<PluginInstallPolicySource>,
|
||||
pub auth_policy: PluginAuthPolicy,
|
||||
/// Availability state for installing and using the plugin.
|
||||
#[serde(default)]
|
||||
|
||||
@@ -70,6 +70,10 @@ pub struct ThreadRealtimeStartParams {
|
||||
/// them automatically. Defaults to false.
|
||||
#[ts(optional = nullable)]
|
||||
pub client_managed_handoffs: Option<bool>,
|
||||
/// 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<bool>,
|
||||
/// Sends automatic Codex responses as realtime conversation items instead of handoff appends.
|
||||
#[ts(optional = nullable)]
|
||||
pub codex_responses_as_items: Option<bool>,
|
||||
|
||||
@@ -3650,6 +3650,7 @@ fn plugin_share_list_response_serializes_share_items() {
|
||||
installed: false,
|
||||
enabled: false,
|
||||
install_policy: PluginInstallPolicy::Available,
|
||||
install_policy_source: Some(PluginInstallPolicySource::WorkspaceSetting),
|
||||
auth_policy: PluginAuthPolicy::OnUse,
|
||||
availability: PluginAvailability::Available,
|
||||
interface: None,
|
||||
@@ -3672,6 +3673,7 @@ fn plugin_share_list_response_serializes_share_items() {
|
||||
"installed": false,
|
||||
"enabled": false,
|
||||
"installPolicy": "AVAILABLE",
|
||||
"installPolicySource": "WORKSPACE_SETTING",
|
||||
"authPolicy": "ON_USE",
|
||||
"availability": "AVAILABLE",
|
||||
"interface": null,
|
||||
|
||||
@@ -685,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,
|
||||
}
|
||||
@@ -705,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,
|
||||
}
|
||||
|
||||
@@ -1722,7 +1722,9 @@ impl CodexClient {
|
||||
let request = ClientRequest::LoginAccount {
|
||||
request_id: request_id.clone(),
|
||||
params: codex_app_server_protocol::LoginAccountParams::Chatgpt {
|
||||
app_brand: None,
|
||||
codex_streamlined_login: false,
|
||||
use_hosted_login_success_page: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -213,8 +213,8 @@ 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, 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/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata, nullable remote install-policy provenance in `installPolicySource` (`WORKSPACE_SETTING` or `IMPLICIT_CANONICAL_APP`), 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. Remote rows include nullable `installPolicySource`; local rows return `null`. 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.
|
||||
- `skills/changed` — notification emitted when watched local skill files change.
|
||||
@@ -1301,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.
|
||||
|
||||
@@ -1879,7 +1879,9 @@ review when allowed by configuration requirements.
|
||||
|
||||
Use `apps._default.default_tools_approval_mode` to set the approval mode for
|
||||
tools without a per-app or per-tool override. Supported values are `"auto"`,
|
||||
`"prompt"`, and `"approve"`. Tool-level `approval_mode` takes precedence over
|
||||
`"prompt"`, `"writes"`, and `"approve"`. The `"writes"` mode prompts for tools
|
||||
that do not advertise `readOnlyHint = true` and skips declared read-only tools.
|
||||
Tool-level `approval_mode` takes precedence over
|
||||
the per-app `default_tools_approval_mode`, which takes precedence over the
|
||||
`apps._default` value. Managed tool requirements take precedence over all of
|
||||
these settings. When none are configured, the mode defaults to `"auto"`.
|
||||
@@ -1929,8 +1931,8 @@ 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.
|
||||
@@ -1993,6 +1995,11 @@ Field notes:
|
||||
{ "id": 3, "result": { "type": "chatgpt", "loginId": "<uuid>", "authUrl": "https://chatgpt.com/…&redirect_uri=http%3A%2F%2Flocalhost%3A<port>%2Fauth%2Fcallback" } }
|
||||
```
|
||||
2. Open `authUrl` in a browser; the app-server hosts the local callback.
|
||||
By default, a successful callback redirects to the local success page. Clients may set
|
||||
`useHostedLoginSuccessPage: true` to redirect successful callbacks that do not require
|
||||
organization setup to the hosted Codex success page instead. When hosted login success is
|
||||
enabled, clients may set `appBrand` to `"codex"` or `"chatgpt"` to select the matching hosted
|
||||
page artwork; omitted or `null` values default to `"codex"`.
|
||||
3. Wait for notifications:
|
||||
```json
|
||||
{ "method": "account/login/completed", "params": { "loginId": "<uuid>", "success": true, "error": null } }
|
||||
@@ -2032,7 +2039,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": { … } } }
|
||||
```
|
||||
|
||||
@@ -2043,23 +2073,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)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ pub(crate) fn auth_mode_to_api(auth_mode: AuthMode) -> ApiAuthMode {
|
||||
AuthMode::ApiKey => ApiAuthMode::ApiKey,
|
||||
AuthMode::Chatgpt => ApiAuthMode::Chatgpt,
|
||||
AuthMode::ChatgptAuthTokens => ApiAuthMode::ChatgptAuthTokens,
|
||||
AuthMode::Headers => ApiAuthMode::Headers,
|
||||
AuthMode::AgentIdentity => ApiAuthMode::AgentIdentity,
|
||||
AuthMode::PersonalAccessToken => ApiAuthMode::PersonalAccessToken,
|
||||
AuthMode::BedrockApiKey => ApiAuthMode::BedrockApiKey,
|
||||
|
||||
@@ -22,7 +22,6 @@ use codex_app_server_protocol::CommandExecutionSource;
|
||||
use codex_app_server_protocol::CommandExecutionStatus;
|
||||
use codex_app_server_protocol::DeprecationNoticeNotification;
|
||||
use codex_app_server_protocol::DynamicToolCallParams;
|
||||
use codex_app_server_protocol::DynamicToolCallStatus;
|
||||
use codex_app_server_protocol::ErrorNotification;
|
||||
use codex_app_server_protocol::ExecPolicyAmendment as V2ExecPolicyAmendment;
|
||||
use codex_app_server_protocol::FileChangeApprovalDecision;
|
||||
@@ -90,6 +89,8 @@ use codex_core::ThreadManager;
|
||||
use codex_core::review_format::format_review_findings_block;
|
||||
use codex_core::review_prompts;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::items::CollabAgentTool as CoreCollabAgentTool;
|
||||
use codex_protocol::items::TurnItem as CoreTurnItem;
|
||||
use codex_protocol::items::parse_hook_prompt_message;
|
||||
use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile;
|
||||
use codex_protocol::plan_tool::UpdatePlanArgs;
|
||||
@@ -822,51 +823,8 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
on_request_permissions_response(pending_response, conversation, thread_state).await;
|
||||
});
|
||||
}
|
||||
EventMsg::DynamicToolCallRequest(request) => {
|
||||
let call_id = request.call_id;
|
||||
let turn_id = request.turn_id;
|
||||
let namespace = request.namespace;
|
||||
let tool = request.tool;
|
||||
let arguments = request.arguments;
|
||||
let item = ThreadItem::DynamicToolCall {
|
||||
id: call_id.clone(),
|
||||
namespace: namespace.clone(),
|
||||
tool: tool.clone(),
|
||||
arguments: arguments.clone(),
|
||||
status: DynamicToolCallStatus::InProgress,
|
||||
content_items: None,
|
||||
success: None,
|
||||
duration_ms: None,
|
||||
};
|
||||
let notification = ItemStartedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: turn_id.clone(),
|
||||
started_at_ms: request.started_at_ms,
|
||||
item,
|
||||
};
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ItemStarted(notification))
|
||||
.await;
|
||||
let params = DynamicToolCallParams {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: turn_id.clone(),
|
||||
call_id: call_id.clone(),
|
||||
namespace,
|
||||
tool: tool.clone(),
|
||||
arguments: arguments.clone(),
|
||||
};
|
||||
let (_pending_request_id, rx) = outgoing
|
||||
.send_request(ServerRequestPayload::DynamicToolCall(params))
|
||||
.await;
|
||||
tokio::spawn(async move {
|
||||
crate::dynamic_tools::on_call_response(call_id, rx, conversation).await;
|
||||
});
|
||||
}
|
||||
EventMsg::McpToolCallBegin(_) | EventMsg::McpToolCallEnd(_) => {
|
||||
// Deprecated MCP tool-call events are still fanned out for legacy clients.
|
||||
// App-server v2 receives the canonical TurnItem::McpToolCall lifecycle instead.
|
||||
}
|
||||
msg @ (EventMsg::DynamicToolCallResponse(_)
|
||||
EventMsg::DynamicToolCallRequest(_)
|
||||
| EventMsg::DynamicToolCallResponse(_)
|
||||
| EventMsg::CollabAgentSpawnBegin(_)
|
||||
| EventMsg::CollabAgentSpawnEnd(_)
|
||||
| EventMsg::CollabAgentInteractionBegin(_)
|
||||
@@ -874,9 +832,23 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
| EventMsg::CollabWaitingBegin(_)
|
||||
| EventMsg::CollabWaitingEnd(_)
|
||||
| EventMsg::CollabCloseBegin(_)
|
||||
| EventMsg::CollabCloseEnd(_)
|
||||
| EventMsg::CollabResumeBegin(_)
|
||||
| EventMsg::CollabResumeEnd(_)
|
||||
| EventMsg::AgentMessageContentDelta(_)
|
||||
| EventMsg::SubAgentActivity(_)
|
||||
| EventMsg::ExecCommandBegin(_)
|
||||
| EventMsg::ExecCommandEnd(_) => {
|
||||
// Deprecated item lifecycle events are still fanned out for raw-event and rollout
|
||||
// compatibility consumers.
|
||||
// App-server v2 receives canonical TurnItem lifecycle instead, and dispatches
|
||||
// dynamic tool requests from canonical DynamicToolCall starts.
|
||||
}
|
||||
EventMsg::McpToolCallBegin(_) | EventMsg::McpToolCallEnd(_) => {
|
||||
// Deprecated MCP tool-call events are still fanned out for raw-event and rollout
|
||||
// compatibility consumers.
|
||||
// App-server v2 receives the canonical TurnItem::McpToolCall lifecycle instead.
|
||||
}
|
||||
msg @ (EventMsg::AgentMessageContentDelta(_)
|
||||
| EventMsg::PlanDelta(_)
|
||||
| EventMsg::ReasoningContentDelta(_)
|
||||
| EventMsg::ReasoningRawContentDelta(_)
|
||||
@@ -888,43 +860,9 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
);
|
||||
outgoing.send_server_notification(notification).await;
|
||||
}
|
||||
EventMsg::SubAgentActivity(activity) => {
|
||||
if activity.kind == SubAgentActivityKind::Interrupted
|
||||
&& thread_manager
|
||||
.get_thread(activity.agent_thread_id)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
thread_watch_manager
|
||||
.remove_thread(&activity.agent_thread_id.to_string())
|
||||
.await;
|
||||
}
|
||||
let notification = item_event_to_server_notification(
|
||||
EventMsg::SubAgentActivity(activity),
|
||||
&conversation_id.to_string(),
|
||||
&event_turn_id,
|
||||
);
|
||||
outgoing.send_server_notification(notification).await;
|
||||
}
|
||||
EventMsg::CollabCloseEnd(end_event) => {
|
||||
if thread_manager
|
||||
.get_thread(end_event.receiver_thread_id)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
thread_watch_manager
|
||||
.remove_thread(&end_event.receiver_thread_id.to_string())
|
||||
.await;
|
||||
}
|
||||
let notification = item_event_to_server_notification(
|
||||
EventMsg::CollabCloseEnd(end_event),
|
||||
&conversation_id.to_string(),
|
||||
&event_turn_id,
|
||||
);
|
||||
outgoing.send_server_notification(notification).await;
|
||||
}
|
||||
EventMsg::ContextCompacted(..) => {
|
||||
// Core still fans out this deprecated event for legacy clients;
|
||||
// Core still fans out this deprecated event for raw-event and rollout compatibility
|
||||
// consumers;
|
||||
// v2 clients receive the canonical ContextCompaction item instead.
|
||||
}
|
||||
EventMsg::DeprecationNotice(event) => {
|
||||
@@ -1027,10 +965,63 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
.send_server_notification(ServerNotification::ItemCompleted(completed))
|
||||
.await;
|
||||
}
|
||||
msg @ (EventMsg::ItemStarted(_)
|
||||
| EventMsg::ItemCompleted(_)
|
||||
| EventMsg::PatchApplyUpdated(_)
|
||||
| EventMsg::TerminalInteraction(_)) => {
|
||||
EventMsg::ItemStarted(event) => {
|
||||
let should_emit = match &event.item {
|
||||
// Approval and guardian flows can emit the command start notification before core
|
||||
// emits the canonical item. Reuse the same set to suppress that duplicate.
|
||||
CoreTurnItem::CommandExecution(item) => thread_state
|
||||
.lock()
|
||||
.await
|
||||
.turn_summary
|
||||
.command_execution_started
|
||||
.insert(item.id.clone()),
|
||||
_ => true,
|
||||
};
|
||||
let dynamic_tool_call_params = match &event.item {
|
||||
CoreTurnItem::DynamicToolCall(item) => Some(DynamicToolCallParams {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: event.turn_id.clone(),
|
||||
call_id: item.id.clone(),
|
||||
namespace: item.namespace.clone(),
|
||||
tool: item.tool.clone(),
|
||||
arguments: item.arguments.clone(),
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
if should_emit {
|
||||
let notification = item_event_to_server_notification(
|
||||
EventMsg::ItemStarted(event),
|
||||
&conversation_id.to_string(),
|
||||
&event_turn_id,
|
||||
);
|
||||
outgoing.send_server_notification(notification).await;
|
||||
}
|
||||
if let Some(params) = dynamic_tool_call_params {
|
||||
let call_id = params.call_id.clone();
|
||||
let (_pending_request_id, rx) = outgoing
|
||||
.send_request(ServerRequestPayload::DynamicToolCall(params))
|
||||
.await;
|
||||
tokio::spawn(async move {
|
||||
crate::dynamic_tools::on_call_response(call_id, rx, conversation).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
EventMsg::ItemCompleted(event) => {
|
||||
apply_canonical_item_completed_side_effects(
|
||||
&thread_manager,
|
||||
&thread_watch_manager,
|
||||
&thread_state,
|
||||
&event.item,
|
||||
)
|
||||
.await;
|
||||
let notification = item_event_to_server_notification(
|
||||
EventMsg::ItemCompleted(event),
|
||||
&conversation_id.to_string(),
|
||||
&event_turn_id,
|
||||
);
|
||||
outgoing.send_server_notification(notification).await;
|
||||
}
|
||||
msg @ (EventMsg::PatchApplyUpdated(_) | EventMsg::TerminalInteraction(_)) => {
|
||||
let notification = item_event_to_server_notification(
|
||||
msg,
|
||||
&conversation_id.to_string(),
|
||||
@@ -1103,36 +1094,10 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
.await;
|
||||
}
|
||||
EventMsg::PatchApplyBegin(_) | EventMsg::PatchApplyEnd(_) => {
|
||||
// Core still fans out these deprecated events for legacy clients;
|
||||
// Core still fans out these deprecated events for raw-event and rollout compatibility
|
||||
// consumers;
|
||||
// v2 clients receive the canonical FileChange item instead.
|
||||
}
|
||||
EventMsg::ExecCommandBegin(exec_command_begin_event) => {
|
||||
if matches!(
|
||||
exec_command_begin_event.source,
|
||||
codex_protocol::protocol::ExecCommandSource::UnifiedExecInteraction
|
||||
) {
|
||||
// TerminalInteraction is the v2 surface for unified exec
|
||||
// stdin/poll events. Suppress the legacy CommandExecution
|
||||
// item so clients do not render the same wait twice.
|
||||
return;
|
||||
}
|
||||
let item_id = exec_command_begin_event.call_id.clone();
|
||||
let first_start = {
|
||||
let mut state = thread_state.lock().await;
|
||||
state
|
||||
.turn_summary
|
||||
.command_execution_started
|
||||
.insert(item_id.clone())
|
||||
};
|
||||
if first_start {
|
||||
let notification = item_event_to_server_notification(
|
||||
EventMsg::ExecCommandBegin(exec_command_begin_event),
|
||||
&conversation_id.to_string(),
|
||||
&event_turn_id,
|
||||
);
|
||||
outgoing.send_server_notification(notification).await;
|
||||
}
|
||||
}
|
||||
EventMsg::ExecCommandOutputDelta(exec_command_output_delta_event) => {
|
||||
let notification = item_event_to_server_notification(
|
||||
EventMsg::ExecCommandOutputDelta(exec_command_output_delta_event),
|
||||
@@ -1141,31 +1106,6 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
);
|
||||
outgoing.send_server_notification(notification).await;
|
||||
}
|
||||
EventMsg::ExecCommandEnd(exec_command_end_event) => {
|
||||
let call_id = exec_command_end_event.call_id.clone();
|
||||
{
|
||||
let mut state = thread_state.lock().await;
|
||||
state
|
||||
.turn_summary
|
||||
.command_execution_started
|
||||
.remove(&call_id);
|
||||
}
|
||||
if matches!(
|
||||
exec_command_end_event.source,
|
||||
codex_protocol::protocol::ExecCommandSource::UnifiedExecInteraction
|
||||
) {
|
||||
// The paired begin event is suppressed above; keep the
|
||||
// completion out of v2 as well so no orphan legacy item is
|
||||
// emitted for unified exec interactions.
|
||||
return;
|
||||
}
|
||||
let notification = item_event_to_server_notification(
|
||||
EventMsg::ExecCommandEnd(exec_command_end_event),
|
||||
&conversation_id.to_string(),
|
||||
&event_turn_id,
|
||||
);
|
||||
outgoing.send_server_notification(notification).await;
|
||||
}
|
||||
// If this is a TurnAborted, reply to any pending interrupt requests.
|
||||
EventMsg::TurnAborted(turn_aborted_event) => {
|
||||
// All per-thread requests are bound to a turn, so abort them.
|
||||
@@ -1369,6 +1309,52 @@ async fn emit_turn_completed_with_status(
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn apply_canonical_item_completed_side_effects(
|
||||
thread_manager: &Arc<ThreadManager>,
|
||||
thread_watch_manager: &ThreadWatchManager,
|
||||
thread_state: &Arc<Mutex<ThreadState>>,
|
||||
item: &CoreTurnItem,
|
||||
) {
|
||||
match item {
|
||||
CoreTurnItem::CommandExecution(item) => {
|
||||
thread_state
|
||||
.lock()
|
||||
.await
|
||||
.turn_summary
|
||||
.command_execution_started
|
||||
.remove(&item.id);
|
||||
}
|
||||
CoreTurnItem::SubAgentActivity(activity)
|
||||
if activity.kind == SubAgentActivityKind::Interrupted =>
|
||||
{
|
||||
remove_missing_thread_watch(
|
||||
thread_manager,
|
||||
thread_watch_manager,
|
||||
activity.agent_thread_id,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
CoreTurnItem::CollabAgentToolCall(item) if item.tool == CoreCollabAgentTool::CloseAgent => {
|
||||
for thread_id in &item.receiver_thread_ids {
|
||||
remove_missing_thread_watch(thread_manager, thread_watch_manager, *thread_id).await;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
async fn remove_missing_thread_watch(
|
||||
thread_manager: &Arc<ThreadManager>,
|
||||
thread_watch_manager: &ThreadWatchManager,
|
||||
thread_id: ThreadId,
|
||||
) {
|
||||
if thread_manager.get_thread(thread_id).await.is_err() {
|
||||
thread_watch_manager
|
||||
.remove_thread(&thread_id.to_string())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn start_command_execution_item(
|
||||
conversation_id: &ThreadId,
|
||||
@@ -2190,10 +2176,15 @@ mod tests {
|
||||
use codex_app_server_protocol::AutoReviewDecisionSource;
|
||||
use codex_app_server_protocol::GuardianApprovalReviewStatus;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::TurnPlanStepStatus;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::items::DynamicToolCallItem;
|
||||
use codex_protocol::items::DynamicToolCallStatus as CoreDynamicToolCallStatus;
|
||||
use codex_protocol::items::HookPromptFragment;
|
||||
use codex_protocol::items::SubAgentActivityItem;
|
||||
use codex_protocol::items::TurnItem as CoreTurnItem;
|
||||
use codex_protocol::items::build_hook_prompt_message;
|
||||
use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions;
|
||||
use codex_protocol::models::NetworkPermissions as CoreNetworkPermissions;
|
||||
@@ -2210,11 +2201,12 @@ mod tests {
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::GuardianAssessmentEvent;
|
||||
use codex_protocol::protocol::GuardianAssessmentStatus;
|
||||
use codex_protocol::protocol::ItemCompletedEvent;
|
||||
use codex_protocol::protocol::ItemStartedEvent;
|
||||
use codex_protocol::protocol::RateLimitSnapshot;
|
||||
use codex_protocol::protocol::RateLimitWindow;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentActivityEvent;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
use codex_protocol::protocol::TokenUsageInfo;
|
||||
use codex_protocol::protocol::UserMessageEvent;
|
||||
@@ -3437,13 +3429,17 @@ mod tests {
|
||||
apply_bespoke_event_handling(
|
||||
Event {
|
||||
id: "turn-1".to_string(),
|
||||
msg: EventMsg::SubAgentActivity(SubAgentActivityEvent {
|
||||
event_id: "activity-1".to_string(),
|
||||
occurred_at_ms: 42,
|
||||
agent_thread_id: child_thread_id,
|
||||
agent_path: AgentPath::try_from("/root/worker")
|
||||
.expect("agent path should parse"),
|
||||
kind: SubAgentActivityKind::Interrupted,
|
||||
msg: EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
thread_id: conversation_id,
|
||||
turn_id: "turn-1".to_string(),
|
||||
item: CoreTurnItem::SubAgentActivity(SubAgentActivityItem {
|
||||
id: "activity-1".to_string(),
|
||||
kind: SubAgentActivityKind::Interrupted,
|
||||
agent_thread_id: child_thread_id,
|
||||
agent_path: AgentPath::try_from("/root/worker")
|
||||
.expect("agent path should parse"),
|
||||
}),
|
||||
completed_at_ms: 42,
|
||||
}),
|
||||
},
|
||||
conversation_id,
|
||||
@@ -3487,6 +3483,92 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn canonical_dynamic_tool_start_emits_item_and_requests_client() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let config = load_default_config_for_test(&codex_home).await;
|
||||
let thread_manager = Arc::new(
|
||||
codex_core::test_support::thread_manager_with_models_provider_and_home(
|
||||
CodexAuth::create_dummy_chatgpt_auth_for_testing(),
|
||||
config.model_provider.clone(),
|
||||
config.codex_home.to_path_buf(),
|
||||
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
),
|
||||
);
|
||||
let codex_core::NewThread {
|
||||
thread_id: conversation_id,
|
||||
thread: conversation,
|
||||
..
|
||||
} = thread_manager.start_thread(config).await?;
|
||||
let (tx, mut rx) = mpsc::channel(CHANNEL_CAPACITY);
|
||||
let outgoing = Arc::new(OutgoingMessageSender::new(
|
||||
tx,
|
||||
codex_analytics::AnalyticsEventsClient::disabled(),
|
||||
));
|
||||
let outgoing = ThreadScopedOutgoingMessageSender::new(
|
||||
outgoing,
|
||||
vec![ConnectionId(1)],
|
||||
conversation_id,
|
||||
);
|
||||
|
||||
apply_bespoke_event_handling(
|
||||
Event {
|
||||
id: "turn-1".to_string(),
|
||||
msg: EventMsg::ItemStarted(ItemStartedEvent {
|
||||
thread_id: conversation_id,
|
||||
turn_id: "turn-1".to_string(),
|
||||
item: CoreTurnItem::DynamicToolCall(DynamicToolCallItem {
|
||||
id: "dynamic-1".to_string(),
|
||||
namespace: Some("apps".to_string()),
|
||||
tool: "lookup".to_string(),
|
||||
arguments: json!({"id": "123"}),
|
||||
status: CoreDynamicToolCallStatus::InProgress,
|
||||
content_items: None,
|
||||
success: None,
|
||||
error: None,
|
||||
duration: None,
|
||||
}),
|
||||
started_at_ms: 42,
|
||||
}),
|
||||
},
|
||||
conversation_id,
|
||||
conversation,
|
||||
thread_manager,
|
||||
outgoing,
|
||||
new_thread_state(),
|
||||
ThreadWatchManager::new(),
|
||||
Arc::new(tokio::sync::Semaphore::new(/*permits*/ 1)),
|
||||
"test-provider".to_string(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let item_started = recv_broadcast_message(&mut rx).await?;
|
||||
let OutgoingMessage::AppServerNotification(ServerNotification::ItemStarted(payload)) =
|
||||
item_started
|
||||
else {
|
||||
bail!("unexpected message: {item_started:?}");
|
||||
};
|
||||
assert_eq!(payload.item.id(), "dynamic-1");
|
||||
|
||||
let request = recv_broadcast_message(&mut rx).await?;
|
||||
let OutgoingMessage::Request(ServerRequest::DynamicToolCall { params, .. }) = request
|
||||
else {
|
||||
bail!("unexpected message: {request:?}");
|
||||
};
|
||||
assert_eq!(
|
||||
params,
|
||||
DynamicToolCallParams {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
call_id: "dynamic-1".to_string(),
|
||||
namespace: Some("apps".to_string()),
|
||||
tool: "lookup".to_string(),
|
||||
arguments: json!({"id": "123"}),
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_turn_complete_emits_completed_without_error() -> Result<()> {
|
||||
let conversation_id = ThreadId::new();
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use codex_app_server_protocol::ChatgptAuthTokensRefreshParams;
|
||||
use codex_app_server_protocol::ChatgptAuthTokensRefreshReason;
|
||||
use codex_app_server_protocol::ChatgptAuthTokensRefreshResponse;
|
||||
use codex_app_server_protocol::ServerRequestPayload;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::ExternalAuthFuture;
|
||||
use codex_login::auth::ExternalAuth;
|
||||
use codex_login::auth::ExternalAuthRefreshContext;
|
||||
use codex_login::auth::ExternalAuthRefreshReason;
|
||||
use tokio::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::outgoing_message::OutgoingMessageSender;
|
||||
|
||||
const EXTERNAL_AUTH_REFRESH_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
pub(crate) struct ExternalAuthBridge {
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
auth: RwLock<CodexAuth>,
|
||||
}
|
||||
|
||||
impl ExternalAuthBridge {
|
||||
pub(crate) fn new(outgoing: Arc<OutgoingMessageSender>, auth: CodexAuth) -> Self {
|
||||
Self {
|
||||
outgoing,
|
||||
auth: RwLock::new(auth),
|
||||
}
|
||||
}
|
||||
|
||||
async fn refresh(&self, context: ExternalAuthRefreshContext) -> std::io::Result<CodexAuth> {
|
||||
let reason = match context.reason {
|
||||
ExternalAuthRefreshReason::Unauthorized => ChatgptAuthTokensRefreshReason::Unauthorized,
|
||||
};
|
||||
let params = ChatgptAuthTokensRefreshParams {
|
||||
reason,
|
||||
previous_account_id: context.previous_account_id,
|
||||
};
|
||||
|
||||
let (request_id, rx) = self
|
||||
.outgoing
|
||||
.send_request(ServerRequestPayload::ChatgptAuthTokensRefresh(params))
|
||||
.await;
|
||||
let result = match timeout(EXTERNAL_AUTH_REFRESH_TIMEOUT, rx).await {
|
||||
Ok(result) => {
|
||||
let result = result.map_err(|err| {
|
||||
std::io::Error::other(format!("auth refresh request canceled: {err}"))
|
||||
})?;
|
||||
result.map_err(|err| {
|
||||
std::io::Error::other(format!(
|
||||
"auth refresh request failed: code={} message={}",
|
||||
err.code, err.message
|
||||
))
|
||||
})?
|
||||
}
|
||||
Err(_) => {
|
||||
let _canceled = self.outgoing.cancel_request(&request_id).await;
|
||||
return Err(std::io::Error::other(format!(
|
||||
"auth refresh request timed out after {}s",
|
||||
EXTERNAL_AUTH_REFRESH_TIMEOUT.as_secs()
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let response: ChatgptAuthTokensRefreshResponse =
|
||||
serde_json::from_value(result).map_err(std::io::Error::other)?;
|
||||
let auth = CodexAuth::from_external_chatgpt_tokens(
|
||||
response.access_token.as_str(),
|
||||
response.chatgpt_account_id.as_str(),
|
||||
response.chatgpt_plan_type.as_deref(),
|
||||
)?;
|
||||
*self
|
||||
.auth
|
||||
.write()
|
||||
.map_err(|_| std::io::Error::other("external auth lock is poisoned"))? = auth.clone();
|
||||
Ok(auth)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExternalAuth for ExternalAuthBridge {
|
||||
fn resolve(&self) -> ExternalAuthFuture<'_, CodexAuth> {
|
||||
Box::pin(async {
|
||||
self.auth
|
||||
.read()
|
||||
.map(|auth| auth.clone())
|
||||
.map_err(|_| std::io::Error::other("external auth lock is poisoned"))
|
||||
})
|
||||
}
|
||||
|
||||
fn refresh(&self, context: ExternalAuthRefreshContext) -> ExternalAuthFuture<'_, CodexAuth> {
|
||||
Box::pin(ExternalAuthBridge::refresh(self, context))
|
||||
}
|
||||
}
|
||||
@@ -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<InProcessClientHandle> {
|
||||
pub async fn start(mut args: InProcessStartArgs) -> IoResult<InProcessClientHandle> {
|
||||
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?;
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ mod current_time;
|
||||
mod dynamic_tools;
|
||||
mod error_code;
|
||||
mod extensions;
|
||||
mod external_auth;
|
||||
mod filters;
|
||||
mod fs_watch;
|
||||
mod fuzzy_file_search;
|
||||
@@ -320,6 +321,16 @@ fn exec_policy_warning_location(err: &ExecPolicyError) -> (Option<String>, 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 +623,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) {
|
||||
|
||||
@@ -50,9 +50,6 @@ use crate::transport::AppServerTransport;
|
||||
use crate::transport::RemoteControlHandle;
|
||||
use codex_analytics::AnalyticsEventsClient;
|
||||
use codex_analytics::AppServerRpcTransport;
|
||||
use codex_app_server_protocol::ChatgptAuthTokensRefreshParams;
|
||||
use codex_app_server_protocol::ChatgptAuthTokensRefreshReason;
|
||||
use codex_app_server_protocol::ChatgptAuthTokensRefreshResponse;
|
||||
use codex_app_server_protocol::ClientNotification;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::ClientResponsePayload;
|
||||
@@ -63,7 +60,6 @@ use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::JSONRPCNotification;
|
||||
use codex_app_server_protocol::JSONRPCRequest;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::ServerRequestPayload;
|
||||
use codex_app_server_protocol::experimental_required_message;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_chatgpt::workspace_settings;
|
||||
@@ -74,12 +70,7 @@ use codex_feedback::CodexFeedback;
|
||||
use codex_goal_extension::GoalService;
|
||||
use codex_home::CodexHomeUserInstructionsProvider;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::auth::ExternalAuth;
|
||||
use codex_login::auth::ExternalAuthRefreshContext;
|
||||
use codex_login::auth::ExternalAuthRefreshReason;
|
||||
use codex_login::auth::ExternalAuthTokens;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::auth::AuthMode as LoginAuthMode;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::W3cTraceContext;
|
||||
use codex_rollout::StateDbHandle;
|
||||
@@ -95,7 +86,6 @@ use tracing::Instrument;
|
||||
|
||||
use crate::models_refresh_worker::ModelsRefreshWorker;
|
||||
|
||||
const EXTERNAL_AUTH_REFRESH_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const CONNECTION_RPC_DRAIN_TIMEOUT: Duration = Duration::from_secs(/*secs*/ 30);
|
||||
|
||||
fn deserialize_client_request(
|
||||
@@ -109,80 +99,6 @@ fn deserialize_client_request(
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ExternalAuthRefreshBridge {
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
}
|
||||
|
||||
impl ExternalAuthRefreshBridge {
|
||||
fn map_reason(reason: ExternalAuthRefreshReason) -> ChatgptAuthTokensRefreshReason {
|
||||
match reason {
|
||||
ExternalAuthRefreshReason::Unauthorized => ChatgptAuthTokensRefreshReason::Unauthorized,
|
||||
}
|
||||
}
|
||||
|
||||
async fn refresh(
|
||||
&self,
|
||||
context: ExternalAuthRefreshContext,
|
||||
) -> std::io::Result<ExternalAuthTokens> {
|
||||
let params = ChatgptAuthTokensRefreshParams {
|
||||
reason: Self::map_reason(context.reason),
|
||||
previous_account_id: context.previous_account_id,
|
||||
};
|
||||
|
||||
let (request_id, rx) = self
|
||||
.outgoing
|
||||
.send_request(ServerRequestPayload::ChatgptAuthTokensRefresh(params))
|
||||
.await;
|
||||
|
||||
let result = match timeout(EXTERNAL_AUTH_REFRESH_TIMEOUT, rx).await {
|
||||
Ok(result) => {
|
||||
// Two failure scenarios:
|
||||
// 1) `oneshot::Receiver` failed (sender dropped) => request canceled/channel closed.
|
||||
// 2) client answered with JSON-RPC error payload => propagate code/message.
|
||||
let result = result.map_err(|err| {
|
||||
std::io::Error::other(format!("auth refresh request canceled: {err}"))
|
||||
})?;
|
||||
result.map_err(|err| {
|
||||
std::io::Error::other(format!(
|
||||
"auth refresh request failed: code={} message={}",
|
||||
err.code, err.message
|
||||
))
|
||||
})?
|
||||
}
|
||||
Err(_) => {
|
||||
let _canceled = self.outgoing.cancel_request(&request_id).await;
|
||||
return Err(std::io::Error::other(format!(
|
||||
"auth refresh request timed out after {}s",
|
||||
EXTERNAL_AUTH_REFRESH_TIMEOUT.as_secs()
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let response: ChatgptAuthTokensRefreshResponse =
|
||||
serde_json::from_value(result).map_err(std::io::Error::other)?;
|
||||
|
||||
Ok(ExternalAuthTokens::chatgpt(
|
||||
response.access_token,
|
||||
response.chatgpt_account_id,
|
||||
response.chatgpt_plan_type,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl ExternalAuth for ExternalAuthRefreshBridge {
|
||||
fn auth_mode(&self) -> LoginAuthMode {
|
||||
LoginAuthMode::Chatgpt
|
||||
}
|
||||
|
||||
fn refresh(
|
||||
&self,
|
||||
context: ExternalAuthRefreshContext,
|
||||
) -> codex_login::ExternalAuthFuture<'_, ExternalAuthTokens> {
|
||||
Box::pin(ExternalAuthRefreshBridge::refresh(self, context))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct MessageProcessor {
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
models_refresh_worker: ModelsRefreshWorker,
|
||||
@@ -327,9 +243,6 @@ impl MessageProcessor {
|
||||
remote_control_handle,
|
||||
plugin_startup_tasks,
|
||||
} = args;
|
||||
auth_manager.set_external_auth(Arc::new(ExternalAuthRefreshBridge {
|
||||
outgoing: outgoing.clone(),
|
||||
}));
|
||||
let thread_state_manager = ThreadStateManager::new();
|
||||
// The thread store is intentionally process-scoped. Config reloads can
|
||||
// affect per-thread behavior, but they must not move newly started,
|
||||
@@ -447,7 +360,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 +408,7 @@ impl MessageProcessor {
|
||||
state_db.clone(),
|
||||
log_db,
|
||||
Arc::clone(&skills_watcher),
|
||||
config_warnings,
|
||||
);
|
||||
let turn_processor = TurnRequestProcessor::new(
|
||||
auth_manager.clone(),
|
||||
|
||||
@@ -94,6 +94,7 @@ use codex_app_server_protocol::ListMcpServerStatusResponse;
|
||||
use codex_app_server_protocol::LoginAccountParams;
|
||||
use codex_app_server_protocol::LoginAccountResponse;
|
||||
use codex_app_server_protocol::LoginApiKeyParams;
|
||||
use codex_app_server_protocol::LoginAppBrand;
|
||||
use codex_app_server_protocol::LogoutAccountResponse;
|
||||
use codex_app_server_protocol::MarketplaceAddParams;
|
||||
use codex_app_server_protocol::MarketplaceAddResponse;
|
||||
@@ -156,7 +157,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;
|
||||
@@ -298,6 +302,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;
|
||||
@@ -374,10 +380,12 @@ use codex_feedback::FeedbackUploadOptions;
|
||||
use codex_git_utils::git_diff_to_remote;
|
||||
use codex_git_utils::resolve_root_git_project_for_trust;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CODEX_OPEN_APP_URL;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::LoginSuccessPage;
|
||||
use codex_login::LoginSuccessPageBrand;
|
||||
use codex_login::ServerOptions as LoginServerOptions;
|
||||
use codex_login::ShutdownHandle;
|
||||
use codex_login::auth::login_with_chatgpt_auth_tokens;
|
||||
use codex_login::complete_device_code_login;
|
||||
use codex_login::login_with_api_key;
|
||||
use codex_login::oauth_client_id;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
use crate::auth_mode::auth_mode_to_api;
|
||||
use crate::external_auth::ExternalAuthBridge;
|
||||
use chrono::DateTime;
|
||||
|
||||
mod rate_limit_resets;
|
||||
@@ -9,9 +10,11 @@ const LOGIN_CHATGPT_TIMEOUT: Duration = Duration::from_secs(10 * 60);
|
||||
const ACCOUNT_TOKEN_USAGE_FETCH_TIMEOUT: Duration = Duration::from_secs(/*secs*/ 10);
|
||||
const ACCOUNT_WORKSPACE_MESSAGES_FETCH_TIMEOUT: Duration =
|
||||
Duration::from_millis(/*millis*/ 1000);
|
||||
// The override is intentionally available only in debug builds, matching the login path below.
|
||||
// Login overrides are intentionally available only in debug builds.
|
||||
#[cfg(debug_assertions)]
|
||||
const LOGIN_ISSUER_OVERRIDE_ENV_VAR: &str = "CODEX_APP_SERVER_LOGIN_ISSUER";
|
||||
#[cfg(debug_assertions)]
|
||||
const LOGIN_OPEN_APP_URL_OVERRIDE_ENV_VAR: &str = "CODEX_APP_SERVER_DEV_OPEN_APP_URL";
|
||||
|
||||
enum ActiveLogin {
|
||||
Browser {
|
||||
@@ -253,9 +256,25 @@ impl AccountRequestProcessor {
|
||||
.await;
|
||||
}
|
||||
LoginAccountParams::Chatgpt {
|
||||
app_brand,
|
||||
codex_streamlined_login,
|
||||
use_hosted_login_success_page,
|
||||
} => {
|
||||
self.login_chatgpt_v2(request_id, codex_streamlined_login)
|
||||
let login_success_page = if use_hosted_login_success_page {
|
||||
let app_brand = match app_brand.unwrap_or_default() {
|
||||
LoginAppBrand::Codex => LoginSuccessPageBrand::Codex,
|
||||
LoginAppBrand::Chatgpt => LoginSuccessPageBrand::Chatgpt,
|
||||
};
|
||||
LoginSuccessPage::Hosted {
|
||||
url: CODEX_OPEN_APP_URL.parse().map_err(|err| {
|
||||
internal_error(format!("invalid Codex open app URL: {err}"))
|
||||
})?,
|
||||
app_brand,
|
||||
}
|
||||
} else {
|
||||
LoginSuccessPage::default()
|
||||
};
|
||||
self.login_chatgpt_v2(request_id, codex_streamlined_login, login_success_page)
|
||||
.await;
|
||||
}
|
||||
LoginAccountParams::ChatgptDeviceCode => {
|
||||
@@ -341,6 +360,7 @@ impl AccountRequestProcessor {
|
||||
async fn login_chatgpt_common(
|
||||
&self,
|
||||
codex_streamlined_login: bool,
|
||||
login_success_page: LoginSuccessPage,
|
||||
) -> std::result::Result<LoginServerOptions, JSONRPCErrorError> {
|
||||
let config = self.config.as_ref();
|
||||
|
||||
@@ -357,6 +377,7 @@ impl AccountRequestProcessor {
|
||||
let opts = LoginServerOptions {
|
||||
open_browser: false,
|
||||
codex_streamlined_login,
|
||||
login_success_page,
|
||||
..LoginServerOptions::new(
|
||||
config.codex_home.to_path_buf(),
|
||||
oauth_client_id(),
|
||||
@@ -374,6 +395,14 @@ impl AccountRequestProcessor {
|
||||
{
|
||||
opts.issuer = issuer;
|
||||
}
|
||||
if let LoginSuccessPage::Hosted { url, .. } = &mut opts.login_success_page
|
||||
&& let Ok(open_app_url) = std::env::var(LOGIN_OPEN_APP_URL_OVERRIDE_ENV_VAR)
|
||||
&& !open_app_url.trim().is_empty()
|
||||
{
|
||||
*url = open_app_url
|
||||
.parse()
|
||||
.map_err(|err| internal_error(format!("invalid Codex open app URL: {err}")))?;
|
||||
}
|
||||
opts
|
||||
};
|
||||
|
||||
@@ -393,16 +422,22 @@ impl AccountRequestProcessor {
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
codex_streamlined_login: bool,
|
||||
login_success_page: LoginSuccessPage,
|
||||
) {
|
||||
let result = self.login_chatgpt_response(codex_streamlined_login).await;
|
||||
let result = self
|
||||
.login_chatgpt_response(codex_streamlined_login, login_success_page)
|
||||
.await;
|
||||
self.outgoing.send_result(request_id, result).await;
|
||||
}
|
||||
|
||||
async fn login_chatgpt_response(
|
||||
&self,
|
||||
codex_streamlined_login: bool,
|
||||
login_success_page: LoginSuccessPage,
|
||||
) -> Result<LoginAccountResponse, JSONRPCErrorError> {
|
||||
let opts = self.login_chatgpt_common(codex_streamlined_login).await?;
|
||||
let opts = self
|
||||
.login_chatgpt_common(codex_streamlined_login, login_success_page)
|
||||
.await?;
|
||||
let server = run_login_server(opts)
|
||||
.map_err(|err| internal_error(format!("failed to start login server: {err}")))?;
|
||||
let login_id = Uuid::new_v4();
|
||||
@@ -474,7 +509,10 @@ impl AccountRequestProcessor {
|
||||
&self,
|
||||
) -> Result<LoginAccountResponse, JSONRPCErrorError> {
|
||||
let opts = self
|
||||
.login_chatgpt_common(/*codex_streamlined_login*/ false)
|
||||
.login_chatgpt_common(
|
||||
/*codex_streamlined_login*/ false,
|
||||
LoginSuccessPage::default(),
|
||||
)
|
||||
.await?;
|
||||
let device_code = request_device_code(&opts)
|
||||
.await
|
||||
@@ -617,14 +655,19 @@ impl AccountRequestProcessor {
|
||||
)));
|
||||
}
|
||||
|
||||
login_with_chatgpt_auth_tokens(
|
||||
&self.config.codex_home,
|
||||
let auth = CodexAuth::from_external_chatgpt_tokens(
|
||||
&access_token,
|
||||
&chatgpt_account_id,
|
||||
chatgpt_plan_type.as_deref(),
|
||||
)
|
||||
.map_err(|err| internal_error(format!("failed to set external auth: {err}")))?;
|
||||
self.auth_manager.reload().await;
|
||||
self.auth_manager
|
||||
.set_external_auth(Arc::new(ExternalAuthBridge::new(
|
||||
Arc::clone(&self.outgoing),
|
||||
auth,
|
||||
)))
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to set external auth: {err}")))?;
|
||||
self.config_manager.replace_cloud_config_bundle_loader(
|
||||
self.auth_manager.clone(),
|
||||
self.config.chatgpt_base_url.clone(),
|
||||
@@ -812,7 +855,9 @@ impl AccountRequestProcessor {
|
||||
let auth_mode = auth_mode_to_api(auth.api_auth_mode());
|
||||
let (reported_auth_method, token_opt) = if matches!(
|
||||
auth,
|
||||
CodexAuth::AgentIdentity(_) | CodexAuth::PersonalAccessToken(_)
|
||||
CodexAuth::Headers(_)
|
||||
| CodexAuth::AgentIdentity(_)
|
||||
| CodexAuth::PersonalAccessToken(_)
|
||||
) || include_token
|
||||
&& permanent_refresh_failure
|
||||
{
|
||||
@@ -891,9 +936,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 +967,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 +984,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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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<RateLimitResetCreditsSummary> {
|
||||
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::<u64>().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<RateLimitResetCreditsSummary, String> {
|
||||
let credits = details
|
||||
.credits
|
||||
.into_iter()
|
||||
.map(rate_limit_reset_credit_from_backend)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(RateLimitResetCreditsSummary {
|
||||
available_count: details.available_count,
|
||||
credits: Some(credits),
|
||||
})
|
||||
}
|
||||
|
||||
fn rate_limit_reset_credit_from_backend(
|
||||
credit: BackendRateLimitResetCreditDetails,
|
||||
) -> Result<RateLimitResetCredit, String> {
|
||||
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<i64, String> {
|
||||
DateTime::parse_from_rfc3339(timestamp)
|
||||
.map(|timestamp| timestamp.timestamp())
|
||||
.map_err(|err| format!("failed to parse timestamp `{timestamp}`: {err}"))
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ impl AppsRequestProcessor {
|
||||
request_id: &ConnectionRequestId,
|
||||
params: AppsListParams,
|
||||
) -> Result<Option<AppsListResponse>, JSONRPCErrorError> {
|
||||
let installed_start = Instant::now();
|
||||
let reload = params.force_refetch;
|
||||
let thread = if let Some(thread_id) = params.thread_id.as_deref() {
|
||||
let (_, loaded_thread) = self.load_thread(thread_id).await?;
|
||||
Some(loaded_thread)
|
||||
@@ -70,20 +72,24 @@ impl AppsRequestProcessor {
|
||||
.features
|
||||
.apps_enabled_for_auth(auth.as_ref().is_some_and(CodexAuth::uses_codex_backend))
|
||||
{
|
||||
return Ok(Some(AppsListResponse {
|
||||
let response = AppsListResponse {
|
||||
data: Vec::new(),
|
||||
next_cursor: None,
|
||||
}));
|
||||
};
|
||||
record_legacy_apps_installed_duration(installed_start, reload);
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
if !self
|
||||
.workspace_codex_plugins_enabled(&config, auth.as_ref())
|
||||
.await
|
||||
{
|
||||
return Ok(Some(AppsListResponse {
|
||||
let response = AppsListResponse {
|
||||
data: Vec::new(),
|
||||
next_cursor: None,
|
||||
}));
|
||||
};
|
||||
record_legacy_apps_installed_duration(installed_start, reload);
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
let request = request_id.clone();
|
||||
@@ -103,6 +109,7 @@ impl AppsRequestProcessor {
|
||||
environment_manager,
|
||||
mcp_manager,
|
||||
plugins_manager,
|
||||
installed_start,
|
||||
) => {}
|
||||
}
|
||||
});
|
||||
@@ -113,6 +120,7 @@ impl AppsRequestProcessor {
|
||||
self.shutdown_token.cancel();
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn apps_list_task(
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
request_id: ConnectionRequestId,
|
||||
@@ -121,7 +129,9 @@ impl AppsRequestProcessor {
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
mcp_manager: Arc<McpManager>,
|
||||
plugins_manager: Arc<PluginsManager>,
|
||||
installed_start: Instant,
|
||||
) {
|
||||
let reload = params.force_refetch;
|
||||
let retry_params = params.clone();
|
||||
let retry_config = config.clone();
|
||||
let retry_environment_manager = Arc::clone(&environment_manager);
|
||||
@@ -136,6 +146,9 @@ impl AppsRequestProcessor {
|
||||
plugins_manager,
|
||||
)
|
||||
.await;
|
||||
if result.is_ok() {
|
||||
record_legacy_apps_installed_duration(installed_start, reload);
|
||||
}
|
||||
let should_retry = result
|
||||
.as_ref()
|
||||
.is_ok_and(|(_, codex_apps_ready)| !codex_apps_ready);
|
||||
@@ -363,6 +376,20 @@ impl AppsRequestProcessor {
|
||||
}
|
||||
|
||||
const APP_LIST_LOAD_TIMEOUT: Duration = Duration::from_secs(90);
|
||||
// `app/list` is the legacy request-path baseline for the future `app/installed` endpoint;
|
||||
// `path=legacy` keeps it separate from the new snapshot-backed implementation in dashboards.
|
||||
const APPS_INSTALLED_DURATION_METRIC: &str = "codex.apps.installed.duration_ms";
|
||||
|
||||
fn record_legacy_apps_installed_duration(started_at: Instant, reload: bool) {
|
||||
let reload = if reload { "true" } else { "false" };
|
||||
if let Some(metrics) = codex_otel::global() {
|
||||
let _ = metrics.record_duration(
|
||||
APPS_INSTALLED_DURATION_METRIC,
|
||||
started_at.elapsed(),
|
||||
&[("path", "legacy"), ("reload", reload)],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum AppListLoadResult {
|
||||
Accessible(Result<AccessibleConnectorsStatus, String>),
|
||||
|
||||
@@ -163,6 +163,7 @@ fn convert_configured_marketplace_plugin_to_plugin_summary(
|
||||
share_context,
|
||||
source: marketplace_plugin_source_to_info(plugin.source),
|
||||
install_policy: plugin.policy.installation.into(),
|
||||
install_policy_source: None,
|
||||
auth_policy: plugin.policy.authentication.into(),
|
||||
availability: PluginAvailability::Available,
|
||||
interface: plugin.interface.map(local_plugin_interface_to_info),
|
||||
@@ -1104,6 +1105,7 @@ impl PluginRequestProcessor {
|
||||
installed: outcome.plugin.installed,
|
||||
enabled: outcome.plugin.enabled,
|
||||
install_policy: outcome.plugin.policy.installation.into(),
|
||||
install_policy_source: None,
|
||||
auth_policy: outcome.plugin.policy.authentication.into(),
|
||||
availability: PluginAvailability::Available,
|
||||
interface: outcome.plugin.interface.map(local_plugin_interface_to_info),
|
||||
@@ -1546,6 +1548,7 @@ impl PluginRequestProcessor {
|
||||
&remote_marketplace_name,
|
||||
/*plugin_id*/ None,
|
||||
error_type,
|
||||
/*sub_error_type*/ None,
|
||||
err.to_string(),
|
||||
);
|
||||
remote_plugin_catalog_error_to_jsonrpc(
|
||||
@@ -1589,11 +1592,13 @@ impl PluginRequestProcessor {
|
||||
)
|
||||
.map_err(|err| {
|
||||
let error_type = remote_plugin_bundle_install_error_type(&err);
|
||||
let sub_error_type = err.sub_error_type();
|
||||
self.track_plugin_install_failed_for_remote_plugin(
|
||||
&remote_plugin_id,
|
||||
&actual_remote_marketplace_name,
|
||||
Some(&resolved_plugin_id),
|
||||
error_type,
|
||||
sub_error_type,
|
||||
err.to_string(),
|
||||
);
|
||||
remote_plugin_bundle_install_error_to_jsonrpc(err)
|
||||
@@ -1606,11 +1611,13 @@ impl PluginRequestProcessor {
|
||||
.await
|
||||
.map_err(|err| {
|
||||
let error_type = remote_plugin_bundle_install_error_type(&err);
|
||||
let sub_error_type = err.sub_error_type();
|
||||
self.track_plugin_install_failed_for_remote_plugin(
|
||||
&remote_plugin_id,
|
||||
&actual_remote_marketplace_name,
|
||||
Some(&resolved_plugin_id),
|
||||
error_type,
|
||||
sub_error_type,
|
||||
err.to_string(),
|
||||
);
|
||||
remote_plugin_bundle_install_error_to_jsonrpc(err)
|
||||
@@ -1633,6 +1640,7 @@ impl PluginRequestProcessor {
|
||||
&actual_remote_marketplace_name,
|
||||
Some(&result.plugin_id),
|
||||
error_type,
|
||||
/*sub_error_type*/ None,
|
||||
err.to_string(),
|
||||
);
|
||||
remote_plugin_catalog_error_to_jsonrpc(err, "install remote plugin")
|
||||
@@ -1730,12 +1738,14 @@ impl PluginRequestProcessor {
|
||||
marketplace_name: &str,
|
||||
plugin_id: Option<&PluginId>,
|
||||
error_type: &'static str,
|
||||
sub_error_type: Option<String>,
|
||||
error_message: String,
|
||||
) {
|
||||
tracing::warn!(
|
||||
remote_plugin_id = %remote_plugin_id,
|
||||
marketplace_name = %marketplace_name,
|
||||
error_type = %error_type,
|
||||
sub_error_type = sub_error_type.as_deref(),
|
||||
error = %error_message,
|
||||
"remote plugin install failed"
|
||||
);
|
||||
@@ -2180,6 +2190,7 @@ fn remote_plugin_summary_to_info(summary: RemoteCatalogPluginSummary) -> PluginS
|
||||
installed: summary.installed,
|
||||
enabled: summary.enabled,
|
||||
install_policy: summary.install_policy,
|
||||
install_policy_source: summary.install_policy_source,
|
||||
auth_policy: summary.auth_policy,
|
||||
availability: summary.availability,
|
||||
interface: summary.interface,
|
||||
|
||||
@@ -364,6 +364,7 @@ pub(crate) struct ThreadRequestProcessor {
|
||||
pub(super) log_db: Option<LogDbLayer>,
|
||||
pub(super) background_tasks: TaskTracker,
|
||||
pub(super) skills_watcher: Arc<SkillsWatcher>,
|
||||
pub(super) initial_config_warnings: Arc<Vec<ConfigWarningNotification>>,
|
||||
}
|
||||
|
||||
/// Outcome of trying to satisfy a resume request from an already loaded thread.
|
||||
@@ -395,6 +396,7 @@ impl ThreadRequestProcessor {
|
||||
state_db: Option<StateDbHandle>,
|
||||
log_db: Option<LogDbLayer>,
|
||||
skills_watcher: Arc<SkillsWatcher>,
|
||||
initial_config_warnings: Vec<ConfigWarningNotification>,
|
||||
) -> Self {
|
||||
Self {
|
||||
auth_manager,
|
||||
@@ -413,6 +415,7 @@ impl ThreadRequestProcessor {
|
||||
log_db,
|
||||
background_tasks: TaskTracker::new(),
|
||||
skills_watcher,
|
||||
initial_config_warnings: Arc::new(initial_config_warnings),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -968,6 +971,7 @@ impl ThreadRequestProcessor {
|
||||
};
|
||||
let request_trace = request_context.request_trace();
|
||||
let config_manager = self.config_manager.clone();
|
||||
let initial_config_warnings = Arc::clone(&self.initial_config_warnings);
|
||||
let outgoing = Arc::clone(&listener_task_context.outgoing);
|
||||
let error_request_id = request_id.clone();
|
||||
let thread_start_task = async move {
|
||||
@@ -990,6 +994,7 @@ impl ThreadRequestProcessor {
|
||||
allow_provider_model_fallback,
|
||||
experimental_raw_events,
|
||||
request_trace,
|
||||
initial_config_warnings,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -1066,6 +1071,7 @@ impl ThreadRequestProcessor {
|
||||
allow_provider_model_fallback: bool,
|
||||
experimental_raw_events: bool,
|
||||
request_trace: Option<W3cTraceContext>,
|
||||
initial_config_warnings: Arc<Vec<ConfigWarningNotification>>,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let thread_start_started_at = std::time::Instant::now();
|
||||
let requested_cwd = typesafe_overrides.cwd.clone();
|
||||
@@ -1139,6 +1145,21 @@ impl ThreadRequestProcessor {
|
||||
.map_err(|err| config_load_error(&err))?;
|
||||
}
|
||||
|
||||
if let Ok(Some(err)) =
|
||||
codex_core::check_execpolicy_for_warnings(&config.config_layer_stack).await
|
||||
{
|
||||
let notification = crate::exec_policy_config_warning(&err);
|
||||
if !initial_config_warnings.contains(¬ification) {
|
||||
listener_task_context
|
||||
.outgoing
|
||||
.send_server_notification_to_connections(
|
||||
&[request_id.connection_id],
|
||||
ServerNotification::ConfigWarning(notification),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
let environments = environments.unwrap_or_else(|| {
|
||||
listener_task_context
|
||||
.thread_manager
|
||||
|
||||
@@ -32,7 +32,7 @@ pub(super) fn redact_thread_resume_payloads(turns: &mut [Turn]) {
|
||||
}
|
||||
true
|
||||
}
|
||||
ThreadItem::ImageGeneration { .. } => false,
|
||||
ThreadItem::ImageGeneration(_) => false,
|
||||
_ => true,
|
||||
});
|
||||
}
|
||||
@@ -52,6 +52,7 @@ fn redacted_mcp_tool_call_result() -> McpToolCallResult {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_app_server_protocol::ImageGenerationItem;
|
||||
use codex_app_server_protocol::McpToolCallAppContext;
|
||||
use codex_app_server_protocol::McpToolCallError;
|
||||
use codex_app_server_protocol::McpToolCallStatus;
|
||||
@@ -100,13 +101,13 @@ mod tests {
|
||||
error: None,
|
||||
duration_ms: Some(8),
|
||||
},
|
||||
ThreadItem::ImageGeneration {
|
||||
ThreadItem::ImageGeneration(ImageGenerationItem {
|
||||
id: "ig-1".to_string(),
|
||||
status: "completed".to_string(),
|
||||
revised_prompt: Some("revised".to_string()),
|
||||
result: "base64-result".to_string(),
|
||||
saved_path: Some(test_path_buf("/tmp/ig-1.png").abs()),
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
redact_thread_resume_payloads(&mut thread.turns);
|
||||
|
||||
@@ -1002,6 +1002,9 @@ impl TurnRequestProcessor {
|
||||
thread.as_ref(),
|
||||
Op::RealtimeConversationStart(ConversationStartParams {
|
||||
client_managed_handoffs: params.client_managed_handoffs.unwrap_or(false),
|
||||
flush_transcript_tail_on_session_end: params
|
||||
.flush_transcript_tail_on_session_end
|
||||
.unwrap_or(false),
|
||||
codex_responses_as_items: params.codex_responses_as_items.unwrap_or(false),
|
||||
codex_response_item_prefix: params.codex_response_item_prefix,
|
||||
codex_response_handoff_prefix: params.codex_response_handoff_prefix,
|
||||
|
||||
@@ -27,6 +27,7 @@ codex-protocol = { workspace = true }
|
||||
codex-utils-cargo-bin = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
tokio = { workspace = true, features = [
|
||||
"io-std",
|
||||
"macros",
|
||||
|
||||
@@ -49,6 +49,7 @@ use serde::de::DeserializeOwned;
|
||||
pub use test_app_server::DEFAULT_CLIENT_NAME;
|
||||
pub use test_app_server::DISABLE_PLUGIN_STARTUP_TASKS_ARG;
|
||||
pub use test_app_server::TestAppServer;
|
||||
pub use test_app_server::TestAppServerBuilder;
|
||||
|
||||
pub fn to_response<T: DeserializeOwned>(response: JSONRPCResponse) -> anyhow::Result<T> {
|
||||
let value = serde_json::to_value(response.result)?;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::ExitStatus;
|
||||
use std::process::Stdio;
|
||||
use std::sync::atomic::AtomicI64;
|
||||
@@ -111,6 +112,7 @@ use codex_app_server_protocol::TurnCompletedNotification;
|
||||
use codex_app_server_protocol::TurnEnvironmentParams;
|
||||
use codex_app_server_protocol::TurnInterruptParams;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::TurnSteerParams;
|
||||
use codex_app_server_protocol::WindowsSandboxSetupStartParams;
|
||||
use codex_exec_server::CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR;
|
||||
@@ -121,6 +123,7 @@ use codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR;
|
||||
use codex_login::default_client::CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR;
|
||||
use core_test_support::test_codex::TestEnv;
|
||||
use core_test_support::test_codex::test_env;
|
||||
use tempfile::TempDir;
|
||||
use tokio::process::Command;
|
||||
|
||||
use crate::json_logging::JsonLogCapture;
|
||||
@@ -137,6 +140,8 @@ pub struct TestAppServer {
|
||||
pending_messages: VecDeque<JSONRPCMessage>,
|
||||
auto_env: Option<TestEnv>,
|
||||
json_logs: JsonLogCapture,
|
||||
codex_home: PathBuf,
|
||||
_owned_codex_home: Option<TempDir>,
|
||||
}
|
||||
|
||||
pub const DEFAULT_CLIENT_NAME: &str = "codex-app-server-tests";
|
||||
@@ -144,12 +149,28 @@ pub const DISABLE_PLUGIN_STARTUP_TASKS_ARG: &str = "--disable-plugin-startup-tas
|
||||
const DISABLE_MANAGED_CONFIG_ENV_VAR: &str = "CODEX_APP_SERVER_DISABLE_MANAGED_CONFIG";
|
||||
|
||||
impl TestAppServer {
|
||||
/// Starts building a server with a temporary CODEX_HOME and the standard
|
||||
/// automatic test environment.
|
||||
pub fn builder() -> TestAppServerBuilder {
|
||||
TestAppServerBuilder {
|
||||
codex_home: None,
|
||||
environment: TestAppServerEnvironment::Auto,
|
||||
program: None,
|
||||
env_overrides: Vec::new(),
|
||||
args: vec![DISABLE_PLUGIN_STARTUP_TASKS_ARG.to_string()],
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn wait_for_exit(&mut self) -> std::io::Result<ExitStatus> {
|
||||
self.process.wait().await
|
||||
}
|
||||
|
||||
pub async fn new(codex_home: &Path) -> anyhow::Result<Self> {
|
||||
Self::new_with_env_and_args(codex_home, &[], &[DISABLE_PLUGIN_STARTUP_TASKS_ARG]).await
|
||||
Self::builder()
|
||||
.with_codex_home(codex_home)
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await
|
||||
}
|
||||
|
||||
/// Starts an app server with the standard test environment and retains it
|
||||
@@ -163,7 +184,7 @@ impl TestAppServer {
|
||||
/// URL-based configuration, this helper rejects a `codex_home` containing
|
||||
/// that file.
|
||||
pub async fn new_with_auto_env(codex_home: &Path) -> anyhow::Result<Self> {
|
||||
Self::new_with_auto_env_and_env(codex_home, &[]).await
|
||||
Self::builder().with_codex_home(codex_home).build().await
|
||||
}
|
||||
|
||||
/// Starts an auto-environment app server that emits JSON logs.
|
||||
@@ -173,64 +194,39 @@ impl TestAppServer {
|
||||
codex_home: &Path,
|
||||
rust_log: impl Into<String>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let rust_log = rust_log.into();
|
||||
Self::new_with_auto_env_and_env(
|
||||
codex_home,
|
||||
&[
|
||||
("LOG_FORMAT", Some("json")),
|
||||
("RUST_LOG", Some(rust_log.as_str())),
|
||||
],
|
||||
)
|
||||
.await
|
||||
Self::builder()
|
||||
.with_codex_home(codex_home)
|
||||
.with_json_logging(rust_log)
|
||||
.build()
|
||||
.await
|
||||
}
|
||||
|
||||
async fn new_with_auto_env_and_env(
|
||||
codex_home: &Path,
|
||||
extra_env_overrides: &[(&str, Option<&str>)],
|
||||
) -> anyhow::Result<Self> {
|
||||
let environments_toml = codex_home.join("environments.toml");
|
||||
ensure!(
|
||||
!environments_toml
|
||||
.try_exists()
|
||||
.with_context(|| format!("check whether {} exists", environments_toml.display()))?,
|
||||
"new_with_auto_env cannot be used when {} exists",
|
||||
environments_toml.display()
|
||||
);
|
||||
|
||||
let auto_env = test_env().await?;
|
||||
// Noise registry configuration takes precedence over the URL-based
|
||||
// provider, so clear inherited values to keep the selection hermetic.
|
||||
let mut env_overrides = vec![
|
||||
(
|
||||
CODEX_EXEC_SERVER_URL_ENV_VAR,
|
||||
auto_env.environment().exec_server_url(),
|
||||
),
|
||||
(CODEX_EXEC_SERVER_NOISE_REGISTRY_URL_ENV_VAR, None),
|
||||
(CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID_ENV_VAR, None),
|
||||
(CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR, None),
|
||||
(CODEX_EXEC_SERVER_NOISE_CHATGPT_ACCOUNT_ID_ENV_VAR, None),
|
||||
];
|
||||
env_overrides.extend_from_slice(extra_env_overrides);
|
||||
let mut app_server = Self::new_with_env(codex_home, &env_overrides).await?;
|
||||
app_server.auto_env = Some(auto_env);
|
||||
Ok(app_server)
|
||||
/// Returns the automatically selected test environment retained by this server.
|
||||
///
|
||||
/// Tests can use the environment to arrange target-native filesystem fixtures before starting
|
||||
/// a thread. Returns an error unless this server was created with [`Self::new_with_auto_env`].
|
||||
pub fn auto_env(&self) -> anyhow::Result<&TestEnv> {
|
||||
self.auto_env
|
||||
.as_ref()
|
||||
.context("auto environment is unavailable; use TestAppServer::new_with_auto_env")
|
||||
}
|
||||
|
||||
/// Returns app-server protocol parameters for the automatically selected
|
||||
/// test environment. Returns an error unless this server was created with
|
||||
/// [`Self::new_with_auto_env`].
|
||||
pub fn auto_env_params(&self) -> anyhow::Result<TurnEnvironmentParams> {
|
||||
let selection = self
|
||||
.auto_env
|
||||
.as_ref()
|
||||
.context("auto environment is unavailable; use TestAppServer::new_with_auto_env")?
|
||||
.selection();
|
||||
let selection = self.auto_env()?.selection();
|
||||
Ok(TurnEnvironmentParams {
|
||||
environment_id: selection.environment_id.clone(),
|
||||
cwd: selection.cwd.clone().into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the effective CODEX_HOME used by the child app-server.
|
||||
pub fn codex_home(&self) -> &Path {
|
||||
&self.codex_home
|
||||
}
|
||||
|
||||
/// Waits for a JSON stderr event whose structured `event.name` field matches.
|
||||
pub async fn wait_for_json_log_event(
|
||||
&self,
|
||||
@@ -254,33 +250,56 @@ impl TestAppServer {
|
||||
}
|
||||
|
||||
pub async fn new_without_managed_config(codex_home: &Path) -> anyhow::Result<Self> {
|
||||
Self::new_with_env(codex_home, &[(DISABLE_MANAGED_CONFIG_ENV_VAR, Some("1"))]).await
|
||||
Self::builder()
|
||||
.with_codex_home(codex_home)
|
||||
.without_auto_env()
|
||||
.without_managed_config()
|
||||
.build()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn new_without_managed_config_with_env(
|
||||
codex_home: &Path,
|
||||
env_overrides: &[(&str, Option<&str>)],
|
||||
) -> anyhow::Result<Self> {
|
||||
let mut all_env_overrides = vec![(DISABLE_MANAGED_CONFIG_ENV_VAR, Some("1"))];
|
||||
all_env_overrides.extend_from_slice(env_overrides);
|
||||
Self::new_with_env(codex_home, &all_env_overrides).await
|
||||
Self::builder()
|
||||
.with_codex_home(codex_home)
|
||||
.without_auto_env()
|
||||
.without_managed_config()
|
||||
.with_env_overrides(env_overrides)
|
||||
.build()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn new_with_plugin_startup_tasks(codex_home: &Path) -> anyhow::Result<Self> {
|
||||
Self::new_with_env_and_args(codex_home, &[], &[]).await
|
||||
Self::builder()
|
||||
.with_codex_home(codex_home)
|
||||
.without_auto_env()
|
||||
.with_plugin_startup_tasks()
|
||||
.build()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn new_with_env_and_plugin_startup_tasks(
|
||||
codex_home: &Path,
|
||||
env_overrides: &[(&str, Option<&str>)],
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::new_with_env_and_args(codex_home, env_overrides, &[]).await
|
||||
Self::builder()
|
||||
.with_codex_home(codex_home)
|
||||
.without_auto_env()
|
||||
.with_plugin_startup_tasks()
|
||||
.with_env_overrides(env_overrides)
|
||||
.build()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn new_with_args(codex_home: &Path, args: &[&str]) -> anyhow::Result<Self> {
|
||||
let mut all_args = vec![DISABLE_PLUGIN_STARTUP_TASKS_ARG];
|
||||
all_args.extend_from_slice(args);
|
||||
Self::new_with_env_and_args(codex_home, &[], &all_args).await
|
||||
Self::builder()
|
||||
.with_codex_home(codex_home)
|
||||
.without_auto_env()
|
||||
.with_args(args)
|
||||
.build()
|
||||
.await
|
||||
}
|
||||
|
||||
/// Creates a new MCP process, allowing tests to override or remove
|
||||
@@ -292,12 +311,12 @@ impl TestAppServer {
|
||||
codex_home: &Path,
|
||||
env_overrides: &[(&str, Option<&str>)],
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::new_with_env_and_args(
|
||||
codex_home,
|
||||
env_overrides,
|
||||
&[DISABLE_PLUGIN_STARTUP_TASKS_ARG],
|
||||
)
|
||||
.await
|
||||
Self::builder()
|
||||
.with_codex_home(codex_home)
|
||||
.without_auto_env()
|
||||
.with_env_overrides(env_overrides)
|
||||
.build()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn new_with_program_and_env(
|
||||
@@ -305,23 +324,13 @@ impl TestAppServer {
|
||||
program: &Path,
|
||||
env_overrides: &[(&str, Option<&str>)],
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::new_with_program_env_and_args(
|
||||
codex_home,
|
||||
program,
|
||||
env_overrides,
|
||||
&[DISABLE_PLUGIN_STARTUP_TASKS_ARG],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn new_with_env_and_args(
|
||||
codex_home: &Path,
|
||||
env_overrides: &[(&str, Option<&str>)],
|
||||
args: &[&str],
|
||||
) -> anyhow::Result<Self> {
|
||||
let program = codex_utils_cargo_bin::cargo_bin("codex-app-server")
|
||||
.context("should find binary for codex-app-server")?;
|
||||
Self::new_with_program_env_and_args(codex_home, &program, env_overrides, args).await
|
||||
Self::builder()
|
||||
.with_codex_home(codex_home)
|
||||
.without_auto_env()
|
||||
.with_program(program)
|
||||
.with_env_overrides(env_overrides)
|
||||
.build()
|
||||
.await
|
||||
}
|
||||
|
||||
async fn new_with_program_env_and_args(
|
||||
@@ -392,6 +401,8 @@ impl TestAppServer {
|
||||
pending_messages: VecDeque::new(),
|
||||
auto_env: None,
|
||||
json_logs,
|
||||
codex_home: codex_home.to_path_buf(),
|
||||
_owned_codex_home: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -546,8 +557,8 @@ impl TestAppServer {
|
||||
chatgpt_account_id,
|
||||
chatgpt_plan_type,
|
||||
};
|
||||
let params = Some(serde_json::to_value(params)?);
|
||||
self.send_request("account/login/start", params).await
|
||||
self.send_login_account_request(serde_json::to_value(params)?)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Send a `feedback/upload` JSON-RPC request.
|
||||
@@ -1052,6 +1063,39 @@ impl TestAppServer {
|
||||
self.send_request("turn/start", params).await
|
||||
}
|
||||
|
||||
/// Start a turn and return its matching typed completion notification.
|
||||
pub async fn start_turn_and_wait_for_completion(
|
||||
&mut self,
|
||||
params: TurnStartParams,
|
||||
) -> anyhow::Result<TurnCompletedNotification> {
|
||||
let thread_id = params.thread_id.clone();
|
||||
let request_id = self.send_turn_start_request(params).await?;
|
||||
let response = self
|
||||
.read_stream_until_response_message(RequestId::Integer(request_id))
|
||||
.await?;
|
||||
let TurnStartResponse { turn } = crate::to_response(response)?;
|
||||
let notification = self
|
||||
.read_stream_until_matching_notification(
|
||||
"turn/completed for started turn",
|
||||
|notification| {
|
||||
notification.method == "turn/completed"
|
||||
&& notification.params.as_ref().is_some_and(|params| {
|
||||
serde_json::from_value::<TurnCompletedNotification>(params.clone())
|
||||
.is_ok_and(|completed| {
|
||||
completed.thread_id == thread_id && completed.turn.id == turn.id
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let params = notification
|
||||
.params
|
||||
.context("turn/completed notification must include params")?;
|
||||
let completed = serde_json::from_value(params)
|
||||
.context("failed to deserialize turn/completed notification")?;
|
||||
Ok(completed)
|
||||
}
|
||||
|
||||
/// Send a `thread/inject_items` JSON-RPC request (v2).
|
||||
pub async fn send_thread_inject_items_request(
|
||||
&mut self,
|
||||
@@ -1380,6 +1424,14 @@ impl TestAppServer {
|
||||
self.send_request("account/logout", /*params*/ None).await
|
||||
}
|
||||
|
||||
/// Send an `account/login/start` JSON-RPC request.
|
||||
pub async fn send_login_account_request(
|
||||
&mut self,
|
||||
params: serde_json::Value,
|
||||
) -> anyhow::Result<i64> {
|
||||
self.send_request("account/login/start", Some(params)).await
|
||||
}
|
||||
|
||||
/// Send an `account/login/start` JSON-RPC request for API key login.
|
||||
pub async fn send_login_account_api_key_request(
|
||||
&mut self,
|
||||
@@ -1389,7 +1441,7 @@ impl TestAppServer {
|
||||
"type": "apiKey",
|
||||
"apiKey": api_key,
|
||||
});
|
||||
self.send_request("account/login/start", Some(params)).await
|
||||
self.send_login_account_request(params).await
|
||||
}
|
||||
|
||||
/// Send an `account/login/start` JSON-RPC request for ChatGPT login.
|
||||
@@ -1397,7 +1449,7 @@ impl TestAppServer {
|
||||
let params = serde_json::json!({
|
||||
"type": "chatgpt"
|
||||
});
|
||||
self.send_request("account/login/start", Some(params)).await
|
||||
self.send_login_account_request(params).await
|
||||
}
|
||||
|
||||
/// Send an `account/login/start` JSON-RPC request for ChatGPT device code login.
|
||||
@@ -1405,7 +1457,7 @@ impl TestAppServer {
|
||||
let params = serde_json::json!({
|
||||
"type": "chatgptDeviceCode"
|
||||
});
|
||||
self.send_request("account/login/start", Some(params)).await
|
||||
self.send_login_account_request(params).await
|
||||
}
|
||||
|
||||
/// Send an `account/login/cancel` JSON-RPC request.
|
||||
@@ -1752,6 +1804,164 @@ impl TestAppServer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for TestAppServer.
|
||||
pub struct TestAppServerBuilder {
|
||||
codex_home: Option<PathBuf>,
|
||||
environment: TestAppServerEnvironment,
|
||||
program: Option<PathBuf>,
|
||||
env_overrides: Vec<(String, Option<String>)>,
|
||||
args: Vec<String>,
|
||||
}
|
||||
|
||||
enum TestAppServerEnvironment {
|
||||
Auto,
|
||||
None,
|
||||
}
|
||||
|
||||
impl TestAppServerBuilder {
|
||||
/// Uses this existing CODEX_HOME instead of a temporary one.
|
||||
pub fn with_codex_home(mut self, codex_home: &Path) -> Self {
|
||||
self.codex_home = Some(codex_home.to_path_buf());
|
||||
self
|
||||
}
|
||||
|
||||
/// Starts app-server without the standard automatic test environment.
|
||||
pub fn without_auto_env(mut self) -> Self {
|
||||
self.environment = TestAppServerEnvironment::None;
|
||||
self
|
||||
}
|
||||
|
||||
/// Uses this app-server binary instead of the standard test binary.
|
||||
pub fn with_program(mut self, program: &Path) -> Self {
|
||||
self.program = Some(program.to_path_buf());
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds command-line arguments after the default test arguments.
|
||||
pub fn with_args(mut self, args: &[&str]) -> Self {
|
||||
self.args
|
||||
.extend(args.iter().map(|argument| (*argument).to_string()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables startup tasks that the default test arguments disable.
|
||||
pub fn with_plugin_startup_tasks(mut self) -> Self {
|
||||
self.args
|
||||
.retain(|argument| argument != DISABLE_PLUGIN_STARTUP_TASKS_ARG);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds child-process environment overrides.
|
||||
///
|
||||
/// Some values set variables and None values remove inherited variables.
|
||||
pub fn with_env_overrides(mut self, env_overrides: &[(&str, Option<&str>)]) -> Self {
|
||||
self.env_overrides
|
||||
.extend(env_overrides.iter().map(|(key, value)| {
|
||||
(
|
||||
(*key).to_string(),
|
||||
value.map(std::string::ToString::to_string),
|
||||
)
|
||||
}));
|
||||
self
|
||||
}
|
||||
|
||||
/// Prevents the child from loading managed configuration.
|
||||
pub fn without_managed_config(self) -> Self {
|
||||
self.with_env_overrides(&[(DISABLE_MANAGED_CONFIG_ENV_VAR, Some("1"))])
|
||||
}
|
||||
|
||||
/// Configures the child to emit JSON logs at the requested Rust log level.
|
||||
pub fn with_json_logging(self, rust_log: impl Into<String>) -> Self {
|
||||
let rust_log = rust_log.into();
|
||||
let mut builder = self.with_env_overrides(&[("LOG_FORMAT", Some("json"))]);
|
||||
builder
|
||||
.env_overrides
|
||||
.push(("RUST_LOG".to_string(), Some(rust_log)));
|
||||
builder
|
||||
}
|
||||
|
||||
/// Builds a server with a temporary CODEX_HOME and automatic environment
|
||||
/// by default.
|
||||
pub async fn build(self) -> anyhow::Result<TestAppServer> {
|
||||
let Self {
|
||||
codex_home,
|
||||
environment,
|
||||
program,
|
||||
mut env_overrides,
|
||||
args,
|
||||
} = self;
|
||||
let (codex_home, owned_codex_home) = match codex_home {
|
||||
Some(codex_home) => (codex_home, None),
|
||||
None => {
|
||||
let owned_codex_home = TempDir::new()?;
|
||||
(
|
||||
owned_codex_home.path().to_path_buf(),
|
||||
Some(owned_codex_home),
|
||||
)
|
||||
}
|
||||
};
|
||||
let auto_env = match environment {
|
||||
TestAppServerEnvironment::Auto => {
|
||||
let environments_toml = codex_home.join("environments.toml");
|
||||
ensure!(
|
||||
!environments_toml.try_exists().with_context(|| format!(
|
||||
"check whether {} exists",
|
||||
environments_toml.display()
|
||||
))?,
|
||||
"automatic environment cannot be used when {} exists",
|
||||
environments_toml.display()
|
||||
);
|
||||
let auto_env = test_env().await?;
|
||||
// Noise registry configuration takes precedence over the URL-based
|
||||
// provider, so clear inherited values to keep the selection hermetic.
|
||||
let mut auto_env_overrides = vec![
|
||||
(
|
||||
CODEX_EXEC_SERVER_URL_ENV_VAR.to_string(),
|
||||
auto_env.environment().exec_server_url().map(str::to_string),
|
||||
),
|
||||
(
|
||||
CODEX_EXEC_SERVER_NOISE_REGISTRY_URL_ENV_VAR.to_string(),
|
||||
None,
|
||||
),
|
||||
(
|
||||
CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID_ENV_VAR.to_string(),
|
||||
None,
|
||||
),
|
||||
(CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR.to_string(), None),
|
||||
(
|
||||
CODEX_EXEC_SERVER_NOISE_CHATGPT_ACCOUNT_ID_ENV_VAR.to_string(),
|
||||
None,
|
||||
),
|
||||
];
|
||||
auto_env_overrides.append(&mut env_overrides);
|
||||
env_overrides = auto_env_overrides;
|
||||
Some(auto_env)
|
||||
}
|
||||
TestAppServerEnvironment::None => None,
|
||||
};
|
||||
let program = match program {
|
||||
Some(program) => program,
|
||||
None => codex_utils_cargo_bin::cargo_bin("codex-app-server")
|
||||
.context("should find binary for codex-app-server")?,
|
||||
};
|
||||
let env_overrides = env_overrides
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_deref()))
|
||||
.collect::<Vec<_>>();
|
||||
let args = args.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
let mut app_server = TestAppServer::new_with_program_env_and_args(
|
||||
&codex_home,
|
||||
&program,
|
||||
&env_overrides,
|
||||
&args,
|
||||
)
|
||||
.await?;
|
||||
app_server.auto_env = auto_env;
|
||||
app_server._owned_codex_home = owned_codex_home;
|
||||
Ok(app_server)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestAppServer {
|
||||
fn drop(&mut self) {
|
||||
// These tests spawn a `codex-app-server` child process.
|
||||
|
||||
@@ -115,8 +115,12 @@ async fn get_auth_status_no_auth() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path())?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -142,7 +146,11 @@ async fn get_auth_status_with_api_key() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path())?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
login_with_api_key_via_request(&mut mcp, "sk-test-key").await?;
|
||||
@@ -186,15 +194,16 @@ async fn personal_access_token_without_email_supports_auth_status_and_account_re
|
||||
.await;
|
||||
|
||||
let authapi_base_url = server.uri();
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[
|
||||
("OPENAI_API_KEY", None),
|
||||
("CODEX_ACCESS_TOKEN", Some("at-test-token")),
|
||||
("CODEX_AUTHAPI_BASE_URL", Some(authapi_base_url.as_str())),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -256,7 +265,11 @@ async fn get_auth_status_with_api_key_when_auth_not_required() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml_custom_provider(codex_home.path(), /*requires_openai_auth*/ false)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
login_with_api_key_via_request(&mut mcp, "sk-test-key").await?;
|
||||
@@ -289,7 +302,11 @@ async fn get_auth_status_with_api_key_no_include_token() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path())?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
login_with_api_key_via_request(&mut mcp, "sk-test-key").await?;
|
||||
@@ -317,7 +334,11 @@ async fn get_auth_status_with_api_key_refresh_requested() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path())?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
login_with_api_key_via_request(&mut mcp, "sk-test-key").await?;
|
||||
@@ -373,17 +394,18 @@ async fn get_auth_status_omits_token_after_permanent_refresh_failure() -> Result
|
||||
.await;
|
||||
|
||||
let refresh_url = format!("{}/oauth/token", server.uri());
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[
|
||||
("OPENAI_API_KEY", None),
|
||||
(
|
||||
REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR,
|
||||
Some(refresh_url.as_str()),
|
||||
),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -455,17 +477,18 @@ async fn get_auth_status_omits_token_after_proactive_refresh_failure() -> Result
|
||||
.await;
|
||||
|
||||
let refresh_url = format!("{}/oauth/token", server.uri());
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[
|
||||
("OPENAI_API_KEY", None),
|
||||
(
|
||||
REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR,
|
||||
Some(refresh_url.as_str()),
|
||||
),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -522,17 +545,18 @@ async fn get_auth_status_returns_token_after_proactive_refresh_recovery() -> Res
|
||||
.await;
|
||||
|
||||
let refresh_url = format!("{}/oauth/token", server.uri());
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[
|
||||
("OPENAI_API_KEY", None),
|
||||
(
|
||||
REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR,
|
||||
Some(refresh_url.as_str()),
|
||||
),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let failed_request_id = mcp
|
||||
@@ -599,7 +623,11 @@ async fn login_api_key_rejected_when_forced_chatgpt() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml_forced_login(codex_home.path(), "chatgpt")?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
|
||||
@@ -92,7 +92,11 @@ async fn get_conversation_summary_by_thread_id_reads_rollout() -> Result<()> {
|
||||
))?,
|
||||
);
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -217,7 +221,11 @@ async fn get_conversation_summary_by_relative_rollout_path_resolves_from_codex_h
|
||||
let relative_path = rollout_path.strip_prefix(codex_home.path())?.to_path_buf();
|
||||
let expected = expected_summary(thread_id, normalized_canonical_path(rollout_path)?);
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
|
||||
@@ -46,7 +46,11 @@ shell_snapshot = false
|
||||
|
||||
async fn initialized_mcp(codex_home: &TempDir) -> Result<TestAppServer> {
|
||||
create_config_toml(codex_home.path())?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
Ok(mcp)
|
||||
}
|
||||
@@ -236,7 +240,11 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() -> Result<()> {
|
||||
.to_string();
|
||||
|
||||
// Start MCP server and initialize.
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let root_path = root.path().to_string_lossy().to_string();
|
||||
@@ -302,7 +310,11 @@ async fn test_fuzzy_file_search_accepts_cancellation_token() -> Result<()> {
|
||||
|
||||
std::fs::write(root.path().join("alpha.txt"), "contents")?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let root_path = root.path().to_string_lossy().to_string();
|
||||
|
||||
@@ -68,11 +68,11 @@ async fn app_server_emits_structured_tool_call_timing_event() -> Result<()> {
|
||||
"compact",
|
||||
)?;
|
||||
|
||||
let mut app_server = TestAppServer::new_with_auto_env_and_json_logging(
|
||||
codex_home.path(),
|
||||
"warn,codex_core::tools::parallel=info",
|
||||
)
|
||||
.await?;
|
||||
let mut app_server = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.with_json_logging("warn,codex_core::tools::parallel=info")
|
||||
.build()
|
||||
.await?;
|
||||
timeout(READ_TIMEOUT, app_server.initialize()).await??;
|
||||
|
||||
let thread_start_id = app_server
|
||||
|
||||
@@ -57,6 +57,7 @@ use wiremock::matchers::path;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
|
||||
const LOGIN_ISSUER_ENV_VAR: &str = "CODEX_APP_SERVER_LOGIN_ISSUER";
|
||||
const LOGIN_OPEN_APP_URL_ENV_VAR: &str = "CODEX_APP_SERVER_DEV_OPEN_APP_URL";
|
||||
const WORKSPACE_ID_ALLOWED: &str = "123e4567-e89b-42d3-a456-426614174000";
|
||||
const WORKSPACE_ID_SECOND_ALLOWED: &str = "123e4567-e89b-42d3-a456-426614174001";
|
||||
const WORKSPACE_ID_DISALLOWED: &str = "123e4567-e89b-42d3-a456-426614174002";
|
||||
@@ -181,7 +182,7 @@ async fn mock_device_code_token_failure(server: &MockServer, status: u16) {
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn mock_device_code_oauth_token(server: &MockServer, id_token: &str) {
|
||||
async fn mock_oauth_token(server: &MockServer, id_token: &str) {
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/oauth/token"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
@@ -206,8 +207,12 @@ async fn logout_account_removes_auth_and_notifies() -> Result<()> {
|
||||
)?;
|
||||
assert!(codex_home.path().join("auth.json").exists());
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let id = mcp.send_logout_account_request().await?;
|
||||
@@ -274,8 +279,12 @@ async fn set_auth_token_updates_account_and_notifies() -> Result<()> {
|
||||
.chatgpt_account_id(WORKSPACE_ID_EMBEDDED),
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let set_id = mcp
|
||||
@@ -327,6 +336,27 @@ async fn set_auth_token_updates_account_and_notifies() -> Result<()> {
|
||||
}
|
||||
);
|
||||
|
||||
let logout_id = mcp.send_logout_account_request().await?;
|
||||
let logout_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(logout_id)),
|
||||
)
|
||||
.await??;
|
||||
let _: LogoutAccountResponse = to_response(logout_resp)?;
|
||||
|
||||
let get_id = mcp
|
||||
.send_get_account_request(GetAccountParams {
|
||||
refresh_token: false,
|
||||
})
|
||||
.await?;
|
||||
let get_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(get_id)),
|
||||
)
|
||||
.await??;
|
||||
let account: GetAccountResponse = to_response(get_resp)?;
|
||||
assert_eq!(account.account, None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -349,8 +379,12 @@ async fn account_read_refresh_token_is_noop_in_external_mode() -> Result<()> {
|
||||
.chatgpt_account_id(WORKSPACE_ID_EMBEDDED),
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let set_id = mcp
|
||||
@@ -475,8 +509,12 @@ async fn external_auth_refreshes_on_unauthorized() -> Result<()> {
|
||||
.chatgpt_account_id(WORKSPACE_ID_REFRESHED),
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let set_id = mcp
|
||||
@@ -583,8 +621,12 @@ async fn external_auth_refresh_error_fails_turn() -> Result<()> {
|
||||
.chatgpt_account_id(WORKSPACE_ID_INITIAL),
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let set_id = mcp
|
||||
@@ -707,8 +749,12 @@ async fn external_auth_refresh_mismatched_workspace_fails_turn() -> Result<()> {
|
||||
.chatgpt_account_id(WORKSPACE_ID_DISALLOWED),
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let set_id = mcp
|
||||
@@ -824,8 +870,12 @@ async fn external_auth_refresh_invalid_access_token_fails_turn() -> Result<()> {
|
||||
.chatgpt_account_id(WORKSPACE_ID_INITIAL),
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let set_id = mcp
|
||||
@@ -918,7 +968,11 @@ async fn login_account_api_key_succeeds_and_notifies() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let req_id = mcp
|
||||
@@ -972,7 +1026,11 @@ async fn login_account_api_key_rejected_when_forced_chatgpt() -> Result<()> {
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -1002,7 +1060,11 @@ async fn login_account_chatgpt_rejected_when_forced_api() -> Result<()> {
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp.send_login_account_chatgpt_request().await?;
|
||||
@@ -1035,14 +1097,15 @@ async fn login_account_chatgpt_device_code_returns_error_when_disabled() -> Resu
|
||||
mock_device_code_usercode_failure(&mock_server, /*status*/ 404).await;
|
||||
|
||||
let issuer = mock_server.uri();
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[
|
||||
("OPENAI_API_KEY", None),
|
||||
(LOGIN_ISSUER_ENV_VAR, Some(issuer.as_str())),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp.send_login_account_chatgpt_device_code_request().await?;
|
||||
@@ -1097,17 +1160,18 @@ async fn login_account_chatgpt_device_code_succeeds_and_notifies() -> Result<()>
|
||||
.plan_type("pro")
|
||||
.chatgpt_account_id(WORKSPACE_ID_DEVICE),
|
||||
)?;
|
||||
mock_device_code_oauth_token(&mock_server, &id_token).await;
|
||||
mock_oauth_token(&mock_server, &id_token).await;
|
||||
|
||||
let issuer = mock_server.uri();
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[
|
||||
("OPENAI_API_KEY", None),
|
||||
(LOGIN_ISSUER_ENV_VAR, Some(issuer.as_str())),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp.send_login_account_chatgpt_device_code_request().await?;
|
||||
@@ -1177,14 +1241,15 @@ async fn login_account_chatgpt_device_code_failure_notifies_without_account_upda
|
||||
mock_device_code_token_failure(&mock_server, /*status*/ 500).await;
|
||||
|
||||
let issuer = mock_server.uri();
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[
|
||||
("OPENAI_API_KEY", None),
|
||||
(LOGIN_ISSUER_ENV_VAR, Some(issuer.as_str())),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp.send_login_account_chatgpt_device_code_request().await?;
|
||||
@@ -1252,14 +1317,15 @@ async fn login_account_chatgpt_device_code_can_be_cancelled() -> Result<()> {
|
||||
mock_device_code_token_failure(&mock_server, /*status*/ 404).await;
|
||||
|
||||
let issuer = mock_server.uri();
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[
|
||||
("OPENAI_API_KEY", None),
|
||||
(LOGIN_ISSUER_ENV_VAR, Some(issuer.as_str())),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp.send_login_account_chatgpt_device_code_request().await?;
|
||||
@@ -1325,7 +1391,11 @@ async fn login_account_chatgpt_start_can_be_cancelled() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp.send_login_account_chatgpt_request().await?;
|
||||
@@ -1391,14 +1461,15 @@ async fn login_account_chatgpt_uses_debug_oauth_overrides() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[
|
||||
(CLIENT_ID_OVERRIDE_ENV_VAR, Some("staging-client")),
|
||||
(LOGIN_ISSUER_ENV_VAR, Some("https://auth.example.com")),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp.send_login_account_chatgpt_request().await?;
|
||||
@@ -1436,6 +1507,78 @@ async fn login_account_chatgpt_uses_debug_oauth_overrides() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
// Serialize tests that launch the login server since it binds to a fixed port.
|
||||
#[serial(login_port)]
|
||||
async fn login_account_chatgpt_redirects_to_hosted_success_page() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?;
|
||||
let mock_server = MockServer::start().await;
|
||||
let id_token = encode_id_token(
|
||||
&ChatGptIdTokenClaims::new()
|
||||
.email("hosted@example.com")
|
||||
.plan_type("pro")
|
||||
.chatgpt_account_id(WORKSPACE_ID_EMBEDDED),
|
||||
)?;
|
||||
mock_oauth_token(&mock_server, &id_token).await;
|
||||
let issuer = mock_server.uri();
|
||||
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[
|
||||
(LOGIN_ISSUER_ENV_VAR, Some(issuer.as_str())),
|
||||
(
|
||||
LOGIN_OPEN_APP_URL_ENV_VAR,
|
||||
Some("http://localhost:3000/codex/open-app"),
|
||||
),
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_login_account_request(json!({
|
||||
"type": "chatgpt",
|
||||
"appBrand": "chatgpt",
|
||||
"useHostedLoginSuccessPage": true,
|
||||
}))
|
||||
.await?;
|
||||
let resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let login: LoginAccountResponse = to_response(resp)?;
|
||||
let LoginAccountResponse::Chatgpt { auth_url, .. } = login else {
|
||||
bail!("unexpected login response: {login:?}");
|
||||
};
|
||||
let auth_url = Url::parse(&auth_url)?;
|
||||
let callback_url = auth_url
|
||||
.query_pairs()
|
||||
.find_map(|(key, value)| (key == "redirect_uri").then(|| value.into_owned()))
|
||||
.ok_or_else(|| anyhow::anyhow!("missing redirect_uri"))?;
|
||||
let state = auth_url
|
||||
.query_pairs()
|
||||
.find_map(|(key, value)| (key == "state").then(|| value.into_owned()))
|
||||
.ok_or_else(|| anyhow::anyhow!("missing state"))?;
|
||||
let client = reqwest::Client::builder()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.build()?;
|
||||
|
||||
let response = client
|
||||
.get(format!("{callback_url}?code=test-code&state={state}"))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
assert_eq!(response.status(), 302);
|
||||
assert_eq!(
|
||||
response.headers()["location"].to_str()?,
|
||||
"http://localhost:3000/codex/open-app?source=login&app_brand=chatgpt"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
// Serialize tests that launch the login server since it binds to a fixed port.
|
||||
#[serial(login_port)]
|
||||
@@ -1443,7 +1586,11 @@ async fn set_auth_token_cancels_active_chatgpt_login() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
// Initiate the ChatGPT login flow
|
||||
@@ -1518,7 +1665,11 @@ async fn login_account_chatgpt_includes_forced_workspace_query_param() -> Result
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp.send_login_account_chatgpt_request().await?;
|
||||
@@ -1555,7 +1706,11 @@ async fn login_account_chatgpt_includes_forced_workspace_allowlist_query_param()
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp.send_login_account_chatgpt_request().await?;
|
||||
@@ -1594,8 +1749,12 @@ async fn get_account_no_auth() -> Result<()> {
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let params = GetAccountParams {
|
||||
@@ -1626,7 +1785,11 @@ async fn get_account_with_api_key() -> Result<()> {
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let req_id = mcp
|
||||
@@ -1670,7 +1833,11 @@ async fn get_account_when_auth_not_required() -> Result<()> {
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let params = GetAccountParams {
|
||||
@@ -1711,7 +1878,11 @@ region = "us-west-2"
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let params = GetAccountParams {
|
||||
@@ -1754,7 +1925,11 @@ async fn get_account_with_managed_bedrock_provider() -> Result<()> {
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -1799,8 +1974,12 @@ async fn get_account_with_chatgpt() -> Result<()> {
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let params = GetAccountParams {
|
||||
@@ -1842,8 +2021,12 @@ async fn get_account_with_chatgpt_without_email() -> Result<()> {
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -1905,17 +2088,18 @@ async fn get_account_omits_chatgpt_after_permanent_refresh_failure() -> Result<(
|
||||
.await;
|
||||
|
||||
let refresh_url = format!("{}/oauth/token", server.uri());
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[
|
||||
("OPENAI_API_KEY", None),
|
||||
(
|
||||
REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR,
|
||||
Some(refresh_url.as_str()),
|
||||
),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let auth_status_request_id = mcp
|
||||
@@ -1971,8 +2155,12 @@ async fn get_account_with_chatgpt_missing_plan_claim_returns_unknown() -> Result
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let params = GetAccountParams {
|
||||
|
||||
@@ -8,7 +8,9 @@ use std::time::Duration;
|
||||
use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use app_test_support::ChatGptAuthFixture;
|
||||
use app_test_support::ChatGptIdTokenClaims;
|
||||
use app_test_support::TestAppServer;
|
||||
use app_test_support::encode_id_token;
|
||||
use app_test_support::to_response;
|
||||
use app_test_support::write_chatgpt_auth;
|
||||
use axum::Json;
|
||||
@@ -29,6 +31,7 @@ use codex_app_server_protocol::AppsListParams;
|
||||
use codex_app_server_protocol::AppsListResponse;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::LoginAccountResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
@@ -63,7 +66,11 @@ const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
#[tokio::test]
|
||||
async fn list_apps_returns_empty_when_connectors_disabled() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
@@ -129,7 +136,11 @@ async fn list_apps_returns_empty_with_api_key_auth() -> Result<()> {
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -156,6 +167,89 @@ async fn list_apps_returns_empty_with_api_key_auth() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_apps_uses_external_chatgpt_auth() -> Result<()> {
|
||||
let access_token = encode_id_token(
|
||||
&ChatGptIdTokenClaims::new()
|
||||
.email("external@example.com")
|
||||
.plan_type("pro")
|
||||
.chatgpt_account_id("account-123"),
|
||||
)?;
|
||||
let connectors = vec![AppInfo {
|
||||
id: "beta".to_string(),
|
||||
name: "Beta".to_string(),
|
||||
description: Some("Beta connector".to_string()),
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
icon_assets: None,
|
||||
icon_dark_assets: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: None,
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}];
|
||||
let tools = vec![connector_tool("beta", "Beta App")?];
|
||||
let (server_url, server_handle, _) = start_apps_server_with_delays_and_control_inner(
|
||||
connectors,
|
||||
tools,
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
/*workspace_plugins_enabled*/ true,
|
||||
&access_token,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
write_connectors_config(codex_home.path(), &server_url)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
let login_id = mcp
|
||||
.send_chatgpt_auth_tokens_login_request(
|
||||
access_token,
|
||||
"account-123".to_string(),
|
||||
Some("pro".to_string()),
|
||||
)
|
||||
.await?;
|
||||
let login_response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(login_id)),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(
|
||||
to_response::<LoginAccountResponse>(login_response)?,
|
||||
LoginAccountResponse::ChatgptAuthTokens {}
|
||||
);
|
||||
|
||||
let request_id = mcp
|
||||
.send_apps_list_request(AppsListParams {
|
||||
limit: None,
|
||||
cursor: None,
|
||||
thread_id: None,
|
||||
force_refetch: true,
|
||||
})
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let AppsListResponse { data, next_cursor } = to_response(response)?;
|
||||
|
||||
assert_eq!(data.len(), 1);
|
||||
assert_eq!(data[0].id, "beta");
|
||||
assert!(data[0].is_accessible);
|
||||
assert!(next_cursor.is_none());
|
||||
|
||||
server_handle.abort();
|
||||
let _ = server_handle.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_apps_returns_empty_when_workspace_codex_plugins_disabled() -> Result<()> {
|
||||
let connectors = vec![AppInfo {
|
||||
@@ -193,7 +287,12 @@ async fn list_apps_returns_empty_when_workspace_codex_plugins_disabled() -> Resu
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new_without_managed_config(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.without_managed_config()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -238,7 +337,11 @@ async fn list_apps_includes_plugin_apps_for_chatgpt_auth() -> Result<()> {
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -298,7 +401,10 @@ async fn list_apps_uses_thread_feature_flag_when_thread_id_is_provided() -> Resu
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_auto_env(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let start_request = mcp
|
||||
@@ -411,7 +517,11 @@ async fn list_apps_keeps_apps_with_app_only_tools_accessible() -> Result<()> {
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -486,7 +596,11 @@ enabled = false
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -609,7 +723,11 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<(
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -759,7 +877,11 @@ async fn list_apps_waits_for_accessible_data_before_emitting_directory_updates()
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -871,7 +993,11 @@ async fn list_apps_does_not_emit_empty_interim_updates() -> Result<()> {
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -986,7 +1112,11 @@ async fn list_apps_paginates_results() -> Result<()> {
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let first_request = mcp
|
||||
@@ -1112,7 +1242,11 @@ async fn list_apps_force_refetch_preserves_previous_cache_on_failure() -> Result
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let initial_request = mcp
|
||||
@@ -1242,7 +1376,11 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let warm_request = mcp
|
||||
@@ -1557,6 +1695,7 @@ async fn start_apps_server_with_workspace_plugins_enabled(
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
workspace_plugins_enabled,
|
||||
"chatgpt-token",
|
||||
)
|
||||
.await?;
|
||||
Ok((server_url, server_handle))
|
||||
@@ -1574,6 +1713,7 @@ async fn start_apps_server_with_delays_and_control(
|
||||
directory_delay,
|
||||
tools_delay,
|
||||
/*workspace_plugins_enabled*/ true,
|
||||
"chatgpt-token",
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1584,13 +1724,14 @@ async fn start_apps_server_with_delays_and_control_inner(
|
||||
directory_delay: Duration,
|
||||
tools_delay: Duration,
|
||||
workspace_plugins_enabled: bool,
|
||||
expected_bearer: &str,
|
||||
) -> Result<(String, JoinHandle<()>, AppsServerControl)> {
|
||||
let response = Arc::new(StdMutex::new(
|
||||
json!({ "apps": connectors, "next_token": null }),
|
||||
));
|
||||
let tools = Arc::new(StdMutex::new(tools));
|
||||
let state = AppsServerState {
|
||||
expected_bearer: "Bearer chatgpt-token".to_string(),
|
||||
expected_bearer: format!("Bearer {expected_bearer}"),
|
||||
expected_account_id: "account-123".to_string(),
|
||||
response: response.clone(),
|
||||
directory_delay,
|
||||
|
||||
@@ -67,8 +67,12 @@ async fn attestation_generate_round_trip_adds_header_to_responses_websocket_hand
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
let initialized = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.initialize_with_capabilities(
|
||||
|
||||
@@ -41,7 +41,10 @@ async fn thread_start_with_auto_env_exposes_fixture_cwd_to_model() -> Result<()>
|
||||
"compact",
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_auto_env(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
let expected_environment = mcp.auto_env_params()?;
|
||||
|
||||
@@ -110,14 +113,17 @@ async fn auto_env_rejects_explicit_environment_config() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(codex_home.path().join("environments.toml"), "")?;
|
||||
|
||||
let result = TestAppServer::new_with_auto_env(codex_home.path()).await;
|
||||
let result = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await;
|
||||
let Err(err) = result else {
|
||||
anyhow::bail!("auto-env construction unexpectedly succeeded");
|
||||
};
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
format!(
|
||||
"new_with_auto_env cannot be used when {} exists",
|
||||
"automatic environment cannot be used when {} exists",
|
||||
codex_home.path().join("environments.toml").display()
|
||||
)
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user