mirror of
https://github.com/openai/codex.git
synced 2026-11-11 21:10:39 +00:00
Merge origin/main into dev/se-8040-nonblocking-mcp-auth
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()
|
||||
@@ -1,5 +1,8 @@
|
||||
# Core crate ownership.
|
||||
/codex-rs/arg0/ @openai/codex-core-agent-team
|
||||
/codex-rs/codex-mcp/ @openai/codex-core-agent-team
|
||||
/codex-rs/core/ @openai/codex-core-agent-team
|
||||
/codex-rs/exec-server/ @openai/codex-core-agent-team
|
||||
/codex-rs/exec-server-protocol/ @openai/codex-core-agent-team
|
||||
/codex-rs/ext/extension-api/ @openai/codex-core-agent-team
|
||||
/codex-rs/prompts/ @openai/codex-core-agent-team
|
||||
|
||||
@@ -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,8 @@ 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"
|
||||
- id: setup_ci
|
||||
uses: ./.github/actions/setup-ci
|
||||
|
||||
- name: Set up Bazel
|
||||
uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0
|
||||
@@ -38,30 +21,15 @@ runs:
|
||||
# and Windows can use the runner's standalone Bazel, ignoring .bazelversion.
|
||||
with:
|
||||
bazelisk-version: 1.28.1
|
||||
# setup-bazel writes an explicit output_base, which otherwise overrides
|
||||
# BAZEL_OUTPUT_USER_ROOT and leaves Bazel's I/O-heavy trees on C:.
|
||||
output-base: ${{ steps.setup_ci.outputs.bazel-output-base }}
|
||||
|
||||
- name: Configure Bazel repository cache
|
||||
id: configure_bazel_repository_cache
|
||||
shell: pwsh
|
||||
run: |
|
||||
# Keep the repository cache under HOME on all runners. Windows `D:\a`
|
||||
# cache paths match `.bazelrc`, but `actions/cache/restore` currently
|
||||
# returns HTTP 400 for that path in the Windows clippy job.
|
||||
$repositoryCachePath = Join-Path $HOME '.cache/bazel-repo-cache'
|
||||
"repository-cache-path=$repositoryCachePath" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
"BAZEL_REPOSITORY_CACHE=$repositoryCachePath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
|
||||
- name: Configure Bazel output root (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
# Use the shortest available drive to reduce argv/path length issues,
|
||||
# but avoid the drive root because some Windows test launchers mis-handle
|
||||
# MANIFEST paths there.
|
||||
$hasDDrive = Test-Path 'D:\'
|
||||
$bazelOutputUserRoot = if ($hasDDrive) { 'D:\b' } else { 'C:\b' }
|
||||
$repoContentsCache = Join-Path $env:RUNNER_TEMP "bazel-repo-contents-cache-$env:GITHUB_RUN_ID-$env:GITHUB_JOB"
|
||||
"BAZEL_OUTPUT_USER_ROOT=$bazelOutputUserRoot" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
"BAZEL_REPO_CONTENTS_CACHE=$repoContentsCache" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
"repository-cache-path=$env:BAZEL_REPOSITORY_CACHE" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
|
||||
- name: Expose MSVC SDK environment (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
@@ -124,8 +92,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,97 @@
|
||||
name: setup-ci
|
||||
description: Prepare common tools and environment shared by CI jobs.
|
||||
outputs:
|
||||
bazel-output-base:
|
||||
description: Filesystem path used for Bazel's output base.
|
||||
value: ${{ steps.configure_ci_build_paths.outputs.bazel-output-base }}
|
||||
cargo-target-dir:
|
||||
description: Filesystem path used for Cargo's target directory.
|
||||
value: ${{ steps.configure_ci_build_paths.outputs.cargo-target-dir }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
# setup-ci expects either this step or the Unix fallback below to define
|
||||
# CI_BUILD_ROOT. Windows puts it on a Dev Drive because Cargo and Bazel
|
||||
# spend significant time reading and writing build/cache trees.
|
||||
- name: Configure Dev Drive (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: ./.github/scripts/setup-dev-drive.ps1
|
||||
|
||||
- name: Configure CI build root (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: echo "CI_BUILD_ROOT=$HOME/.cache/codex-ci" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Configure CI build paths
|
||||
id: configure_ci_build_paths
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# setup-bazel passes output_base explicitly, so keep both it and the
|
||||
# user root under the shared build root. Keep these directory names tiny
|
||||
# on every platform so Windows Bazel paths do not overflow argv or
|
||||
# confuse test MANIFEST handling.
|
||||
bazel_output_base="$CI_BUILD_ROOT/o"
|
||||
bazel_output_user_root="$CI_BUILD_ROOT/b"
|
||||
bazel_repository_cache="$CI_BUILD_ROOT/bazel-repository-cache"
|
||||
bazel_repo_contents_cache="$CI_BUILD_ROOT/bazel-repo-contents-cache-$GITHUB_RUN_ID-$GITHUB_JOB"
|
||||
cargo_target_dir="$CI_BUILD_ROOT/cargo-target"
|
||||
tmp="$CI_BUILD_ROOT/tmp"
|
||||
|
||||
build_dirs=(
|
||||
"$bazel_output_base"
|
||||
"$bazel_output_user_root"
|
||||
"$bazel_repository_cache"
|
||||
"$bazel_repo_contents_cache"
|
||||
"$cargo_target_dir"
|
||||
"$tmp"
|
||||
)
|
||||
mkdir -p "${build_dirs[@]}"
|
||||
echo "bazel-output-base=$bazel_output_base" >> "$GITHUB_OUTPUT"
|
||||
echo "cargo-target-dir=$cargo_target_dir" >> "$GITHUB_OUTPUT"
|
||||
|
||||
{
|
||||
echo "BAZEL_OUTPUT_BASE=$bazel_output_base"
|
||||
echo "BAZEL_OUTPUT_USER_ROOT=$bazel_output_user_root"
|
||||
echo "BAZEL_REPOSITORY_CACHE=$bazel_repository_cache"
|
||||
echo "BAZEL_REPO_CONTENTS_CACHE=$bazel_repo_contents_cache"
|
||||
echo "CARGO_TARGET_DIR=$cargo_target_dir"
|
||||
echo "TEMP=$tmp"
|
||||
echo "TMP=$tmp"
|
||||
} >> "$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>
|
||||
|
||||
@@ -131,25 +131,54 @@ def bazel_args_without_remote_execution(args: Sequence[str]) -> list[str]:
|
||||
def bazel_args_with_remote_config(
|
||||
args: Sequence[str], env: Mapping[str, str]
|
||||
) -> list[str]:
|
||||
command_idx = next(
|
||||
(idx for idx, arg in enumerate(args) if not arg.startswith("-")),
|
||||
None,
|
||||
)
|
||||
if command_idx is None:
|
||||
raise ValueError("expected a Bazel command")
|
||||
|
||||
config = remote_config(args, env)
|
||||
if config is None:
|
||||
return bazel_args_without_remote_execution(args)
|
||||
configured_args = bazel_args_without_remote_execution(args)
|
||||
else:
|
||||
# `remote_config()` returns a configuration only when this key is present.
|
||||
api_key = env["BUILDBUDDY_API_KEY"]
|
||||
remote_args = [
|
||||
f"--config={config}",
|
||||
f"--remote_header=x-buildbuddy-api-key={api_key}",
|
||||
]
|
||||
|
||||
# `remote_config()` returns a configuration only when this key is present.
|
||||
api_key = env["BUILDBUDDY_API_KEY"]
|
||||
remote_args = [
|
||||
f"--config={config}",
|
||||
f"--remote_header=x-buildbuddy-api-key={api_key}",
|
||||
# Insert immediately after the Bazel command. This keeps wrapper-added
|
||||
# options out of positional payloads and lets later CI configs override
|
||||
# shared RBE defaults such as the Windows cross-compilation exec platforms.
|
||||
configured_args = [
|
||||
*args[: command_idx + 1],
|
||||
*remote_args,
|
||||
*args[command_idx + 1 :],
|
||||
]
|
||||
|
||||
try:
|
||||
separator_idx = configured_args.index("--")
|
||||
except ValueError:
|
||||
separator_idx = len(configured_args)
|
||||
|
||||
cache_args = [
|
||||
f"{option_prefix}{env[env_name]}"
|
||||
for env_name, option_prefix in (
|
||||
("BAZEL_REPO_CONTENTS_CACHE", "--repo_contents_cache="),
|
||||
("BAZEL_REPOSITORY_CACHE", "--repository_cache="),
|
||||
)
|
||||
if env.get(env_name)
|
||||
and not any(
|
||||
arg.startswith(option_prefix) for arg in configured_args[:separator_idx]
|
||||
)
|
||||
]
|
||||
return [
|
||||
*configured_args[:separator_idx],
|
||||
*cache_args,
|
||||
*configured_args[separator_idx:],
|
||||
]
|
||||
|
||||
# Insert immediately after the Bazel command. This keeps wrapper-added
|
||||
# options out of positional payloads and lets later CI configs override
|
||||
# shared RBE defaults such as the Windows cross-compilation exec platforms.
|
||||
insertion_idx = next(
|
||||
(idx + 1 for idx, arg in enumerate(args) if not arg.startswith("-")),
|
||||
len(args),
|
||||
)
|
||||
return [*args[:insertion_idx], *remote_args, *args[insertion_idx:]]
|
||||
|
||||
|
||||
def bazel_command(*args: str, env: Mapping[str, str] | None = None) -> list[str]:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
# Configure a fast drive for Windows CI jobs.
|
||||
#
|
||||
# GitHub-hosted Windows runners do not always expose a secondary D: volume. When
|
||||
# they do not, try to create a Dev Drive VHD and fall back to C: if the runner
|
||||
# image does not allow that provisioning path.
|
||||
# they do not, create a Dev Drive VHD. CI depends on this path for its
|
||||
# build directories where CI spends significant time doing I/O, so fail the
|
||||
# job if no real Dev Drive is available.
|
||||
|
||||
function Use-FallbackDrive {
|
||||
param([string]$Reason)
|
||||
function Test-DevDrive {
|
||||
param([string]$Drive)
|
||||
|
||||
Write-Warning "$Reason Falling back to C:"
|
||||
return "C:"
|
||||
& fsutil devdrv query $Drive *> $null
|
||||
return $LASTEXITCODE -eq 0
|
||||
}
|
||||
|
||||
function Invoke-BestEffort {
|
||||
@@ -21,10 +22,14 @@ function Invoke-BestEffort {
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Path "D:\") {
|
||||
Write-Output "Using existing drive at D:"
|
||||
if ((Test-Path "D:\") -and (Test-DevDrive "D:")) {
|
||||
Write-Output "Using existing Dev Drive at D:"
|
||||
$Drive = "D:"
|
||||
} else {
|
||||
if (Test-Path "D:\") {
|
||||
Write-Output "Existing D: volume is not a Dev Drive; provisioning a new Dev Drive VHD."
|
||||
}
|
||||
|
||||
try {
|
||||
$VhdPath = Join-Path $env:RUNNER_TEMP "codex-dev-drive.vhdx"
|
||||
$SizeBytes = 64GB
|
||||
@@ -42,21 +47,17 @@ if (Test-Path "D:\") {
|
||||
|
||||
$Drive = "$($Volume.DriveLetter):"
|
||||
|
||||
if (-not (Test-DevDrive $Drive)) {
|
||||
throw "Provisioned volume at $Drive did not pass Dev Drive verification."
|
||||
}
|
||||
|
||||
Invoke-BestEffort { fsutil devdrv trust $Drive } "Trusting Dev Drive $Drive"
|
||||
Invoke-BestEffort { fsutil devdrv enable /disallowAv } "Disabling AV filter attachment for Dev Drives"
|
||||
Invoke-BestEffort { fsutil devdrv query $Drive } "Querying Dev Drive $Drive"
|
||||
|
||||
Write-Output "Using Dev Drive at $Drive"
|
||||
} catch {
|
||||
$Drive = Use-FallbackDrive "Failed to create Dev Drive: $($_.Exception.Message)"
|
||||
throw "Failed to create Dev Drive: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
$Tmp = "$Drive\codex-tmp"
|
||||
New-Item -Path $Tmp -ItemType Directory -Force | Out-Null
|
||||
|
||||
@(
|
||||
"DEV_DRIVE=$Drive"
|
||||
"TMP=$Tmp"
|
||||
"TEMP=$Tmp"
|
||||
) | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
"CI_BUILD_ROOT=$Drive" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
|
||||
@@ -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()
|
||||
@@ -214,6 +214,48 @@ class RunBazelWithBuildBuddyTest(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_bazel_command_uses_configured_local_caches(self) -> None:
|
||||
env = {
|
||||
"BAZEL_REPO_CONTENTS_CACHE": "/tmp/bazel-repo-contents",
|
||||
"BAZEL_REPOSITORY_CACHE": "/tmp/bazel-repository",
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
run_bazel_with_buildbuddy.bazel_command(
|
||||
"build",
|
||||
"--config=local",
|
||||
"//codex-rs/...",
|
||||
env=env,
|
||||
),
|
||||
[
|
||||
"bazel",
|
||||
"build",
|
||||
"--config=local",
|
||||
"//codex-rs/...",
|
||||
"--repo_contents_cache=/tmp/bazel-repo-contents",
|
||||
"--repository_cache=/tmp/bazel-repository",
|
||||
],
|
||||
)
|
||||
|
||||
def test_bazel_command_adds_local_caches_before_separator(self) -> None:
|
||||
self.assertEqual(
|
||||
run_bazel_with_buildbuddy.bazel_command(
|
||||
"build",
|
||||
"//codex-rs/...",
|
||||
"--",
|
||||
"--program-arg",
|
||||
env={"BAZEL_REPOSITORY_CACHE": "/tmp/bazel-repository"},
|
||||
),
|
||||
[
|
||||
"bazel",
|
||||
"build",
|
||||
"//codex-rs/...",
|
||||
"--repository_cache=/tmp/bazel-repository",
|
||||
"--",
|
||||
"--program-arg",
|
||||
],
|
||||
)
|
||||
|
||||
def test_main_preserves_spaced_argument_and_child_exit_status(self) -> None:
|
||||
spaced_arg = (
|
||||
r"--test_env=PATH=C:\Program Files\PowerShell\7;C:\Program Files\Git\bin"
|
||||
|
||||
@@ -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,18 @@ version = "149.2.0"
|
||||
)
|
||||
)
|
||||
|
||||
def test_shared_ci_setup_changes_require_canary_and_source_build(self) -> None:
|
||||
for path in (
|
||||
".github/actions/setup-ci/action.yml",
|
||||
".github/scripts/setup-dev-drive.ps1",
|
||||
):
|
||||
with self.subTest(path=path):
|
||||
changed_files = {path}
|
||||
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,9 +21,11 @@ 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",
|
||||
".github/scripts/setup-dev-drive.ps1",
|
||||
".github/scripts/v8_canary_changes.py",
|
||||
".github/workflows/postmerge-ci.yml",
|
||||
".github/workflows/rusty-v8-release.yml",
|
||||
@@ -40,8 +42,10 @@ 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/setup-dev-drive.ps1",
|
||||
".github/scripts/v8_canary_changes.py",
|
||||
".github/workflows/rusty-v8-release.yml",
|
||||
".github/workflows/v8-canary.yml",
|
||||
@@ -72,6 +76,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 +109,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 +178,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
|
||||
|
||||
@@ -28,6 +30,9 @@ jobs:
|
||||
- name: Test Codex package builder
|
||||
run: python3 -m unittest discover -s scripts/codex_package -p 'test_*.py'
|
||||
|
||||
- name: Test standalone installer
|
||||
run: python3 -m unittest discover -s scripts/install -p 'test_*.py'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
|
||||
with:
|
||||
@@ -73,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
|
||||
@@ -76,10 +71,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure Dev Drive (Windows)
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
shell: pwsh
|
||||
run: ../.github/scripts/setup-dev-drive.ps1
|
||||
- uses: ./.github/actions/setup-ci
|
||||
|
||||
- name: Install Linux build dependencies
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
@@ -91,9 +83,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 }}
|
||||
@@ -142,11 +131,7 @@ jobs:
|
||||
echo "Using sccache GitHub backend"
|
||||
else
|
||||
echo "SCCACHE_GHA_ENABLED=false" >> "$GITHUB_ENV"
|
||||
if [[ -n "${DEV_DRIVE:-}" ]]; then
|
||||
echo "SCCACHE_DIR=${DEV_DRIVE}\\.sccache" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "SCCACHE_DIR=${{ github.workspace }}/.sccache" >> "$GITHUB_ENV"
|
||||
fi
|
||||
echo "SCCACHE_DIR=${CI_BUILD_ROOT}/sccache" >> "$GITHUB_ENV"
|
||||
echo "Using sccache local disk + actions/cache fallback"
|
||||
fi
|
||||
|
||||
@@ -212,7 +197,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 +205,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 +214,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 +291,8 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: ./.github/actions/setup-ci
|
||||
|
||||
- name: Install Linux build dependencies
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
shell: bash
|
||||
@@ -316,9 +303,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 +360,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 +414,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
|
||||
|
||||
@@ -160,8 +160,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/bazel-repo-cache
|
||||
path: ${{ steps.setup_bazel.outputs.repository-cache-path }}
|
||||
key: bazel-cache-x86_64-unknown-linux-gnu-${{ hashFiles('MODULE.bazel', 'codex-rs/Cargo.lock', 'codex-rs/Cargo.toml') }}
|
||||
|
||||
- name: Check for a clean worktree
|
||||
|
||||
@@ -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' }}
|
||||
@@ -317,14 +312,17 @@ jobs:
|
||||
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
|
||||
|
||||
- id: setup_ci
|
||||
uses: ./.github/actions/setup-ci
|
||||
|
||||
- name: Configure git for upstream checkout
|
||||
shell: bash
|
||||
run: git config --global core.symlinks true
|
||||
@@ -367,17 +365,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') }}
|
||||
${{ steps.setup_ci.outputs.cargo-target-dir }}/sccache
|
||||
${{ steps.setup_ci.outputs.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: ${{ steps.setup_ci.outputs.cargo-target-dir }}/sccache
|
||||
SCCACHE_IDLE_TIMEOUT: 0
|
||||
run: |
|
||||
$version = "v0.8.2"
|
||||
@@ -412,7 +410,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
|
||||
@@ -253,7 +253,7 @@ Use `just bench-smoke` to dry-run the benchmark for a single iteration to ensure
|
||||
|
||||
- Tests should exercise app-server's public JSON-RPC API.
|
||||
- Use similar server mocking as for core integration tests.
|
||||
- Use `TestAppServer::new_with_auto_env()` and `TestAppServer::send_thread_start_request_with_auto_env()`
|
||||
- Use `TestAppServer::builder().build()` and `TestAppServer::send_thread_start_request_with_auto_env()`
|
||||
by default to ensure that new tests work with foreign app/exec OSes. See `$remote-tests` for
|
||||
details.
|
||||
|
||||
|
||||
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\":{}}",
|
||||
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
# https://git-cliff.org/docs/configuration
|
||||
|
||||
[changelog]
|
||||
header = """
|
||||
# Changelog
|
||||
|
||||
You can install any of these versions: `npm install -g @openai/codex@<version>`
|
||||
"""
|
||||
|
||||
body = """
|
||||
{% if version -%}
|
||||
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{%- else %}
|
||||
## [unreleased]
|
||||
{% endif %}
|
||||
|
||||
{%- for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | striptags | trim }}
|
||||
|
||||
{% for commit in commits %}- {% if commit.scope %}*({{ commit.scope }})* {% endif %}{% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}
|
||||
{% endfor %}
|
||||
|
||||
{%- endfor -%}
|
||||
"""
|
||||
|
||||
footer = """
|
||||
<!-- generated - do not edit -->
|
||||
"""
|
||||
|
||||
trim = true
|
||||
postprocessors = []
|
||||
|
||||
[git]
|
||||
conventional_commits = true
|
||||
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
|
||||
{ message = "^fix", group = "<!-- 1 -->🪲 Bug Fixes" },
|
||||
{ message = "^bump", group = "<!-- 6 -->🛳️ Release" },
|
||||
# Fallback – skip anything that didn't match the above rules.
|
||||
{ message = ".*", group = "<!-- 10 -->💼 Other" },
|
||||
]
|
||||
|
||||
filter_unconventional = false
|
||||
sort_commits = "oldest"
|
||||
topo_order = false
|
||||
+50
-5
@@ -11,6 +11,7 @@ import { fileURLToPath } from "url";
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const require = createRequire(import.meta.url);
|
||||
const codexPackageRoot = realpathSync(path.join(__dirname, ".."));
|
||||
|
||||
const PLATFORM_PACKAGE_BY_TARGET = {
|
||||
"x86_64-unknown-linux-musl": "@openai/codex-linux-x64",
|
||||
@@ -98,7 +99,9 @@ function findCodexExecutable() {
|
||||
const updateCommand =
|
||||
packageManager === "bun"
|
||||
? "bun install -g @openai/codex@latest"
|
||||
: "npm install -g @openai/codex@latest";
|
||||
: packageManager === "pnpm"
|
||||
? "pnpm add -g @openai/codex@latest"
|
||||
: "npm install -g @openai/codex@latest";
|
||||
throw new Error(
|
||||
`Missing optional dependency ${platformPackage}. Reinstall Codex: ${updateCommand}`,
|
||||
);
|
||||
@@ -112,11 +115,47 @@ const binaryPath = findCodexExecutable();
|
||||
// and guarantees that when either the child terminates or the parent
|
||||
// receives a fatal signal, both processes exit in a predictable manner.
|
||||
|
||||
function isPnpmOwnedCodexInstall(nodeModulesDir) {
|
||||
if (!existsSync(path.join(nodeModulesDir, ".modules.yaml"))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return (
|
||||
realpathSync(path.join(nodeModulesDir, "@openai", "codex")) ===
|
||||
codexPackageRoot
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use heuristics to detect the package manager that was used to install Codex
|
||||
* in order to give the user a hint about how to update it.
|
||||
*/
|
||||
function detectPackageManager() {
|
||||
// pnpm's owning node_modules directory can be several parents above the
|
||||
// package in isolated global layouts. Search ancestors of both the canonical
|
||||
// package root and lexical entrypoint because pnpm may link either path.
|
||||
const entrypointDir = path.dirname(path.resolve(process.argv[1]));
|
||||
for (const startDir of new Set([codexPackageRoot, entrypointDir])) {
|
||||
const filesystemRoot = path.parse(startDir).root;
|
||||
for (
|
||||
let currentDir = startDir;
|
||||
currentDir !== filesystemRoot;
|
||||
currentDir = path.dirname(currentDir)
|
||||
) {
|
||||
if (isPnpmOwnedCodexInstall(path.join(currentDir, "node_modules"))) {
|
||||
return "pnpm";
|
||||
}
|
||||
}
|
||||
|
||||
if (isPnpmOwnedCodexInstall(path.join(filesystemRoot, "node_modules"))) {
|
||||
return "pnpm";
|
||||
}
|
||||
}
|
||||
|
||||
const userAgent = process.env.npm_config_user_agent || "";
|
||||
if (/\bbun\//.test(userAgent)) {
|
||||
return "bun";
|
||||
@@ -137,15 +176,21 @@ function detectPackageManager() {
|
||||
return userAgent ? "npm" : null;
|
||||
}
|
||||
|
||||
const packageManager = detectPackageManager();
|
||||
const packageManagerEnvVar =
|
||||
detectPackageManager() === "bun"
|
||||
packageManager === "bun"
|
||||
? "CODEX_MANAGED_BY_BUN"
|
||||
: "CODEX_MANAGED_BY_NPM";
|
||||
: packageManager === "pnpm"
|
||||
? "CODEX_MANAGED_BY_PNPM"
|
||||
: "CODEX_MANAGED_BY_NPM";
|
||||
const env = {
|
||||
...process.env,
|
||||
[packageManagerEnvVar]: "1",
|
||||
CODEX_MANAGED_PACKAGE_ROOT: realpathSync(path.join(__dirname, "..")),
|
||||
CODEX_MANAGED_PACKAGE_ROOT: codexPackageRoot,
|
||||
};
|
||||
delete env.CODEX_MANAGED_BY_NPM;
|
||||
delete env.CODEX_MANAGED_BY_BUN;
|
||||
delete env.CODEX_MANAGED_BY_PNPM;
|
||||
env[packageManagerEnvVar] = "1";
|
||||
|
||||
const child = spawn(binaryPath, process.argv.slice(2), {
|
||||
stdio: "inherit",
|
||||
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
name: Cargo audit
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: codex-rs
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
|
||||
- name: Install cargo-audit
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-audit
|
||||
- name: Run cargo audit
|
||||
run: cargo audit --deny warnings
|
||||
Generated
+88
-29
@@ -409,10 +409,14 @@ dependencies = [
|
||||
"codex-protocol",
|
||||
"codex-utils-cargo-bin",
|
||||
"core_test_support",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shlex",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"url",
|
||||
"uuid",
|
||||
"wiremock",
|
||||
]
|
||||
@@ -1936,8 +1940,10 @@ dependencies = [
|
||||
"bytes",
|
||||
"chrono",
|
||||
"codex-client",
|
||||
"codex-http-client",
|
||||
"codex-protocol",
|
||||
"codex-utils-rustls-provider",
|
||||
"codex-websocket-client",
|
||||
"eventsource-stream",
|
||||
"futures",
|
||||
"http 1.4.0",
|
||||
@@ -1992,6 +1998,7 @@ dependencies = [
|
||||
"codex-guardian",
|
||||
"codex-home",
|
||||
"codex-hooks",
|
||||
"codex-http-client",
|
||||
"codex-image-generation-extension",
|
||||
"codex-login",
|
||||
"codex-mcp",
|
||||
@@ -2106,6 +2113,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"codex-experimental-api-macros",
|
||||
"codex-extension-items",
|
||||
"codex-protocol",
|
||||
"codex-shell-command",
|
||||
"codex-utils-absolute-path",
|
||||
@@ -2262,7 +2270,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"codex-api",
|
||||
"codex-backend-openapi-models",
|
||||
"codex-client",
|
||||
"codex-http-client",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-protocol",
|
||||
@@ -2392,33 +2400,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 +2439,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 +2513,7 @@ dependencies = [
|
||||
"codex-utils-cargo-bin",
|
||||
"pretty_assertions",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
@@ -2660,12 +2648,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",
|
||||
@@ -2770,6 +2760,7 @@ dependencies = [
|
||||
"codex-extension-api",
|
||||
"codex-features",
|
||||
"codex-home",
|
||||
"codex-image-generation-extension",
|
||||
"codex-login",
|
||||
"codex-model-provider-info",
|
||||
"codex-models-manager",
|
||||
@@ -2820,6 +2811,7 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
"tracing-test",
|
||||
"url",
|
||||
"which 8.0.0",
|
||||
"wiremock",
|
||||
"zip 2.4.2",
|
||||
]
|
||||
@@ -2915,9 +2907,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 +3023,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"
|
||||
@@ -3077,6 +3081,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"codex-login",
|
||||
"codex-protocol",
|
||||
"mime_guess",
|
||||
"pretty_assertions",
|
||||
"sentry",
|
||||
"tracing",
|
||||
@@ -3212,6 +3217,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"
|
||||
@@ -3221,6 +3257,7 @@ dependencies = [
|
||||
"codex-core",
|
||||
"codex-exec-server",
|
||||
"codex-extension-api",
|
||||
"codex-extension-items",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-model-provider-info",
|
||||
@@ -3263,6 +3300,7 @@ dependencies = [
|
||||
"clap",
|
||||
"codex-core",
|
||||
"codex-install-context",
|
||||
"codex-network-proxy",
|
||||
"codex-process-hardening",
|
||||
"codex-protocol",
|
||||
"codex-sandboxing",
|
||||
@@ -3302,8 +3340,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",
|
||||
@@ -3406,6 +3444,7 @@ dependencies = [
|
||||
"codex-exec-server",
|
||||
"codex-extension-api",
|
||||
"codex-home",
|
||||
"codex-image-generation-extension",
|
||||
"codex-login",
|
||||
"codex-protocol",
|
||||
"codex-shell-command",
|
||||
@@ -3517,8 +3556,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",
|
||||
@@ -3555,6 +3594,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"codex-collaboration-mode-templates",
|
||||
"codex-http-client",
|
||||
"codex-login",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
@@ -3699,6 +3739,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"codex-async-utils",
|
||||
"codex-execpolicy",
|
||||
"codex-extension-items",
|
||||
"codex-network-proxy",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-image",
|
||||
@@ -4056,6 +4097,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-code-mode",
|
||||
"codex-connectors",
|
||||
"codex-extension-items",
|
||||
"codex-features",
|
||||
"codex-file-system",
|
||||
"codex-protocol",
|
||||
@@ -4438,6 +4480,7 @@ dependencies = [
|
||||
"codex-api",
|
||||
"codex-core",
|
||||
"codex-extension-api",
|
||||
"codex-extension-items",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-model-provider-info",
|
||||
@@ -4450,6 +4493,22 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-websocket-client"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-http-client",
|
||||
"codex-utils-rustls-provider",
|
||||
"futures",
|
||||
"pretty_assertions",
|
||||
"rcgen",
|
||||
"rustls",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-tungstenite",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-windows-sandbox"
|
||||
version = "0.0.0"
|
||||
@@ -4820,9 +4879,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",
|
||||
@@ -88,6 +90,7 @@ members = [
|
||||
"tui",
|
||||
"tools",
|
||||
"v8-poc",
|
||||
"websocket-client",
|
||||
"utils/absolute-path",
|
||||
"utils/path-uri",
|
||||
"utils/cargo-bin",
|
||||
@@ -164,6 +167,8 @@ 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-websocket-client = { path = "websocket-client" }
|
||||
codex-config = { path = "config" }
|
||||
codex-connectors = { path = "connectors" }
|
||||
codex-connectors-extension = { path = "ext/connectors" }
|
||||
@@ -178,6 +183,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" }
|
||||
@@ -423,6 +429,7 @@ thiserror = "2.0.17"
|
||||
time = "0.3.47"
|
||||
tiny_http = "0.12"
|
||||
tokio = "1"
|
||||
tokio-rustls = "0.26.4"
|
||||
tokio-stream = "0.1.18"
|
||||
tokio-test = "0.4"
|
||||
tokio-tungstenite = { version = "0.28.0", features = [
|
||||
|
||||
@@ -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;
|
||||
@@ -139,6 +140,7 @@ use codex_app_server_protocol::TurnStatus as AppServerTurnStatus;
|
||||
use codex_app_server_protocol::TurnSteerParams;
|
||||
use codex_app_server_protocol::TurnSteerResponse;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
use codex_app_server_protocol::WebSearchItem;
|
||||
use codex_login::default_client::DEFAULT_ORIGINATOR;
|
||||
use codex_login::default_client::originator;
|
||||
use codex_plugin::AppConnectorId;
|
||||
@@ -4400,18 +4402,18 @@ async fn turn_event_counts_completed_tool_items() {
|
||||
agent_thread_id: "thread-child".to_string(),
|
||||
agent_path: "/root/child".to_string(),
|
||||
},
|
||||
ThreadItem::WebSearch {
|
||||
ThreadItem::WebSearch(WebSearchItem {
|
||||
id: "web-1".to_string(),
|
||||
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 {
|
||||
|
||||
@@ -402,8 +402,8 @@ impl TurnToolCounts {
|
||||
ThreadItem::CollabAgentToolCall { .. } | ThreadItem::SubAgentActivity { .. } => {
|
||||
self.subagent_tool_call += 1;
|
||||
}
|
||||
ThreadItem::WebSearch { .. } => self.web_search += 1,
|
||||
ThreadItem::ImageGeneration { .. } => self.image_generation += 1,
|
||||
ThreadItem::WebSearch(_) => self.web_search += 1,
|
||||
ThreadItem::ImageGeneration(_) => self.image_generation += 1,
|
||||
ThreadItem::UserMessage { .. }
|
||||
| ThreadItem::HookPrompt { .. }
|
||||
| ThreadItem::AgentMessage { .. }
|
||||
@@ -1732,9 +1732,9 @@ fn tracked_tool_item_id(item: &ThreadItem) -> Option<&str> {
|
||||
| ThreadItem::FileChange { id, .. }
|
||||
| ThreadItem::McpToolCall { id, .. }
|
||||
| ThreadItem::DynamicToolCall { id, .. }
|
||||
| ThreadItem::CollabAgentToolCall { id, .. }
|
||||
| ThreadItem::WebSearch { id, .. }
|
||||
| ThreadItem::ImageGeneration { id, .. } => Some(id),
|
||||
| ThreadItem::CollabAgentToolCall { id, .. } => Some(id),
|
||||
ThreadItem::WebSearch(item) => Some(&item.id),
|
||||
ThreadItem::ImageGeneration(item) => Some(&item.id),
|
||||
ThreadItem::UserMessage { .. }
|
||||
| ThreadItem::HookPrompt { .. }
|
||||
| ThreadItem::AgentMessage { .. }
|
||||
@@ -2027,11 +2027,11 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
|
||||
},
|
||||
))
|
||||
}
|
||||
ThreadItem::WebSearch { id, query, action } => {
|
||||
ThreadItem::WebSearch(item) => {
|
||||
let base = tool_item_base(
|
||||
thread_id,
|
||||
turn_id,
|
||||
id.clone(),
|
||||
item.id.clone(),
|
||||
"web_search".to_string(),
|
||||
ToolItemOutcome {
|
||||
terminal_status: ToolItemTerminalStatus::Completed,
|
||||
@@ -2051,24 +2051,18 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
|
||||
event_type: "codex_web_search_event",
|
||||
event_params: CodexWebSearchEventParams {
|
||||
base,
|
||||
web_search_action: action.as_ref().map(web_search_action_kind),
|
||||
query_present: !query.trim().is_empty(),
|
||||
query_count: web_search_query_count(query, action.as_ref()),
|
||||
web_search_action: item.action.as_ref().map(web_search_action_kind),
|
||||
query_present: !item.query.trim().is_empty(),
|
||||
query_count: web_search_query_count(&item.query, item.action.as_ref()),
|
||||
},
|
||||
}))
|
||||
}
|
||||
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": [
|
||||
|
||||
+135
@@ -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"
|
||||
},
|
||||
@@ -14350,6 +14403,14 @@
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/v2/PluginSource"
|
||||
},
|
||||
"version": {
|
||||
"default": null,
|
||||
"description": "Version advertised by the remote marketplace backend when available.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -14525,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": [
|
||||
@@ -14537,6 +14665,13 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RateLimitResetType": {
|
||||
"enum": [
|
||||
"codexRateLimits",
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"RateLimitSnapshot": {
|
||||
"properties": {
|
||||
"credits": {
|
||||
|
||||
+135
@@ -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"
|
||||
},
|
||||
@@ -10754,6 +10807,14 @@
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/PluginSource"
|
||||
},
|
||||
"version": {
|
||||
"default": null,
|
||||
"description": "Version advertised by the remote marketplace backend when available.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -10929,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": [
|
||||
@@ -10941,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"
|
||||
},
|
||||
@@ -540,6 +557,14 @@
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/PluginSource"
|
||||
},
|
||||
"version": {
|
||||
"default": null,
|
||||
"description": "Version advertised by the remote marketplace backend when available.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"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"
|
||||
},
|
||||
@@ -540,6 +557,14 @@
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/PluginSource"
|
||||
},
|
||||
"version": {
|
||||
"default": null,
|
||||
"description": "Version advertised by the remote marketplace backend when available.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
@@ -680,6 +697,14 @@
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/PluginSource"
|
||||
},
|
||||
"version": {
|
||||
"default": null,
|
||||
"description": "Version advertised by the remote marketplace backend when available.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
@@ -496,6 +513,14 @@
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/PluginSource"
|
||||
},
|
||||
"version": {
|
||||
"default": null,
|
||||
"description": "Version advertised by the remote marketplace backend when available.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
+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, };
|
||||
@@ -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 { WebSearchAction } from "./v2/WebSearchAction";
|
||||
|
||||
export type WebSearchItem = { id: string, query: string, action: WebSearchAction | null, };
|
||||
@@ -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";
|
||||
@@ -81,6 +82,7 @@ export type { Tool } from "./Tool";
|
||||
export type { Verbosity } from "./Verbosity";
|
||||
export type { WebSearchAction } from "./WebSearchAction";
|
||||
export type { WebSearchContextSize } from "./WebSearchContextSize";
|
||||
export type { WebSearchItem } from "./WebSearchItem";
|
||||
export type { WebSearchLocation } from "./WebSearchLocation";
|
||||
export type { WebSearchMode } from "./WebSearchMode";
|
||||
export type { WebSearchToolConfig } from "./WebSearchToolConfig";
|
||||
|
||||
@@ -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";
|
||||
@@ -13,6 +14,10 @@ export type PluginSummary = { id: string,
|
||||
* Backend remote plugin identifier when available.
|
||||
*/
|
||||
remotePluginId: string | null,
|
||||
/**
|
||||
* Version advertised by the remote marketplace backend when available.
|
||||
*/
|
||||
version: string | null,
|
||||
/**
|
||||
* Version of the locally materialized plugin package when available.
|
||||
*/
|
||||
@@ -20,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,10 +1,11 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
import type { ImageGenerationItem } from "../ImageGenerationItem";
|
||||
import type { LegacyAppPathString } from "../LegacyAppPathString";
|
||||
import type { MessagePhase } from "../MessagePhase";
|
||||
import type { ReasoningEffort } from "../ReasoningEffort";
|
||||
import type { WebSearchItem } from "../WebSearchItem";
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
import type { CollabAgentState } from "./CollabAgentState";
|
||||
import type { CollabAgentTool } from "./CollabAgentTool";
|
||||
@@ -24,7 +25,6 @@ import type { MemoryCitation } from "./MemoryCitation";
|
||||
import type { PatchApplyStatus } from "./PatchApplyStatus";
|
||||
import type { SubAgentActivityKind } from "./SubAgentActivityKind";
|
||||
import type { UserInput } from "./UserInput";
|
||||
import type { WebSearchAction } from "./WebSearchAction";
|
||||
|
||||
export type ThreadItem = { "type": "userMessage", id: string, clientId: string | null, content: Array<UserInput>, } | { "type": "hookPrompt", id: string, fragments: Array<HookPromptFragment>, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, memoryCitation: MemoryCitation | null, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array<string>, content: Array<string>, } | { "type": "commandExecution", id: string,
|
||||
/**
|
||||
@@ -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" } & WebSearchItem | { "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,
|
||||
|
||||
@@ -33,6 +33,9 @@ use codex_protocol::protocol::GuardianAssessmentAction;
|
||||
use codex_protocol::protocol::GuardianAssessmentEvent;
|
||||
use codex_protocol::protocol::PatchApplyBeginEvent;
|
||||
use codex_protocol::protocol::PatchApplyEndEvent;
|
||||
use codex_protocol::protocol::ReviewOutputEvent;
|
||||
use codex_protocol::review_format::REVIEW_FALLBACK_MESSAGE;
|
||||
use codex_protocol::review_format::render_review_output_text;
|
||||
use codex_shell_command::parse_command::parse_command;
|
||||
use codex_shell_command::parse_command::shlex_join;
|
||||
use codex_utils_path_uri::PathConvention;
|
||||
@@ -41,6 +44,12 @@ use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use tracing::warn;
|
||||
|
||||
pub(crate) fn review_output_text(output: Option<&ReviewOutputEvent>) -> String {
|
||||
output
|
||||
.map(render_review_output_text)
|
||||
.unwrap_or_else(|| REVIEW_FALLBACK_MESSAGE.to_string())
|
||||
}
|
||||
|
||||
pub fn build_file_change_approval_request_item(
|
||||
payload: &ApplyPatchApprovalRequestEvent,
|
||||
) -> ThreadItem {
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::protocol::item_builders::build_file_change_approval_request_item;
|
||||
use crate::protocol::item_builders::build_file_change_begin_item;
|
||||
use crate::protocol::item_builders::build_file_change_end_item;
|
||||
use crate::protocol::item_builders::build_item_from_guardian_event;
|
||||
use crate::protocol::item_builders::review_output_text;
|
||||
use crate::protocol::v2::CollabAgentState;
|
||||
use crate::protocol::v2::CollabAgentTool;
|
||||
use crate::protocol::v2::CollabAgentToolCallStatus;
|
||||
@@ -21,7 +22,11 @@ use crate::protocol::v2::TurnError;
|
||||
use crate::protocol::v2::TurnItemsView;
|
||||
use crate::protocol::v2::TurnStatus;
|
||||
use crate::protocol::v2::UserInput;
|
||||
#[cfg(test)]
|
||||
use crate::protocol::v2::WebSearchAction;
|
||||
use crate::protocol::v2::WebSearchItem;
|
||||
use crate::protocol::v2::web_search_action_from_core;
|
||||
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;
|
||||
@@ -45,7 +50,6 @@ use codex_protocol::protocol::McpToolCallBeginEvent;
|
||||
use codex_protocol::protocol::McpToolCallEndEvent;
|
||||
use codex_protocol::protocol::PatchApplyBeginEvent;
|
||||
use codex_protocol::protocol::PatchApplyEndEvent;
|
||||
use codex_protocol::protocol::ReviewOutputEvent;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::ThreadRolledBackEvent;
|
||||
use codex_protocol::protocol::TurnAbortedEvent;
|
||||
@@ -55,6 +59,8 @@ use codex_protocol::protocol::UserMessageEvent;
|
||||
use codex_protocol::protocol::ViewImageToolCallEvent;
|
||||
use codex_protocol::protocol::WebSearchBeginEvent;
|
||||
use codex_protocol::protocol::WebSearchEndEvent;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::review_format::REVIEW_FALLBACK_MESSAGE;
|
||||
use std::collections::HashMap;
|
||||
use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
@@ -586,15 +592,23 @@ impl ThreadHistoryBuilder {
|
||||
turn_id: &str,
|
||||
item: &codex_protocol::items::TurnItem,
|
||||
) {
|
||||
let is_review_mode_item = matches!(
|
||||
item,
|
||||
codex_protocol::items::TurnItem::EnteredReviewMode(_)
|
||||
| codex_protocol::items::TurnItem::ExitedReviewMode(_)
|
||||
);
|
||||
let should_upsert = match item {
|
||||
codex_protocol::items::TurnItem::Plan(plan) => !plan.text.is_empty(),
|
||||
codex_protocol::items::TurnItem::Sleep(_)
|
||||
| codex_protocol::items::TurnItem::HookPrompt(_)
|
||||
| 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(_)
|
||||
| codex_protocol::items::TurnItem::EnteredReviewMode(_)
|
||||
| codex_protocol::items::TurnItem::ExitedReviewMode(_) => true,
|
||||
codex_protocol::items::TurnItem::UserMessage(_)
|
||||
| codex_protocol::items::TurnItem::HookPrompt(_)
|
||||
| codex_protocol::items::TurnItem::AgentMessage(_)
|
||||
| codex_protocol::items::TurnItem::Reasoning(_)
|
||||
| codex_protocol::items::TurnItem::WebSearch(_)
|
||||
@@ -606,25 +620,30 @@ impl ThreadHistoryBuilder {
|
||||
};
|
||||
|
||||
if should_upsert {
|
||||
self.upsert_item_in_turn_id(turn_id, ThreadItem::from(item.clone()));
|
||||
let item = ThreadItem::from(item.clone());
|
||||
if is_review_mode_item {
|
||||
self.upsert_review_mode_item(Some(turn_id), item);
|
||||
} else {
|
||||
self.upsert_item_in_turn_id(turn_id, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_web_search_begin(&mut self, payload: &WebSearchBeginEvent) {
|
||||
let item = ThreadItem::WebSearch {
|
||||
let item = ThreadItem::WebSearch(WebSearchItem {
|
||||
id: payload.call_id.clone(),
|
||||
query: String::new(),
|
||||
action: None,
|
||||
};
|
||||
});
|
||||
self.upsert_item_in_current_turn(item);
|
||||
}
|
||||
|
||||
fn handle_web_search_end(&mut self, payload: &WebSearchEndEvent) {
|
||||
let item = ThreadItem::WebSearch {
|
||||
let item = ThreadItem::WebSearch(WebSearchItem {
|
||||
id: payload.call_id.clone(),
|
||||
query: payload.query.clone(),
|
||||
action: Some(WebSearchAction::from(payload.action.clone())),
|
||||
};
|
||||
action: Some(web_search_action_from_core(payload.action.clone())),
|
||||
});
|
||||
self.upsert_item_in_current_turn(item);
|
||||
}
|
||||
|
||||
@@ -827,24 +846,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);
|
||||
}
|
||||
|
||||
@@ -1103,26 +1122,55 @@ impl ThreadHistoryBuilder {
|
||||
self.push_item_in_current_turn(ThreadItem::ContextCompaction { id });
|
||||
}
|
||||
|
||||
fn handle_entered_review_mode(&mut self, payload: &codex_protocol::protocol::ReviewRequest) {
|
||||
fn handle_entered_review_mode(
|
||||
&mut self,
|
||||
payload: &codex_protocol::protocol::EnteredReviewModeEvent,
|
||||
) {
|
||||
let review = payload
|
||||
.user_facing_hint
|
||||
.clone()
|
||||
.unwrap_or_else(|| "Review requested.".to_string());
|
||||
let id = self.next_item_id();
|
||||
self.push_item_in_current_turn(ThreadItem::EnteredReviewMode { id, review });
|
||||
let id = payload
|
||||
.item_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| self.next_item_id());
|
||||
self.upsert_review_mode_item(
|
||||
payload.turn_id.as_deref(),
|
||||
ThreadItem::EnteredReviewMode { id, review },
|
||||
);
|
||||
}
|
||||
|
||||
fn handle_exited_review_mode(
|
||||
&mut self,
|
||||
payload: &codex_protocol::protocol::ExitedReviewModeEvent,
|
||||
) {
|
||||
let review = payload
|
||||
.review_output
|
||||
let review = review_output_text(payload.review_output.as_ref());
|
||||
let id = payload
|
||||
.item_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| self.next_item_id());
|
||||
self.upsert_review_mode_item(
|
||||
payload.turn_id.as_deref(),
|
||||
ThreadItem::ExitedReviewMode { id, review },
|
||||
);
|
||||
}
|
||||
|
||||
fn upsert_review_mode_item(&mut self, turn_id: Option<&str>, item: ThreadItem) {
|
||||
let Some(turn_id) = turn_id else {
|
||||
self.upsert_item_in_current_turn(item);
|
||||
return;
|
||||
};
|
||||
let current_turn_matches = self
|
||||
.current_turn
|
||||
.as_ref()
|
||||
.map(render_review_output_text)
|
||||
.unwrap_or_else(|| REVIEW_FALLBACK_MESSAGE.to_string());
|
||||
let id = self.next_item_id();
|
||||
self.push_item_in_current_turn(ThreadItem::ExitedReviewMode { id, review });
|
||||
.is_some_and(|turn| turn.id == turn_id);
|
||||
if !current_turn_matches && !self.turns.iter().any(|turn| turn.id == turn_id) {
|
||||
self.finish_current_turn();
|
||||
let turn = self.new_turn(Some(turn_id.to_string()));
|
||||
self.record_changed_pending_turn(&turn);
|
||||
self.current_turn = Some(turn);
|
||||
}
|
||||
self.upsert_item_in_turn_id(turn_id, item);
|
||||
}
|
||||
|
||||
fn handle_error(&mut self, payload: &ErrorEvent) {
|
||||
@@ -1438,17 +1486,6 @@ impl ThreadHistoryBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
const REVIEW_FALLBACK_MESSAGE: &str = "Reviewer failed to output a response.";
|
||||
|
||||
fn render_review_output_text(output: &ReviewOutputEvent) -> String {
|
||||
let explanation = output.overall_explanation.trim();
|
||||
if explanation.is_empty() {
|
||||
REVIEW_FALLBACK_MESSAGE.to_string()
|
||||
} else {
|
||||
explanation.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_dynamic_tool_content_items(
|
||||
items: &[codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem],
|
||||
) -> Vec<DynamicToolCallOutputContentItem> {
|
||||
@@ -1548,10 +1585,13 @@ 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;
|
||||
use codex_protocol::items::CommandExecutionStatus as CoreCommandExecutionStatus;
|
||||
use codex_protocol::items::EnteredReviewModeItem as CoreEnteredReviewModeItem;
|
||||
use codex_protocol::items::ExitedReviewModeItem as CoreExitedReviewModeItem;
|
||||
use codex_protocol::items::HookPromptFragment as CoreHookPromptFragment;
|
||||
use codex_protocol::items::SleepItem as CoreSleepItem;
|
||||
use codex_protocol::items::TurnItem as CoreTurnItem;
|
||||
@@ -1569,12 +1609,15 @@ mod tests {
|
||||
use codex_protocol::protocol::CodexErrorInfo;
|
||||
use codex_protocol::protocol::CompactedItem;
|
||||
use codex_protocol::protocol::DynamicToolCallResponseEvent;
|
||||
use codex_protocol::protocol::EnteredReviewModeEvent;
|
||||
use codex_protocol::protocol::ExecCommandEndEvent;
|
||||
use codex_protocol::protocol::ExecCommandSource;
|
||||
use codex_protocol::protocol::ExitedReviewModeEvent;
|
||||
use codex_protocol::protocol::ItemStartedEvent;
|
||||
use codex_protocol::protocol::McpInvocation;
|
||||
use codex_protocol::protocol::McpToolCallEndEvent;
|
||||
use codex_protocol::protocol::PatchApplyBeginEvent;
|
||||
use codex_protocol::protocol::ReviewTarget;
|
||||
use codex_protocol::protocol::ThreadRolledBackEvent;
|
||||
use codex_protocol::protocol::TurnAbortReason;
|
||||
use codex_protocol::protocol::TurnAbortedEvent;
|
||||
@@ -1699,6 +1742,111 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn review_mode_events_replay_persisted_ids() {
|
||||
let events = vec![
|
||||
EventMsg::EnteredReviewMode(EnteredReviewModeEvent {
|
||||
target: ReviewTarget::Custom {
|
||||
instructions: "review this".into(),
|
||||
},
|
||||
user_facing_hint: Some("Review requested.".into()),
|
||||
turn_id: Some("turn-1".into()),
|
||||
item_id: Some("entered-review".into()),
|
||||
}),
|
||||
EventMsg::ExitedReviewMode(ExitedReviewModeEvent {
|
||||
turn_id: Some("turn-1".into()),
|
||||
item_id: Some("exited-review".into()),
|
||||
review_output: None,
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".into(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
];
|
||||
|
||||
let mut builder = ThreadHistoryBuilder::new();
|
||||
for event in &events {
|
||||
builder.handle_event(event);
|
||||
}
|
||||
let turns = builder.finish();
|
||||
|
||||
assert_eq!(turns[0].id, "turn-1");
|
||||
assert_eq!(
|
||||
turns[0].items,
|
||||
vec![
|
||||
ThreadItem::EnteredReviewMode {
|
||||
id: "entered-review".into(),
|
||||
review: "Review requested.".into(),
|
||||
},
|
||||
ThreadItem::ExitedReviewMode {
|
||||
id: "exited-review".into(),
|
||||
review: REVIEW_FALLBACK_MESSAGE.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn review_mode_items_replay_without_turn_started() {
|
||||
let thread_id = ThreadId::new();
|
||||
let entered = CoreTurnItem::EnteredReviewMode(CoreEnteredReviewModeItem {
|
||||
id: "entered-review".into(),
|
||||
target: ReviewTarget::Custom {
|
||||
instructions: "review this".into(),
|
||||
},
|
||||
user_facing_hint: "Review requested.".into(),
|
||||
});
|
||||
let exited = CoreTurnItem::ExitedReviewMode(CoreExitedReviewModeItem {
|
||||
id: "exited-review".into(),
|
||||
review_output: None,
|
||||
});
|
||||
let events = vec![
|
||||
EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
thread_id,
|
||||
turn_id: "turn-1".into(),
|
||||
item: entered,
|
||||
completed_at_ms: 0,
|
||||
}),
|
||||
EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
thread_id,
|
||||
turn_id: "turn-1".into(),
|
||||
item: exited,
|
||||
completed_at_ms: 0,
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".into(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
];
|
||||
|
||||
let mut builder = ThreadHistoryBuilder::new();
|
||||
for event in &events {
|
||||
builder.handle_event(event);
|
||||
}
|
||||
let turns = builder.finish();
|
||||
|
||||
assert_eq!(turns[0].id, "turn-1");
|
||||
assert_eq!(
|
||||
turns[0].items,
|
||||
vec![
|
||||
ThreadItem::EnteredReviewMode {
|
||||
id: "entered-review".into(),
|
||||
review: "Review requested.".into(),
|
||||
},
|
||||
ThreadItem::ExitedReviewMode {
|
||||
id: "exited-review".into(),
|
||||
review: REVIEW_FALLBACK_MESSAGE.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuilds_user_message_image_details_from_legacy_events() {
|
||||
let local_path = PathBuf::from("/tmp/local.png");
|
||||
@@ -1846,6 +1994,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 +2260,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()),
|
||||
},
|
||||
}),
|
||||
],
|
||||
}
|
||||
);
|
||||
@@ -2492,14 +2694,14 @@ mod tests {
|
||||
assert_eq!(turns[0].items.len(), 4);
|
||||
assert_eq!(
|
||||
turns[0].items[1],
|
||||
ThreadItem::WebSearch {
|
||||
ThreadItem::WebSearch(WebSearchItem {
|
||||
id: "search-1".into(),
|
||||
query: "codex".into(),
|
||||
action: Some(WebSearchAction::Search {
|
||||
query: Some("codex".into()),
|
||||
queries: None,
|
||||
}),
|
||||
}
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
turns[0].items[2],
|
||||
@@ -3825,6 +4027,37 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_hook_prompt_completion_updates_turn_history() {
|
||||
let hook_prompt = CoreTurnItem::HookPrompt(codex_protocol::items::HookPromptItem {
|
||||
id: "hook-prompt-1".into(),
|
||||
fragments: vec![CoreHookPromptFragment::from_single_hook(
|
||||
"Retry with tests.",
|
||||
"hook-run-1",
|
||||
)],
|
||||
});
|
||||
let expected_item = ThreadItem::from(hook_prompt.clone());
|
||||
let mut builder = ThreadHistoryBuilder::new();
|
||||
builder.handle_event(&EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}));
|
||||
builder.handle_event(&EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
thread_id: ThreadId::new(),
|
||||
turn_id: "turn-a".into(),
|
||||
item: hook_prompt,
|
||||
completed_at_ms: 0,
|
||||
}));
|
||||
|
||||
assert_eq!(
|
||||
builder.active_turn_snapshot().expect("active turn").items,
|
||||
vec![expected_item]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_plain_user_response_items_in_rollout_replay() {
|
||||
let items = vec![
|
||||
@@ -3872,7 +4105,6 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
changes,
|
||||
ThreadHistoryChangeSet {
|
||||
@@ -3919,20 +4151,19 @@ mod tests {
|
||||
},
|
||||
}),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
changes,
|
||||
ThreadHistoryChangeSet {
|
||||
changed_items: vec![ThreadHistoryItemChange {
|
||||
turn_id: "rollout-0".into(),
|
||||
item: ThreadItem::WebSearch {
|
||||
item: ThreadItem::WebSearch(WebSearchItem {
|
||||
id: "search-1".into(),
|
||||
query: "codex".into(),
|
||||
action: Some(WebSearchAction::Search {
|
||||
query: Some("codex".into()),
|
||||
queries: None,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}],
|
||||
changed_turns: Vec::new(),
|
||||
removed_turn_ids: Vec::new(),
|
||||
@@ -3954,7 +4185,6 @@ mod tests {
|
||||
text: "raw content".into(),
|
||||
}),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
changes,
|
||||
ThreadHistoryChangeSet {
|
||||
@@ -4054,20 +4284,19 @@ mod tests {
|
||||
},
|
||||
})),
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
changes,
|
||||
ThreadHistoryChangeSet {
|
||||
changed_items: vec![ThreadHistoryItemChange {
|
||||
turn_id: "rollout-0".into(),
|
||||
item: ThreadItem::WebSearch {
|
||||
item: ThreadItem::WebSearch(WebSearchItem {
|
||||
id: "search-1".into(),
|
||||
query: "codex".into(),
|
||||
action: Some(WebSearchAction::Search {
|
||||
query: Some("codex".into()),
|
||||
queries: None,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}],
|
||||
changed_turns: vec![ThreadHistoryTurnChange {
|
||||
turn_id: "rollout-0".into(),
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,12 @@ use super::UserInput;
|
||||
use super::shared::v2_enum_from_core;
|
||||
use crate::protocol::item_builders::command_actions_for_path_uri;
|
||||
use crate::protocol::item_builders::convert_patch_changes;
|
||||
use crate::protocol::item_builders::review_output_text;
|
||||
use codex_experimental_api_macros::ExperimentalApi;
|
||||
use codex_extension_items::ExtensionItem;
|
||||
pub use codex_extension_items::image_generation::ImageGenerationItem;
|
||||
pub use codex_extension_items::web_search::WebSearchAction;
|
||||
pub use codex_extension_items::web_search::WebSearchItem;
|
||||
use codex_protocol::approvals::GuardianAssessmentAction as CoreGuardianAssessmentAction;
|
||||
use codex_protocol::approvals::GuardianAssessmentDecisionSource as CoreGuardianAssessmentDecisionSource;
|
||||
use codex_protocol::approvals::GuardianCommandSource as CoreGuardianCommandSource;
|
||||
@@ -246,7 +251,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 {
|
||||
@@ -353,13 +361,7 @@ pub enum ThreadItem {
|
||||
agent_thread_id: String,
|
||||
agent_path: String,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
WebSearch {
|
||||
id: String,
|
||||
query: String,
|
||||
action: Option<WebSearchAction>,
|
||||
},
|
||||
WebSearch(WebSearchItem),
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
ImageView {
|
||||
@@ -373,26 +375,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)]
|
||||
@@ -429,13 +429,13 @@ impl ThreadItem {
|
||||
| ThreadItem::DynamicToolCall { id, .. }
|
||||
| ThreadItem::CollabAgentToolCall { id, .. }
|
||||
| ThreadItem::SubAgentActivity { id, .. }
|
||||
| ThreadItem::WebSearch { id, .. }
|
||||
| ThreadItem::ImageView { id, .. }
|
||||
| ThreadItem::Sleep { id, .. }
|
||||
| ThreadItem::ImageGeneration { id, .. }
|
||||
| ThreadItem::EnteredReviewMode { id, .. }
|
||||
| ThreadItem::ExitedReviewMode { id, .. }
|
||||
| ThreadItem::ContextCompaction { id, .. } => id,
|
||||
ThreadItem::WebSearch(item) => &item.id,
|
||||
ThreadItem::ImageGeneration(item) => &item.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -783,40 +783,20 @@ impl TryFrom<GuardianApprovalReviewAction> for CoreGuardianAssessmentAction {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum WebSearchAction {
|
||||
Search {
|
||||
query: Option<String>,
|
||||
queries: Option<Vec<String>>,
|
||||
},
|
||||
OpenPage {
|
||||
url: Option<String>,
|
||||
},
|
||||
FindInPage {
|
||||
url: Option<String>,
|
||||
pattern: Option<String>,
|
||||
},
|
||||
#[serde(other)]
|
||||
Other,
|
||||
}
|
||||
|
||||
impl From<codex_protocol::models::WebSearchAction> for WebSearchAction {
|
||||
fn from(value: codex_protocol::models::WebSearchAction) -> Self {
|
||||
match value {
|
||||
codex_protocol::models::WebSearchAction::Search { query, queries } => {
|
||||
WebSearchAction::Search { query, queries }
|
||||
}
|
||||
codex_protocol::models::WebSearchAction::OpenPage { url } => {
|
||||
WebSearchAction::OpenPage { url }
|
||||
}
|
||||
codex_protocol::models::WebSearchAction::FindInPage { url, pattern } => {
|
||||
WebSearchAction::FindInPage { url, pattern }
|
||||
}
|
||||
codex_protocol::models::WebSearchAction::Other => WebSearchAction::Other,
|
||||
pub(crate) fn web_search_action_from_core(
|
||||
value: codex_protocol::models::WebSearchAction,
|
||||
) -> WebSearchAction {
|
||||
match value {
|
||||
codex_protocol::models::WebSearchAction::Search { query, queries } => {
|
||||
WebSearchAction::Search { query, queries }
|
||||
}
|
||||
codex_protocol::models::WebSearchAction::OpenPage { url } => {
|
||||
WebSearchAction::OpenPage { url }
|
||||
}
|
||||
codex_protocol::models::WebSearchAction::FindInPage { url, pattern } => {
|
||||
WebSearchAction::FindInPage { url, pattern }
|
||||
}
|
||||
codex_protocol::models::WebSearchAction::Other => WebSearchAction::Other,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -918,11 +898,11 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
agent_thread_id: activity.agent_thread_id.to_string(),
|
||||
agent_path: String::from(activity.agent_path),
|
||||
},
|
||||
CoreTurnItem::WebSearch(search) => ThreadItem::WebSearch {
|
||||
CoreTurnItem::WebSearch(search) => ThreadItem::WebSearch(WebSearchItem {
|
||||
id: search.id,
|
||||
query: search.query,
|
||||
action: Some(WebSearchAction::from(search.action)),
|
||||
},
|
||||
action: Some(web_search_action_from_core(search.action)),
|
||||
}),
|
||||
CoreTurnItem::ImageView(image) => ThreadItem::ImageView {
|
||||
id: image.id,
|
||||
path: image.path.into(),
|
||||
@@ -931,12 +911,26 @@ 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),
|
||||
ExtensionItem::WebSearch(item) => ThreadItem::WebSearch(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::EnteredReviewMode(review) => ThreadItem::EnteredReviewMode {
|
||||
id: review.id,
|
||||
review: review.user_facing_hint,
|
||||
},
|
||||
CoreTurnItem::ExitedReviewMode(review) => ThreadItem::ExitedReviewMode {
|
||||
id: review.id,
|
||||
review: review_output_text(review.review_output.as_ref()),
|
||||
},
|
||||
CoreTurnItem::FileChange(file_change) => ThreadItem::FileChange {
|
||||
id: file_change.id,
|
||||
|
||||
@@ -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 {
|
||||
@@ -600,6 +611,9 @@ pub struct PluginSummary {
|
||||
pub id: String,
|
||||
/// Backend remote plugin identifier when available.
|
||||
pub remote_plugin_id: Option<String>,
|
||||
/// Version advertised by the remote marketplace backend when available.
|
||||
#[serde(default)]
|
||||
pub version: Option<String>,
|
||||
/// Version of the locally materialized plugin package when available.
|
||||
#[serde(default)]
|
||||
pub local_version: Option<String>,
|
||||
@@ -610,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>,
|
||||
|
||||
@@ -19,7 +19,7 @@ use codex_protocol::items::ReasoningItem;
|
||||
use codex_protocol::items::SubAgentActivityItem;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::items::UserMessageItem;
|
||||
use codex_protocol::items::WebSearchItem;
|
||||
use codex_protocol::items::WebSearchItem as CoreWebSearchItem;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use codex_protocol::mcp::McpServerInfo;
|
||||
use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation;
|
||||
@@ -2770,7 +2770,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
}
|
||||
);
|
||||
|
||||
let search_item = TurnItem::WebSearch(WebSearchItem {
|
||||
let search_item = TurnItem::WebSearch(CoreWebSearchItem {
|
||||
id: "search-1".to_string(),
|
||||
query: "docs".to_string(),
|
||||
action: CoreWebSearchAction::Search {
|
||||
@@ -2779,16 +2779,24 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
},
|
||||
});
|
||||
|
||||
let expected_search_item = WebSearchItem {
|
||||
id: "search-1".to_string(),
|
||||
query: "docs".to_string(),
|
||||
action: Some(WebSearchAction::Search {
|
||||
query: Some("docs".to_string()),
|
||||
queries: None,
|
||||
}),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
ThreadItem::from(search_item),
|
||||
ThreadItem::WebSearch {
|
||||
id: "search-1".to_string(),
|
||||
query: "docs".to_string(),
|
||||
action: Some(WebSearchAction::Search {
|
||||
query: Some("docs".to_string()),
|
||||
queries: None,
|
||||
}),
|
||||
}
|
||||
ThreadItem::WebSearch(expected_search_item.clone())
|
||||
);
|
||||
assert_eq!(
|
||||
ThreadItem::from(TurnItem::Extension(
|
||||
codex_extension_items::ExtensionItem::WebSearch(expected_search_item.clone()),
|
||||
)),
|
||||
ThreadItem::WebSearch(expected_search_item)
|
||||
);
|
||||
|
||||
let image_view_item = TurnItem::ImageView(ImageViewItem {
|
||||
@@ -3642,6 +3650,7 @@ fn plugin_share_list_response_serializes_share_items() {
|
||||
remote_plugin_id: Some(
|
||||
"plugins~Plugin_00000000000000000000000000000000".to_string(),
|
||||
),
|
||||
version: None,
|
||||
local_version: None,
|
||||
name: "gmail".to_string(),
|
||||
share_context: None,
|
||||
@@ -3649,6 +3658,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,
|
||||
@@ -3663,6 +3673,7 @@ fn plugin_share_list_response_serializes_share_items() {
|
||||
"plugin": {
|
||||
"id": "gmail@openai-curated-remote",
|
||||
"remotePluginId": "plugins~Plugin_00000000000000000000000000000000",
|
||||
"version": null,
|
||||
"localVersion": null,
|
||||
"name": "gmail",
|
||||
"shareContext": null,
|
||||
@@ -3670,6 +3681,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,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ codex_rust_crate(
|
||||
crate_name = "codex_app_server",
|
||||
extra_binaries = [
|
||||
"//codex-rs/bwrap:bwrap",
|
||||
"//codex-rs/code-mode-host:codex-code-mode-host",
|
||||
],
|
||||
extra_binaries_non_windows = [
|
||||
"//codex-rs/cli:codex",
|
||||
|
||||
@@ -12,6 +12,10 @@ path = "src/main.rs"
|
||||
name = "codex-app-server-test-notify-capture"
|
||||
path = "src/bin/notify_capture.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "exec-server"
|
||||
path = "src/bin/exec_server.rs"
|
||||
|
||||
[lib]
|
||||
name = "codex_app_server"
|
||||
path = "src/lib.rs"
|
||||
@@ -47,6 +51,7 @@ codex-guardian = { workspace = true }
|
||||
codex-git-utils = { workspace = true }
|
||||
codex-file-watcher = { workspace = true }
|
||||
codex-hooks = { workspace = true }
|
||||
codex-http-client = { workspace = true }
|
||||
codex-otel = { workspace = true }
|
||||
codex-plugin = { workspace = true }
|
||||
codex-shell-command = { workspace = true }
|
||||
|
||||
@@ -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, 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;
|
||||
@@ -87,10 +86,9 @@ use codex_app_server_protocol::guardian_auto_approval_review_notification;
|
||||
use codex_app_server_protocol::item_event_to_server_notification;
|
||||
use codex_core::CodexThread;
|
||||
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::parse_hook_prompt_message;
|
||||
use codex_protocol::items::CollabAgentTool as CoreCollabAgentTool;
|
||||
use codex_protocol::items::TurnItem as CoreTurnItem;
|
||||
use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile;
|
||||
use codex_protocol::plan_tool::UpdatePlanArgs;
|
||||
use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo;
|
||||
@@ -100,7 +98,6 @@ use codex_protocol::protocol::ExecApprovalRequestEvent;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::RealtimeEvent;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_protocol::protocol::ReviewOutputEvent;
|
||||
use codex_protocol::protocol::SubAgentActivityKind;
|
||||
use codex_protocol::protocol::TokenCountEvent;
|
||||
use codex_protocol::protocol::TurnAbortedEvent;
|
||||
@@ -822,51 +819,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 +828,25 @@ 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(_)
|
||||
| EventMsg::EnteredReviewMode(_)
|
||||
| EventMsg::ExitedReviewMode(_) => {
|
||||
// Deprecated item lifecycle events are still fanned out for raw-event and rollout
|
||||
// compatibility consumers.
|
||||
// App-server v2 receives TurnItem lifecycle instead, and dispatches dynamic tool
|
||||
// requests from 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 +858,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) => {
|
||||
@@ -1000,37 +936,63 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
.await;
|
||||
}
|
||||
EventMsg::ViewImageToolCall(_) => {}
|
||||
EventMsg::EnteredReviewMode(review_request) => {
|
||||
let review = review_request
|
||||
.user_facing_hint
|
||||
.unwrap_or_else(|| review_prompts::user_facing_hint(&review_request.target));
|
||||
let item = ThreadItem::EnteredReviewMode {
|
||||
id: event_turn_id.clone(),
|
||||
review,
|
||||
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 started = ItemStartedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: event_turn_id.clone(),
|
||||
started_at_ms: now_unix_timestamp_ms(),
|
||||
item: item.clone(),
|
||||
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,
|
||||
};
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ItemStarted(started))
|
||||
.await;
|
||||
let completed = ItemCompletedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: event_turn_id.clone(),
|
||||
completed_at_ms: now_unix_timestamp_ms(),
|
||||
item,
|
||||
};
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ItemCompleted(completed))
|
||||
.await;
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
msg @ (EventMsg::ItemStarted(_)
|
||||
| EventMsg::ItemCompleted(_)
|
||||
| EventMsg::PatchApplyUpdated(_)
|
||||
| EventMsg::TerminalInteraction(_)) => {
|
||||
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(),
|
||||
@@ -1058,42 +1020,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
.send_server_notification(ServerNotification::HookCompleted(notification))
|
||||
.await;
|
||||
}
|
||||
EventMsg::ExitedReviewMode(review_event) => {
|
||||
let review = match review_event.review_output {
|
||||
Some(output) => render_review_output_text(&output),
|
||||
None => REVIEW_FALLBACK_MESSAGE.to_string(),
|
||||
};
|
||||
let item = ThreadItem::ExitedReviewMode {
|
||||
id: event_turn_id.clone(),
|
||||
review,
|
||||
};
|
||||
let started = ItemStartedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: event_turn_id.clone(),
|
||||
started_at_ms: now_unix_timestamp_ms(),
|
||||
item: item.clone(),
|
||||
};
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ItemStarted(started))
|
||||
.await;
|
||||
let completed = ItemCompletedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: event_turn_id.clone(),
|
||||
completed_at_ms: now_unix_timestamp_ms(),
|
||||
item,
|
||||
};
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ItemCompleted(completed))
|
||||
.await;
|
||||
}
|
||||
EventMsg::RawResponseItem(raw_response_item_event) => {
|
||||
maybe_emit_hook_prompt_item_completed(
|
||||
conversation_id,
|
||||
&event_turn_id,
|
||||
&raw_response_item_event.item,
|
||||
&outgoing,
|
||||
)
|
||||
.await;
|
||||
maybe_emit_raw_response_item_completed(
|
||||
conversation_id,
|
||||
&event_turn_id,
|
||||
@@ -1103,36 +1030,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 +1042,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 +1245,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,
|
||||
@@ -1476,45 +1398,6 @@ async fn maybe_emit_raw_response_item_completed(
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) async fn maybe_emit_hook_prompt_item_completed(
|
||||
conversation_id: ThreadId,
|
||||
turn_id: &str,
|
||||
item: &codex_protocol::models::ResponseItem,
|
||||
outgoing: &ThreadScopedOutgoingMessageSender,
|
||||
) {
|
||||
let codex_protocol::models::ResponseItem::Message {
|
||||
role, content, id, ..
|
||||
} = item
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if role != "user" {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(hook_prompt) = parse_hook_prompt_message(id.as_ref(), content) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let notification = ItemCompletedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: turn_id.to_string(),
|
||||
completed_at_ms: now_unix_timestamp_ms(),
|
||||
item: ThreadItem::HookPrompt {
|
||||
id: hook_prompt.id,
|
||||
fragments: hook_prompt
|
||||
.fragments
|
||||
.into_iter()
|
||||
.map(codex_app_server_protocol::HookPromptFragment::from)
|
||||
.collect(),
|
||||
},
|
||||
};
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ItemCompleted(notification))
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn find_and_remove_turn_summary(
|
||||
_conversation_id: ThreadId,
|
||||
thread_state: &Arc<Mutex<ThreadState>>,
|
||||
@@ -1966,28 +1849,6 @@ fn request_permissions_response_from_client_result(
|
||||
}))
|
||||
}
|
||||
|
||||
const REVIEW_FALLBACK_MESSAGE: &str = "Reviewer failed to output a response.";
|
||||
|
||||
fn render_review_output_text(output: &ReviewOutputEvent) -> String {
|
||||
let mut sections = Vec::new();
|
||||
let explanation = output.overall_explanation.trim();
|
||||
if !explanation.is_empty() {
|
||||
sections.push(explanation.to_string());
|
||||
}
|
||||
if !output.findings.is_empty() {
|
||||
let findings = format_review_findings_block(&output.findings, /*selection*/ None);
|
||||
let trimmed = findings.trim();
|
||||
if !trimmed.is_empty() {
|
||||
sections.push(trimmed.to_string());
|
||||
}
|
||||
}
|
||||
if sections.is_empty() {
|
||||
REVIEW_FALLBACK_MESSAGE.to_string()
|
||||
} else {
|
||||
sections.join("\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
fn map_file_change_approval_decision(decision: FileChangeApprovalDecision) -> ReviewDecision {
|
||||
match decision {
|
||||
FileChangeApprovalDecision::Accept => ReviewDecision::Approved,
|
||||
@@ -2190,11 +2051,14 @@ 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::HookPromptFragment;
|
||||
use codex_protocol::items::build_hook_prompt_message;
|
||||
use codex_protocol::items::DynamicToolCallItem;
|
||||
use codex_protocol::items::DynamicToolCallStatus as CoreDynamicToolCallStatus;
|
||||
use codex_protocol::items::SubAgentActivityItem;
|
||||
use codex_protocol::items::TurnItem as CoreTurnItem;
|
||||
use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions;
|
||||
use codex_protocol::models::NetworkPermissions as CoreNetworkPermissions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
@@ -2210,11 +2074,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 +3302,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 +3356,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();
|
||||
@@ -3989,55 +3944,4 @@ mod tests {
|
||||
assert!(rx.try_recv().is_err(), "no extra messages expected");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_hook_prompt_raw_response_emits_item_completed() -> Result<()> {
|
||||
let (tx, mut rx) = mpsc::channel(CHANNEL_CAPACITY);
|
||||
let outgoing = Arc::new(OutgoingMessageSender::new(
|
||||
tx,
|
||||
codex_analytics::AnalyticsEventsClient::disabled(),
|
||||
));
|
||||
let conversation_id = ThreadId::new();
|
||||
let outgoing = ThreadScopedOutgoingMessageSender::new(
|
||||
outgoing,
|
||||
vec![ConnectionId(1)],
|
||||
conversation_id,
|
||||
);
|
||||
let item = build_hook_prompt_message(&[
|
||||
HookPromptFragment::from_single_hook("Retry with tests.", "hook-run-1"),
|
||||
HookPromptFragment::from_single_hook("Then summarize cleanly.", "hook-run-2"),
|
||||
])
|
||||
.expect("hook prompt message");
|
||||
|
||||
maybe_emit_hook_prompt_item_completed(conversation_id, "turn-1", &item, &outgoing).await;
|
||||
|
||||
let msg = recv_broadcast_message(&mut rx).await?;
|
||||
match msg {
|
||||
OutgoingMessage::AppServerNotification(ServerNotification::ItemCompleted(
|
||||
notification,
|
||||
)) => {
|
||||
assert_eq!(notification.thread_id, conversation_id.to_string());
|
||||
assert_eq!(notification.turn_id, "turn-1");
|
||||
assert_eq!(
|
||||
notification.item,
|
||||
ThreadItem::HookPrompt {
|
||||
id: notification.item.id().to_string(),
|
||||
fragments: vec![
|
||||
codex_app_server_protocol::HookPromptFragment {
|
||||
text: "Retry with tests.".into(),
|
||||
hook_run_id: "hook-run-1".into(),
|
||||
},
|
||||
codex_app_server_protocol::HookPromptFragment {
|
||||
text: "Then summarize cleanly.".into(),
|
||||
hook_run_id: "hook-run-2".into(),
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
other => bail!("unexpected message: {other:?}"),
|
||||
}
|
||||
assert!(rx.try_recv().is_err(), "no extra messages expected");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
//! Cargo entry point for the minimal exec-server integration-test fixture.
|
||||
//!
|
||||
//! This mirrors `//codex-rs/exec-server/testing:exec-server` so Cargo-backed
|
||||
//! app-server integration tests can receive `CARGO_BIN_EXE_exec-server`. It
|
||||
//! also handles the filesystem-helper argv mode because exec-server re-execs
|
||||
//! `codex_self_exe` for sandboxed filesystem requests.
|
||||
|
||||
use codex_exec_server::ExecServerRuntimePaths;
|
||||
use std::ffi::OsStr;
|
||||
|
||||
const CODEX_LINUX_SANDBOX_EXE_ENV_VAR: &str = "CODEX_TEST_LINUX_SANDBOX_EXE";
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut args = std::env::args_os();
|
||||
let _ = args.next();
|
||||
if args.next().as_deref() == Some(OsStr::new(codex_exec_server::CODEX_FS_HELPER_ARG1)) {
|
||||
codex_exec_server::run_fs_helper_main();
|
||||
}
|
||||
|
||||
let current_exe = std::env::current_exe()?;
|
||||
let codex_linux_sandbox_exe =
|
||||
std::env::var_os(CODEX_LINUX_SANDBOX_EXE_ENV_VAR).map(std::path::PathBuf::from);
|
||||
let runtime_paths = ExecServerRuntimePaths::new(current_exe, codex_linux_sandbox_exe)?;
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()?
|
||||
.block_on(codex_exec_server::run_main(
|
||||
"ws://127.0.0.1:0",
|
||||
runtime_paths,
|
||||
))
|
||||
}
|
||||
@@ -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,
|
||||
@@ -386,7 +299,8 @@ impl MessageProcessor {
|
||||
)
|
||||
});
|
||||
let models_manager = thread_manager.get_models_manager();
|
||||
let models_refresh_worker = crate::models_refresh_worker::spawn(&models_manager);
|
||||
let models_refresh_worker =
|
||||
crate::models_refresh_worker::spawn(&models_manager, config.http_client_factory());
|
||||
thread_manager
|
||||
.plugins_manager()
|
||||
.set_analytics_events_client(analytics_events_client.clone());
|
||||
@@ -447,7 +361,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 +409,7 @@ impl MessageProcessor {
|
||||
state_db.clone(),
|
||||
log_db,
|
||||
Arc::clone(&skills_watcher),
|
||||
config_warnings,
|
||||
);
|
||||
let turn_processor = TurnRequestProcessor::new(
|
||||
auth_manager.clone(),
|
||||
|
||||
@@ -5,6 +5,7 @@ use codex_app_server_protocol::ModelServiceTier;
|
||||
use codex_app_server_protocol::ModelUpgradeInfo;
|
||||
use codex_app_server_protocol::ReasoningEffortOption;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_http_client::HttpClientFactory;
|
||||
use codex_models_manager::manager::RefreshStrategy;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::openai_models::ReasoningEffortPreset;
|
||||
@@ -12,9 +13,10 @@ use codex_protocol::openai_models::ReasoningEffortPreset;
|
||||
pub async fn supported_models(
|
||||
thread_manager: Arc<ThreadManager>,
|
||||
include_hidden: bool,
|
||||
http_client_factory: HttpClientFactory,
|
||||
) -> Vec<Model> {
|
||||
thread_manager
|
||||
.list_models(RefreshStrategy::OnlineIfUncached)
|
||||
.list_models(RefreshStrategy::OnlineIfUncached, http_client_factory)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter(|preset| include_hidden || preset.show_in_picker)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_http_client::HttpClientFactory;
|
||||
use codex_models_manager::manager::RefreshStrategy;
|
||||
use codex_models_manager::manager::SharedModelsManager;
|
||||
use tokio::task::JoinHandle;
|
||||
@@ -26,12 +27,16 @@ impl Drop for ModelsRefreshWorker {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn spawn(models_manager: &SharedModelsManager) -> ModelsRefreshWorker {
|
||||
spawn_with_interval(models_manager, MODELS_REFRESH_INTERVAL)
|
||||
pub(crate) fn spawn(
|
||||
models_manager: &SharedModelsManager,
|
||||
http_client_factory: HttpClientFactory,
|
||||
) -> ModelsRefreshWorker {
|
||||
spawn_with_interval(models_manager, http_client_factory, MODELS_REFRESH_INTERVAL)
|
||||
}
|
||||
|
||||
fn spawn_with_interval(
|
||||
models_manager: &SharedModelsManager,
|
||||
http_client_factory: HttpClientFactory,
|
||||
refresh_interval: Duration,
|
||||
) -> ModelsRefreshWorker {
|
||||
let models_manager = Arc::downgrade(models_manager);
|
||||
@@ -45,7 +50,9 @@ fn spawn_with_interval(
|
||||
let Some(models_manager) = models_manager.upgrade() else {
|
||||
break;
|
||||
};
|
||||
models_manager.list_models(RefreshStrategy::Online).await;
|
||||
models_manager
|
||||
.list_models(RefreshStrategy::Online, http_client_factory.clone())
|
||||
.await;
|
||||
drop(models_manager);
|
||||
|
||||
tokio::select! {
|
||||
|
||||
@@ -3,6 +3,8 @@ use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_http_client::HttpClientFactory;
|
||||
use codex_http_client::OutboundProxyPolicy;
|
||||
use codex_models_manager::manager::ModelsEndpointClient;
|
||||
use codex_models_manager::manager::ModelsEndpointFuture;
|
||||
use codex_models_manager::manager::OpenAiModelsManager;
|
||||
@@ -55,6 +57,7 @@ impl ModelsEndpointClient for TestModelsEndpoint {
|
||||
fn list_models<'a>(
|
||||
&'a self,
|
||||
_client_version: &'a str,
|
||||
_http_client_factory: HttpClientFactory,
|
||||
) -> ModelsEndpointFuture<'a, CoreResult<(Vec<ModelInfo>, Option<String>)>> {
|
||||
Box::pin(async move {
|
||||
let fetch_index = self.fetch_count.fetch_add(1, Ordering::SeqCst);
|
||||
@@ -79,7 +82,11 @@ async fn refreshes_immediately_periodically_and_stops_when_dropped() {
|
||||
endpoint.clone(),
|
||||
/*auth_manager*/ None,
|
||||
));
|
||||
let worker = spawn_with_interval(&models_manager, Duration::from_millis(10));
|
||||
let worker = spawn_with_interval(
|
||||
&models_manager,
|
||||
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
|
||||
Duration::from_millis(10),
|
||||
);
|
||||
|
||||
endpoint.wait_for_fetch_count(/*expected*/ 2).await;
|
||||
drop(worker);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::bespoke_event_handling::apply_bespoke_event_handling;
|
||||
use crate::bespoke_event_handling::maybe_emit_hook_prompt_item_completed;
|
||||
use crate::command_exec::CommandExecManager;
|
||||
use crate::command_exec::StartCommandExecParams;
|
||||
use crate::config_manager::ConfigManager;
|
||||
@@ -94,6 +93,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 +156,10 @@ use codex_app_server_protocol::PluginSource;
|
||||
use codex_app_server_protocol::PluginSummary;
|
||||
use codex_app_server_protocol::PluginUninstallParams;
|
||||
use codex_app_server_protocol::PluginUninstallResponse;
|
||||
use codex_app_server_protocol::RateLimitResetCredit;
|
||||
use codex_app_server_protocol::RateLimitResetCreditStatus;
|
||||
use codex_app_server_protocol::RateLimitResetCreditsSummary;
|
||||
use codex_app_server_protocol::RateLimitResetType;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ReviewDelivery as ApiReviewDelivery;
|
||||
use codex_app_server_protocol::ReviewStartParams;
|
||||
@@ -298,6 +301,8 @@ use codex_backend_client::CodexWorkspaceMessage as BackendWorkspaceMessage;
|
||||
use codex_backend_client::CodexWorkspaceMessageType as BackendWorkspaceMessageType;
|
||||
use codex_backend_client::CodexWorkspaceMessagesResponse as BackendWorkspaceMessagesResponse;
|
||||
use codex_backend_client::ConsumeRateLimitResetCreditCode as BackendConsumeRateLimitResetCreditCode;
|
||||
use codex_backend_client::RateLimitResetCreditDetails as BackendRateLimitResetCreditDetails;
|
||||
use codex_backend_client::RateLimitResetCreditsDetails as BackendRateLimitResetCreditsDetails;
|
||||
use codex_backend_client::RequestError as BackendRequestError;
|
||||
use codex_backend_client::TokenUsageProfile;
|
||||
use codex_chatgpt::connectors;
|
||||
@@ -374,10 +379,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;
|
||||
@@ -431,8 +438,8 @@ use codex_protocol::protocol::SessionConfiguredEvent;
|
||||
use codex_protocol::protocol::SessionMetaLine;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelections;
|
||||
use codex_protocol::protocol::USER_MESSAGE_BEGIN;
|
||||
use codex_protocol::protocol::W3cTraceContext;
|
||||
use codex_protocol::protocol::strip_user_message_prefix;
|
||||
use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS;
|
||||
use codex_protocol::user_input::UserInput as CoreInputItem;
|
||||
use codex_rmcp_client::perform_oauth_login_return_url_with_http_client;
|
||||
@@ -618,10 +625,10 @@ pub(crate) use self::thread_summary::summary_to_thread;
|
||||
pub(crate) use self::thread_summary::thread_settings_from_config_snapshot;
|
||||
pub(crate) use self::thread_summary::thread_settings_from_core_snapshot;
|
||||
|
||||
pub(crate) fn build_api_turns_from_rollout_items(items: &[RolloutItem]) -> Vec<Turn> {
|
||||
pub(crate) fn build_legacy_api_turns_from_rollout_items(items: &[RolloutItem]) -> Vec<Turn> {
|
||||
let mut builder = ThreadHistoryBuilder::new();
|
||||
for item in items {
|
||||
if is_persisted_rollout_item(item) {
|
||||
if is_persisted_rollout_item(item, codex_protocol::protocol::ThreadHistoryMode::Legacy) {
|
||||
builder.handle_rollout_item(item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user