Compare commits

..

1 Commits

Author SHA1 Message Date
Michael Zeng
205d9335af exec-server: archive agent identity registry handoff experiment 2026-05-14 21:36:21 -07:00
1762 changed files with 52638 additions and 139427 deletions

View File

@@ -38,50 +38,24 @@ common:windows --test_env=WINDIR
common --test_env=RUST_MIN_STACK=8388608 # 8 MiB
common --test_output=errors
common --nobuild_runfile_links
# These settings tune BuildBuddy/RBE behavior but do not contact a remote
# service unless a `buildbuddy-*` configuration below supplies an endpoint.
common --bes_results_url=https://app.buildbuddy.io/invocation/
common --bes_backend=grpcs://remote.buildbuddy.io
common --remote_cache=grpcs://remote.buildbuddy.io
common --remote_download_toplevel
common --nobuild_runfile_links
common --remote_timeout=3600
common --noexperimental_throttle_remote_action_building
common --experimental_remote_execution_keepalive
common --grpc_keepalive_time=30s
# Opt-in remote configurations selected by
# `.github/scripts/run_bazel_with_buildbuddy.py`. Plain Bazel commands do not
# contact BuildBuddy unless a user selects one of these configurations.
# Use the generic host for cache, BES, and downloads without remote execution.
common:buildbuddy-generic --bes_backend=grpcs://remote.buildbuddy.io
common:buildbuddy-generic --bes_results_url=https://app.buildbuddy.io/invocation/
common:buildbuddy-generic --remote_cache=grpcs://remote.buildbuddy.io
common:buildbuddy-generic --experimental_remote_downloader=grpcs://remote.buildbuddy.io
# Add remote execution on the generic host.
common:buildbuddy-generic-rbe --config=buildbuddy-generic
common:buildbuddy-generic-rbe --config=remote
common:buildbuddy-generic-rbe --remote_executor=grpcs://remote.buildbuddy.io
# Use the OpenAI tenant for cache, BES, and downloads without remote execution.
common:buildbuddy-openai --bes_backend=grpcs://openai.buildbuddy.io
common:buildbuddy-openai --bes_results_url=https://openai.buildbuddy.io/invocation/
common:buildbuddy-openai --remote_cache=grpcs://openai.buildbuddy.io
common:buildbuddy-openai --experimental_remote_downloader=grpcs://openai.buildbuddy.io
# Add remote execution on the OpenAI tenant.
common:buildbuddy-openai-rbe --config=buildbuddy-openai
common:buildbuddy-openai-rbe --config=remote
common:buildbuddy-openai-rbe --remote_executor=grpcs://openai.buildbuddy.io
common --experimental_remote_downloader=grpcs://remote.buildbuddy.io
# This limits both in-flight executions and concurrent downloads. Even with high number
# of jobs execution will still be limited by CPU cores, so this just pays a bit of
# memory in exchange for higher download concurrency.
common --jobs=30
# Shared remote execution policy. The endpoint-bearing `buildbuddy-*-rbe`
# configurations include this group; CI configs override TestRunner below
# when tests must remain local on their runner.
common:remote --strategy=remote
common:remote --extra_execution_platforms=//:rbe
common:remote --remote_executor=grpcs://remote.buildbuddy.io
common:remote --jobs=800
# TODO(team): Evaluate if this actually helps, zbarsky is not sure, everything seems bottlenecked on `core` either way.
# Enable pipelined compilation since we are not bound by local CPU count.
@@ -172,11 +146,15 @@ common:ci-windows --repo_contents_cache=D:/a/.cache/bazel-repo-contents-cache
# Linux crossbuilds don't work until we untangle the libc constraint mess.
common:ci-linux --config=ci-bazel
common:ci-linux --build_metadata=TAG_os=linux
common:ci-linux --config=remote
common:ci-linux --strategy=remote
common:ci-linux --platforms=//:rbe
# On mac, we can run all the build actions remotely but test actions locally.
common:ci-macos --config=ci-bazel
common:ci-macos --build_metadata=TAG_os=macos
common:ci-macos --config=remote
common:ci-macos --strategy=remote
common:ci-macos --strategy=TestRunner=darwin-sandbox,local
# On Windows, use Linux remote execution for build actions but keep test actions
@@ -184,7 +162,9 @@ common:ci-macos --strategy=TestRunner=darwin-sandbox,local
# still run against Windows binaries.
common:ci-windows-cross --config=ci-windows
common:ci-windows-cross --build_metadata=TAG_windows_cross_compile=true
common:ci-windows-cross --config=remote
common:ci-windows-cross --host_platform=//:rbe
common:ci-windows-cross --strategy=remote
common:ci-windows-cross --strategy=TestRunner=local
common:ci-windows-cross --local_test_jobs=4
common:ci-windows-cross --test_env=RUST_TEST_THREADS=1
@@ -200,6 +180,8 @@ common:ci-windows-cross --extra_toolchains=//:windows_gnullvm_tests_on_msvc_host
common:ci-v8 --config=ci
common:ci-v8 --build_metadata=TAG_workflow=v8
common:ci-v8 --build_metadata=TAG_os=linux
common:ci-v8 --config=remote
common:ci-v8 --strategy=remote
# Source-built Bazel V8 artifacts use the in-process sandbox by default. This
# does not affect Cargo's default prebuilt rusty_v8 path.
@@ -211,10 +193,5 @@ common --@v8//:v8_enable_sandbox=True
common:v8-release-compat --@v8//:v8_enable_pointer_compression=False
common:v8-release-compat --@v8//:v8_enable_sandbox=False
# Match rusty_v8's upstream GN release contract for published artifacts: every
# target object uses Chromium's custom libc++ headers and the archive folds in
# the matching runtime objects.
common:rusty-v8-upstream-libcxx --@v8//:v8_use_rusty_v8_custom_libcxx=True
# Optional per-user local overrides.
try-import %workspace%/user.bazelrc

View File

@@ -8,4 +8,4 @@ script = ""
[[actions]]
name = "Run"
icon = "run"
command = "cargo +1.95.0 run --manifest-path=codex-rs/Cargo.toml --bin codex -- -c mcp_oauth_credentials_store=file"
command = "cargo +1.93.0 run --manifest-path=codex-rs/Cargo.toml --bin codex -- -c mcp_oauth_credentials_store=file"

View File

@@ -1,72 +0,0 @@
---
name: update-v8-version
description: Update Codex's pinned `v8` / `rusty_v8` versions, validate the release-candidate path, and investigate failed V8 canary or artifact builds. Use when asked to bump V8, update `rusty_v8` artifacts, prepare or validate a V8 release candidate, check `v8-canary`, or diagnose why a V8 version update no longer builds.
---
# Update V8 Version
## Core Workflow
1. Read `third_party/v8/README.md` and follow its version-bump sequence. Treat
that document as the release-process source of truth.
2. Inspect and update the concrete repo surfaces that carry the pin:
- `codex-rs/Cargo.toml`
- `codex-rs/Cargo.lock`
- `MODULE.bazel`
- `third_party/v8/BUILD.bazel`
- `third_party/v8/README.md`
- the matching `third_party/v8/rusty_v8_<version>.sha256` manifest when the
remaining prebuilt inputs change
3. Keep the existing checksum helpers in the loop:
```bash
python3 .github/scripts/rusty_v8_bazel.py update-module-bazel
python3 .github/scripts/rusty_v8_bazel.py check-module-bazel
python3 -m unittest discover -s .github/scripts -p test_rusty_v8_bazel.py
```
4. Validate the release-candidate path before broadening the work:
- Prefer checking the `v8-canary` CI result for the candidate branch or PR
when one exists, using GitHub check tooling or `gh` as appropriate.
- If CI is unavailable or the user asked for a local-only check, run the
closest local validation that is practical for the changed surface and say
explicitly that it is a local substitute, not the full hosted canary.
5. If the canary path passes, stop there. Summarize the result and encourage the
user to commit the candidate changes or proceed with the release flow they
requested. Do not publish tags, releases, or pushes unless the user asked.
## Failure Path
Enter this path only when the canary or local build path fails.
1. Capture the failing target, workflow job, and first actionable error.
2. Compare the currently pinned version with the target version at the relevant
upstream tag or SHA. Inspect both:
- `denoland/rusty_v8`
- upstream V8 source at the target Bazel-pinned version
3. Track build-relevant deltas rather than broad source churn:
- generated binding layout changes
- archive or asset naming changes
- GN/Bazel target changes
- custom libc++ / libc++abi / llvm-libc inputs
- sandbox or pointer-compression feature relationships
- patch hunks in `patches/` that no longer apply or no longer match upstream
4. Trace each failing delta back into Codex's build graph:
- `MODULE.bazel`
- `third_party/v8/BUILD.bazel`
- `.github/scripts/rusty_v8_bazel.py`
- `.github/workflows/v8-canary.yml`
- `.github/workflows/rusty-v8-release.yml`
5. Update only the pieces required to restore the target version's build and
artifact contract. Keep patch explanations and doc changes close to the
affected files.
6. Re-run the focused validation. If it becomes green, return to the normal
workflow and stop with a concise summary plus the remaining release step.
## Reporting
- Say whether validation came from hosted `v8-canary` or from a local
substitute.
- Distinguish "version bump complete" from "release published".
- When blocked, report the upstream delta that matters, the Codex file it hits,
and the next concrete fix to try.

View File

@@ -1,4 +0,0 @@
interface:
display_name: "Update V8 Version"
short_description: "Guide V8 bumps and release validation"
default_prompt: "Use $update-v8-version to update Codex to a new v8 release and validate the release-candidate path."

View File

@@ -3,7 +3,7 @@ FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04
ARG TZ
ARG DEBIAN_FRONTEND=noninteractive
ARG NODE_MAJOR=22
ARG RUST_TOOLCHAIN=1.95.0
ARG RUST_TOOLCHAIN=1.92.0
# Keep this in sync with .devcontainer/codex-install/package.json and pnpm-lock.yaml.
ARG CODEX_NPM_VERSION=0.121.0

View File

@@ -7,7 +7,7 @@
"args": {
"TZ": "${localEnv:TZ:UTC}",
"NODE_MAJOR": "22",
"RUST_TOOLCHAIN": "1.95.0",
"RUST_TOOLCHAIN": "1.92.0",
"CODEX_NPM_VERSION": "0.121.0"
}
},

1
.github/CODEOWNERS vendored
View File

@@ -1,7 +1,6 @@
# Core crate ownership.
/codex-rs/core/ @openai/codex-core-agent-team
/codex-rs/ext/extension-api/ @openai/codex-core-agent-team
/codex-rs/prompts/ @openai/codex-core-agent-team
# Keep ownership changes reviewed by the same team.
/.github/CODEOWNERS @openai/codex-core-agent-team

View File

@@ -1,17 +0,0 @@
name: setup-msvc-env
description: Expose an MSVC developer environment for the requested Windows target.
inputs:
target:
description: Rust target triple that will be built on this Windows runner.
required: true
host-arch:
description: Optional Visual Studio host architecture override.
required: false
default: ""
runs:
using: composite
steps:
- name: Expose MSVC SDK environment
shell: pwsh
run: '& "$env:GITHUB_ACTION_PATH/setup-msvc-env.ps1" -Target "${{ inputs.target }}" -HostArch "${{ inputs.host-arch }}"'

View File

@@ -1,257 +0,0 @@
param(
[Parameter(Mandatory = $true)]
[string]$Target,
[string]$HostArch = ""
)
# Cargo can cross-compile the Rust code for Windows ARM64 on a Windows x64
# runner, but rustup alone does not expose the matching MSVC/UCRT include and
# library paths. Ask Visual Studio for the target-specific developer
# environment, then persist the relevant variables through GITHUB_ENV so the
# later Cargo step sees the same environment as a normal VsDevCmd shell.
switch ($Target) {
"x86_64-pc-windows-msvc" {
$TargetArch = "x64"
$RequiredComponent = "Microsoft.VisualStudio.Component.VC.Tools.x86.x64"
}
"aarch64-pc-windows-msvc" {
$TargetArch = "arm64"
$RequiredComponent = "Microsoft.VisualStudio.Component.VC.Tools.ARM64"
}
default {
throw "Unsupported Windows MSVC target: $Target"
}
}
# VsDevCmd needs both sides of the cross compile: the architecture of the
# machine running the tools and the architecture of the binaries being linked.
# Infer the host from the runner unless a caller needs to override it.
if (-not $HostArch) {
$HostArch = if ($env:PROCESSOR_ARCHITEW6432 -eq "ARM64" -or $env:PROCESSOR_ARCHITECTURE -eq "ARM64") {
"arm64"
} else {
"x64"
}
}
$VsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
if (-not (Test-Path $VsWhere)) {
throw "vswhere.exe not found"
}
# Require the target VC tools component, not merely any Visual Studio install,
# so an x64 archive producer cannot silently link ARM64 tests with the wrong
# SDK/toolchain layout.
$InstallPath = & $VsWhere -latest -products * -requires $RequiredComponent -property installationPath 2>$null
if (-not $InstallPath) {
throw "Could not locate a Visual Studio installation with component $RequiredComponent"
}
$VsDevCmd = Join-Path $InstallPath "Common7\Tools\VsDevCmd.bat"
if (-not (Test-Path $VsDevCmd)) {
throw "VsDevCmd.bat not found at $VsDevCmd"
}
$VarsToExport = @(
"INCLUDE",
"LIB",
"LIBPATH",
"PATH",
"UCRTVersion",
"UniversalCRTSdkDir",
"VCINSTALLDIR",
"VCToolsInstallDir",
"WindowsLibPath",
"WindowsSdkBinPath",
"WindowsSdkDir",
"WindowsSDKLibVersion",
"WindowsSDKVersion"
)
# Run VsDevCmd inside cmd.exe because it is a batch file, then copy just the
# variables Cargo/rustc need into the GitHub Actions environment file. PowerShell
# cannot mutate the parent composite-action environment directly.
$EnvLines = & cmd.exe /c ('"{0}" -no_logo -arch={1} -host_arch={2} >nul && set' -f $VsDevCmd, $TargetArch, $HostArch)
$VcToolsInstallDir = $null
foreach ($Line in $EnvLines) {
if ($Line -notmatch "^(.*?)=(.*)$") {
continue
}
$Name = $Matches[1]
$Value = $Matches[2]
if ($VarsToExport -contains $Name) {
if ($Name -ieq "Path") {
$Name = "PATH"
}
if ($Name -eq "VCToolsInstallDir") {
$VcToolsInstallDir = $Value
}
"$Name=$Value" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
}
}
if (-not $VcToolsInstallDir) {
throw "VCToolsInstallDir was not exported by VsDevCmd.bat"
}
# Prefer Rust's bundled linker when rustup provides one, then Visual Studio's
# LLVM linker, and finally MSVC link.exe. This keeps the cross-compile path close
# to Rust's normal Windows MSVC behavior while still working on runner images
# where one of those linkers is absent.
$Linker = $null
$Rustc = Get-Command rustc -ErrorAction SilentlyContinue
if ($Rustc) {
$Sysroot = (& rustc --print sysroot 2>$null).Trim()
$RustHost = & rustc -vV 2>$null | Select-String "^host: " | ForEach-Object { $_.Line.Substring(6) }
if ($RustHost) {
$RustHost = $RustHost.Trim()
}
if ($Sysroot -and $RustHost) {
$RustLld = Join-Path $Sysroot "lib\rustlib\$RustHost\bin\rust-lld.exe"
if (Test-Path $RustLld) {
$Linker = $RustLld
}
}
}
if (-not $Linker) {
$Linker = Join-Path $InstallPath "VC\Tools\Llvm\x64\bin\lld-link.exe"
}
if (-not (Test-Path $Linker)) {
$Linker = Join-Path $VcToolsInstallDir "bin\Host${HostArch}\${TargetArch}\link.exe"
}
if (-not (Test-Path $Linker)) {
throw "Windows linker not found at $Linker"
}
# rustc passes `/arm64hazardfree` for ARM64 MSVC links. The lld variants on our
# Windows x64 archive producers reject that flag, including when rustc places it
# inside a response file. Compile a tiny forwarding wrapper that strips only
# that unsupported flag, then delegate every other argument to the real linker.
if ($TargetArch -eq "arm64" -and (Split-Path -Leaf $Linker) -match "lld") {
$WrapperDir = Join-Path $env:RUNNER_TEMP "msvc-lld-wrapper"
New-Item -Path $WrapperDir -ItemType Directory -Force | Out-Null
$WrapperPath = Join-Path $WrapperDir "lld-link-wrapper.exe"
$WrapperSource = @'
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
internal static class Program
{
private static int Main(string[] args)
{
var linker = Environment.GetEnvironmentVariable("MSVC_REAL_LINKER");
if (string.IsNullOrEmpty(linker))
{
Console.Error.WriteLine("MSVC_REAL_LINKER is not set");
return 1;
}
var startInfo = new ProcessStartInfo(linker)
{
UseShellExecute = false,
};
var filteredArgs = new List<string> { "-flavor", "link", "/defaultlib:ucrt", "/nodefaultlib:libucrt" };
foreach (var arg in args)
{
if (!string.Equals(arg, "/arm64hazardfree", StringComparison.OrdinalIgnoreCase))
{
filteredArgs.Add(QuoteArgument(FilterResponseFile(arg)));
}
}
startInfo.Arguments = string.Join(" ", filteredArgs);
using var process = Process.Start(startInfo);
if (process is null)
{
Console.Error.WriteLine($"Failed to start linker: {linker}");
return 1;
}
process.WaitForExit();
return process.ExitCode;
}
private static string FilterResponseFile(string argument)
{
if (argument.Length < 2 || argument[0] != '@')
{
return argument;
}
var responsePath = argument.Substring(1);
if (!File.Exists(responsePath))
{
return argument;
}
var filteredResponsePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".rsp");
var responseContents = Regex.Replace(
File.ReadAllText(responsePath),
"/arm64hazardfree",
string.Empty,
RegexOptions.IgnoreCase);
File.WriteAllText(filteredResponsePath, responseContents);
return "@" + filteredResponsePath;
}
private static string QuoteArgument(string argument)
{
if (argument.Length == 0)
{
return "\"\"";
}
if (argument.IndexOfAny(new[] { ' ', '\t', '"' }) < 0)
{
return argument;
}
var quoted = new StringBuilder("\"");
var backslashes = 0;
foreach (var character in argument)
{
if (character == '\\')
{
backslashes++;
continue;
}
if (character == '"')
{
quoted.Append('\\', (backslashes * 2) + 1);
quoted.Append(character);
backslashes = 0;
continue;
}
quoted.Append('\\', backslashes);
backslashes = 0;
quoted.Append(character);
}
quoted.Append('\\', backslashes * 2);
quoted.Append('"');
return quoted.ToString();
}
}
'@
$WrapperSourcePath = Join-Path $WrapperDir "lld-link-wrapper.cs"
$WrapperSource | Out-File -FilePath $WrapperSourcePath -Encoding utf8
$Csc = Join-Path $InstallPath "MSBuild\Current\Bin\Roslyn\csc.exe"
if (-not (Test-Path $Csc)) {
throw "csc.exe not found at $Csc"
}
& $Csc /nologo /target:exe /out:$WrapperPath $WrapperSourcePath
if ($LASTEXITCODE -ne 0) {
throw "Failed to compile lld-link wrapper"
}
"MSVC_REAL_LINKER=$Linker" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
$Linker = $WrapperPath
}
Write-Output "Using Windows linker: $Linker"
$CargoTarget = $Target.ToUpperInvariant().Replace("-", "_")
"CARGO_TARGET_${CargoTarget}_LINKER=$Linker" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append

View File

@@ -1,20 +1,29 @@
name: setup-rusty-v8
description: Download and verify Codex-built rusty_v8 artifacts for Cargo builds.
name: setup-rusty-v8-musl
description: Download and verify musl rusty_v8 artifacts for Cargo builds.
inputs:
target:
description: Rust target triple with Codex-built V8 release artifacts.
description: Rust musl target triple.
required: true
runs:
using: composite
steps:
- name: Configure rusty_v8 artifact overrides and verify checksums
- name: Configure musl rusty_v8 artifact overrides and verify checksums
shell: bash
env:
TARGET: ${{ inputs.target }}
run: |
set -euo pipefail
case "${TARGET}" in
x86_64-unknown-linux-musl|aarch64-unknown-linux-musl)
;;
*)
echo "Unsupported musl rusty_v8 target: ${TARGET}" >&2
exit 1
;;
esac
version="$(python3 "${GITHUB_WORKSPACE}/.github/scripts/rusty_v8_bazel.py" resolved-v8-crate-version)"
release_tag="rusty-v8-v${version}"
base_url="https://github.com/openai/codex/releases/download/${release_tag}"
@@ -22,21 +31,19 @@ runs:
archive_path="${binding_dir}/librusty_v8_release_${TARGET}.a.gz"
binding_path="${binding_dir}/src_binding_release_${TARGET}.rs"
checksums_path="${binding_dir}/rusty_v8_release_${TARGET}.sha256"
checksums_source="${GITHUB_WORKSPACE}/third_party/v8/rusty_v8_${version//./_}.sha256"
mkdir -p "${binding_dir}"
curl -fsSL "${base_url}/librusty_v8_release_${TARGET}.a.gz" -o "${archive_path}"
curl -fsSL "${base_url}/src_binding_release_${TARGET}.rs" -o "${binding_path}"
curl -fsSL "${base_url}/rusty_v8_release_${TARGET}.sha256" -o "${checksums_path}"
grep -E " (librusty_v8_release_${TARGET}[.]a[.]gz|src_binding_release_${TARGET}[.]rs)$" \
"${checksums_source}" > "${checksums_path}"
if [[ "$(wc -l < "${checksums_path}")" -ne 2 ]]; then
echo "Expected exactly two checksums for ${TARGET} in ${checksums_path}" >&2
echo "Expected exactly two checksums for ${TARGET} in ${checksums_source}" >&2
exit 1
fi
if command -v sha256sum >/dev/null 2>&1; then
(cd "${binding_dir}" && sha256sum -c "${checksums_path}")
else
(cd "${binding_dir}" && shasum -a 256 -c "${checksums_path}")
fi
(cd "${binding_dir}" && sha256sum -c "${checksums_path}")
echo "RUSTY_V8_ARCHIVE=${archive_path}" >> "${GITHUB_ENV}"
echo "RUSTY_V8_SRC_BINDING_PATH=${binding_path}" >> "${GITHUB_ENV}"

View File

@@ -3,56 +3,56 @@
"codex": {
"platforms": {
"macos-aarch64": {
"regex": "^codex-package-aarch64-apple-darwin\\.tar\\.zst$",
"path": "bin/codex"
"regex": "^codex-aarch64-apple-darwin\\.zst$",
"path": "codex"
},
"macos-x86_64": {
"regex": "^codex-package-x86_64-apple-darwin\\.tar\\.zst$",
"path": "bin/codex"
"regex": "^codex-x86_64-apple-darwin\\.zst$",
"path": "codex"
},
"linux-x86_64": {
"regex": "^codex-package-x86_64-unknown-linux-musl\\.tar\\.zst$",
"path": "bin/codex"
"regex": "^codex-x86_64-unknown-linux-musl-bundle\\.tar\\.zst$",
"path": "codex"
},
"linux-aarch64": {
"regex": "^codex-package-aarch64-unknown-linux-musl\\.tar\\.zst$",
"path": "bin/codex"
"regex": "^codex-aarch64-unknown-linux-musl-bundle\\.tar\\.zst$",
"path": "codex"
},
"windows-x86_64": {
"regex": "^codex-package-x86_64-pc-windows-msvc\\.tar\\.zst$",
"path": "bin/codex.exe"
"regex": "^codex-x86_64-pc-windows-msvc\\.exe\\.zst$",
"path": "codex.exe"
},
"windows-aarch64": {
"regex": "^codex-package-aarch64-pc-windows-msvc\\.tar\\.zst$",
"path": "bin/codex.exe"
"regex": "^codex-aarch64-pc-windows-msvc\\.exe\\.zst$",
"path": "codex.exe"
}
}
},
"codex-app-server": {
"platforms": {
"macos-aarch64": {
"regex": "^codex-app-server-package-aarch64-apple-darwin\\.tar\\.zst$",
"path": "bin/codex-app-server"
"regex": "^codex-app-server-aarch64-apple-darwin\\.zst$",
"path": "codex-app-server"
},
"macos-x86_64": {
"regex": "^codex-app-server-package-x86_64-apple-darwin\\.tar\\.zst$",
"path": "bin/codex-app-server"
"regex": "^codex-app-server-x86_64-apple-darwin\\.zst$",
"path": "codex-app-server"
},
"linux-x86_64": {
"regex": "^codex-app-server-package-x86_64-unknown-linux-musl\\.tar\\.zst$",
"path": "bin/codex-app-server"
"regex": "^codex-app-server-x86_64-unknown-linux-musl\\.zst$",
"path": "codex-app-server"
},
"linux-aarch64": {
"regex": "^codex-app-server-package-aarch64-unknown-linux-musl\\.tar\\.zst$",
"path": "bin/codex-app-server"
"regex": "^codex-app-server-aarch64-unknown-linux-musl\\.zst$",
"path": "codex-app-server"
},
"windows-x86_64": {
"regex": "^codex-app-server-package-x86_64-pc-windows-msvc\\.tar\\.zst$",
"path": "bin/codex-app-server.exe"
"regex": "^codex-app-server-x86_64-pc-windows-msvc\\.exe\\.zst$",
"path": "codex-app-server.exe"
},
"windows-aarch64": {
"regex": "^codex-app-server-package-aarch64-pc-windows-msvc\\.tar\\.zst$",
"path": "bin/codex-app-server.exe"
"regex": "^codex-app-server-aarch64-pc-windows-msvc\\.exe\\.zst$",
"path": "codex-app-server.exe"
}
}
},

124
.github/dotslash-unsigned-config.json vendored Normal file
View File

@@ -0,0 +1,124 @@
{
"outputs": {
"codex-unsigned": {
"platforms": {
"macos-aarch64": {
"regex": "^codex-aarch64-apple-darwin-unsigned\\.zst$",
"path": "codex"
},
"macos-x86_64": {
"regex": "^codex-x86_64-apple-darwin-unsigned\\.zst$",
"path": "codex"
},
"linux-x86_64": {
"regex": "^codex-x86_64-unknown-linux-musl-bundle\\.tar\\.zst$",
"path": "codex"
},
"linux-aarch64": {
"regex": "^codex-aarch64-unknown-linux-musl-bundle\\.tar\\.zst$",
"path": "codex"
},
"windows-x86_64": {
"regex": "^codex-x86_64-pc-windows-msvc\\.exe\\.zst$",
"path": "codex.exe"
},
"windows-aarch64": {
"regex": "^codex-aarch64-pc-windows-msvc\\.exe\\.zst$",
"path": "codex.exe"
}
}
},
"codex-app-server-unsigned": {
"platforms": {
"macos-aarch64": {
"regex": "^codex-app-server-aarch64-apple-darwin-unsigned\\.zst$",
"path": "codex-app-server"
},
"macos-x86_64": {
"regex": "^codex-app-server-x86_64-apple-darwin-unsigned\\.zst$",
"path": "codex-app-server"
},
"linux-x86_64": {
"regex": "^codex-app-server-x86_64-unknown-linux-musl\\.zst$",
"path": "codex-app-server"
},
"linux-aarch64": {
"regex": "^codex-app-server-aarch64-unknown-linux-musl\\.zst$",
"path": "codex-app-server"
},
"windows-x86_64": {
"regex": "^codex-app-server-x86_64-pc-windows-msvc\\.exe\\.zst$",
"path": "codex-app-server.exe"
},
"windows-aarch64": {
"regex": "^codex-app-server-aarch64-pc-windows-msvc\\.exe\\.zst$",
"path": "codex-app-server.exe"
}
}
},
"codex-responses-api-proxy-unsigned": {
"platforms": {
"macos-aarch64": {
"regex": "^codex-responses-api-proxy-aarch64-apple-darwin-unsigned\\.zst$",
"path": "codex-responses-api-proxy"
},
"macos-x86_64": {
"regex": "^codex-responses-api-proxy-x86_64-apple-darwin-unsigned\\.zst$",
"path": "codex-responses-api-proxy"
},
"linux-x86_64": {
"regex": "^codex-responses-api-proxy-x86_64-unknown-linux-musl\\.zst$",
"path": "codex-responses-api-proxy"
},
"linux-aarch64": {
"regex": "^codex-responses-api-proxy-aarch64-unknown-linux-musl\\.zst$",
"path": "codex-responses-api-proxy"
},
"windows-x86_64": {
"regex": "^codex-responses-api-proxy-x86_64-pc-windows-msvc\\.exe\\.zst$",
"path": "codex-responses-api-proxy.exe"
},
"windows-aarch64": {
"regex": "^codex-responses-api-proxy-aarch64-pc-windows-msvc\\.exe\\.zst$",
"path": "codex-responses-api-proxy.exe"
}
}
},
"bwrap": {
"platforms": {
"linux-x86_64": {
"regex": "^bwrap-x86_64-unknown-linux-musl\\.zst$",
"path": "bwrap"
},
"linux-aarch64": {
"regex": "^bwrap-aarch64-unknown-linux-musl\\.zst$",
"path": "bwrap"
}
}
},
"codex-command-runner": {
"platforms": {
"windows-x86_64": {
"regex": "^codex-command-runner-x86_64-pc-windows-msvc\\.exe\\.zst$",
"path": "codex-command-runner.exe"
},
"windows-aarch64": {
"regex": "^codex-command-runner-aarch64-pc-windows-msvc\\.exe\\.zst$",
"path": "codex-command-runner.exe"
}
}
},
"codex-windows-sandbox-setup": {
"platforms": {
"windows-x86_64": {
"regex": "^codex-windows-sandbox-setup-x86_64-pc-windows-msvc\\.exe\\.zst$",
"path": "codex-windows-sandbox-setup.exe"
},
"windows-aarch64": {
"regex": "^codex-windows-sandbox-setup-aarch64-pc-windows-msvc\\.exe\\.zst$",
"path": "codex-windows-sandbox-setup.exe"
}
}
}
}
}

View File

@@ -7,11 +7,6 @@
"format": "tar.gz",
"path": "codex-zsh/bin/zsh"
},
"macos-x86_64": {
"name": "codex-zsh-x86_64-apple-darwin.tar.gz",
"format": "tar.gz",
"path": "codex-zsh/bin/zsh"
},
"linux-x86_64": {
"name": "codex-zsh-x86_64-unknown-linux-musl.tar.gz",
"format": "tar.gz",

View File

@@ -1,119 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: archive-release-symbols-and-strip-binaries.sh \
--target <rust-target> \
--artifact-name <artifact-name> \
--release-dir <dir> \
--archive-dir <dir> \
--binaries "<space-delimited binary basenames>"
EOF
}
target=""
artifact_name=""
release_dir=""
archive_dir=""
binaries=""
while [[ $# -gt 0 ]]; do
case "$1" in
--target)
target="${2:?--target requires a value}"
shift 2
;;
--artifact-name)
artifact_name="${2:?--artifact-name requires a value}"
shift 2
;;
--release-dir)
release_dir="${2:?--release-dir requires a value}"
shift 2
;;
--archive-dir)
archive_dir="${2:?--archive-dir requires a value}"
shift 2
;;
--binaries)
binaries="${2:?--binaries requires a value}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unexpected argument: $1" >&2
usage >&2
exit 1
;;
esac
done
if [[ -z "$target" || -z "$artifact_name" || -z "$release_dir" || -z "$archive_dir" || -z "$binaries" ]]; then
usage >&2
exit 1
fi
symbols_root="${RUNNER_TEMP:-/tmp}/codex-symbols-${artifact_name}"
symbols_dir="${symbols_root}/codex-symbols-${artifact_name}"
archive_path="${archive_dir%/}/codex-symbols-${artifact_name}.tar.gz"
rm -rf "$symbols_root"
mkdir -p "$symbols_dir" "$archive_dir"
read -r -a binary_names <<< "$binaries"
case "$target" in
*apple-darwin)
for binary in "${binary_names[@]}"; do
binary_path="${release_dir%/}/${binary}"
dsym_path="${binary_path}.dSYM"
if [[ ! -f "$binary_path" ]]; then
echo "Binary $binary_path not found" >&2
exit 1
fi
if [[ ! -d "$dsym_path" ]]; then
echo "dSYM $dsym_path not found" >&2
exit 1
fi
cp -RL "$dsym_path" "${symbols_dir}/${binary}.dSYM"
strip -S -x "$binary_path"
done
;;
*linux*)
objcopy_bin="${OBJCOPY:-objcopy}"
strip_bin="${STRIP:-strip}"
for binary in "${binary_names[@]}"; do
binary_path="${release_dir%/}/${binary}"
debug_path="${symbols_dir}/${binary}.debug"
if [[ ! -f "$binary_path" ]]; then
echo "Binary $binary_path not found" >&2
exit 1
fi
"$objcopy_bin" --only-keep-debug "$binary_path" "$debug_path"
"$strip_bin" --strip-debug --strip-unneeded "$binary_path"
"$objcopy_bin" --add-gnu-debuglink="$debug_path" "$binary_path"
done
;;
*windows*)
for binary in "${binary_names[@]}"; do
pdb_path="${release_dir%/}/${binary}.pdb"
if [[ ! -f "$pdb_path" ]]; then
echo "PDB $pdb_path not found" >&2
exit 1
fi
cp "$pdb_path" "${symbols_dir}/${binary}.pdb"
done
;;
*)
echo "No symbols packaging support for target: $target" >&2
exit 1
;;
esac
rm -f "$archive_path"
tar -C "$symbols_root" -czf "$archive_path" "codex-symbols-${artifact_name}"

View File

@@ -1,172 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: build-codex-package-archive.sh \
--target <rust-target> \
--bundle <primary|app-server> \
--entrypoint-dir <dir> \
--archive-dir <dir> \
[--bwrap-bin <path>] \
[--codex-command-runner-bin <path>] \
[--codex-windows-sandbox-setup-bin <path>] \
[--target-suffixed-entrypoint]
EOF
}
target=""
bundle=""
entrypoint_dir=""
archive_dir=""
target_suffixed_entrypoint="false"
resource_args=()
bwrap_bin_provided="false"
command_runner_bin_provided="false"
sandbox_setup_bin_provided="false"
while [[ $# -gt 0 ]]; do
case "$1" in
--target)
target="${2:?--target requires a value}"
shift 2
;;
--bundle)
bundle="${2:?--bundle requires a value}"
shift 2
;;
--entrypoint-dir)
entrypoint_dir="${2:?--entrypoint-dir requires a value}"
shift 2
;;
--archive-dir)
archive_dir="${2:?--archive-dir requires a value}"
shift 2
;;
--bwrap-bin)
resource_args+=(--bwrap-bin "${2:?--bwrap-bin requires a value}")
bwrap_bin_provided="true"
shift 2
;;
--codex-command-runner-bin)
resource_args+=(
--codex-command-runner-bin
"${2:?--codex-command-runner-bin requires a value}"
)
command_runner_bin_provided="true"
shift 2
;;
--codex-windows-sandbox-setup-bin)
resource_args+=(
--codex-windows-sandbox-setup-bin
"${2:?--codex-windows-sandbox-setup-bin requires a value}"
)
sandbox_setup_bin_provided="true"
shift 2
;;
--target-suffixed-entrypoint)
target_suffixed_entrypoint="true"
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unexpected argument: $1" >&2
usage >&2
exit 1
;;
esac
done
if [[ -z "$target" || -z "$bundle" || -z "$entrypoint_dir" || -z "$archive_dir" ]]; then
usage >&2
exit 1
fi
case "$bundle" in
primary)
variant="codex"
entrypoint="codex"
archive_stem="codex-package"
;;
app-server)
variant="codex-app-server"
entrypoint="codex-app-server"
archive_stem="codex-app-server-package"
;;
*)
echo "No Codex package variant for bundle: $bundle" >&2
exit 1
;;
esac
exe_suffix=""
case "$target" in
*windows*)
exe_suffix=".exe"
;;
esac
entrypoint_name="$entrypoint"
if [[ "$target_suffixed_entrypoint" == "true" ]]; then
entrypoint_name="${entrypoint_name}-${target}"
fi
case "$target" in
*linux*)
bwrap_bin="${entrypoint_dir%/}/bwrap"
if [[ "$bwrap_bin_provided" == "false" && -f "$bwrap_bin" ]]; then
resource_args+=(--bwrap-bin "$bwrap_bin")
fi
;;
*windows*)
command_runner_bin="${entrypoint_dir%/}/codex-command-runner.exe"
sandbox_setup_bin="${entrypoint_dir%/}/codex-windows-sandbox-setup.exe"
if [[ "$command_runner_bin_provided" == "false" && -f "$command_runner_bin" ]]; then
resource_args+=(--codex-command-runner-bin "$command_runner_bin")
fi
if [[ "$sandbox_setup_bin_provided" == "false" && -f "$sandbox_setup_bin" ]]; then
resource_args+=(--codex-windows-sandbox-setup-bin "$sandbox_setup_bin")
fi
;;
esac
repo_root="${GITHUB_WORKSPACE:-}"
if [[ -z "$repo_root" ]]; then
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
fi
if command -v python3 >/dev/null 2>&1; then
python_bin="python3"
else
python_bin="python"
fi
if ! command -v zstd >/dev/null 2>&1 && [[ -x "${repo_root}/.github/workflows/zstd" ]]; then
export PATH="${repo_root}/.github/workflows:${PATH}"
fi
mkdir -p "$archive_dir"
package_dir="${RUNNER_TEMP:-/tmp}/${archive_stem}-${target}"
gzip_archive_path="${archive_dir}/${archive_stem}-${target}.tar.gz"
zstd_archive_path="${archive_dir}/${archive_stem}-${target}.tar.zst"
rm -rf "$package_dir"
python_args=(
"${repo_root}/scripts/build_codex_package.py"
--target "$target"
--variant "$variant"
--entrypoint-bin "${entrypoint_dir%/}/${entrypoint_name}${exe_suffix}"
--cargo-profile release
--package-dir "$package_dir"
--archive-output "$gzip_archive_path"
--archive-output "$zstd_archive_path"
)
if ((${#resource_args[@]} > 0)); then
python_args+=("${resource_args[@]}")
fi
python_args+=(--force)
"$python_bin" "${python_args[@]}"

View File

@@ -150,9 +150,7 @@ for arg in "\$@"; do
args+=("\${arg}")
done
# Zig enables UBSan for debug C builds by default. Rust links these objects
# without Zig's sanitizer runtime, so keep native dependencies uninstrumented.
exec "${zig_bin}" cc -target "${zig_target}" "\${args[@]}" -fno-sanitize=undefined
exec "${zig_bin}" cc -target "${zig_target}" "\${args[@]}"
EOF
cat >"${cxx}" <<EOF
#!/usr/bin/env bash
@@ -209,9 +207,7 @@ for arg in "\$@"; do
args+=("\${arg}")
done
# Zig enables UBSan for debug C++ builds by default. Rust links these objects
# without Zig's sanitizer runtime, so keep native dependencies uninstrumented.
exec "${zig_bin}" c++ -target "${zig_target}" "\${args[@]}" -fno-sanitize=undefined
exec "${zig_bin}" c++ -target "${zig_target}" "\${args[@]}"
EOF
chmod +x "${cc}" "${cxx}"
@@ -274,11 +270,6 @@ echo "PKG_CONFIG_PATH=${pkg_config_path}" >> "$GITHUB_ENV"
pkg_config_path_var="PKG_CONFIG_PATH_${TARGET}"
pkg_config_path_var="${pkg_config_path_var//-/_}"
echo "${pkg_config_path_var}=${libcap_pkgconfig_dir}" >> "$GITHUB_ENV"
pkg_config_libdir_var="PKG_CONFIG_LIBDIR_${TARGET}"
pkg_config_libdir_var="${pkg_config_libdir_var//-/_}"
# Do not let musl cross-builds resolve native libraries from the host glibc
# pkg-config directories. libcap is the only target package provided here.
echo "${pkg_config_libdir_var}=${libcap_pkgconfig_dir}" >> "$GITHUB_ENV"
if [[ -n "${sysroot}" && "${sysroot}" != "/" ]]; then
echo "PKG_CONFIG_SYSROOT_DIR=${sysroot}" >> "$GITHUB_ENV"

View File

@@ -53,20 +53,11 @@ fi
run_bazel() {
if [[ "${RUNNER_OS:-}" == "Windows" ]]; then
MSYS2_ARG_CONV_EXCL='*' "$(dirname "${BASH_SOURCE[0]}")/run_bazel_with_buildbuddy.py" "$@"
MSYS2_ARG_CONV_EXCL='*' bazel "$@"
return
fi
"$(dirname "${BASH_SOURCE[0]}")/run_bazel_with_buildbuddy.py" "$@"
}
run_bazel_with_startup_args() {
if (( ${#bazel_startup_args[@]} > 0 )); then
run_bazel "${bazel_startup_args[@]}" "$@"
return
fi
run_bazel "$@"
bazel "$@"
}
ci_config=ci-linux
@@ -86,16 +77,23 @@ esac
print_bazel_test_log_tails() {
local console_log="$1"
local testlogs_dir
local -a bazel_info_cmd=(bazel)
local -a bazel_info_args=(info)
if [[ -n "${BUILDBUDDY_API_KEY:-}" ]]; then
# `bazel info` needs the same CI config as the failed test invocation so
# platform-specific output roots match. On Windows, omitting `ci-windows`
# would point at `local_windows-fastbuild` even when the test ran with the
# MSVC host platform under `local_windows_msvc-fastbuild`.
bazel_info_args+=("--config=${ci_config}")
if (( ${#bazel_startup_args[@]} > 0 )); then
bazel_info_cmd+=("${bazel_startup_args[@]}")
fi
# `bazel info` needs the same CI config as the failed test invocation so
# platform-specific output roots match. On Windows, omitting `ci-windows`
# would point at `local_windows-fastbuild` even when the test ran with the
# MSVC host platform under `local_windows_msvc-fastbuild`.
if [[ -n "${BUILDBUDDY_API_KEY:-}" ]]; then
bazel_info_args+=(
"--config=${ci_config}"
"--remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}"
)
fi
# Only pass flags that affect Bazel's output-root selection or repository
# lookup. Test/build-only flags such as execution logs or remote download
# mode can make `bazel info` fail, which would hide the real test log path.
@@ -107,7 +105,7 @@ print_bazel_test_log_tails() {
esac
done
testlogs_dir="$(run_bazel_with_startup_args \
testlogs_dir="$(run_bazel "${bazel_info_cmd[@]:1}" \
--noexperimental_remote_repo_contents_cache \
"${bazel_info_args[@]}" \
bazel-testlogs 2>/dev/null || echo bazel-testlogs)"
@@ -256,9 +254,8 @@ if [[ ${#bazel_args[@]} -eq 0 || ${#bazel_targets[@]} -eq 0 ]]; then
fi
if [[ "${RUNNER_OS:-}" == "Windows" && $windows_cross_compile -eq 1 && -z "${BUILDBUDDY_API_KEY:-}" ]]; then
# Windows cross-compilation depends on authenticated RBE. Preserve the local
# Windows build shape when credentials are unavailable.
ci_config=ci-windows
# Fork PRs do not receive the BuildBuddy secret needed for the remote
# cross-compile config. Preserve the previous local Windows build shape.
windows_msvc_host_platform=1
fi
@@ -300,9 +297,9 @@ if [[ "${RUNNER_OS:-}" == "Windows" && $windows_cross_compile -eq 1 && -n "${BUI
fi
if [[ "${RUNNER_OS:-}" == "Windows" && $windows_cross_compile -eq 1 && -z "${BUILDBUDDY_API_KEY:-}" ]]; then
# The Windows cross-compile config depends on authenticated remote
# execution. When credentials are unavailable, keep the local build shape
# and its lower concurrency cap.
# The Windows cross-compile config depends on remote execution. Fork PRs do
# not receive the BuildBuddy secret, so fall back to the existing local build
# shape and keep its lower concurrency cap.
post_config_bazel_args+=(--jobs=8)
fi
@@ -380,31 +377,70 @@ fi
bazel_console_log="$(mktemp)"
trap 'rm -f "$bazel_console_log"' EXIT
bazel_run_args=(
"${bazel_args[@]}"
)
bazel_cmd=(bazel)
if (( ${#bazel_startup_args[@]} > 0 )); then
bazel_cmd+=("${bazel_startup_args[@]}")
fi
if [[ -n "${BUILDBUDDY_API_KEY:-}" ]]; then
echo "BuildBuddy API key is available; using remote Bazel configuration."
bazel_run_args+=("--config=${ci_config}")
# Work around Bazel 9 remote repo contents cache / overlay materialization failures
# seen in CI (for example "is not a symlink" or permission errors while
# materializing external repos such as rules_perl). We still use BuildBuddy for
# remote execution/cache; this only disables the startup-level repo contents cache.
bazel_run_args=(
"${bazel_args[@]}"
"--config=${ci_config}"
"--remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}"
)
if (( ${#post_config_bazel_args[@]} > 0 )); then
bazel_run_args+=("${post_config_bazel_args[@]}")
fi
set +e
run_bazel "${bazel_cmd[@]:1}" \
--noexperimental_remote_repo_contents_cache \
"${bazel_run_args[@]}" \
-- \
"${bazel_targets[@]}" \
2>&1 | tee "$bazel_console_log"
bazel_status=${PIPESTATUS[0]}
set -e
else
echo "BuildBuddy API key is not available; using local Bazel configuration."
# Keep fork/community PRs on Bazel but disable remote services that are
# configured in .bazelrc and require auth.
#
# Flag docs:
# - Command-line reference: https://bazel.build/reference/command-line-reference
# - Remote caching overview: https://bazel.build/remote/caching
# - Remote execution overview: https://bazel.build/remote/rbe
# - Build Event Protocol overview: https://bazel.build/remote/bep
#
# --noexperimental_remote_repo_contents_cache:
# disable remote repo contents cache enabled in .bazelrc startup options.
# https://bazel.build/reference/command-line-reference#startup_options-flag--experimental_remote_repo_contents_cache
# --remote_cache= and --remote_executor=:
# clear remote cache/execution endpoints configured in .bazelrc.
# https://bazel.build/reference/command-line-reference#common_options-flag--remote_cache
# https://bazel.build/reference/command-line-reference#common_options-flag--remote_executor
bazel_run_args=(
"${bazel_args[@]}"
--remote_cache=
--remote_executor=
)
if (( ${#post_config_bazel_args[@]} > 0 )); then
bazel_run_args+=("${post_config_bazel_args[@]}")
fi
set +e
run_bazel "${bazel_cmd[@]:1}" \
--noexperimental_remote_repo_contents_cache \
"${bazel_run_args[@]}" \
-- \
"${bazel_targets[@]}" \
2>&1 | tee "$bazel_console_log"
bazel_status=${PIPESTATUS[0]}
set -e
fi
if (( ${#post_config_bazel_args[@]} > 0 )); then
bazel_run_args+=("${post_config_bazel_args[@]}")
fi
set +e
# Work around Bazel 9 remote repo contents cache / overlay materialization
# failures seen in CI (for example "is not a symlink" or permission errors
# while materializing external repos such as rules_perl). This only disables
# the startup-level repo contents cache; keyed runs still use BuildBuddy.
run_bazel_with_startup_args \
--noexperimental_remote_repo_contents_cache \
"${bazel_run_args[@]}" \
-- \
"${bazel_targets[@]}" \
2>&1 | tee "$bazel_console_log"
bazel_status=${PIPESTATUS[0]}
set -e
if [[ ${bazel_status:-0} -ne 0 ]]; then
if [[ $print_failed_bazel_action_summary -eq 1 ]]; then

View File

@@ -2,17 +2,48 @@
set -euo pipefail
# Run target-discovery queries with the same startup settings as the main
# build/test invocation so they can reuse the same Bazel server. Queries only
# enumerate labels, so they intentionally do not select CI or remote configs.
# Run Bazel queries with the same CI startup settings as the main build/test
# invocation so target-discovery queries can reuse the same Bazel server.
if [[ $# -lt 2 || "${@: -2:1}" != "--" ]]; then
echo "Usage: $0 [<bazel query args>...] -- <query expression>" >&2
query_args=()
windows_cross_compile=0
while [[ $# -gt 0 ]]; do
case "$1" in
--windows-cross-compile)
windows_cross_compile=1
shift
;;
--)
shift
break
;;
*)
query_args+=("$1")
shift
;;
esac
done
if [[ $# -ne 1 ]]; then
echo "Usage: $0 [--windows-cross-compile] [<bazel query args>...] -- <query expression>" >&2
exit 1
fi
query_args=("${@:1:$#-2}")
query_expression="${@: -1}"
query_expression="$1"
ci_config=ci-linux
case "${RUNNER_OS:-}" in
macOS)
ci_config=ci-macos
;;
Windows)
if [[ $windows_cross_compile -eq 1 ]]; then
ci_config=ci-windows-cross
else
ci_config=ci-windows
fi
;;
esac
bazel_startup_args=()
if [[ -n "${BAZEL_OUTPUT_USER_ROOT:-}" ]]; then
@@ -29,6 +60,12 @@ run_bazel() {
}
bazel_query_args=(--noexperimental_remote_repo_contents_cache query)
if [[ -n "${BUILDBUDDY_API_KEY:-}" ]]; then
bazel_query_args+=(
"--config=${ci_config}"
"--remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}"
)
fi
if [[ -n "${BAZEL_REPO_CONTENTS_CACHE:-}" ]]; then
bazel_query_args+=("--repo_contents_cache=${BAZEL_REPO_CONTENTS_CACHE}")
@@ -38,10 +75,7 @@ if [[ -n "${BAZEL_REPOSITORY_CACHE:-}" ]]; then
bazel_query_args+=("--repository_cache=${BAZEL_REPOSITORY_CACHE}")
fi
if (( ${#query_args[@]} > 0 )); then
bazel_query_args+=("${query_args[@]}")
fi
bazel_query_args+=("$query_expression")
bazel_query_args+=("${query_args[@]}" "$query_expression")
if (( ${#bazel_startup_args[@]} > 0 )); then
run_bazel "${bazel_startup_args[@]}" "${bazel_query_args[@]}"

View File

@@ -1,142 +0,0 @@
#!/usr/bin/env python3
import json
import os
import sys
from collections.abc import Mapping
from collections.abc import Sequence
from pathlib import Path
OPENAI_REPOSITORY = "openai/codex"
# Remote configurations select cache/BES/download endpoints. Their -rbe forms
# also select the matching remote executor endpoint.
GENERIC_REMOTE_CONFIG = "buildbuddy-generic"
OPENAI_REMOTE_CONFIG = "buildbuddy-openai"
# These CI configurations require remote build execution. The wrapper supplies
# an RBE configuration, which also includes the common `remote` settings.
REMOTE_EXECUTION_CONFIGS = {
"--config=ci-linux",
"--config=ci-macos",
"--config=ci-v8",
"--config=ci-windows-cross",
}
# Only authenticated workflow runs executing trusted upstream code may use the
# OpenAI BuildBuddy host. A pull request event without proof that its head is
# in the upstream repository fails closed to the generic host.
def is_trusted_upstream_run(env: Mapping[str, str]) -> bool:
# `GITHUB_REPOSITORY` is easy to set locally. Requiring GitHub's workflow
# marker prevents a local command from opting itself into the OpenAI host.
if (
env.get("GITHUB_ACTIONS") != "true"
or env.get("GITHUB_REPOSITORY") != OPENAI_REPOSITORY
):
return False
# Non-PR workflow runs in `openai/codex` execute upstream refs, so they are
# trusted. Fork code reaches these workflows only through pull requests.
if env.get("GITHUB_EVENT_NAME") != "pull_request":
return True
event_path = env.get("GITHUB_EVENT_PATH")
if not event_path:
return False
try:
event = json.loads(Path(event_path).read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return False
try:
return event["pull_request"]["head"]["repo"]["fork"] is False
except (KeyError, TypeError):
return False
def uses_openai_host(env: Mapping[str, str]) -> bool:
return bool(env.get("BUILDBUDDY_API_KEY")) and is_trusted_upstream_run(env)
def uses_remote_execution(args: Sequence[str]) -> bool:
try:
separator_idx = args.index("--")
except ValueError:
separator_idx = len(args)
return any(arg in REMOTE_EXECUTION_CONFIGS for arg in args[:separator_idx])
def remote_config(args: Sequence[str], env: Mapping[str, str]) -> str | None:
if not env.get("BUILDBUDDY_API_KEY"):
return None
config = OPENAI_REMOTE_CONFIG if uses_openai_host(env) else GENERIC_REMOTE_CONFIG
if uses_remote_execution(args):
config += "-rbe"
return config
def bazel_args_without_remote_execution(args: Sequence[str]) -> list[str]:
# Remote CI configs require BuildBuddy credentials. Removing them preserves
# the local fallback used for fork pull requests.
try:
separator_idx = args.index("--")
except ValueError:
separator_idx = len(args)
return [
*(arg for arg in args[:separator_idx] if arg not in REMOTE_EXECUTION_CONFIGS),
*args[separator_idx:],
]
def bazel_args_with_remote_config(
args: Sequence[str], env: Mapping[str, str]
) -> list[str]:
config = remote_config(args, env)
if config is None:
return bazel_args_without_remote_execution(args)
# `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.
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]:
env = os.environ if env is None else env
bazel = env.get("CODEX_BAZEL_BIN", "bazel")
return [bazel, *bazel_args_with_remote_config(args, env)]
def main() -> None:
config = remote_config(sys.argv[1:], os.environ)
if config is None:
print(
"BuildBuddy key unavailable; using local Bazel configuration.",
file=sys.stderr,
)
else:
host_description = (
"OpenAI tenant" if uses_openai_host(os.environ) else "generic"
)
print(
f"Using {host_description} BuildBuddy configuration: {config}.",
file=sys.stderr,
)
command = bazel_command(*sys.argv[1:])
# Replace the wrapper so Bazel receives signals directly and supplies the
# command exit status; a subprocess parent would have no remaining work.
os.execvp(command[0], command)
if __name__ == "__main__":
main()

View File

@@ -9,14 +9,13 @@ import re
import shutil
import subprocess
import sys
import tempfile
import tomllib
from pathlib import Path
from run_bazel_with_buildbuddy import bazel_command
from rusty_v8_module_bazel import (
RustyV8ChecksumError,
check_module_bazel,
rusty_v8_http_file_versions,
update_module_bazel,
)
@@ -24,27 +23,34 @@ from rusty_v8_module_bazel import (
ROOT = Path(__file__).resolve().parents[2]
MODULE_BAZEL = ROOT / "MODULE.bazel"
RUSTY_V8_CHECKSUMS_DIR = ROOT / "third_party" / "v8"
RELEASE_ARTIFACT_PROFILE = "release"
SANDBOX_ARTIFACT_PROFILE = "ptrcomp_sandbox_release"
ARTIFACT_BAZEL_CONFIGS = ["rusty-v8-upstream-libcxx"]
MUSL_RUNTIME_ARCHIVE_LABELS = [
"@llvm//runtimes/libcxx:libcxx.static",
"@llvm//runtimes/libcxx:libcxxabi.static",
]
LLVM_AR_LABEL = "@llvm//tools:llvm-ar"
LLVM_RANLIB_LABEL = "@llvm//tools:llvm-ranlib"
def bazel_execroot() -> Path:
output = subprocess.check_output(
bazel_command("info", "execution_root"),
result = subprocess.run(
["bazel", "info", "execution_root"],
cwd=ROOT,
check=True,
capture_output=True,
text=True,
)
return Path(output.strip())
return Path(result.stdout.strip())
def bazel_output_base() -> Path:
output = subprocess.check_output(
bazel_command("info", "output_base"),
result = subprocess.run(
["bazel", "info", "output_base"],
cwd=ROOT,
check=True,
capture_output=True,
text=True,
)
return Path(output.strip())
return Path(result.stdout.strip())
def bazel_output_path(path: str) -> Path:
@@ -61,8 +67,9 @@ def bazel_output_files(
) -> list[Path]:
expression = "set(" + " ".join(labels) + ")"
bazel_configs = bazel_configs or []
output = subprocess.check_output(
bazel_command(
result = subprocess.run(
[
"bazel",
"cquery",
"-c",
compilation_mode,
@@ -70,13 +77,13 @@ def bazel_output_files(
*[f"--config={config}" for config in bazel_configs],
"--output=files",
expression,
),
],
cwd=ROOT,
check=True,
capture_output=True,
text=True,
)
return [
bazel_output_path(line.strip()) for line in output.splitlines() if line.strip()
]
return [bazel_output_path(line.strip()) for line in result.stdout.splitlines() if line.strip()]
def bazel_build(
@@ -84,20 +91,18 @@ def bazel_build(
labels: list[str],
compilation_mode: str = "fastbuild",
bazel_configs: list[str] | None = None,
download_toplevel: bool = False,
) -> None:
bazel_configs = bazel_configs or []
download_args = ["--remote_download_toplevel"] if download_toplevel else []
subprocess.run(
bazel_command(
[
"bazel",
"build",
"-c",
compilation_mode,
f"--platforms=@llvm//platforms:{platform}",
*[f"--config={config}" for config in bazel_configs],
*download_args,
*labels,
),
],
cwd=ROOT,
check=True,
)
@@ -109,15 +114,11 @@ def ensure_bazel_output_files(
compilation_mode: str = "fastbuild",
bazel_configs: list[str] | None = None,
) -> list[Path]:
# Bazel output paths can be reused across config flips, so existence alone
# does not prove the files match the requested flags.
bazel_build(
platform,
labels,
compilation_mode,
bazel_configs,
download_toplevel=True,
)
outputs = bazel_output_files(platform, labels, compilation_mode, bazel_configs)
if all(path.exists() for path in outputs):
return outputs
bazel_build(platform, labels, compilation_mode, bazel_configs)
outputs = bazel_output_files(platform, labels, compilation_mode, bazel_configs)
missing = [str(path) for path in outputs if not path.exists()]
if missing:
@@ -125,18 +126,9 @@ def ensure_bazel_output_files(
return outputs
def artifact_bazel_configs(bazel_configs: list[str] | None = None) -> list[str]:
configured = list(ARTIFACT_BAZEL_CONFIGS)
for config in bazel_configs or []:
if config not in configured:
configured.append(config)
return configured
def release_pair_label(target: str, sandbox: bool = False) -> str:
def release_pair_label(target: str) -> str:
target_suffix = target.replace("-", "_")
pair_kind = "sandbox_release_pair" if sandbox else "release_pair"
return f"//third_party/v8:rusty_v8_{pair_kind}_{target_suffix}"
return f"//third_party/v8:rusty_v8_release_pair_{target_suffix}"
def resolved_v8_crate_version() -> str:
@@ -157,7 +149,7 @@ def resolved_v8_crate_version() -> str:
matches = sorted(
set(
re.findall(
r"https://static\.crates\.io/crates/v8/v8-([0-9]+\.[0-9]+\.[0-9]+)\.crate",
r'https://static\.crates\.io/crates/v8/v8-([0-9]+\.[0-9]+\.[0-9]+)\.crate',
module_bazel,
)
)
@@ -177,16 +169,6 @@ def rusty_v8_checksum_manifest_path(version: str) -> Path:
def command_version(version: str | None) -> str:
if version is not None:
return version
manifest_versions = rusty_v8_http_file_versions(MODULE_BAZEL.read_text())
if len(manifest_versions) == 1:
return manifest_versions[0]
if len(manifest_versions) > 1:
raise SystemExit(
"expected at most one rusty_v8 http_file version in MODULE.bazel, "
f"found: {manifest_versions}; pass --version explicitly"
)
return resolved_v8_crate_version()
@@ -198,82 +180,66 @@ def command_manifest_path(manifest: Path | None, version: str) -> Path:
return ROOT / manifest
def staged_archive_name(target: str, source_path: Path, artifact_profile: str) -> str:
if target.endswith("-pc-windows-msvc"):
return f"rusty_v8_{artifact_profile}_{target}.lib.gz"
return f"librusty_v8_{artifact_profile}_{target}.a.gz"
def staged_archive_name(target: str, source_path: Path) -> str:
if source_path.suffix == ".lib":
return f"rusty_v8_release_{target}.lib.gz"
return f"librusty_v8_release_{target}.a.gz"
def staged_binding_name(target: str, artifact_profile: str) -> str:
return f"src_binding_{artifact_profile}_{target}.rs"
def is_musl_archive_target(target: str, source_path: Path) -> bool:
return target.endswith("-unknown-linux-musl") and source_path.suffix == ".a"
def staged_checksums_name(target: str, artifact_profile: str) -> str:
return f"rusty_v8_{artifact_profile}_{target}.sha256"
def single_bazel_output_file(
platform: str,
label: str,
compilation_mode: str = "fastbuild",
bazel_configs: list[str] | None = None,
) -> Path:
outputs = ensure_bazel_output_files(platform, [label], compilation_mode, bazel_configs)
if len(outputs) != 1:
raise SystemExit(f"expected exactly one output for {label}, found {outputs}")
return outputs[0]
def stage_artifacts(
target: str,
def merged_musl_archive(
platform: str,
lib_path: Path,
binding_path: Path,
output_dir: Path,
sandbox: bool,
) -> None:
missing_paths = [
str(path) for path in [lib_path, binding_path] if not path.exists()
compilation_mode: str = "fastbuild",
bazel_configs: list[str] | None = None,
) -> Path:
llvm_ar = single_bazel_output_file(platform, LLVM_AR_LABEL, compilation_mode, bazel_configs)
llvm_ranlib = single_bazel_output_file(
platform,
LLVM_RANLIB_LABEL,
compilation_mode,
bazel_configs,
)
runtime_archives = [
single_bazel_output_file(platform, label, compilation_mode, bazel_configs)
for label in MUSL_RUNTIME_ARCHIVE_LABELS
]
if missing_paths:
raise SystemExit(f"missing release outputs for {target}: {missing_paths}")
output_dir.mkdir(parents=True, exist_ok=True)
artifact_profile = SANDBOX_ARTIFACT_PROFILE if sandbox else RELEASE_ARTIFACT_PROFILE
staged_library = output_dir / staged_archive_name(
target, lib_path, artifact_profile
temp_dir = Path(tempfile.mkdtemp(prefix="rusty-v8-musl-stage-"))
merged_archive = temp_dir / lib_path.name
merge_commands = "\n".join(
[
f"create {merged_archive}",
f"addlib {lib_path}",
*[f"addlib {archive}" for archive in runtime_archives],
"save",
"end",
]
)
staged_binding = output_dir / staged_binding_name(target, artifact_profile)
with lib_path.open("rb") as src, staged_library.open("wb") as dst:
with gzip.GzipFile(
filename="",
mode="wb",
fileobj=dst,
compresslevel=6,
mtime=0,
) as gz:
shutil.copyfileobj(src, gz)
shutil.copyfile(binding_path, staged_binding)
staged_checksums = output_dir / staged_checksums_name(target, artifact_profile)
with staged_checksums.open("w", encoding="utf-8") as checksums:
for path in [staged_library, staged_binding]:
digest = hashlib.sha256()
with path.open("rb") as artifact:
for chunk in iter(lambda: artifact.read(1024 * 1024), b""):
digest.update(chunk)
checksums.write(f"{digest.hexdigest()} {path.name}\n")
print(staged_library)
print(staged_binding)
print(staged_checksums)
def upstream_release_pair_paths(source_root: Path, target: str) -> tuple[Path, Path]:
lib_name = (
"rusty_v8.lib" if target.endswith("-pc-windows-msvc") else "librusty_v8.a"
subprocess.run(
[str(llvm_ar), "-M"],
cwd=ROOT,
check=True,
input=merge_commands,
text=True,
)
gn_out = source_root / "target" / 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,
sandbox: bool = False,
) -> None:
lib_path, binding_path = upstream_release_pair_paths(source_root, target)
stage_artifacts(target, lib_path, binding_path, output_dir, sandbox)
subprocess.run([str(llvm_ranlib), str(merged_archive)], cwd=ROOT, check=True)
return merged_archive
def stage_release_pair(
@@ -282,12 +248,10 @@ def stage_release_pair(
output_dir: Path,
compilation_mode: str = "fastbuild",
bazel_configs: list[str] | None = None,
sandbox: bool = False,
) -> None:
bazel_configs = artifact_bazel_configs(bazel_configs)
outputs = ensure_bazel_output_files(
platform,
[release_pair_label(target, sandbox)],
[release_pair_label(target)],
compilation_mode,
bazel_configs,
)
@@ -302,7 +266,39 @@ def stage_release_pair(
except StopIteration as exc:
raise SystemExit(f"missing Rust binding output for {target}") from exc
stage_artifacts(target, lib_path, binding_path, output_dir, sandbox)
output_dir.mkdir(parents=True, exist_ok=True)
staged_library = output_dir / staged_archive_name(target, lib_path)
staged_binding = output_dir / f"src_binding_release_{target}.rs"
source_archive = (
merged_musl_archive(platform, lib_path, compilation_mode, bazel_configs)
if is_musl_archive_target(target, lib_path)
else lib_path
)
with source_archive.open("rb") as src, staged_library.open("wb") as dst:
with gzip.GzipFile(
filename="",
mode="wb",
fileobj=dst,
compresslevel=6,
mtime=0,
) as gz:
shutil.copyfileobj(src, gz)
shutil.copyfile(binding_path, staged_binding)
staged_checksums = output_dir / f"rusty_v8_release_{target}.sha256"
with staged_checksums.open("w", encoding="utf-8") as checksums:
for path in [staged_library, staged_binding]:
digest = hashlib.sha256()
with path.open("rb") as artifact:
for chunk in iter(lambda: artifact.read(1024 * 1024), b""):
digest.update(chunk)
checksums.write(f"{digest.hexdigest()} {path.name}\n")
print(staged_library)
print(staged_binding)
print(staged_checksums)
def parse_args() -> argparse.Namespace:
@@ -313,7 +309,6 @@ def parse_args() -> argparse.Namespace:
stage_release_pair_parser.add_argument("--platform", required=True)
stage_release_pair_parser.add_argument("--target", required=True)
stage_release_pair_parser.add_argument("--output-dir", required=True)
stage_release_pair_parser.add_argument("--sandbox", action="store_true")
stage_release_pair_parser.add_argument(
"--bazel-config",
action="append",
@@ -326,16 +321,6 @@ def parse_args() -> argparse.Namespace:
choices=["fastbuild", "opt", "dbg"],
)
stage_upstream_release_pair_parser = subparsers.add_parser(
"stage-upstream-release-pair"
)
stage_upstream_release_pair_parser.add_argument(
"--source-root", 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)
stage_upstream_release_pair_parser.add_argument("--sandbox", action="store_true")
subparsers.add_parser("resolved-v8-crate-version")
check_module_bazel_parser = subparsers.add_parser("check-module-bazel")
@@ -368,15 +353,6 @@ def main() -> int:
output_dir=Path(args.output_dir),
compilation_mode=args.compilation_mode,
bazel_configs=args.bazel_configs,
sandbox=args.sandbox,
)
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),
sandbox=args.sandbox,
)
return 0
if args.command == "resolved-v8-crate-version":

View File

@@ -9,7 +9,6 @@ from pathlib import Path
SHA256_RE = re.compile(r"[0-9a-f]{64}")
HTTP_FILE_BLOCK_RE = re.compile(r"(?ms)^http_file\(\n.*?^\)\n?")
HTTP_FILE_VERSION_RE = re.compile(r"^rusty_v8_([0-9]+)_([0-9]+)_([0-9]+)_")
class RustyV8ChecksumError(ValueError):
@@ -96,18 +95,6 @@ def rusty_v8_http_files(module_bazel: str, version: str) -> list[RustyV8HttpFile
return entries
def rusty_v8_http_file_versions(module_bazel: str) -> list[str]:
versions = set()
for match in HTTP_FILE_BLOCK_RE.finditer(module_bazel):
name = string_field(match.group(0), "name")
if not name:
continue
version_match = HTTP_FILE_VERSION_RE.match(name)
if version_match:
versions.add(".".join(version_match.groups()))
return sorted(versions)
def module_entry_set_errors(
entries: list[RustyV8HttpFile],
checksums: dict[str, str],

View File

@@ -1,62 +0,0 @@
# 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.
function Use-FallbackDrive {
param([string]$Reason)
Write-Warning "$Reason Falling back to C:"
return "C:"
}
function Invoke-BestEffort {
param([scriptblock]$Script, [string]$Description)
try {
& $Script
} catch {
Write-Warning "$Description failed: $($_.Exception.Message)"
}
}
if (Test-Path "D:\") {
Write-Output "Using existing drive at D:"
$Drive = "D:"
} else {
try {
$VhdPath = Join-Path $env:RUNNER_TEMP "codex-dev-drive.vhdx"
$SizeBytes = 64GB
if (Test-Path $VhdPath) {
Remove-Item -Path $VhdPath -Force
}
New-VHD -Path $VhdPath -SizeBytes $SizeBytes -Dynamic -ErrorAction Stop | Out-Null
$Mounted = Mount-VHD -Path $VhdPath -Passthru -ErrorAction Stop
$Disk = $Mounted | Get-Disk -ErrorAction Stop
$Disk | Initialize-Disk -PartitionStyle GPT -ErrorAction Stop
$Partition = $Disk | New-Partition -AssignDriveLetter -UseMaximumSize -ErrorAction Stop
$Volume = $Partition | Format-Volume -FileSystem ReFS -NewFileSystemLabel "CodexDevDrive" -DevDrive -Confirm:$false -Force -ErrorAction Stop
$Drive = "$($Volume.DriveLetter):"
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)"
}
}
$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

View File

@@ -1,184 +0,0 @@
#!/usr/bin/env python3
import json
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
import run_bazel_with_buildbuddy
class RunBazelWithBuildBuddyTest(unittest.TestCase):
def github_env(
self,
temp_dir: str,
*,
repository: str = "openai/codex",
fork: bool = False,
event_name: str = "pull_request",
) -> dict[str, str]:
event_path = Path(temp_dir) / "event.json"
event_path.write_text(
json.dumps({"pull_request": {"head": {"repo": {"fork": fork}}}}),
encoding="utf-8",
)
return {
"BUILDBUDDY_API_KEY": "token",
"GITHUB_ACTIONS": "true",
"GITHUB_EVENT_NAME": event_name,
"GITHUB_EVENT_PATH": str(event_path),
"GITHUB_REPOSITORY": repository,
}
def test_keyless_invocation_drops_remote_ci_configuration(self) -> None:
self.assertIsNone(
run_bazel_with_buildbuddy.remote_config(
["build", "--config=ci-linux", "//codex-rs/cli:codex"],
{},
)
)
self.assertEqual(
run_bazel_with_buildbuddy.bazel_args_with_remote_config(
["build", "--config=ci-linux", "--", "//codex-rs/cli:codex"],
{},
),
["build", "--", "//codex-rs/cli:codex"],
)
def test_program_arguments_after_separator_do_not_select_or_lose_rbe(self) -> None:
args = ["run", "//codex-rs/cli:codex", "--", "--config=remote"]
self.assertEqual(
run_bazel_with_buildbuddy.bazel_args_with_remote_config(args, {}),
args,
)
self.assertEqual(
run_bazel_with_buildbuddy.remote_config(
args, {"BUILDBUDDY_API_KEY": "fork-token"}
),
"buildbuddy-generic",
)
def test_upstream_push_selects_openai_rbe_before_target_separator(self) -> None:
with TemporaryDirectory() as temp_dir:
env = self.github_env(temp_dir, event_name="push")
self.assertEqual(
run_bazel_with_buildbuddy.bazel_args_with_remote_config(
["build", "--config=ci-linux", "--", "//codex-rs/cli:codex"],
env,
),
[
"build",
"--config=buildbuddy-openai-rbe",
"--remote_header=x-buildbuddy-api-key=token",
"--config=ci-linux",
"--",
"//codex-rs/cli:codex",
],
)
def test_windows_cross_ci_configuration_follows_remote_configuration(self) -> None:
env = {"BUILDBUDDY_API_KEY": "fork-token"}
self.assertEqual(
run_bazel_with_buildbuddy.bazel_args_with_remote_config(
["build", "--config=ci-windows-cross", "//codex-rs/cli:codex"],
env,
),
[
"build",
"--config=buildbuddy-generic-rbe",
"--remote_header=x-buildbuddy-api-key=fork-token",
"--config=ci-windows-cross",
"//codex-rs/cli:codex",
],
)
def test_query_remote_configuration_is_inserted_before_expression(self) -> None:
expression = 'kind("rust_library rule", //codex-rs/...)'
env = {"BUILDBUDDY_API_KEY": "fork-token"}
for command in ("query", "cquery", "aquery"):
with self.subTest(command=command):
self.assertEqual(
run_bazel_with_buildbuddy.bazel_args_with_remote_config(
[
command,
"--config=ci-windows-cross",
"--output=label",
expression,
],
env,
),
[
command,
"--config=buildbuddy-generic-rbe",
"--remote_header=x-buildbuddy-api-key=fork-token",
"--config=ci-windows-cross",
"--output=label",
expression,
],
)
def test_same_repository_pull_request_selects_openai_host(self) -> None:
with TemporaryDirectory() as temp_dir:
self.assertEqual(
run_bazel_with_buildbuddy.remote_config(
["build", "--config=ci-v8"], self.github_env(temp_dir)
),
"buildbuddy-openai-rbe",
)
def test_fork_pull_request_cannot_select_openai_host(self) -> None:
with TemporaryDirectory() as temp_dir:
env = self.github_env(temp_dir, fork=True)
self.assertEqual(
run_bazel_with_buildbuddy.remote_config(
["build", "--config=ci-v8"], env
),
"buildbuddy-generic-rbe",
)
def test_run_in_fork_repository_cannot_select_openai_host(self) -> None:
with TemporaryDirectory() as temp_dir:
env = self.github_env(temp_dir, repository="contributor/codex")
self.assertEqual(
run_bazel_with_buildbuddy.remote_config(
["build", "--config=ci-v8"], env
),
"buildbuddy-generic-rbe",
)
def test_pull_request_without_readable_event_payload_fails_closed(self) -> None:
for event_path in (None, "missing-event.json"):
env = {
"BUILDBUDDY_API_KEY": "token",
"GITHUB_ACTIONS": "true",
"GITHUB_EVENT_NAME": "pull_request",
"GITHUB_REPOSITORY": "openai/codex",
}
if event_path is not None:
env["GITHUB_EVENT_PATH"] = event_path
with self.subTest(event_path=event_path):
self.assertEqual(
run_bazel_with_buildbuddy.remote_config(["build"], env),
"buildbuddy-generic",
)
def test_bazel_command_uses_configured_binary_locally(self) -> None:
self.assertEqual(
run_bazel_with_buildbuddy.bazel_command(
"info",
"execution_root",
env={"CODEX_BAZEL_BIN": "fake-bazel"},
),
["fake-bazel", "info", "execution_root"],
)
if __name__ == "__main__":
unittest.main()

View File

@@ -4,291 +4,11 @@ from __future__ import annotations
import textwrap
import unittest
from os import environ
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import patch
import rusty_v8_bazel
import rusty_v8_module_bazel
class RustyV8BazelTest(unittest.TestCase):
def test_consumer_selectors_track_resolved_crate_version(self) -> None:
build_bazel = (
rusty_v8_bazel.ROOT / "third_party" / "v8" / "BUILD.bazel"
).read_text()
version_suffix = rusty_v8_bazel.resolved_v8_crate_version().replace(".", "_")
for selector in [
"aarch64_apple_darwin_bazel",
"aarch64_pc_windows_gnullvm",
"aarch64_pc_windows_msvc",
"aarch64_unknown_linux_gnu_bazel",
"aarch64_unknown_linux_musl_release_base",
"x86_64_apple_darwin_bazel",
"x86_64_pc_windows_gnullvm",
"x86_64_pc_windows_msvc",
"x86_64_unknown_linux_gnu_bazel",
"x86_64_unknown_linux_musl_release",
]:
self.assertIn(
f":v8_{version_suffix}_{selector}",
build_bazel,
)
for selector in [
"aarch64_apple_darwin",
"aarch64_pc_windows_gnullvm",
"aarch64_pc_windows_msvc",
"aarch64_unknown_linux_gnu",
"aarch64_unknown_linux_musl",
"x86_64_apple_darwin",
"x86_64_pc_windows_gnullvm",
"x86_64_pc_windows_msvc",
"x86_64_unknown_linux_gnu",
"x86_64_unknown_linux_musl",
]:
self.assertIn(
f":src_binding_release_{selector}_{version_suffix}_release",
build_bazel,
)
def test_command_version_tracks_remaining_http_file_assets(self) -> None:
with TemporaryDirectory() as temp_dir:
module_bazel = Path(temp_dir) / "MODULE.bazel"
module_bazel.write_text(
textwrap.dedent(
"""\
http_file(
name = "rusty_v8_146_4_0_x86_64_unknown_linux_gnu_archive",
downloaded_file_path = "librusty_v8_release_x86_64-unknown-linux-gnu.a.gz",
urls = ["https://example.test/archive.gz"],
)
"""
)
)
with patch.object(rusty_v8_bazel, "MODULE_BAZEL", module_bazel):
self.assertEqual("146.4.0", rusty_v8_bazel.command_version(None))
def test_artifact_bazel_configs_always_enable_upstream_libcxx(self) -> None:
self.assertEqual(
["rusty-v8-upstream-libcxx"],
rusty_v8_bazel.artifact_bazel_configs(),
)
self.assertEqual(
["rusty-v8-upstream-libcxx", "v8-release-compat"],
rusty_v8_bazel.artifact_bazel_configs(["v8-release-compat"]),
)
self.assertEqual(
["rusty-v8-upstream-libcxx", "v8-release-compat"],
rusty_v8_bazel.artifact_bazel_configs(
["rusty-v8-upstream-libcxx", "v8-release-compat"]
),
)
def test_bazel_commands_use_shared_buildbuddy_remote_config_library(self) -> None:
with patch.dict(environ, {}, clear=True):
self.assertEqual(
[
"bazel",
"build",
"//third_party/v8:release",
],
rusty_v8_bazel.bazel_command(
"build",
"--config=ci-v8",
"//third_party/v8:release",
),
)
with patch.dict(environ, {"BUILDBUDDY_API_KEY": "token"}, clear=True):
self.assertEqual(
[
"bazel",
"build",
"--config=buildbuddy-generic-rbe",
"--remote_header=x-buildbuddy-api-key=token",
"--config=ci-v8",
"//third_party/v8:release",
],
rusty_v8_bazel.bazel_command(
"build",
"--config=ci-v8",
"//third_party/v8:release",
),
)
def test_release_pair_labels_and_staged_names_distinguish_sandbox_artifacts(
self,
) -> None:
self.assertEqual(
"//third_party/v8:rusty_v8_release_pair_x86_64_unknown_linux_musl",
rusty_v8_bazel.release_pair_label("x86_64-unknown-linux-musl"),
)
self.assertEqual(
"//third_party/v8:rusty_v8_sandbox_release_pair_x86_64_unknown_linux_musl",
rusty_v8_bazel.release_pair_label(
"x86_64-unknown-linux-musl", sandbox=True
),
)
self.assertEqual(
"//third_party/v8:rusty_v8_sandbox_release_pair_x86_64_apple_darwin",
rusty_v8_bazel.release_pair_label("x86_64-apple-darwin", sandbox=True),
)
self.assertEqual(
"librusty_v8_release_x86_64-unknown-linux-musl.a.gz",
rusty_v8_bazel.staged_archive_name(
"x86_64-unknown-linux-musl",
Path("libv8.a"),
rusty_v8_bazel.RELEASE_ARTIFACT_PROFILE,
),
)
self.assertEqual(
"rusty_v8_ptrcomp_sandbox_release_x86_64-pc-windows-msvc.lib.gz",
rusty_v8_bazel.staged_archive_name(
"x86_64-pc-windows-msvc",
Path("v8.a"),
rusty_v8_bazel.SANDBOX_ARTIFACT_PROFILE,
),
)
self.assertEqual(
"src_binding_ptrcomp_sandbox_release_x86_64-unknown-linux-musl.rs",
rusty_v8_bazel.staged_binding_name(
"x86_64-unknown-linux-musl",
rusty_v8_bazel.SANDBOX_ARTIFACT_PROFILE,
),
)
self.assertEqual(
"rusty_v8_ptrcomp_sandbox_release_x86_64-unknown-linux-musl.sha256",
rusty_v8_bazel.staged_checksums_name(
"x86_64-unknown-linux-musl",
rusty_v8_bazel.SANDBOX_ARTIFACT_PROFILE,
),
)
def test_stage_artifacts(self) -> None:
with TemporaryDirectory() as source_dir, TemporaryDirectory() as output_dir:
source_root = Path(source_dir)
archive = source_root / "librusty_v8.a"
binding = source_root / "src_binding.rs"
archive.write_bytes(b"archive")
binding.write_text("binding")
rusty_v8_bazel.stage_artifacts(
"aarch64-apple-darwin",
archive,
binding,
Path(output_dir),
sandbox=True,
)
self.assertEqual(
{
"librusty_v8_ptrcomp_sandbox_release_aarch64-apple-darwin.a.gz",
"src_binding_ptrcomp_sandbox_release_aarch64-apple-darwin.rs",
"rusty_v8_ptrcomp_sandbox_release_aarch64-apple-darwin.sha256",
},
{path.name for path in Path(output_dir).iterdir()},
)
def test_upstream_release_pair_paths(self) -> None:
self.assertEqual(
(
Path(
"/tmp/rusty_v8/target/x86_64-apple-darwin/release/gn_out/obj/"
"librusty_v8.a"
),
Path(
"/tmp/rusty_v8/target/x86_64-apple-darwin/release/gn_out/"
"src_binding.rs"
),
),
rusty_v8_bazel.upstream_release_pair_paths(
Path("/tmp/rusty_v8"),
"x86_64-apple-darwin",
),
)
self.assertEqual(
(
Path(
"/tmp/rusty_v8/target/x86_64-pc-windows-msvc/release/gn_out/"
"obj/rusty_v8.lib"
),
Path(
"/tmp/rusty_v8/target/x86_64-pc-windows-msvc/release/gn_out/"
"src_binding.rs"
),
),
rusty_v8_bazel.upstream_release_pair_paths(
Path("/tmp/rusty_v8"),
"x86_64-pc-windows-msvc",
),
)
def test_stage_upstream_release_pair(self) -> None:
with TemporaryDirectory() as source_dir, TemporaryDirectory() as output_dir:
source_root = Path(source_dir)
gn_out = (
source_root / "target" / "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),
sandbox=True,
)
self.assertEqual(
{
"rusty_v8_ptrcomp_sandbox_release_x86_64-pc-windows-msvc.lib.gz",
"src_binding_ptrcomp_sandbox_release_x86_64-pc-windows-msvc.rs",
"rusty_v8_ptrcomp_sandbox_release_x86_64-pc-windows-msvc.sha256",
},
{path.name for path in Path(output_dir).iterdir()},
)
def test_ensure_bazel_output_files_rebuilds_existing_outputs(self) -> None:
with TemporaryDirectory() as output_dir:
output = Path(output_dir) / "libv8.a"
output.write_bytes(b"archive")
with (
patch.object(rusty_v8_bazel, "bazel_build") as bazel_build,
patch.object(
rusty_v8_bazel,
"bazel_output_files",
return_value=[output],
) as bazel_output_files,
):
self.assertEqual(
[output],
rusty_v8_bazel.ensure_bazel_output_files(
"macos_arm64",
["//third_party/v8:pair"],
"opt",
["rusty-v8-upstream-libcxx"],
),
)
bazel_build.assert_called_once_with(
"macos_arm64",
["//third_party/v8:pair"],
"opt",
["rusty-v8-upstream-libcxx"],
download_toplevel=True,
)
bazel_output_files.assert_called_once_with(
"macos_arm64",
["//third_party/v8:pair"],
"opt",
["rusty-v8-upstream-libcxx"],
)
def test_update_module_bazel_replaces_and_inserts_sha256(self) -> None:
module_bazel = textwrap.dedent(
"""\
@@ -401,34 +121,6 @@ class RustyV8BazelTest(unittest.TestCase):
"146.4.0",
)
def test_rusty_v8_http_file_versions(self) -> None:
module_bazel = textwrap.dedent(
"""\
http_file(
name = "rusty_v8_146_4_0_x86_64_unknown_linux_gnu_archive",
downloaded_file_path = "archive.gz",
urls = ["https://example.test/archive.gz"],
)
http_file(
name = "rusty_v8_147_4_0_x86_64_unknown_linux_gnu_archive",
downloaded_file_path = "new-archive.gz",
urls = ["https://example.test/new-archive.gz"],
)
http_file(
name = "unrelated_archive",
downloaded_file_path = "other.gz",
urls = ["https://example.test/other.gz"],
)
"""
)
self.assertEqual(
["146.4.0", "147.4.0"],
rusty_v8_module_bazel.rusty_v8_http_file_versions(module_bazel),
)
if __name__ == "__main__":
unittest.main()

View File

@@ -21,8 +21,7 @@ The workflows in this directory are split so that pull requests get fast, review
- `rust-ci-full.yml` is the full Cargo-native verification workflow.
It keeps the heavier checks off the PR path while still validating them after merge:
- the full Cargo `clippy` matrix
- the full Cargo `nextest` matrix via per-platform archive-backed shards
- Windows ARM64 nextest archives cross-compiled on Windows x64, then replayed on native Windows ARM64 shards
- the full Cargo `nextest` matrix
- release-profile Cargo builds
- cross-platform `argument-comment-lint`
- Linux remote-env tests

View File

@@ -15,7 +15,6 @@ concurrency:
# See https://docs.github.com/en/actions/using-jobs/using-concurrency and https://docs.github.com/en/actions/learn-github-actions/contexts for more info.
group: concurrency-group::${{ github.workflow }}::${{ github.event.pull_request.number > 0 && format('pr-{0}', github.event.pull_request.number) || github.ref_name }}${{ github.ref_name == 'main' && format('::{0}', github.run_id) || ''}}
cancel-in-progress: ${{ github.ref_name != 'main' }}
jobs:
test:
# PRs use the sharded Windows cross-compiled test jobs below. Post-merge
@@ -56,17 +55,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'
with:
tool: just
- name: Check rusty_v8 MODULE.bazel checksums
if: matrix.os == 'ubuntu-24.04' && matrix.target == 'x86_64-unknown-linux-gnu'
shell: bash
run: |
python3 .github/scripts/rusty_v8_bazel.py check-module-bazel
just test-github-scripts
python3 -m unittest discover -s .github/scripts -p test_rusty_v8_bazel.py
- name: Prepare Bazel CI
id: prepare_bazel
@@ -147,9 +141,7 @@ jobs:
- 2
- 3
- 4
runs-on:
group: codex-runners
labels: codex-windows-x64
runs-on: windows-latest
name: Bazel test on windows-latest for x86_64-pc-windows-gnullvm shard ${{ matrix.shard }}/4
steps:
@@ -254,9 +246,7 @@ jobs:
# it a larger timeout.
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
timeout-minutes: 40
runs-on:
group: codex-runners
labels: codex-windows-x64
runs-on: windows-latest
name: Bazel test on windows-latest for x86_64-pc-windows-gnullvm (native main)
steps:
@@ -342,10 +332,7 @@ jobs:
target: aarch64-apple-darwin
- os: windows-latest
target: x86_64-pc-windows-gnullvm
runs_on:
group: codex-runners
labels: codex-windows-x64
runs-on: ${{ matrix.runs_on || matrix.os }}
runs-on: ${{ matrix.os }}
name: Bazel clippy on ${{ matrix.os }} for ${{ matrix.target }}
steps:
@@ -435,10 +422,7 @@ jobs:
target: aarch64-apple-darwin
- os: windows-latest
target: x86_64-pc-windows-gnullvm
runs_on:
group: codex-runners
labels: codex-windows-x64
runs-on: ${{ matrix.runs_on || matrix.os }}
runs-on: ${{ matrix.os }}
name: Verify release build on ${{ matrix.os }} for ${{ matrix.target }}
steps:

View File

@@ -6,11 +6,6 @@ on:
branches:
- main
# 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
@@ -25,10 +20,10 @@ jobs:
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
- name: Run cargo-deny
uses: EmbarkStudios/cargo-deny-action@82eb9f621fbc699dd0918f3ea06864c14cc84246 # v2
with:
rust-version: 1.95.0
rust-version: 1.93.0
manifest-path: ./codex-rs/Cargo.toml

View File

@@ -26,9 +26,6 @@ jobs:
- name: Verify Bazel clippy flags match Cargo workspace lints
run: python3 .github/scripts/verify_bazel_clippy_lints.py
- name: Test Codex package builder
run: python3 -m unittest discover -s scripts/codex_package -p 'test_*.py'
- name: Setup pnpm
uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
with:
@@ -42,6 +39,9 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
# stage_npm_packages.py requires DotSlash when staging releases.
- uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
- name: Stage npm package
id: stage_npm_package
env:
@@ -52,13 +52,15 @@ jobs:
# cross-platform native payload required by the npm package layout.
# Passing the workflow URL directly avoids relying on old rust-v*
# branches remaining discoverable via `gh run list --branch ...`.
CODEX_VERSION=0.133.0-alpha.4
WORKFLOW_URL="https://github.com/openai/codex/actions/runs/26201494185"
CODEX_VERSION=0.125.0
WORKFLOW_URL="https://github.com/openai/codex/actions/runs/24901475298"
OUTPUT_DIR="${RUNNER_TEMP}"
# This reused workflow predates the standalone bwrap artifact.
python3 ./scripts/stage_npm_packages.py \
--release-version "$CODEX_VERSION" \
--workflow-url "$WORKFLOW_URL" \
--package codex \
--allow-missing-native-component bwrap \
--output-dir "$OUTPUT_DIR"
PACK_OUTPUT="${OUTPUT_DIR}/codex-npm-${CODEX_VERSION}.tgz"
echo "pack_output=$PACK_OUTPUT" >> "$GITHUB_OUTPUT"
@@ -74,15 +76,5 @@ 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: Check formatting (run `just fmt` to fix)
run: just fmt-check
- name: Prettier (run `pnpm run format:fix` to fix)
run: pnpm run format

View File

@@ -12,12 +12,17 @@ jobs:
# Prevent runs on forks (requires OpenAI API key, wastes Actions minutes)
if: github.repository == 'openai/codex' && (github.event.action == 'opened' || (github.event.action == 'labeled' && github.event.label.name == 'codex-deduplicate'))
runs-on: ubuntu-latest
environment: issue-triage
permissions:
contents: read
outputs:
codex_output: ${{ steps.codex-all.outputs.final-message }}
issues_json: ${{ steps.normalize-all.outputs.issues_json }}
reason: ${{ steps.normalize-all.outputs.reason }}
has_matches: ${{ steps.normalize-all.outputs.has_matches }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Prepare Codex inputs
env:
GH_TOKEN: ${{ github.token }}
@@ -62,8 +67,6 @@ jobs:
with:
openai-api-key: ${{ secrets.CODEX_OPENAI_API_KEY }}
allow-users: "*"
safety-strategy: drop-sudo
sandbox: read-only
prompt: |
You are an assistant that triages new GitHub issues by identifying potential duplicates.
@@ -97,21 +100,10 @@ jobs:
"additionalProperties": false
}
normalize-duplicates-all:
name: Normalize pass 1 output
needs: gather-duplicates-all
if: ${{ needs.gather-duplicates-all.result == 'success' }}
runs-on: ubuntu-latest
permissions: {}
outputs:
issues_json: ${{ steps.normalize-all.outputs.issues_json }}
reason: ${{ steps.normalize-all.outputs.reason }}
has_matches: ${{ steps.normalize-all.outputs.has_matches }}
steps:
- id: normalize-all
name: Normalize pass 1 output
env:
CODEX_OUTPUT: ${{ needs.gather-duplicates-all.outputs.codex_output }}
CODEX_OUTPUT: ${{ steps.codex-all.outputs.final-message }}
CURRENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
run: |
set -eo pipefail
@@ -154,16 +146,21 @@ jobs:
gather-duplicates-open:
name: Identify potential duplicates (open issues fallback)
# Pass 1 Codex execution drops sudo on its runner, so run the fallback in a fresh job.
needs: normalize-duplicates-all
if: ${{ needs.normalize-duplicates-all.result == 'success' && needs.normalize-duplicates-all.outputs.has_matches != 'true' }}
# Pass 1 may drop sudo on the runner, so run the fallback in a fresh job.
needs: gather-duplicates-all
if: ${{ needs.gather-duplicates-all.result == 'success' && needs.gather-duplicates-all.outputs.has_matches != 'true' }}
runs-on: ubuntu-latest
environment: issue-triage
permissions:
contents: read
outputs:
codex_output: ${{ steps.codex-open.outputs.final-message }}
issues_json: ${{ steps.normalize-open.outputs.issues_json }}
reason: ${{ steps.normalize-open.outputs.reason }}
has_matches: ${{ steps.normalize-open.outputs.has_matches }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Prepare Codex inputs
env:
GH_TOKEN: ${{ github.token }}
@@ -206,8 +203,6 @@ jobs:
with:
openai-api-key: ${{ secrets.CODEX_OPENAI_API_KEY }}
allow-users: "*"
safety-strategy: drop-sudo
sandbox: read-only
prompt: |
You are an assistant that triages new GitHub issues by identifying potential duplicates.
@@ -241,21 +236,10 @@ jobs:
"additionalProperties": false
}
normalize-duplicates-open:
name: Normalize pass 2 output
needs: gather-duplicates-open
if: ${{ needs.gather-duplicates-open.result == 'success' }}
runs-on: ubuntu-latest
permissions: {}
outputs:
issues_json: ${{ steps.normalize-open.outputs.issues_json }}
reason: ${{ steps.normalize-open.outputs.reason }}
has_matches: ${{ steps.normalize-open.outputs.has_matches }}
steps:
- id: normalize-open
name: Normalize pass 2 output
env:
CODEX_OUTPUT: ${{ needs.gather-duplicates-open.outputs.codex_output }}
CODEX_OUTPUT: ${{ steps.codex-open.outputs.final-message }}
CURRENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
run: |
set -eo pipefail
@@ -299,9 +283,9 @@ jobs:
select-final:
name: Select final duplicate set
needs:
- normalize-duplicates-all
- normalize-duplicates-open
if: ${{ always() && needs.normalize-duplicates-all.result == 'success' && (needs.normalize-duplicates-open.result == 'success' || needs.normalize-duplicates-open.result == 'skipped') }}
- gather-duplicates-all
- gather-duplicates-open
if: ${{ always() && needs.gather-duplicates-all.result == 'success' && (needs.gather-duplicates-open.result == 'success' || needs.gather-duplicates-open.result == 'skipped') }}
runs-on: ubuntu-latest
permissions:
contents: read
@@ -311,12 +295,12 @@ jobs:
- id: select-final
name: Select final duplicate set
env:
PASS1_ISSUES: ${{ needs.normalize-duplicates-all.outputs.issues_json }}
PASS1_REASON: ${{ needs.normalize-duplicates-all.outputs.reason }}
PASS2_ISSUES: ${{ needs.normalize-duplicates-open.outputs.issues_json }}
PASS2_REASON: ${{ needs.normalize-duplicates-open.outputs.reason }}
PASS1_HAS_MATCHES: ${{ needs.normalize-duplicates-all.outputs.has_matches }}
PASS2_HAS_MATCHES: ${{ needs.normalize-duplicates-open.outputs.has_matches }}
PASS1_ISSUES: ${{ needs.gather-duplicates-all.outputs.issues_json }}
PASS1_REASON: ${{ needs.gather-duplicates-all.outputs.reason }}
PASS2_ISSUES: ${{ needs.gather-duplicates-open.outputs.issues_json }}
PASS2_REASON: ${{ needs.gather-duplicates-open.outputs.reason }}
PASS1_HAS_MATCHES: ${{ needs.gather-duplicates-all.outputs.has_matches }}
PASS2_HAS_MATCHES: ${{ needs.gather-duplicates-open.outputs.has_matches }}
run: |
set -eo pipefail

View File

@@ -1,152 +0,0 @@
name: Issue Labeler
on:
issues:
types:
- opened
- labeled
jobs:
gather-labels:
name: Generate label suggestions
# Prevent runs on forks (requires OpenAI API key, wastes Actions minutes)
if: github.repository == 'openai/codex' && (github.event.action == 'opened' || (github.event.action == 'labeled' && github.event.label.name == 'codex-label'))
runs-on: ubuntu-latest
environment: issue-triage
permissions:
contents: read
outputs:
codex_output: ${{ steps.codex.outputs.final-message }}
steps:
- id: codex
uses: openai/codex-action@5c3f4ccdb2b8790f73d6b21751ac00e602aa0c02 # v1.7
with:
openai-api-key: ${{ secrets.CODEX_OPENAI_API_KEY }}
allow-users: "*"
safety-strategy: drop-sudo
sandbox: read-only
prompt: |
You are an assistant that reviews GitHub issues for the repository.
Your job is to choose the most appropriate labels for the issue described later in this prompt.
Follow these rules:
- Add one (and only one) of the following three labels to distinguish the type of issue. Default to "bug" if unsure.
1. bug — Reproducible defects in Codex products (CLI, VS Code extension, web, auth).
2. enhancement — Feature requests or usability improvements that ask for new capabilities, better ergonomics, or quality-of-life tweaks.
3. documentation — Updates or corrections needed in docs/README/config references (broken links, missing examples, outdated keys, clarification requests).
- If applicable, add one of the following labels to specify which sub-product or product surface the issue relates to.
1. CLI — the Codex command line interface.
2. extension — VS Code (or other IDE) extension-specific issues.
3. app - Issues related to the Codex desktop application.
4. codex-web — Issues targeting the Codex web UI/Cloud experience.
5. github-action — Issues with the Codex GitHub action.
6. iOS — Issues with the Codex iOS app.
- Additionally add zero or more of the following labels that are relevant to the issue content. Prefer a small set of precise labels over many broad ones.
- For agent-area issues, prefer the most specific applicable label. Use "agent" only as a fallback for agent-related issues that do not fit a more specific agent-area label. Prefer "app-server" over "session" or "config" when the issue is about app-server protocol, API, RPC, schema, launch, or bridge behavior. Use "memory" for agentic memory storage/retrieval and "performance" for high process memory utilization or memory leaks.
1. windows-os — Bugs or friction specific to Windows environments (always when PowerShell is mentioned, path handling, copy/paste, OS-specific auth or tooling failures).
2. mcp — Topics involving Model Context Protocol servers/clients.
3. mcp-server — Problems related to the codex mcp-server command, where codex runs as an MCP server.
4. azure — Problems or requests tied to Azure OpenAI deployments.
5. model-behavior — Undesirable LLM behavior: forgetting goals, refusing work, hallucinating environment details, quota misreports, or other reasoning/performance anomalies.
6. code-review — Issues related to the code review feature or functionality.
7. safety-check - Issues related to cyber risk detection or trusted access verification.
8. auth - Problems related to authentication, login, or access tokens.
9. exec - Problems related to the "codex exec" command or functionality.
10. hooks - Problems related to event hooks
11. context - Problems related to compaction, context windows, or available context reporting.
12. skills - Problems related to skills or plugins
13. custom-model - Problems that involve using custom model providers, local models, or OSS models.
14. rate-limits - Problems related to token limits, rate limits, or token usage reporting.
15. sandbox - Issues related to local sandbox environments or tool call approvals to override sandbox restrictions.
16. tool-calls - Problems related to specific tool call invocations including unexpected errors, failures, or hangs.
17. TUI - Problems with the terminal user interface (TUI) including keyboard shortcuts, copy & pasting, menus, or screen update issues.
18. app-server - Issues involving the app-server protocol or interfaces, including SDK/API payloads, thread/* and turn/* RPCs, app-server launch behavior, external app/controller bridges, and app-server protocol/schema behavior.
19. connectivity - Network connectivity or endpoint issues, including reconnecting messages, stream dropped/disconnected errors, websocket/SSE/transport failures, timeout/network/VPN/proxy/API endpoint failures, and related retry behavior.
20. subagent - Issues involving subagents, sub-agents, or multi-agent behavior, including spawn_agent, wait_agent, close_agent, worker/explorer roles, delegation, agent teams, lifecycle, model/config inheritance, quotas, and orchestration.
21. session - Issues involving session or thread management, including resume, fork, archive, rename/title, thread history, rollout persistence, compaction, checkpoints, retention, and cross-session state.
22. config - Issues involving config.toml, config keys, config key merging, config updates, profiles, hooks config, project config, agent role TOMLs, instruction/personality config, and config schema behavior.
23. plan - Issues involving plan mode, planning workflows, or plan-specific tools/behavior.
24. computer-use - Issues involving agentic computer use or SkyComputerUseService.
25. browser - Issues involving agentic browser use, IAB, or the built-in browser within the Codex app.
26. memory - Issues involving agentic memory storage and retrieval.
27. imagen - Issues involving image generation.
28. remote - Issues involving remote access, remote control, or SSH.
29. performance - Issues involving slow, laggy performance, high memory utilization, or memory leaks.
30. automations - Issues involving scheduled automation tasks or heartbeats.
31. pets - Issues involving pets avatars and animations.
32. agent - Fallback only for core agent loop or agent-related issues that do not fit app-server, connectivity, subagent, session, config, plan, computer-use, browser, memory, imagen, remote, performance, automations, or pets.
Issue number: ${{ github.event.issue.number }}
Issue title:
${{ github.event.issue.title }}
Issue body:
${{ github.event.issue.body }}
Repository full name:
${{ github.repository }}
output-schema: |
{
"type": "object",
"properties": {
"labels": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": ["labels"],
"additionalProperties": false
}
apply-labels:
name: Apply labels from Codex output
needs: gather-labels
if: ${{ needs.gather-labels.result != 'skipped' }}
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
CODEX_OUTPUT: ${{ needs.gather-labels.outputs.codex_output }}
steps:
- name: Apply labels
run: |
json=${CODEX_OUTPUT//$'\r'/}
if [ -z "$json" ]; then
echo "Codex produced no output. Skipping label application."
exit 0
fi
if ! printf '%s' "$json" | jq -e 'type == "object" and (.labels | type == "array")' >/dev/null 2>&1; then
echo "Codex output did not include a labels array. Raw output: $json"
exit 0
fi
labels=$(printf '%s' "$json" | jq -r '.labels[] | tostring')
if [ -z "$labels" ]; then
echo "Codex returned an empty array. Nothing to do."
exit 0
fi
cmd=(gh issue edit "$ISSUE_NUMBER")
while IFS= read -r label; do
cmd+=(--add-label "$label")
done <<< "$labels"
"${cmd[@]}" || true
- name: Remove codex-label trigger
if: ${{ always() && github.event.action == 'labeled' && github.event.label.name == 'codex-label' }}
run: |
gh issue edit "$ISSUE_NUMBER" --remove-label codex-label || true
echo "Attempted to remove label: codex-label"

View File

@@ -1,91 +0,0 @@
name: python-sdk-release
on:
push:
tags:
- "python-v*"
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
jobs:
build-python-sdk:
if: github.repository == 'openai/codex'
name: build-python-sdk
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Validate tag and build Python SDK package
shell: bash
run: |
set -euo pipefail
sdk_version="${GITHUB_REF_NAME#python-v}"
if [[ ! "${sdk_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+b[0-9]+$ ]]; then
echo "Python SDK release tags must identify a beta release, for example python-v0.1.0b1."
exit 1
fi
# The pinned runtime currently publishes a musllinux Linux wheel.
# Build in Alpine so release type generation installs that wheel.
docker run --rm \
--user "$(id -u):$(id -g)" \
-e HOME=/tmp/codex-python-sdk-home \
-e UV_LINK_MODE=copy \
-e SDK_VERSION="${sdk_version}" \
-e SDK_STAGE_DIR="${RUNNER_TEMP}/openai-codex" \
-e SDK_DIST_DIR="${GITHUB_WORKSPACE}/dist/python-sdk" \
-v "${GITHUB_WORKSPACE}:${GITHUB_WORKSPACE}" \
-v "${RUNNER_TEMP}:${RUNNER_TEMP}" \
-w "${GITHUB_WORKSPACE}/sdk/python" \
python:3.12-alpine \
sh -euxc '
python -m venv /tmp/release-tools
/tmp/release-tools/bin/python -m pip install build twine uv==0.11.3
/tmp/release-tools/bin/uv sync --extra dev --frozen
/tmp/release-tools/bin/uv run --extra dev --frozen python scripts/update_sdk_artifacts.py \
stage-sdk "${SDK_STAGE_DIR}" \
--sdk-version "${SDK_VERSION}"
/tmp/release-tools/bin/python -m build \
--wheel \
--sdist \
--outdir "${SDK_DIST_DIR}" \
"${SDK_STAGE_DIR}"
/tmp/release-tools/bin/python -m twine check --strict "${SDK_DIST_DIR}/"*
'
- name: Upload Python SDK package
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: python-sdk-package
path: dist/python-sdk/*
if-no-files-found: error
publish-python-sdk:
name: publish-python-sdk
needs: build-python-sdk
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: read
id-token: write # Required for PyPI trusted publishing.
steps:
- name: Download Python SDK package
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: python-sdk-package
path: dist/python-sdk
- name: Publish Python SDK to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
packages-dir: dist/python-sdk

View File

@@ -1,464 +0,0 @@
name: rust-ci-full nextest platform
on:
workflow_call:
inputs:
runner:
required: true
type: string
runner_group:
required: false
default: ""
type: string
runner_labels:
required: false
default: ""
type: string
archive_runner:
required: false
default: ""
type: string
archive_runner_group:
required: false
default: ""
type: string
archive_runner_labels:
required: false
default: ""
type: string
target:
required: true
type: string
profile:
required: true
type: string
artifact_id:
required: true
type: string
remote_env:
required: false
default: false
type: boolean
test_threads:
required: false
default: 0
type: number
use_sccache:
required: false
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
runs-on: ${{ inputs.archive_runner_group != '' && fromJSON(format('{{"group":"{0}","labels":"{1}"}}', inputs.archive_runner_group, inputs.archive_runner_labels)) || inputs.archive_runner != '' && inputs.archive_runner || inputs.runner_group != '' && fromJSON(format('{{"group":"{0}","labels":"{1}"}}', inputs.runner_group, inputs.runner_labels)) || inputs.runner }}
timeout-minutes: 60
defaults:
run:
working-directory: codex-rs
env:
# Windows ARM64 archives are built on Windows x64, while their shards run
# on native Windows ARM64. Key producer-side caches by the archive runner
# so the cross-compile build reuses the Windows x64 cache lineage.
ARCHIVE_CACHE_RUNNER: ${{ inputs.archive_runner != '' && inputs.archive_runner || inputs.runner }}
USE_SCCACHE: ${{ inputs.use_sccache && 'true' || 'false' }}
CARGO_INCREMENTAL: "0"
SCCACHE_CACHE_SIZE: 10G
NEXTEST_ARCHIVE_FILE: nextest-${{ inputs.artifact_id }}.tar.zst
TEST_HELPERS_ARTIFACT: nextest-test-helpers-${{ inputs.artifact_id }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Configure Dev Drive (Windows)
if: ${{ runner.os == 'Windows' }}
shell: pwsh
run: ../.github/scripts/setup-dev-drive.ps1
- name: Install Linux build dependencies
if: ${{ runner.os == 'Linux' }}
shell: bash
run: |
set -euo pipefail
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update -y
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --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 }}
- name: Expose MSVC SDK environment (Windows)
if: ${{ runner.os == 'Windows' && inputs.target == 'aarch64-pc-windows-msvc' }}
uses: ./.github/actions/setup-msvc-env
with:
target: ${{ inputs.target }}
- name: Compute lockfile hash
id: lockhash
shell: bash
run: |
set -euo pipefail
echo "hash=$(sha256sum Cargo.lock | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
echo "toolchain_hash=$(sha256sum rust-toolchain.toml | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
- name: Restore cargo home cache
id: cache_cargo_home_restore
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: cargo-home-${{ env.ARCHIVE_CACHE_RUNNER }}-${{ inputs.target }}-${{ inputs.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
restore-keys: |
cargo-home-${{ env.ARCHIVE_CACHE_RUNNER }}-${{ inputs.target }}-${{ inputs.profile }}-
- name: Install sccache
if: ${{ env.USE_SCCACHE == 'true' }}
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: sccache
version: 0.7.5
- name: Configure sccache backend
if: ${{ env.USE_SCCACHE == 'true' }}
shell: bash
run: |
set -euo pipefail
if [[ -n "${ACTIONS_CACHE_URL:-}" && -n "${ACTIONS_RUNTIME_TOKEN:-}" ]]; then
echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV"
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 "Using sccache local disk + actions/cache fallback"
fi
- name: Enable sccache wrapper
if: ${{ env.USE_SCCACHE == 'true' }}
shell: bash
run: |
set -euo pipefail
wrapper="$(command -v sccache)"
if [[ "${RUNNER_OS}" == "Windows" ]] && command -v cygpath >/dev/null 2>&1; then
wrapper="$(cygpath -w "${wrapper}")"
fi
echo "RUSTC_WRAPPER=${wrapper}" >> "$GITHUB_ENV"
echo "CARGO_BUILD_RUSTC_WRAPPER=${wrapper}" >> "$GITHUB_ENV"
- name: Restore sccache cache (fallback)
if: ${{ env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true' }}
id: cache_sccache_restore
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ${{ env.SCCACHE_DIR }}
key: sccache-${{ env.ARCHIVE_CACHE_RUNNER }}-${{ inputs.target }}-${{ inputs.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
restore-keys: |
sccache-${{ env.ARCHIVE_CACHE_RUNNER }}-${{ inputs.target }}-${{ inputs.profile }}-${{ steps.lockhash.outputs.hash }}-
sccache-${{ env.ARCHIVE_CACHE_RUNNER }}-${{ inputs.target }}-${{ inputs.profile }}-
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: nextest
version: 0.9.103
- name: Enable unprivileged user namespaces (Linux)
if: runner.os == 'Linux'
run: |
sudo sysctl -w kernel.unprivileged_userns_clone=1
if sudo sysctl -a 2>/dev/null | grep -q '^kernel.apparmor_restrict_unprivileged_userns'; then
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
fi
- name: Build nextest archive
shell: bash
run: |
set -euo pipefail
archive_dir="${RUNNER_TEMP}/nextest-archive"
mkdir -p "${archive_dir}"
cargo nextest archive \
--target ${{ inputs.target }} \
--cargo-profile ${{ inputs.profile }} \
--timings \
--archive-file "${archive_dir}/${NEXTEST_ARCHIVE_FILE}"
- name: Build runtime test helpers
if: ${{ runner.os == 'Linux' || runner.os == 'Windows' }}
shell: bash
run: |
set -euo pipefail
helper_dir="${RUNNER_TEMP}/${TEST_HELPERS_ARTIFACT}"
mkdir -p "${helper_dir}"
if [[ "${RUNNER_OS}" == "Linux" ]]; then
cargo build \
--target ${{ inputs.target }} \
--profile ${{ inputs.profile }} \
-p codex-linux-sandbox \
--bin codex-linux-sandbox
cp "target/${{ inputs.target }}/${{ inputs.profile }}/codex-linux-sandbox" "${helper_dir}/"
else
cargo build \
--target ${{ inputs.target }} \
--profile ${{ inputs.profile }} \
-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}/"
fi
- name: Upload Cargo timings (nextest)
if: always()
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
if-no-files-found: warn
- name: Upload nextest archive
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: nextest-archive-${{ inputs.artifact_id }}
path: ${{ runner.temp }}/nextest-archive/${{ env.NEXTEST_ARCHIVE_FILE }}
if-no-files-found: error
retention-days: 1
- name: Upload runtime test helpers
if: ${{ runner.os == 'Linux' || runner.os == 'Windows' }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: ${{ env.TEST_HELPERS_ARTIFACT }}
path: ${{ runner.temp }}/${{ env.TEST_HELPERS_ARTIFACT }}/*
if-no-files-found: error
retention-days: 1
- name: Save cargo home cache
if: always() && !cancelled() && steps.cache_cargo_home_restore.outputs.cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: cargo-home-${{ env.ARCHIVE_CACHE_RUNNER }}-${{ inputs.target }}-${{ inputs.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
- name: Save sccache cache (fallback)
if: always() && !cancelled() && env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true'
continue-on-error: true
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ${{ env.SCCACHE_DIR }}
key: sccache-${{ env.ARCHIVE_CACHE_RUNNER }}-${{ inputs.target }}-${{ inputs.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
- name: sccache stats
if: always() && env.USE_SCCACHE == 'true'
continue-on-error: true
run: sccache --show-stats || true
- name: sccache summary
if: always() && env.USE_SCCACHE == 'true'
shell: bash
run: |
{
echo "### sccache stats — ${{ inputs.target }} (tests)";
echo;
echo '```';
sccache --show-stats || true;
echo '```';
} >> "$GITHUB_STEP_SUMMARY"
shard:
name: Tests shard ${{ matrix.shard }}/4
needs: archive
runs-on: ${{ inputs.runner_group != '' && fromJSON(format('{{"group":"{0}","labels":"{1}"}}', inputs.runner_group, inputs.runner_labels)) || inputs.runner }}
timeout-minutes: 60
defaults:
run:
working-directory: codex-rs
env:
NEXTEST_ARCHIVE_FILE: nextest-${{ inputs.artifact_id }}.tar.zst
TEST_HELPERS_ARTIFACT: nextest-test-helpers-${{ inputs.artifact_id }}
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install Linux build dependencies
if: ${{ runner.os == 'Linux' }}
shell: bash
run: |
set -euo pipefail
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update -y
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --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 }}
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: nextest
version: 0.9.103
- name: Enable unprivileged user namespaces (Linux)
if: runner.os == 'Linux'
run: |
sudo sysctl -w kernel.unprivileged_userns_clone=1
if sudo sysctl -a 2>/dev/null | grep -q '^kernel.apparmor_restrict_unprivileged_userns'; then
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
fi
- name: Set up remote test env (Docker)
if: ${{ runner.os == 'Linux' && inputs.remote_env }}
shell: bash
run: |
set -euo pipefail
export CODEX_TEST_REMOTE_ENV_CONTAINER_NAME="codex-remote-test-env-${{ github.run_id }}-${{ matrix.shard }}"
source "${GITHUB_WORKSPACE}/scripts/test-remote-env.sh"
echo "CODEX_TEST_REMOTE_ENV=${CODEX_TEST_REMOTE_ENV}" >> "$GITHUB_ENV"
echo "CODEX_TEST_REMOTE_EXEC_SERVER_URL=${CODEX_TEST_REMOTE_EXEC_SERVER_URL}" >> "$GITHUB_ENV"
- name: Download nextest archive
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: nextest-archive-${{ inputs.artifact_id }}
path: ${{ runner.temp }}/nextest-archive
- name: Download runtime test helpers
if: ${{ runner.os == 'Linux' || runner.os == 'Windows' }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ env.TEST_HELPERS_ARTIFACT }}
path: ${{ runner.temp }}/${{ env.TEST_HELPERS_ARTIFACT }}
- name: tests
id: test
shell: bash
run: |
set -euo pipefail
archive_file="${RUNNER_TEMP}/nextest-archive/${NEXTEST_ARCHIVE_FILE}"
workspace_root="$(pwd)"
if [[ "${RUNNER_OS}" == "Windows" ]]; then
archive_file="$(cygpath -w "${archive_file}")"
workspace_root="$(cygpath -w "${workspace_root}")"
fi
if [[ "${RUNNER_OS}" == "Linux" ]]; then
helper_dir="${RUNNER_TEMP}/${TEST_HELPERS_ARTIFACT}"
helper_target_dir="$(pwd)/target/${{ 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 }}"
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}/"
fi
nextest_args=(
run
--no-fail-fast
--archive-file "${archive_file}"
--workspace-remap "${workspace_root}"
--partition "hash:${{ matrix.shard }}/4"
)
if [[ "${{ inputs.test_threads }}" != "0" ]]; then
nextest_args+=(--test-threads "${{ inputs.test_threads }}")
fi
test_command=(cargo nextest "${nextest_args[@]}")
if [[ "${RUNNER_OS}" == "Linux" ]]; then
sandbox_helper="${helper_target_dir}/codex-linux-sandbox"
test_command=(
env
"CARGO_BIN_EXE_codex-linux-sandbox=${sandbox_helper}"
"CARGO_BIN_EXE_codex_linux_sandbox=${sandbox_helper}"
cargo nextest "${nextest_args[@]}"
)
elif [[ "${RUNNER_OS}" == "Windows" ]]; then
setup_helper="$(cygpath -w "${helper_target_dir}/codex-windows-sandbox-setup.exe")"
command_runner="$(cygpath -w "${helper_target_dir}/codex-command-runner.exe")"
test_command=(
env
"CARGO_BIN_EXE_codex_windows_sandbox_setup=${setup_helper}"
"CARGO_BIN_EXE_codex_command_runner=${command_runner}"
cargo nextest "${nextest_args[@]}"
)
fi
"${test_command[@]}"
env:
RUST_BACKTRACE: 1
RUST_MIN_STACK: "8388608" # 8 MiB
NEXTEST_STATUS_LEVEL: leak
- name: Upload nextest JUnit report
if: always()
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
if-no-files-found: warn
- name: Tear down remote test env
if: ${{ always() && runner.os == 'Linux' && inputs.remote_env }}
shell: bash
run: |
set +e
if [[ "${STEPS_TEST_OUTCOME}" != "success" ]]; then
docker logs "${CODEX_TEST_REMOTE_ENV}" || true
fi
docker rm -f "${CODEX_TEST_REMOTE_ENV}" >/dev/null 2>&1 || true
env:
STEPS_TEST_OUTCOME: ${{ steps.test.outcome }}
- name: verify tests passed
if: steps.test.outcome == 'failure'
run: |
echo "Tests failed. See logs for details."
exit 1
result:
name: Platform result
needs: shard
if: always()
runs-on: ubuntu-24.04
steps:
- name: Confirm test shards passed
shell: bash
run: |
if [[ "${{ needs.shard.result }}" != "success" ]]; then
echo "Nextest shards finished with result: ${{ needs.shard.result }}" >&2
exit 1
fi

View File

@@ -25,16 +25,11 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.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
run: just bench-smoke
cargo_shear:
name: cargo shear
@@ -46,7 +41,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: cargo-shear@1.11.2
@@ -63,7 +58,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
with:
toolchain: nightly-2025-09-18
components: llvm-tools-preview, rustc-dev, rust-src
@@ -260,9 +255,13 @@ jobs:
set -euo pipefail
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update -y
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
packages=(pkg-config libcap-dev)
if [[ "${{ matrix.target }}" == 'x86_64-unknown-linux-musl' || "${{ matrix.target }}" == 'aarch64-unknown-linux-musl' ]]; then
packages+=(libubsan1)
fi
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "${packages[@]}"
fi
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
with:
targets: ${{ matrix.target }}
components: clippy
@@ -344,6 +343,14 @@ jobs:
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Disable sccache wrapper (musl)
shell: bash
run: |
set -euo pipefail
echo "RUSTC_WRAPPER=" >> "$GITHUB_ENV"
echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV"
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Prepare APT cache directories (musl)
shell: bash
@@ -377,9 +384,61 @@ jobs:
shell: bash
run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh"
- if: ${{ !contains(matrix.target, 'windows') }}
name: Configure rusty_v8 artifact overrides and verify checksums
uses: ./.github/actions/setup-rusty-v8
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Configure rustc UBSan wrapper (musl host)
shell: bash
run: |
set -euo pipefail
ubsan=""
if command -v ldconfig >/dev/null 2>&1; then
ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')"
fi
wrapper_root="${RUNNER_TEMP:-/tmp}"
wrapper="${wrapper_root}/rustc-ubsan-wrapper"
cat > "${wrapper}" <<EOF
#!/usr/bin/env bash
set -euo pipefail
if [[ -n "${ubsan}" ]]; then
export LD_PRELOAD="${ubsan}\${LD_PRELOAD:+:\${LD_PRELOAD}}"
fi
exec "\$1" "\${@:2}"
EOF
chmod +x "${wrapper}"
echo "RUSTC_WRAPPER=${wrapper}" >> "$GITHUB_ENV"
echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV"
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Clear sanitizer flags (musl)
shell: bash
run: |
set -euo pipefail
# Clear global Rust flags so host/proc-macro builds don't pull in UBSan.
echo "RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "RUSTDOCFLAGS=" >> "$GITHUB_ENV"
# Override any runner-level Cargo config rustflags as well.
echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV"
sanitize_flags() {
local input="$1"
input="${input//-fsanitize=undefined/}"
input="${input//-fno-sanitize-recover=undefined/}"
input="${input//-fno-sanitize-trap=undefined/}"
echo "$input"
}
cflags="$(sanitize_flags "${CFLAGS-}")"
cxxflags="$(sanitize_flags "${CXXFLAGS-}")"
echo "CFLAGS=${cflags}" >> "$GITHUB_ENV"
echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV"
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl' }}
name: Configure musl rusty_v8 artifact overrides and verify checksums
uses: ./.github/actions/setup-rusty-v8-musl
with:
target: ${{ matrix.target }}
@@ -462,73 +521,235 @@ jobs:
/var/cache/apt
key: apt-${{ matrix.runner }}-${{ matrix.target }}-v1
tests_macos_aarch64:
name: Tests — macos-15-xlarge - aarch64-apple-darwin
uses: ./.github/workflows/rust-ci-full-nextest-platform.yml
with:
runner: macos-15-xlarge
target: aarch64-apple-darwin
profile: ci-test
artifact_id: macos-aarch64
use_sccache: true
secrets: inherit
tests:
name: Tests — ${{ matrix.runner }} - ${{ matrix.target }}${{ matrix.remote_env == 'true' && ' (remote)' || '' }}
runs-on: ${{ matrix.runs_on || matrix.runner }}
# Perhaps we can bring this back down to 30m once we finish the cutover
# from tui_app_server/ to tui/. Incidentally, windows-arm64 was the main
# offender for exceeding the timeout.
timeout-minutes: 45
defaults:
run:
working-directory: codex-rs
env:
# Speed up repeated builds across CI runs by caching compiled objects, except on
# arm64 macOS runners cross-targeting x86_64 where ring/cc-rs can produce
# mixed-architecture archives under sccache.
USE_SCCACHE: ${{ (startsWith(matrix.runner, 'windows') || (matrix.runner == 'macos-15-xlarge' && matrix.target == 'x86_64-apple-darwin')) && 'false' || 'true' }}
CARGO_INCREMENTAL: "0"
SCCACHE_CACHE_SIZE: 10G
tests_linux_x64_remote:
name: Tests — ubuntu-24.04 - x86_64-unknown-linux-gnu (remote)
uses: ./.github/workflows/rust-ci-full-nextest-platform.yml
with:
runner: ubuntu-24.04
runner_group: codex-runners
runner_labels: codex-linux-x64
target: x86_64-unknown-linux-gnu
profile: ci-test
artifact_id: linux-x64-remote
remote_env: true
use_sccache: true
secrets: inherit
strategy:
fail-fast: false
matrix:
include:
- runner: macos-15-xlarge
target: aarch64-apple-darwin
profile: dev
- runner: ubuntu-24.04
target: x86_64-unknown-linux-gnu
profile: dev
remote_env: "true"
runs_on:
group: codex-runners
labels: codex-linux-x64
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
profile: dev
runs_on:
group: codex-runners
labels: codex-linux-arm64
- runner: windows-x64
target: x86_64-pc-windows-msvc
profile: dev
runs_on:
group: codex-runners
labels: codex-windows-x64
- runner: windows-arm64
target: aarch64-pc-windows-msvc
profile: dev
runs_on:
group: codex-runners
labels: codex-windows-arm64
tests_linux_arm64:
name: Tests — ubuntu-24.04-arm - aarch64-unknown-linux-gnu
uses: ./.github/workflows/rust-ci-full-nextest-platform.yml
with:
runner: ubuntu-24.04-arm
runner_group: codex-runners
runner_labels: codex-linux-arm64
target: aarch64-unknown-linux-gnu
profile: ci-test
artifact_id: linux-arm64
use_sccache: true
secrets: inherit
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install Linux build dependencies
if: ${{ runner.os == 'Linux' }}
shell: bash
run: |
set -euo pipefail
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update -y
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev bubblewrap
fi
tests_windows_x64:
name: Tests — windows-x64 - x86_64-pc-windows-msvc
uses: ./.github/workflows/rust-ci-full-nextest-platform.yml
with:
runner: windows-x64
runner_group: codex-runners
runner_labels: codex-windows-x64
target: x86_64-pc-windows-msvc
profile: ci-test
artifact_id: windows-x64
test_threads: 8
secrets: inherit
# Some integration tests rely on DotSlash being installed.
# See https://github.com/openai/codex/pull/7617.
- name: Install DotSlash
uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
tests_windows_arm64:
name: Tests — windows-arm64 - aarch64-pc-windows-msvc
uses: ./.github/workflows/rust-ci-full-nextest-platform.yml
with:
runner: windows-arm64
runner_group: codex-runners
runner_labels: codex-windows-arm64
archive_runner: windows-x64
archive_runner_group: codex-runners
archive_runner_labels: codex-windows-x64
target: aarch64-pc-windows-msvc
profile: ci-test
artifact_id: windows-arm64
test_threads: 8
use_sccache: true
secrets: inherit
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
with:
targets: ${{ matrix.target }}
- name: Compute lockfile hash
id: lockhash
working-directory: codex-rs
shell: bash
run: |
set -euo pipefail
echo "hash=$(sha256sum Cargo.lock | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
echo "toolchain_hash=$(sha256sum rust-toolchain.toml | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
- name: Restore cargo home cache
id: cache_cargo_home_restore
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
restore-keys: |
cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
- name: Install sccache
if: ${{ env.USE_SCCACHE == 'true' }}
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: sccache
version: 0.7.5
- name: Configure sccache backend
if: ${{ env.USE_SCCACHE == 'true' }}
shell: bash
run: |
set -euo pipefail
if [[ -n "${ACTIONS_CACHE_URL:-}" && -n "${ACTIONS_RUNTIME_TOKEN:-}" ]]; then
echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV"
echo "Using sccache GitHub backend"
else
echo "SCCACHE_GHA_ENABLED=false" >> "$GITHUB_ENV"
echo "SCCACHE_DIR=${{ github.workspace }}/.sccache" >> "$GITHUB_ENV"
echo "Using sccache local disk + actions/cache fallback"
fi
- name: Enable sccache wrapper
if: ${{ env.USE_SCCACHE == 'true' }}
shell: bash
run: echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
- name: Restore sccache cache (fallback)
if: ${{ env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true' }}
id: cache_sccache_restore
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ${{ github.workspace }}/.sccache/
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
restore-keys: |
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: nextest
version: 0.9.103
- name: Enable unprivileged user namespaces (Linux)
if: runner.os == 'Linux'
run: |
# Required for bubblewrap to work on Linux CI runners.
sudo sysctl -w kernel.unprivileged_userns_clone=1
# Ubuntu 24.04+ can additionally gate unprivileged user namespaces
# behind AppArmor.
if sudo sysctl -a 2>/dev/null | grep -q '^kernel.apparmor_restrict_unprivileged_userns'; then
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
fi
- name: Set up remote test env (Docker)
if: ${{ runner.os == 'Linux' && matrix.remote_env == 'true' }}
shell: bash
run: |
set -euo pipefail
export CODEX_TEST_REMOTE_ENV_CONTAINER_NAME=codex-remote-test-env
source "${GITHUB_WORKSPACE}/scripts/test-remote-env.sh"
echo "CODEX_TEST_REMOTE_ENV=${CODEX_TEST_REMOTE_ENV}" >> "$GITHUB_ENV"
echo "CODEX_TEST_REMOTE_EXEC_SERVER_URL=${CODEX_TEST_REMOTE_EXEC_SERVER_URL}" >> "$GITHUB_ENV"
- name: tests
id: test
run: cargo nextest run --no-fail-fast --target ${{ matrix.target }} --cargo-profile ci-test --timings
env:
RUST_BACKTRACE: 1
RUST_MIN_STACK: "8388608" # 8 MiB
NEXTEST_STATUS_LEVEL: leak
- name: Upload Cargo timings (nextest)
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: cargo-timings-rust-ci-nextest-${{ matrix.target }}-${{ matrix.profile }}
path: codex-rs/target/**/cargo-timings/cargo-timing.html
if-no-files-found: warn
- name: Save cargo home cache
if: always() && !cancelled() && steps.cache_cargo_home_restore.outputs.cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
- name: Save sccache cache (fallback)
if: always() && !cancelled() && env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true'
continue-on-error: true
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ${{ github.workspace }}/.sccache/
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
- name: sccache stats
if: always() && env.USE_SCCACHE == 'true'
continue-on-error: true
run: sccache --show-stats || true
- name: sccache summary
if: always() && env.USE_SCCACHE == 'true'
shell: bash
run: |
{
echo "### sccache stats — ${{ matrix.target }} (tests)";
echo;
echo '```';
sccache --show-stats || true;
echo '```';
} >> "$GITHUB_STEP_SUMMARY"
- name: Tear down remote test env
if: ${{ always() && runner.os == 'Linux' && matrix.remote_env == 'true' }}
shell: bash
run: |
set +e
if [[ "${STEPS_TEST_OUTCOME}" != "success" ]]; then
docker logs codex-remote-test-env || true
fi
docker rm -f codex-remote-test-env >/dev/null 2>&1 || true
env:
STEPS_TEST_OUTCOME: ${{ steps.test.outcome }}
- name: verify tests passed
if: steps.test.outcome == 'failure'
run: |
echo "Tests failed. See logs for details."
exit 1
# --- Gatherer job for the full post-merge workflow --------------------------
results:
@@ -540,11 +761,7 @@ jobs:
argument_comment_lint_package,
argument_comment_lint_prebuilt,
lint_build,
tests_macos_aarch64,
tests_linux_x64_remote,
tests_linux_arm64,
tests_windows_x64,
tests_windows_arm64,
tests,
]
if: always()
runs-on: ubuntu-24.04
@@ -557,21 +774,13 @@ jobs:
echo "general: ${{ needs.general.result }}"
echo "shear : ${{ needs.cargo_shear.result }}"
echo "lint : ${{ needs.lint_build.result }}"
echo "test macos : ${{ needs.tests_macos_aarch64.result }}"
echo "test linux : ${{ needs.tests_linux_x64_remote.result }}"
echo "test arm64 : ${{ needs.tests_linux_arm64.result }}"
echo "test winx64: ${{ needs.tests_windows_x64.result }}"
echo "test winarm: ${{ needs.tests_windows_arm64.result }}"
echo "tests : ${{ needs.tests.result }}"
[[ '${{ needs.argument_comment_lint_package.result }}' == 'success' ]] || { echo 'argument_comment_lint_package failed'; exit 1; }
[[ '${{ needs.argument_comment_lint_prebuilt.result }}' == 'success' ]] || { echo 'argument_comment_lint_prebuilt failed'; exit 1; }
[[ '${{ needs.general.result }}' == 'success' ]] || { echo 'general failed'; exit 1; }
[[ '${{ needs.cargo_shear.result }}' == 'success' ]] || { echo 'cargo_shear failed'; exit 1; }
[[ '${{ needs.lint_build.result }}' == 'success' ]] || { echo 'lint_build failed'; exit 1; }
[[ '${{ needs.tests_macos_aarch64.result }}' == 'success' ]] || { echo 'tests_macos_aarch64 failed'; exit 1; }
[[ '${{ needs.tests_linux_x64_remote.result }}' == 'success' ]] || { echo 'tests_linux_x64_remote failed'; exit 1; }
[[ '${{ needs.tests_linux_arm64.result }}' == 'success' ]] || { echo 'tests_linux_arm64 failed'; exit 1; }
[[ '${{ needs.tests_windows_x64.result }}' == 'success' ]] || { echo 'tests_windows_x64 failed'; exit 1; }
[[ '${{ needs.tests_windows_arm64.result }}' == 'success' ]] || { echo 'tests_windows_arm64 failed'; exit 1; }
[[ '${{ needs.tests.result }}' == 'success' ]] || { echo 'tests failed'; exit 1; }
- name: sccache summary note
if: always()

View File

@@ -3,11 +3,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 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:
@@ -72,16 +67,11 @@ jobs:
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.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
run: just bench-smoke
cargo_shear:
name: cargo shear
@@ -96,7 +86,7 @@ jobs:
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: cargo-shear@1.11.2
@@ -116,7 +106,7 @@ jobs:
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
- name: Install nightly argument-comment-lint toolchain
shell: bash
run: |

View File

@@ -7,11 +7,6 @@ on:
required: true
type: boolean
# 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:
skip:
if: ${{ !inputs.publish }}
@@ -65,7 +60,7 @@ jobs:
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
with:
toolchain: nightly-2025-09-18
targets: ${{ matrix.target }}

View File

@@ -16,9 +16,6 @@ jobs:
prepare:
# Prevent scheduled runs on forks (no secrets, wastes Actions minutes)
if: github.repository == 'openai/codex'
environment:
name: rust-release-prepare
deployment: false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

View File

@@ -20,11 +20,6 @@ on:
AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME:
required: true
# 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:
build-windows-binaries:
name: Build Windows binaries - ${{ matrix.runner }} - ${{ matrix.target }} - ${{ matrix.bundle }}
@@ -39,8 +34,6 @@ jobs:
working-directory: codex-rs
env:
CARGO_PROFILE_RELEASE_LTO: ${{ inputs.release-lto }}
CARGO_PROFILE_RELEASE_DEBUG: full
CARGO_PROFILE_RELEASE_STRIP: "false"
strategy:
fail-fast: false
@@ -107,22 +100,18 @@ jobs:
Write-Host "Total RAM: $ramGiB GiB"
Write-Host "Disk usage:"
Get-PSDrive -PSProvider FileSystem | Format-Table -AutoSize Name, @{Name='Size(GB)';Expression={[math]::Round(($_.Used + $_.Free) / 1GB, 1)}}, @{Name='Free(GB)';Expression={[math]::Round($_.Free / 1GB, 1)}}
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
with:
targets: ${{ matrix.target }}
- name: Cargo build (Windows binaries)
shell: bash
run: |
target="${{ matrix.target }}"
if [[ "$target" == "x86_64-pc-windows-msvc" ]]; then
export LIBSQLITE3_FLAGS=SQLITE_DISABLE_INTRINSIC
fi
build_args=()
for binary in ${{ matrix.binaries }}; do
build_args+=(--bin "$binary")
done
cargo build --target "$target" --release --timings "${build_args[@]}"
cargo build --target ${{ matrix.target }} --release --timings "${build_args[@]}"
- name: Upload Cargo timings
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
@@ -138,7 +127,6 @@ jobs:
mkdir -p "$output_dir"
for binary in ${{ matrix.binaries }}; do
cp "target/${{ matrix.target }}/release/${binary}.exe" "$output_dir/${binary}.exe"
cp "target/${{ matrix.target }}/release/${binary}.pdb" "$output_dir/${binary}.pdb"
done
- name: Upload Windows binaries
@@ -221,23 +209,6 @@ jobs:
account-name: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }}
- 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" \
--archive-dir "symbols-dist/${{ matrix.target }}" \
--binaries "${WINDOWS_BINARIES}"
- name: Upload symbols archive
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: ${{ matrix.target }}-symbols
path: codex-rs/symbols-dist/${{ matrix.target }}/*
if-no-files-found: error
- name: Stage artifacts
shell: bash
run: |
@@ -249,21 +220,6 @@ jobs:
"$dest/${binary}-${{ matrix.target }}.exe"
done
- name: Install DotSlash
uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
- name: Build Codex package archives
shell: bash
run: |
set -euo pipefail
for bundle in primary app-server; do
bash "${GITHUB_WORKSPACE}/.github/scripts/build-codex-package-archive.sh" \
--target "${{ matrix.target }}" \
--bundle "$bundle" \
--entrypoint-dir "target/${{ matrix.target }}/release" \
--archive-dir "dist/${{ matrix.target }}"
done
- name: Build Python runtime wheel
shell: bash
run: |
@@ -287,12 +243,16 @@ jobs:
stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}"
wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}"
# Keep the helpers next to codex.exe in the runtime wheel so Windows
# sandbox/elevation lookup matches the standalone release zip.
python "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" \
stage-runtime \
"$stage_dir" \
"dist/${{ matrix.target }}/codex-package-${{ matrix.target }}.tar.gz" \
"${GITHUB_WORKSPACE}/codex-rs/target/${{ matrix.target }}/release/codex.exe" \
--codex-version "${GITHUB_REF_NAME}" \
--platform-tag "$platform_tag"
--platform-tag "$platform_tag" \
--resource-binary "${GITHUB_WORKSPACE}/codex-rs/target/${{ matrix.target }}/release/codex-command-runner.exe" \
--resource-binary "${GITHUB_WORKSPACE}/codex-rs/target/${{ matrix.target }}/release/codex-windows-sandbox-setup.exe"
"${RUNNER_TEMP}/python-runtime-build-venv/Scripts/python.exe" -m build --wheel --outdir "$wheel_dir" "$stage_dir"
- name: Upload Python runtime wheel
@@ -302,6 +262,9 @@ jobs:
path: python-runtime-dist/${{ matrix.target }}/*.whl
if-no-files-found: error
- name: Install DotSlash
uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
- name: Compress artifacts
shell: bash
run: |
@@ -320,7 +283,7 @@ jobs:
base="$(basename "$f")"
# Skip files that are already archives (shouldn't happen, but be
# safe).
if [[ "$base" == *.tar.gz || "$base" == *.tar.zst || "$base" == *.zip || "$base" == *.dmg ]]; then
if [[ "$base" == *.tar.gz || "$base" == *.zip || "$base" == *.dmg ]]; then
continue
fi

View File

@@ -69,10 +69,6 @@ jobs:
fail-fast: false
matrix:
include:
- runner: macos-15-large
target: x86_64-apple-darwin
variant: macos-15
archive_name: codex-zsh-x86_64-apple-darwin.tar.gz
- runner: macos-15-xlarge
target: aarch64-apple-darwin
variant: macos-15

File diff suppressed because it is too large Load Diff

View File

@@ -5,11 +5,6 @@ on:
tags:
- "rusty-v8-v*.*.*"
# Cargo's libgit2 transport has been flaky when fetching git dependencies with
# nested submodules. Prefer the system git CLI for Cargo smoke tests.
env:
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
concurrency:
group: ${{ github.workflow }}::${{ github.ref_name }}
cancel-in-progress: false
@@ -51,14 +46,14 @@ jobs:
expected_release_tag="rusty-v8-v${V8_VERSION}"
release_tag="${GITHUB_REF_NAME}"
if [[ "${release_tag}" != "${expected_release_tag}" ]]; then
echo "Tag ${release_tag} does not match expected release tag ${expected_release_tag}." >&2
echo "Tag ${release_tag} does not match resolved v8 crate version ${V8_VERSION}." >&2
exit 1
fi
echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT"
build:
name: Build ${{ matrix.variant }} ${{ matrix.target }}
name: Build ${{ matrix.target }}
needs: metadata
runs-on: ${{ matrix.runner }}
permissions:
@@ -69,77 +64,11 @@ jobs:
matrix:
include:
- runner: ubuntu-24.04
bazel_config: ci-v8
platform: linux_amd64
sandbox: false
target: x86_64-unknown-linux-gnu
variant: release
- runner: ubuntu-24.04
bazel_config: ci-v8
platform: linux_amd64
sandbox: true
target: x86_64-unknown-linux-gnu
variant: ptrcomp-sandbox
- runner: ubuntu-24.04-arm
bazel_config: ci-v8
platform: linux_arm64
sandbox: false
target: aarch64-unknown-linux-gnu
variant: release
- runner: ubuntu-24.04-arm
bazel_config: ci-v8
platform: linux_arm64
sandbox: true
target: aarch64-unknown-linux-gnu
variant: ptrcomp-sandbox
- runner: macos-15-xlarge
bazel_config: ci-macos
platform: macos_amd64
sandbox: false
target: x86_64-apple-darwin
variant: release
- runner: macos-15-xlarge
bazel_config: ci-macos
platform: macos_amd64
sandbox: true
target: x86_64-apple-darwin
variant: ptrcomp-sandbox
- runner: macos-15-xlarge
bazel_config: ci-macos
platform: macos_arm64
sandbox: false
target: aarch64-apple-darwin
variant: release
- runner: macos-15-xlarge
bazel_config: ci-macos
platform: macos_arm64
sandbox: true
target: aarch64-apple-darwin
variant: ptrcomp-sandbox
- runner: ubuntu-24.04
bazel_config: ci-v8
platform: linux_amd64_musl
sandbox: false
target: x86_64-unknown-linux-musl
variant: release
- runner: ubuntu-24.04-arm
bazel_config: ci-v8
platform: linux_arm64_musl
sandbox: false
target: aarch64-unknown-linux-musl
variant: release
- runner: ubuntu-24.04
bazel_config: ci-v8
platform: linux_amd64_musl
sandbox: true
target: x86_64-unknown-linux-musl
variant: ptrcomp-sandbox
- runner: ubuntu-24.04-arm
bazel_config: ci-v8
platform: linux_arm64_musl
sandbox: true
target: aarch64-unknown-linux-musl
variant: ptrcomp-sandbox
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -156,114 +85,61 @@ jobs:
with:
python-version: "3.12"
- name: Set up Rust toolchain for Cargo smoke
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
toolchain: "1.95.0"
- name: Build Bazel V8 release pair
env:
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
PLATFORM: ${{ matrix.platform }}
SANDBOX: ${{ matrix.sandbox }}
TARGET: ${{ matrix.target }}
shell: bash
run: |
set -euo pipefail
target_suffix="${TARGET//-/_}"
pair_kind="release_pair"
if [[ "${SANDBOX}" == "true" ]]; then
pair_kind="sandbox_release_pair"
pair_target="//third_party/v8:rusty_v8_release_pair_${target_suffix}"
extra_targets=()
if [[ "${TARGET}" == *-unknown-linux-musl ]]; then
extra_targets=(
"@llvm//runtimes/libcxx:libcxx.static"
"@llvm//runtimes/libcxx:libcxxabi.static"
)
fi
pair_target="//third_party/v8:rusty_v8_${pair_kind}_${target_suffix}"
bazel_args=(
build
-c
opt
"--platforms=@llvm//platforms:${PLATFORM}"
--config=rusty-v8-upstream-libcxx
--config=v8-release-compat
"${pair_target}"
"${extra_targets[@]}"
--build_metadata=COMMIT_SHA=$(git rev-parse HEAD)
)
if [[ "${SANDBOX}" != "true" ]]; then
bazel_args+=(--config=v8-release-compat)
fi
./.github/scripts/run_bazel_with_buildbuddy.py \
bazel \
--noexperimental_remote_repo_contents_cache \
"${bazel_args[@]}" \
"--config=${{ matrix.bazel_config }}"
--config=ci-v8 \
"--remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}"
- name: Stage release pair
env:
BAZEL_CONFIG: ${{ matrix.bazel_config }}
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
PLATFORM: ${{ matrix.platform }}
SANDBOX: ${{ matrix.sandbox }}
TARGET: ${{ matrix.target }}
shell: bash
run: |
set -euo pipefail
stage_args=(
--platform "${PLATFORM}"
--target "${TARGET}"
--compilation-mode opt
python3 .github/scripts/rusty_v8_bazel.py stage-release-pair \
--platform "${PLATFORM}" \
--target "${TARGET}" \
--compilation-mode opt \
--bazel-config v8-release-compat \
--output-dir "dist/${TARGET}"
--bazel-config "${BAZEL_CONFIG}"
)
if [[ "${SANDBOX}" == "true" ]]; then
stage_args+=(--sandbox)
else
stage_args+=(--bazel-config v8-release-compat)
fi
python3 .github/scripts/rusty_v8_bazel.py stage-release-pair "${stage_args[@]}"
- name: Smoke test staged artifact with Cargo
env:
SANDBOX: ${{ matrix.sandbox }}
TARGET: ${{ matrix.target }}
shell: bash
run: |
set -euo pipefail
host_arch="$(uname -m)"
case "${TARGET}:${host_arch}" in
x86_64-apple-darwin:x86_64|aarch64-apple-darwin:arm64|x86_64-unknown-linux-gnu:x86_64|aarch64-unknown-linux-gnu:aarch64)
;;
*)
echo "Skipping non-native Cargo smoke for ${TARGET} on ${host_arch}."
exit 0
;;
esac
archive="$(find "dist/${TARGET}" -maxdepth 1 -type f -name 'librusty_v8_*.a.gz' -print -quit)"
binding="$(find "dist/${TARGET}" -maxdepth 1 -type f -name 'src_binding_*.rs' -print -quit)"
if [[ -z "${archive}" || -z "${binding}" ]]; then
echo "Missing staged archive or binding for ${TARGET}." >&2
exit 1
fi
cargo_args=(test -p codex-v8-poc)
if [[ "${SANDBOX}" == "true" ]]; then
cargo_args+=(--features sandbox)
fi
(
cd codex-rs
CARGO_TARGET_DIR="${RUNNER_TEMP}/rusty-v8-cargo-smoke-${TARGET}-${SANDBOX}" \
RUSTY_V8_ARCHIVE="${GITHUB_WORKSPACE}/${archive}" \
RUSTY_V8_SRC_BINDING_PATH="${GITHUB_WORKSPACE}/${binding}" \
cargo "${cargo_args[@]}"
)
- name: Upload staged artifacts
- name: Upload staged musl artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: rusty-v8-${{ needs.metadata.outputs.v8_version }}-${{ matrix.variant }}-${{ matrix.target }}
name: rusty-v8-${{ needs.metadata.outputs.v8_version }}-${{ matrix.target }}
path: dist/${{ matrix.target }}/*
publish-release:
@@ -276,8 +152,7 @@ jobs:
actions: read
steps:
- name: Check whether release already exists
id: release
- name: Ensure release tag is new
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ needs.metadata.outputs.release_tag }}
@@ -286,9 +161,8 @@ jobs:
set -euo pipefail
if gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" > /dev/null 2>&1; then
echo "exists=true" >> "${GITHUB_OUTPUT}"
else
echo "exists=false" >> "${GITHUB_OUTPUT}"
echo "Release tag ${RELEASE_TAG} already exists; musl artifact tags are immutable." >&2
exit 1
fi
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
@@ -296,7 +170,6 @@ jobs:
path: dist
- name: Create GitHub Release
if: ${{ steps.release.outputs.exists != 'true' }}
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
with:
tag_name: ${{ needs.metadata.outputs.release_tag }}
@@ -304,14 +177,3 @@ jobs:
files: dist/**
# Keep V8 artifact releases out of Codex's normal "latest release" channel.
prerelease: true
- name: Amend existing GitHub Release
if: ${{ steps.release.outputs.exists == 'true' }}
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
with:
tag_name: ${{ needs.metadata.outputs.release_tag }}
name: ${{ needs.metadata.outputs.release_tag }}
files: dist/**
overwrite_files: true
# Keep V8 artifact releases out of Codex's normal "latest release" channel.
prerelease: true

View File

@@ -3,47 +3,32 @@ name: v8-canary
on:
pull_request:
paths:
- ".bazelrc"
- ".github/actions/setup-bazel-ci/**"
- ".github/scripts/run_bazel_with_buildbuddy.py"
- ".github/scripts/rusty_v8_bazel.py"
- ".github/scripts/rusty_v8_module_bazel.py"
- ".github/workflows/rusty-v8-release.yml"
- ".github/workflows/v8-canary.yml"
- "MODULE.bazel"
- "MODULE.bazel.lock"
- "codex-rs/Cargo.toml"
- "patches/BUILD.bazel"
- "patches/llvm_*.patch"
- "patches/rules_cc_*.patch"
- "patches/v8_*.patch"
- "third_party/v8/**"
push:
branches:
- main
paths:
- ".bazelrc"
- ".github/actions/setup-bazel-ci/**"
- ".github/scripts/run_bazel_with_buildbuddy.py"
- ".github/scripts/rusty_v8_bazel.py"
- ".github/scripts/rusty_v8_module_bazel.py"
- ".github/workflows/rusty-v8-release.yml"
- ".github/workflows/v8-canary.yml"
- "MODULE.bazel"
- "MODULE.bazel.lock"
- "codex-rs/Cargo.toml"
- "patches/BUILD.bazel"
- "patches/llvm_*.patch"
- "patches/rules_cc_*.patch"
- "patches/v8_*.patch"
- "third_party/v8/**"
workflow_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' }}
@@ -74,7 +59,7 @@ jobs:
echo "version=${version}" >> "$GITHUB_OUTPUT"
build:
name: Build ${{ matrix.variant }} ${{ matrix.target }}
name: Build ${{ matrix.target }}
needs: metadata
runs-on: ${{ matrix.runner }}
permissions:
@@ -85,77 +70,12 @@ jobs:
matrix:
include:
- runner: ubuntu-24.04
bazel_config: ci-v8
platform: linux_amd64
sandbox: false
target: x86_64-unknown-linux-gnu
variant: release
- runner: ubuntu-24.04
bazel_config: ci-v8
platform: linux_amd64
sandbox: true
target: x86_64-unknown-linux-gnu
variant: ptrcomp-sandbox
- runner: ubuntu-24.04-arm
bazel_config: ci-v8
platform: linux_arm64
sandbox: false
target: aarch64-unknown-linux-gnu
variant: release
- runner: ubuntu-24.04-arm
bazel_config: ci-v8
platform: linux_arm64
sandbox: true
target: aarch64-unknown-linux-gnu
variant: ptrcomp-sandbox
- runner: macos-15-xlarge
bazel_config: ci-macos
platform: macos_amd64
sandbox: false
target: x86_64-apple-darwin
variant: release
- runner: macos-15-xlarge
bazel_config: ci-macos
platform: macos_amd64
sandbox: true
target: x86_64-apple-darwin
variant: ptrcomp-sandbox
- runner: macos-15-xlarge
bazel_config: ci-macos
platform: macos_arm64
sandbox: false
target: aarch64-apple-darwin
variant: release
- runner: macos-15-xlarge
bazel_config: ci-macos
platform: macos_arm64
sandbox: true
target: aarch64-apple-darwin
variant: ptrcomp-sandbox
- runner: ubuntu-24.04
bazel_config: ci-v8
platform: linux_amd64_musl
sandbox: false
target: x86_64-unknown-linux-musl
variant: release
- runner: ubuntu-24.04
bazel_config: ci-v8
platform: linux_amd64_musl
sandbox: true
target: x86_64-unknown-linux-musl
variant: ptrcomp-sandbox
- runner: ubuntu-24.04-arm
bazel_config: ci-v8
platform: linux_arm64_musl
sandbox: false
target: aarch64-unknown-linux-musl
variant: release
- runner: ubuntu-24.04-arm
bazel_config: ci-v8
platform: linux_arm64_musl
sandbox: true
target: aarch64-unknown-linux-musl
variant: ptrcomp-sandbox
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
@@ -172,246 +92,53 @@ jobs:
with:
python-version: "3.12"
- name: Set up Rust toolchain for Cargo smoke
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
toolchain: "1.95.0"
- name: Build Bazel V8 release pair
env:
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
PLATFORM: ${{ matrix.platform }}
SANDBOX: ${{ matrix.sandbox }}
TARGET: ${{ matrix.target }}
shell: bash
run: |
set -euo pipefail
target_suffix="${TARGET//-/_}"
pair_kind="release_pair"
if [[ "${SANDBOX}" == "true" ]]; then
pair_kind="sandbox_release_pair"
fi
pair_target="//third_party/v8:rusty_v8_${pair_kind}_${target_suffix}"
pair_target="//third_party/v8:rusty_v8_release_pair_${target_suffix}"
extra_targets=(
"@llvm//runtimes/libcxx:libcxx.static"
"@llvm//runtimes/libcxx:libcxxabi.static"
)
bazel_args=(
build
"--platforms=@llvm//platforms:${PLATFORM}"
--config=rusty-v8-upstream-libcxx
--config=v8-release-compat
"${pair_target}"
"${extra_targets[@]}"
--build_metadata=COMMIT_SHA=$(git rev-parse HEAD)
)
if [[ "${SANDBOX}" != "true" ]]; then
bazel_args+=(--config=v8-release-compat)
fi
./.github/scripts/run_bazel_with_buildbuddy.py \
bazel \
--noexperimental_remote_repo_contents_cache \
"${bazel_args[@]}" \
"--config=${{ matrix.bazel_config }}"
--config=ci-v8 \
"--remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}"
- name: Stage release pair
env:
BAZEL_CONFIG: ${{ matrix.bazel_config }}
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
PLATFORM: ${{ matrix.platform }}
SANDBOX: ${{ matrix.sandbox }}
TARGET: ${{ matrix.target }}
shell: bash
run: |
set -euo pipefail
stage_args=(
--platform "${PLATFORM}"
--target "${TARGET}"
python3 .github/scripts/rusty_v8_bazel.py stage-release-pair \
--platform "${PLATFORM}" \
--target "${TARGET}" \
--bazel-config v8-release-compat \
--output-dir "dist/${TARGET}"
--bazel-config "${BAZEL_CONFIG}"
)
if [[ "${SANDBOX}" == "true" ]]; then
stage_args+=(--sandbox)
else
stage_args+=(--bazel-config v8-release-compat)
fi
python3 .github/scripts/rusty_v8_bazel.py stage-release-pair "${stage_args[@]}"
- name: Smoke test staged artifact with Cargo
env:
SANDBOX: ${{ matrix.sandbox }}
TARGET: ${{ matrix.target }}
shell: bash
run: |
set -euo pipefail
host_arch="$(uname -m)"
case "${TARGET}:${host_arch}" in
x86_64-apple-darwin:x86_64|aarch64-apple-darwin:arm64|x86_64-unknown-linux-gnu:x86_64|aarch64-unknown-linux-gnu:aarch64)
;;
*)
echo "Skipping non-native Cargo smoke for ${TARGET} on ${host_arch}."
exit 0
;;
esac
archive="$(find "dist/${TARGET}" -maxdepth 1 -type f -name 'librusty_v8_*.a.gz' -print -quit)"
binding="$(find "dist/${TARGET}" -maxdepth 1 -type f -name 'src_binding_*.rs' -print -quit)"
if [[ -z "${archive}" || -z "${binding}" ]]; then
echo "Missing staged archive or binding for ${TARGET}." >&2
exit 1
fi
cargo_args=(test -p codex-v8-poc)
if [[ "${SANDBOX}" == "true" ]]; then
cargo_args+=(--features sandbox)
fi
(
cd codex-rs
CARGO_TARGET_DIR="${RUNNER_TEMP}/rusty-v8-cargo-smoke-${TARGET}-${SANDBOX}" \
RUSTY_V8_ARCHIVE="${GITHUB_WORKSPACE}/${archive}" \
RUSTY_V8_SRC_BINDING_PATH="${GITHUB_WORKSPACE}/${binding}" \
cargo "${cargo_args[@]}"
)
- name: Upload staged artifacts
- name: Upload staged musl artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: v8-canary-${{ needs.metadata.outputs.v8_version }}-${{ matrix.variant }}-${{ matrix.target }}
path: dist/${{ matrix.target }}/*
build-windows-source:
name: Build ptrcomp-sandbox ${{ matrix.target }} from source
needs: metadata
runs-on: ${{ matrix.runner }}
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- runner: windows-2022
target: x86_64-pc-windows-msvc
- runner: windows-2022
target: aarch64-pc-windows-msvc
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Configure git for upstream checkout
shell: bash
run: git config --global core.symlinks true
- name: Check out upstream rusty_v8
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: denoland/rusty_v8
ref: v${{ needs.metadata.outputs.v8_version }}
path: upstream-rusty-v8
submodules: recursive
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.11"
architecture: x64
- name: Set up Codex Rust toolchain for Cargo smoke
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
toolchain: "1.95.0"
targets: ${{ matrix.target }}
- name: Install rusty_v8 Rust toolchain
env:
TARGET: ${{ matrix.target }}
shell: bash
run: |
set -euo pipefail
rustup toolchain install 1.91.0 --profile minimal --no-self-update
rustup target add --toolchain 1.91.0 "${TARGET}"
- name: Write upstream submodule status
shell: bash
working-directory: upstream-rusty-v8
run: git submodule status --recursive > git_submodule_status.txt
- name: Restore upstream source-build cache
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') }}
restore-keys: |
rusty-v8-source-${{ matrix.target }}-sandbox-
- name: Install and start sccache
shell: pwsh
env:
SCCACHE_CACHE_SIZE: 256M
SCCACHE_DIR: ${{ github.workspace }}/upstream-rusty-v8/target/sccache
SCCACHE_IDLE_TIMEOUT: 0
run: |
$version = "v0.8.2"
$platform = "x86_64-pc-windows-msvc"
$basename = "sccache-$version-$platform"
$url = "https://github.com/mozilla/sccache/releases/download/$version/$basename.tar.gz"
cd ~
curl -LO $url
tar -xzvf "$basename.tar.gz"
. $basename/sccache --start-server
echo "$(pwd)/$basename" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Install Chromium clang for ARM64 MSVC cross build
if: matrix.target == 'aarch64-pc-windows-msvc'
shell: bash
working-directory: upstream-rusty-v8
run: python3 tools/clang/scripts/update.py
- name: Build upstream rusty_v8 sandbox release pair
env:
SCCACHE_IDLE_TIMEOUT: 0
TARGET: ${{ matrix.target }}
V8_FROM_SOURCE: "1"
shell: bash
working-directory: upstream-rusty-v8
run: cargo +1.91.0 build --locked --release --target "${TARGET}" --features v8_enable_sandbox
- name: Stage upstream sandbox release pair
env:
TARGET: ${{ matrix.target }}
shell: bash
run: |
set -euo pipefail
python3 .github/scripts/rusty_v8_bazel.py stage-upstream-release-pair \
--source-root upstream-rusty-v8 \
--target "${TARGET}" \
--output-dir "dist/${TARGET}" \
--sandbox
- name: Smoke link staged artifact with Cargo
env:
TARGET: ${{ matrix.target }}
shell: bash
run: |
set -euo pipefail
archive="$(find "dist/${TARGET}" -maxdepth 1 -type f -name 'rusty_v8_*.lib.gz' -print -quit)"
binding="$(find "dist/${TARGET}" -maxdepth 1 -type f -name 'src_binding_*.rs' -print -quit)"
if [[ -z "${archive}" || -z "${binding}" ]]; then
echo "Missing staged archive or binding for ${TARGET}." >&2
exit 1
fi
(
cd codex-rs
RUSTY_V8_ARCHIVE="${GITHUB_WORKSPACE}/${archive}" \
RUSTY_V8_SRC_BINDING_PATH="${GITHUB_WORKSPACE}/${binding}" \
cargo +1.95.0 test -p codex-v8-poc --target "${TARGET}" --features sandbox --no-run
)
- name: Upload staged artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: v8-canary-${{ needs.metadata.outputs.v8_version }}-ptrcomp-sandbox-${{ matrix.target }}
name: v8-canary-${{ needs.metadata.outputs.v8_version }}-${{ matrix.target }}
path: dist/${{ matrix.target }}/*

View File

@@ -1,6 +1,5 @@
{
"recommendations": [
"BazelBuild.vscode-bazel",
"rust-lang.rust-analyzer",
"charliermarsh.ruff",
"tamasfe.even-better-toml",

View File

@@ -30,7 +30,6 @@ In the codex-rs folder where the rust code lives:
- Prefer private modules and explicitly exported public crate API.
- If you change `ConfigToml` or nested config types, run `just write-config-schema` to update `codex-rs/core/config.schema.json`.
- When working with MCP tool calls, prefer using `codex-rs/codex-mcp/src/mcp_connection_manager.rs` to handle mutation of tools and tool calls. Aim to minimize the footprint of changes and leverage existing abstractions rather than plumbing code through multiple levels of function calls.
- Do not call `reset_client_session` unnecessarily; let the incremental check logic decide whether to reuse the previous request.
- If you change Rust dependencies (`Cargo.toml` or `Cargo.lock`), run `just bazel-lock-update` from the
repo root to refresh `MODULE.bazel.lock`, and include that lockfile update in the same change.
- After dependency changes, run `just bazel-lock-check` from the repo root so lockfile drift is caught
@@ -53,13 +52,12 @@ In the codex-rs folder where the rust code lives:
the new implementation so the invariants stay close to the code that owns them.
- Avoid adding new standalone methods to `codex-rs/tui/src/chatwidget.rs` unless the change is
trivial; prefer new modules/files and keep `chatwidget.rs` focused on orchestration.
- When running Rust commands (e.g. `just fix` or `just test`) be patient with the command and never try to kill them using the PID. Rust lock can make the execution slow, this is expected.
- When running Rust commands (e.g. `just fix` or `cargo test`) be patient with the command and never try to kill them using the PID. Rust lock can make the execution slow, this is expected.
Run `just fmt` (in the `codex-rs` directory) automatically after you have finished making code changes anywhere in this repository; do not ask for approval to run it. Additionally, run the tests:
Run `just fmt` (in `codex-rs` directory) automatically after you have finished making Rust code changes; do not ask for approval to run it. Additionally, run the tests:
1. Do not run `cargo test` directly. Use `just test` so test execution follows the repo defaults.
2. Run the test for the specific project that was changed. For example, if changes were made in `codex-rs/tui`, run `just test -p codex-tui`.
3. Once those pass, if any changes were made in common, core, or protocol, run the complete test suite with `just test`. Avoid `--all-features` for routine local runs because it expands the build matrix and can significantly increase `target/` disk usage; use it only when you specifically need full feature coverage. project-specific or individual tests can be run without asking the user, but do ask the user before running the complete test suite.
1. Run the test for the specific project that was changed. For example, if changes were made in `codex-rs/tui`, run `cargo test -p codex-tui`.
2. Once those pass, if any changes were made in common, core, or protocol, run the complete test suite with `cargo test` (or `just test` if `cargo-nextest` is installed). Avoid `--all-features` for routine local runs because it expands the build matrix and can significantly increase `target/` disk usage; use it only when you specifically need full feature coverage. project-specific or individual tests can be run without asking the user, but do ask the user before running the complete test suite.
Before finalizing a large change to `codex-rs`, run `just fix -p <project>` (in `codex-rs` directory) to fix any linter issues in the code. Prefer scoping with `-p` to avoid slow workspacewide Clippy builds; only run `just fix` without `-p` if you changed shared crates. Do not re-run tests after running `fix` or `fmt`.
@@ -76,49 +74,6 @@ Particularly when introducing a new concept/feature/API, before adding to `codex
Likewise, when reviewing code, do not hesitate to push back on PRs that would unnecessarily add code to `codex-core`.
## Code Review Rules
### Model visible context
Codex maintains a context (history of messages) that is sent to the model in inference requests.
1. No history rewrite - the context must be built up incrementally.
2. Avoid frequent changes to context that cause cache misses.
3. No unbounded items - everything injected in the model context must have a bounded size and a hard cap.
4. No items larger than 10K tokens.
5. Highlight new individual items that can cross >1k tokens as P0. These need an additional manual review.
6. All injected fragments must be defined as structs in `core/context` and implement ContextualUserFragment trait
### Breaking changes
Search for breaking changes in external integration surfaces:
- app-server APIs
- CLI parameters
- configuration loading
- resuming sessions from existing rollouts
### Test authoring guidance
For agent changes prefer integration tests over unit tests. Integration tests are under `core/suite` and use `test_codex` to set up a test instance of codex.
Features that change the agent logic MUST add an integration test:
- Provide a list of major logic changes and user-facing behaviors that need to be tested.
If unit tests are needed, put them in a dedicated test file (\*\_tests.rs).
Avoid test-only functions in the main implementation.
Check whether there are existing helpers to make tests more streamlined and readable.
### Change size guidance (800 lines)
Unless the change is mechanical the total number of changed lines should not exceed 800 lines.
For complex logic changes the size should be under 500 lines.
If the change is larger, explore whether it can be split into reviewable stages and identify the smallest coherent stage to land first.
Base the staging suggestion on the actual diff, dependencies, and affected call sites.
## TUI style conventions
See `codex-rs/tui/styles.md`.
@@ -153,19 +108,6 @@ See `codex-rs/tui/styles.md`.
## Tests
### Test module organization
- When adding a new test module, define its contents in a separate sibling file rather than inline in the implementation file.
- Use an explicit `#[path = "..._tests.rs"]` attribute so the test filename is descriptive and easy to locate:
```rust
#[cfg(test)]
#[path = "parser_tests.rs"]
mod tests;
```
- This applies only when introducing a new test module. Do not move or rewrite existing inline `#[cfg(test)] mod tests { ... }` modules solely to follow this convention.
### Snapshot tests
This repo uses snapshot tests (via `insta`), especially in `codex-rs/tui`, to validate rendered output.
@@ -178,7 +120,7 @@ is easy to review and future diffs stay visual.
When UI or text output changes intentionally, update the snapshots as follows:
- Run tests to generate any updated snapshots:
- `just test -p codex-tui`
- `cargo test -p codex-tui`
- Check whats pending:
- `cargo insta pending-snapshots -p codex-tui`
- Review changes by reading the generated `*.snap.new` files directly in the repo, or preview a specific file:
@@ -272,15 +214,6 @@ These guidelines apply to app-server protocol work in `codex-rs`, especially:
- Regenerate schema fixtures when API shapes change:
`just write-app-server-schema`
(and `just write-app-server-schema --experimental` when experimental API fixtures are affected).
- Validate with `just test -p codex-app-server-protocol`.
- Validate with `cargo test -p codex-app-server-protocol`.
- Avoid boilerplate tests that only assert experimental field markers for individual
request fields in `common.rs`; rely on schema generation/tests and behavioral coverage instead.
## Python Development Best Practices
### Ignore Python 2 compatibility
This project uses Python 3+. You should not use the `__future__` module.
If you need to worry about feature compatibility between different 3.xx point releases, check the
closest `pyproject.toml`'s `requires-python` field to see what minimum runtime version is supported.

View File

@@ -10,7 +10,6 @@ single_version_override(
module_name = "llvm",
patch_strip = 1,
patches = [
"//patches:llvm_rusty_v8_custom_libcxx.patch",
"//patches:llvm_windows_symlink_extract.patch",
],
)
@@ -78,13 +77,6 @@ use_repo(osx, "macos_sdk")
# Needed to disable xcode...
bazel_dep(name = "apple_support", version = "2.1.0")
bazel_dep(name = "rules_cc", version = "0.2.16")
single_version_override(
module_name = "rules_cc",
patch_strip = 1,
patches = [
"//patches:rules_cc_rusty_v8_custom_libcxx.patch",
],
)
bazel_dep(name = "rules_platform", version = "0.1.0")
bazel_dep(name = "rules_rs", version = "0.0.58")
# `rules_rs` still does not model `windows-gnullvm` as a distinct Windows exec
@@ -163,7 +155,7 @@ use_repo(nightly_rust, "rust_toolchains")
toolchains = use_extension("@rules_rs//rs/experimental/toolchains:module_extension.bzl", "toolchains")
toolchains.toolchain(
edition = "2024",
version = "1.95.0",
version = "1.93.0",
)
use_repo(toolchains, "default_rust_toolchains")
@@ -415,18 +407,18 @@ crate.annotation(
inject_repo(crate, "alsa_lib")
bazel_dep(name = "v8", version = "14.7.173.20")
bazel_dep(name = "v8", version = "14.6.202.9")
archive_override(
module_name = "v8",
integrity = "sha256-v/x6I4X38a2wckzUIft3Dh0SUdkuOTokwxyF7lzW8Lc=",
integrity = "sha256-JphDwLAzsd9KvgRZ7eQvNtPU6qGd3XjFt/a/1QITAJU=",
patch_strip = 3,
patches = [
"//patches:v8_module_deps.patch",
"//patches:v8_bazel_rules.patch",
"//patches:v8_source_portability.patch",
],
strip_prefix = "v8-14.7.173.20",
urls = ["https://github.com/v8/v8/archive/refs/tags/14.7.173.20.tar.gz"],
strip_prefix = "v8-14.6.202.9",
urls = ["https://github.com/v8/v8/archive/refs/tags/14.6.202.9.tar.gz"],
)
http_archive(
@@ -438,53 +430,93 @@ http_archive(
urls = ["https://static.crates.io/crates/v8/v8-146.4.0.crate"],
)
http_archive(
name = "v8_crate_147_4_0",
build_file = "//third_party/v8:v8_crate.BUILD.bazel",
sha256 = "2df8fffd507fb18ed000673a83d937f58e60fb07f3306b2274284125b15137cd",
strip_prefix = "v8-147.4.0",
type = "tar.gz",
urls = ["https://static.crates.io/crates/v8/v8-147.4.0.crate"],
)
git_repository = use_repo_rule("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
git_repository(
name = "rusty_v8_libcxx",
build_file = "//third_party/v8:libcxx.BUILD.bazel",
commit = "7ab65651aed6802d2599dcb7a73b1f82d5179d05",
remote = "https://chromium.googlesource.com/external/github.com/llvm/llvm-project/libcxx.git",
)
git_repository(
name = "rusty_v8_libcxxabi",
build_file = "//third_party/v8:libcxxabi.BUILD.bazel",
commit = "8f11bb1d4438d0239d0dfc1bd9456a9f31629dda",
remote = "https://chromium.googlesource.com/external/github.com/llvm/llvm-project/libcxxabi.git",
)
git_repository(
name = "rusty_v8_llvm_libc",
build_file = "//third_party/v8:llvm_libc.BUILD.bazel",
commit = "b3aa5bb702ff9e890179fd1e7d3ba346e17ecf8e",
remote = "https://chromium.googlesource.com/external/github.com/llvm/llvm-project/libc.git",
)
http_file(
name = "rusty_v8_147_4_0_aarch64_pc_windows_msvc_archive",
downloaded_file_path = "rusty_v8_release_aarch64-pc-windows-msvc.lib.gz",
sha256 = "1fa3f94d9e09cff1f6bcce94c478e5cb072c0755f6a0357abadb9dd3b48d8127",
name = "rusty_v8_146_4_0_aarch64_apple_darwin_archive",
downloaded_file_path = "librusty_v8_release_aarch64-apple-darwin.a.gz",
sha256 = "bfe2c9be32a56c28546f0f965825ee68fbf606405f310cc4e17b448a568cf98a",
urls = [
"https://github.com/denoland/rusty_v8/releases/download/v147.4.0/rusty_v8_release_aarch64-pc-windows-msvc.lib.gz",
"https://github.com/denoland/rusty_v8/releases/download/v146.4.0/librusty_v8_release_aarch64-apple-darwin.a.gz",
],
)
http_file(
name = "rusty_v8_147_4_0_x86_64_pc_windows_msvc_archive",
downloaded_file_path = "rusty_v8_release_x86_64-pc-windows-msvc.lib.gz",
sha256 = "e2827ff98b1a9d4c0343000fc5124ac30dfab3007bc0129c168c9355fc2fcd7c",
name = "rusty_v8_146_4_0_aarch64_unknown_linux_gnu_archive",
downloaded_file_path = "librusty_v8_release_aarch64-unknown-linux-gnu.a.gz",
sha256 = "dbf165b07c81bdb054bc046b43d23e69fcf7bcc1a4c1b5b4776983a71062ecd8",
urls = [
"https://github.com/denoland/rusty_v8/releases/download/v147.4.0/rusty_v8_release_x86_64-pc-windows-msvc.lib.gz",
"https://github.com/denoland/rusty_v8/releases/download/v146.4.0/librusty_v8_release_aarch64-unknown-linux-gnu.a.gz",
],
)
http_file(
name = "rusty_v8_146_4_0_aarch64_pc_windows_msvc_archive",
downloaded_file_path = "rusty_v8_release_aarch64-pc-windows-msvc.lib.gz",
sha256 = "ed13363659c6d08583ac8fdc40493445c5767d8b94955a4d5d7bb8d5a81f6bf8",
urls = [
"https://github.com/denoland/rusty_v8/releases/download/v146.4.0/rusty_v8_release_aarch64-pc-windows-msvc.lib.gz",
],
)
http_file(
name = "rusty_v8_146_4_0_x86_64_apple_darwin_archive",
downloaded_file_path = "librusty_v8_release_x86_64-apple-darwin.a.gz",
sha256 = "630cd240f1bbecdb071417dc18387ab81cf67c549c1c515a0b4fcf9eba647bb7",
urls = [
"https://github.com/denoland/rusty_v8/releases/download/v146.4.0/librusty_v8_release_x86_64-apple-darwin.a.gz",
],
)
http_file(
name = "rusty_v8_146_4_0_x86_64_unknown_linux_gnu_archive",
downloaded_file_path = "librusty_v8_release_x86_64-unknown-linux-gnu.a.gz",
sha256 = "e64b4d99e4ae293a2e846244a89b80178ba10382c13fb591c1fa6968f5291153",
urls = [
"https://github.com/denoland/rusty_v8/releases/download/v146.4.0/librusty_v8_release_x86_64-unknown-linux-gnu.a.gz",
],
)
http_file(
name = "rusty_v8_146_4_0_x86_64_pc_windows_msvc_archive",
downloaded_file_path = "rusty_v8_release_x86_64-pc-windows-msvc.lib.gz",
sha256 = "90a9a2346acd3685a355e98df85c24dbe406cb124367d16259a4b5d522621862",
urls = [
"https://github.com/denoland/rusty_v8/releases/download/v146.4.0/rusty_v8_release_x86_64-pc-windows-msvc.lib.gz",
],
)
http_file(
name = "rusty_v8_146_4_0_aarch64_unknown_linux_musl_archive",
downloaded_file_path = "librusty_v8_release_aarch64-unknown-linux-musl.a.gz",
sha256 = "27a08ed26c34297bfd93e514692ccc44b85f8b15c6aa39cf34e784f84fb37e8e",
urls = [
"https://github.com/openai/codex/releases/download/rusty-v8-v146.4.0/librusty_v8_release_aarch64-unknown-linux-musl.a.gz",
],
)
http_file(
name = "rusty_v8_146_4_0_aarch64_unknown_linux_musl_binding",
downloaded_file_path = "src_binding_release_aarch64-unknown-linux-musl.rs",
sha256 = "09f8900ced8297c229246c7a50b2e0ec23c54d0a554f369619cc29863f38dd1a",
urls = [
"https://github.com/openai/codex/releases/download/rusty-v8-v146.4.0/src_binding_release_aarch64-unknown-linux-musl.rs",
],
)
http_file(
name = "rusty_v8_146_4_0_x86_64_unknown_linux_musl_archive",
downloaded_file_path = "librusty_v8_release_x86_64-unknown-linux-musl.a.gz",
sha256 = "20d8271ad712323d352c1383c36e3c4b755abc41ece35819c49c75ec7134d2f8",
urls = [
"https://github.com/openai/codex/releases/download/rusty-v8-v146.4.0/librusty_v8_release_x86_64-unknown-linux-musl.a.gz",
],
)
http_file(
name = "rusty_v8_146_4_0_x86_64_unknown_linux_musl_binding",
downloaded_file_path = "src_binding_release_x86_64-unknown-linux-musl.rs",
sha256 = "09f8900ced8297c229246c7a50b2e0ec23c54d0a554f369619cc29863f38dd1a",
urls = [
"https://github.com/openai/codex/releases/download/rusty-v8-v146.4.0/src_binding_release_x86_64-unknown-linux-musl.rs",
],
)

334
MODULE.bazel.lock generated

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -77,43 +77,33 @@ if (!platformPackage) {
const codexBinaryName = process.platform === "win32" ? "codex.exe" : "codex";
const localVendorRoot = path.join(__dirname, "..", "vendor");
const packageBinaryPath = (vendorRoot) =>
path.join(vendorRoot, targetTriple, "bin", codexBinaryName);
const legacyBinaryPath = (vendorRoot) =>
path.join(vendorRoot, targetTriple, "codex", codexBinaryName);
const localBinaryPath = path.join(
localVendorRoot,
targetTriple,
"codex",
codexBinaryName,
);
function resolveNativePackage(vendorRoot) {
const packageRoot = path.join(vendorRoot, targetTriple);
const binaryPath = packageBinaryPath(vendorRoot);
if (existsSync(binaryPath)) {
return {
binaryPath,
pathDir: path.join(packageRoot, "codex-path"),
};
}
const legacyPath = legacyBinaryPath(vendorRoot);
if (existsSync(legacyPath)) {
return {
binaryPath: legacyPath,
pathDir: path.join(packageRoot, "path"),
};
}
return null;
}
let nativePackage;
let vendorRoot;
try {
const packageJsonPath = require.resolve(`${platformPackage}/package.json`);
nativePackage = resolveNativePackage(
path.join(path.dirname(packageJsonPath), "vendor"),
);
vendorRoot = path.join(path.dirname(packageJsonPath), "vendor");
} catch {
nativePackage = resolveNativePackage(localVendorRoot);
if (existsSync(localBinaryPath)) {
vendorRoot = localVendorRoot;
} else {
const packageManager = detectPackageManager();
const updateCommand =
packageManager === "bun"
? "bun install -g @openai/codex@latest"
: "npm install -g @openai/codex@latest";
throw new Error(
`Missing optional dependency ${platformPackage}. Reinstall Codex: ${updateCommand}`,
);
}
}
if (!nativePackage) {
if (!vendorRoot) {
const packageManager = detectPackageManager();
const updateCommand =
packageManager === "bun"
@@ -124,7 +114,8 @@ if (!nativePackage) {
);
}
const { binaryPath, pathDir } = nativePackage;
const archRoot = path.join(vendorRoot, targetTriple);
const binaryPath = path.join(archRoot, "codex", codexBinaryName);
// Use an asynchronous spawn instead of spawnSync so that Node is able to
// respond to signals (e.g. Ctrl-C / SIGINT) while the native binary is
@@ -168,6 +159,7 @@ function detectPackageManager() {
}
const additionalDirs = [];
const pathDir = path.join(archRoot, "path");
if (existsSync(pathDir)) {
additionalDirs.push(pathDir);
}

View File

@@ -1,7 +1,6 @@
{
"name": "@openai/codex",
"version": "0.0.0-dev",
"description": "Codex CLI is a coding agent from OpenAI that runs locally on your computer.",
"license": "Apache-2.0",
"bin": {
"codex": "bin/codex.js"
@@ -11,7 +10,8 @@
"node": ">=16"
},
"files": [
"bin/codex.js"
"bin",
"vendor"
],
"repository": {
"type": "git",

View File

@@ -11,13 +11,13 @@ example, to stage the CLI, responses proxy, and SDK packages for version `0.6.0`
--package codex-sdk
```
This downloads the required native package archive artifacts, hydrates `vendor/` for
each package, and writes tarballs to `dist/npm/`.
This downloads the native artifacts once, hydrates `vendor/` for each package, and writes
tarballs to `dist/npm/`.
When `--package codex` is provided, the staging helper builds the lightweight
`@openai/codex` meta package plus all platform-native `@openai/codex` variants
that are later published under platform-specific dist-tags.
Direct `build_npm_package.py` invocations are still useful for package-specific
debugging, but native packages expect `--vendor-src` to point at a prehydrated
`vendor/` tree. Release packaging should use `scripts/stage_npm_packages.py`.
If you need to invoke `build_npm_package.py` directly, run
`codex-cli/scripts/install_native_deps.py` first and pass `--vendor-src` pointing to the
directory that contains the populated `vendor/` tree.

View File

@@ -3,7 +3,6 @@
import argparse
import json
import os
import shutil
import subprocess
import sys
@@ -16,7 +15,6 @@ REPO_ROOT = CODEX_CLI_ROOT.parent
RESPONSES_API_PROXY_NPM_ROOT = REPO_ROOT / "codex-rs" / "responses-api-proxy" / "npm"
CODEX_SDK_ROOT = REPO_ROOT / "sdk" / "typescript"
CODEX_NPM_NAME = "@openai/codex"
CODEX_PACKAGE_COMPONENT = "codex-package"
# `npm_name` is the local optional-dependency alias consumed by `bin/codex.js`.
# The underlying package published to npm is always `@openai/codex`.
@@ -71,12 +69,12 @@ PACKAGE_EXPANSIONS: dict[str, list[str]] = {
PACKAGE_NATIVE_COMPONENTS: dict[str, list[str]] = {
"codex": [],
"codex-linux-x64": [CODEX_PACKAGE_COMPONENT],
"codex-linux-arm64": [CODEX_PACKAGE_COMPONENT],
"codex-darwin-x64": [CODEX_PACKAGE_COMPONENT],
"codex-darwin-arm64": [CODEX_PACKAGE_COMPONENT],
"codex-win32-x64": [CODEX_PACKAGE_COMPONENT],
"codex-win32-arm64": [CODEX_PACKAGE_COMPONENT],
"codex-linux-x64": ["bwrap", "codex", "rg"],
"codex-linux-arm64": ["bwrap", "codex", "rg"],
"codex-darwin-x64": ["codex", "rg"],
"codex-darwin-arm64": ["codex", "rg"],
"codex-win32-x64": ["codex", "rg", "codex-windows-sandbox-setup", "codex-command-runner"],
"codex-win32-arm64": ["codex", "rg", "codex-windows-sandbox-setup", "codex-command-runner"],
"codex-responses-api-proxy": ["codex-responses-api-proxy"],
"codex-sdk": [],
}
@@ -88,6 +86,16 @@ PACKAGE_TARGET_FILTERS: dict[str, str] = {
PACKAGE_CHOICES = tuple(PACKAGE_NATIVE_COMPONENTS)
COMPONENT_DEST_DIR: dict[str, str] = {
"bwrap": "codex-resources",
"codex": "codex",
"codex-responses-api-proxy": "codex-responses-api-proxy",
"codex-windows-sandbox-setup": "codex",
"codex-command-runner": "codex",
"rg": "path",
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Build or stage the Codex CLI npm package.")
parser.add_argument(
@@ -130,6 +138,16 @@ def parse_args() -> argparse.Namespace:
type=Path,
help="Directory containing pre-installed native binaries to bundle (vendor root).",
)
parser.add_argument(
"--allow-missing-native-component",
dest="allow_missing_native_components",
action="append",
default=[],
help=(
"Native component that may be absent from --vendor-src. Intended for CI "
"compatibility with older artifact workflows; releases should not use this."
),
)
return parser.parse_args()
@@ -170,6 +188,7 @@ def main() -> int:
staging_dir,
native_components,
target_filter={target_filter} if target_filter else None,
allow_missing_components=set(args.allow_missing_native_components),
)
if release_version:
@@ -234,6 +253,9 @@ def stage_sources(staging_dir: Path, version: str, package: str) -> None:
bin_dir = staging_dir / "bin"
bin_dir.mkdir(parents=True, exist_ok=True)
shutil.copy2(CODEX_CLI_ROOT / "bin" / "codex.js", bin_dir / "codex.js")
rg_manifest = CODEX_CLI_ROOT / "bin" / "rg"
if rg_manifest.exists():
shutil.copy2(rg_manifest, bin_dir / "rg")
readme_src = REPO_ROOT / "README.md"
if readme_src.exists():
@@ -292,7 +314,7 @@ def stage_sources(staging_dir: Path, version: str, package: str) -> None:
package_json["version"] = version
if package == "codex":
package_json["files"] = ["bin/codex.js"]
package_json["files"] = ["bin"]
package_json["optionalDependencies"] = {
CODEX_PLATFORM_PACKAGES[platform_package]["npm_name"]: (
f"npm:{CODEX_NPM_NAME}@"
@@ -325,7 +347,7 @@ def compute_platform_package_version(version: str, platform_tag: str) -> str:
def run_command(cmd: list[str], cwd: Path | None = None) -> None:
print("+", " ".join(cmd), flush=True)
print("+", " ".join(cmd))
subprocess.run(cmd, cwd=cwd, check=True)
@@ -355,12 +377,14 @@ def copy_native_binaries(
staging_dir: Path,
components: list[str],
target_filter: set[str] | None = None,
allow_missing_components: set[str] | None = None,
) -> None:
vendor_src = vendor_src.resolve()
if not vendor_src.exists():
raise RuntimeError(f"Vendor source directory not found: {vendor_src}")
components_set = set(components)
components_set = {component for component in components if component in COMPONENT_DEST_DIR}
allow_missing_components = allow_missing_components or set()
if not components_set:
return
@@ -378,25 +402,24 @@ def copy_native_binaries(
if target_filter is not None and target_dir.name not in target_filter:
continue
dest_target_dir = vendor_dest / target_dir.name
dest_target_dir.mkdir(parents=True, exist_ok=True)
copied_targets.add(target_dir.name)
dest_target_dir = vendor_dest / target_dir.name
for component in components_set:
dest_dir_name = COMPONENT_DEST_DIR.get(component)
if dest_dir_name is None:
continue
if CODEX_PACKAGE_COMPONENT in components_set:
if dest_target_dir.exists():
shutil.rmtree(dest_target_dir)
shutil.copytree(target_dir, dest_target_dir)
else:
dest_target_dir.mkdir(parents=True, exist_ok=True)
for component in sorted(components_set - {CODEX_PACKAGE_COMPONENT}):
src_component_dir = target_dir / component
src_component_dir = target_dir / dest_dir_name
if not src_component_dir.exists():
if component in allow_missing_components:
continue
raise RuntimeError(
f"Missing native component '{component}' in vendor source: {src_component_dir}"
)
dest_component_dir = dest_target_dir / component
dest_component_dir = dest_target_dir / dest_dir_name
if dest_component_dir.exists():
shutil.rmtree(dest_component_dir)
shutil.copytree(src_component_dir, dest_component_dir)
@@ -407,23 +430,16 @@ def copy_native_binaries(
missing_list = ", ".join(missing_targets)
raise RuntimeError(f"Missing target directories in vendor source: {missing_list}")
def run_npm_pack(staging_dir: Path, output_path: Path) -> Path:
output_path = output_path.resolve()
output_path.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix="codex-npm-pack-") as pack_dir_str:
pack_dir = Path(pack_dir_str)
npm_cache_dir = pack_dir / "npm-cache"
npm_logs_dir = pack_dir / "npm-logs"
npm_cache_dir.mkdir()
npm_logs_dir.mkdir()
env = os.environ.copy()
env["NPM_CONFIG_CACHE"] = str(npm_cache_dir)
env["NPM_CONFIG_LOGS_DIR"] = str(npm_logs_dir)
stdout = subprocess.check_output(
["npm", "pack", "--json", "--pack-destination", str(pack_dir)],
cwd=staging_dir,
env=env,
text=True,
)
try:

View File

@@ -0,0 +1,483 @@
#!/usr/bin/env python3
"""Install Codex native binaries (Rust CLI, bwrap, and ripgrep helpers)."""
import argparse
from contextlib import contextmanager
import json
import os
import shutil
import subprocess
import tarfile
import tempfile
import zipfile
from dataclasses import dataclass
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
import sys
from typing import Iterable, Sequence
from urllib.parse import urlparse
from urllib.request import urlopen
SCRIPT_DIR = Path(__file__).resolve().parent
CODEX_CLI_ROOT = SCRIPT_DIR.parent
DEFAULT_WORKFLOW_URL = "https://github.com/openai/codex/actions/runs/17952349351" # rust-v0.40.0
VENDOR_DIR_NAME = "vendor"
RG_MANIFEST = CODEX_CLI_ROOT / "bin" / "rg"
BINARY_TARGETS = (
"x86_64-unknown-linux-musl",
"aarch64-unknown-linux-musl",
"x86_64-apple-darwin",
"aarch64-apple-darwin",
"x86_64-pc-windows-msvc",
"aarch64-pc-windows-msvc",
)
@dataclass(frozen=True)
class BinaryComponent:
artifact_prefix: str # matches the artifact filename prefix (e.g. codex-<target>.zst)
dest_dir: str # directory under vendor/<target>/ where the binary is installed
binary_basename: str # executable name inside dest_dir (before optional .exe)
targets: tuple[str, ...] | None = None # limit installation to specific targets
WINDOWS_TARGETS = tuple(target for target in BINARY_TARGETS if "windows" in target)
LINUX_TARGETS = tuple(target for target in BINARY_TARGETS if "linux" in target)
BINARY_COMPONENTS = {
"bwrap": BinaryComponent(
artifact_prefix="bwrap",
dest_dir="codex-resources",
binary_basename="bwrap",
targets=LINUX_TARGETS,
),
"codex": BinaryComponent(
artifact_prefix="codex",
dest_dir="codex",
binary_basename="codex",
),
"codex-responses-api-proxy": BinaryComponent(
artifact_prefix="codex-responses-api-proxy",
dest_dir="codex-responses-api-proxy",
binary_basename="codex-responses-api-proxy",
),
"codex-windows-sandbox-setup": BinaryComponent(
artifact_prefix="codex-windows-sandbox-setup",
dest_dir="codex",
binary_basename="codex-windows-sandbox-setup",
targets=WINDOWS_TARGETS,
),
"codex-command-runner": BinaryComponent(
artifact_prefix="codex-command-runner",
dest_dir="codex",
binary_basename="codex-command-runner",
targets=WINDOWS_TARGETS,
),
}
RG_TARGET_PLATFORM_PAIRS: list[tuple[str, str]] = [
("x86_64-unknown-linux-musl", "linux-x86_64"),
("aarch64-unknown-linux-musl", "linux-aarch64"),
("x86_64-apple-darwin", "macos-x86_64"),
("aarch64-apple-darwin", "macos-aarch64"),
("x86_64-pc-windows-msvc", "windows-x86_64"),
("aarch64-pc-windows-msvc", "windows-aarch64"),
]
RG_TARGET_TO_PLATFORM = {target: platform for target, platform in RG_TARGET_PLATFORM_PAIRS}
DEFAULT_RG_TARGETS = [target for target, _ in RG_TARGET_PLATFORM_PAIRS]
# urllib.request.urlopen() defaults to no timeout (can hang indefinitely), which is painful in CI.
DOWNLOAD_TIMEOUT_SECS = 60
def _gha_enabled() -> bool:
# GitHub Actions supports "workflow commands" (e.g. ::group:: / ::error::) that make logs
# much easier to scan: groups collapse noisy sections and error annotations surface the
# failure in the UI without changing the actual exception/traceback output.
return os.environ.get("GITHUB_ACTIONS") == "true"
def _gha_escape(value: str) -> str:
# Workflow commands require percent/newline escaping.
return value.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")
def _gha_error(*, title: str, message: str) -> None:
# Emit a GitHub Actions error annotation. This does not replace stdout/stderr logs; it just
# adds a prominent summary line to the job UI so the root cause is easier to spot.
if not _gha_enabled():
return
print(
f"::error title={_gha_escape(title)}::{_gha_escape(message)}",
flush=True,
)
@contextmanager
def _gha_group(title: str):
# Wrap a block in a collapsible log group on GitHub Actions. Outside of GHA this is a no-op
# so local output remains unchanged.
if _gha_enabled():
print(f"::group::{_gha_escape(title)}", flush=True)
try:
yield
finally:
if _gha_enabled():
print("::endgroup::", flush=True)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Install native Codex binaries.")
parser.add_argument(
"--workflow-url",
help=(
"GitHub Actions workflow URL that produced the artifacts. Defaults to a "
"known good run when omitted."
),
)
parser.add_argument(
"--component",
dest="components",
action="append",
choices=tuple(list(BINARY_COMPONENTS) + ["rg"]),
help=(
"Limit installation to the specified components."
" May be repeated. Defaults to bwrap, codex, codex-windows-sandbox-setup,"
" codex-command-runner, and rg."
),
)
parser.add_argument(
"root",
nargs="?",
type=Path,
help=(
"Directory containing package.json for the staged package. If omitted, the "
"repository checkout is used."
),
)
return parser.parse_args()
def main() -> int:
args = parse_args()
codex_cli_root = (args.root or CODEX_CLI_ROOT).resolve()
vendor_dir = codex_cli_root / VENDOR_DIR_NAME
vendor_dir.mkdir(parents=True, exist_ok=True)
components = args.components or [
"bwrap",
"codex",
"codex-windows-sandbox-setup",
"codex-command-runner",
"rg",
]
workflow_url = (args.workflow_url or DEFAULT_WORKFLOW_URL).strip()
if not workflow_url:
workflow_url = DEFAULT_WORKFLOW_URL
workflow_id = workflow_url.rstrip("/").split("/")[-1]
print(f"Downloading native artifacts from workflow {workflow_id}...")
with _gha_group(f"Download native artifacts from workflow {workflow_id}"):
with tempfile.TemporaryDirectory(prefix="codex-native-artifacts-") as artifacts_dir_str:
artifacts_dir = Path(artifacts_dir_str)
_download_artifacts(workflow_id, artifacts_dir)
install_binary_components(
artifacts_dir,
vendor_dir,
[BINARY_COMPONENTS[name] for name in components if name in BINARY_COMPONENTS],
)
if "rg" in components:
with _gha_group("Fetch ripgrep binaries"):
print("Fetching ripgrep binaries...")
fetch_rg(vendor_dir, DEFAULT_RG_TARGETS, manifest_path=RG_MANIFEST)
print(f"Installed native dependencies into {vendor_dir}")
return 0
def fetch_rg(
vendor_dir: Path,
targets: Sequence[str] | None = None,
*,
manifest_path: Path,
) -> list[Path]:
"""Download ripgrep binaries described by the DotSlash manifest."""
if targets is None:
targets = DEFAULT_RG_TARGETS
if not manifest_path.exists():
raise FileNotFoundError(f"DotSlash manifest not found: {manifest_path}")
manifest = _load_manifest(manifest_path)
platforms = manifest.get("platforms", {})
vendor_dir.mkdir(parents=True, exist_ok=True)
targets = list(targets)
if not targets:
return []
task_configs: list[tuple[str, str, dict]] = []
for target in targets:
platform_key = RG_TARGET_TO_PLATFORM.get(target)
if platform_key is None:
raise ValueError(f"Unsupported ripgrep target '{target}'.")
platform_info = platforms.get(platform_key)
if platform_info is None:
raise RuntimeError(f"Platform '{platform_key}' not found in manifest {manifest_path}.")
task_configs.append((target, platform_key, platform_info))
results: dict[str, Path] = {}
max_workers = min(len(task_configs), max(1, (os.cpu_count() or 1)))
print("Installing ripgrep binaries for targets: " + ", ".join(targets))
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_map = {
executor.submit(
_fetch_single_rg,
vendor_dir,
target,
platform_key,
platform_info,
manifest_path,
): target
for target, platform_key, platform_info in task_configs
}
for future in as_completed(future_map):
target = future_map[future]
try:
results[target] = future.result()
except Exception as exc:
_gha_error(
title="ripgrep install failed",
message=f"target={target} error={exc!r}",
)
raise RuntimeError(f"Failed to install ripgrep for target {target}.") from exc
print(f" installed ripgrep for {target}")
return [results[target] for target in targets]
def _download_artifacts(workflow_id: str, dest_dir: Path) -> None:
cmd = [
"gh",
"run",
"download",
"--dir",
str(dest_dir),
"--repo",
"openai/codex",
workflow_id,
]
subprocess.check_call(cmd)
def install_binary_components(
artifacts_dir: Path,
vendor_dir: Path,
selected_components: Sequence[BinaryComponent],
) -> None:
if not selected_components:
return
for component in selected_components:
component_targets = list(component.targets or BINARY_TARGETS)
print(
f"Installing {component.binary_basename} binaries for targets: "
+ ", ".join(component_targets)
)
max_workers = min(len(component_targets), max(1, (os.cpu_count() or 1)))
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {
executor.submit(
_install_single_binary,
artifacts_dir,
vendor_dir,
target,
component,
): target
for target in component_targets
}
for future in as_completed(futures):
installed_path = future.result()
print(f" installed {installed_path}")
def _install_single_binary(
artifacts_dir: Path,
vendor_dir: Path,
target: str,
component: BinaryComponent,
) -> Path:
artifact_subdir = artifacts_dir / target
archive_name = _archive_name_for_target(component.artifact_prefix, target)
archive_path = artifact_subdir / archive_name
if not archive_path.exists():
raise FileNotFoundError(f"Expected artifact not found: {archive_path}")
dest_dir = vendor_dir / target / component.dest_dir
dest_dir.mkdir(parents=True, exist_ok=True)
binary_name = (
f"{component.binary_basename}.exe" if "windows" in target else component.binary_basename
)
dest = dest_dir / binary_name
dest.unlink(missing_ok=True)
extract_archive(archive_path, "zst", None, dest)
if "windows" not in target:
dest.chmod(0o755)
return dest
def _archive_name_for_target(artifact_prefix: str, target: str) -> str:
if "windows" in target:
return f"{artifact_prefix}-{target}.exe.zst"
return f"{artifact_prefix}-{target}.zst"
def _fetch_single_rg(
vendor_dir: Path,
target: str,
platform_key: str,
platform_info: dict,
manifest_path: Path,
) -> Path:
providers = platform_info.get("providers", [])
if not providers:
raise RuntimeError(f"No providers listed for platform '{platform_key}' in {manifest_path}.")
url = providers[0]["url"]
archive_format = platform_info.get("format", "zst")
archive_member = platform_info.get("path")
digest = platform_info.get("digest")
expected_size = platform_info.get("size")
dest_dir = vendor_dir / target / "path"
dest_dir.mkdir(parents=True, exist_ok=True)
is_windows = platform_key.startswith("win")
binary_name = "rg.exe" if is_windows else "rg"
dest = dest_dir / binary_name
with tempfile.TemporaryDirectory() as tmp_dir_str:
tmp_dir = Path(tmp_dir_str)
archive_filename = os.path.basename(urlparse(url).path)
download_path = tmp_dir / archive_filename
print(
f" downloading ripgrep for {target} ({platform_key}) from {url}",
flush=True,
)
try:
_download_file(url, download_path)
except Exception as exc:
_gha_error(
title="ripgrep download failed",
message=f"target={target} platform={platform_key} url={url} error={exc!r}",
)
raise RuntimeError(
"Failed to download ripgrep "
f"(target={target}, platform={platform_key}, format={archive_format}, "
f"expected_size={expected_size!r}, digest={digest!r}, url={url}, dest={download_path})."
) from exc
dest.unlink(missing_ok=True)
try:
extract_archive(download_path, archive_format, archive_member, dest)
except Exception as exc:
raise RuntimeError(
"Failed to extract ripgrep "
f"(target={target}, platform={platform_key}, format={archive_format}, "
f"member={archive_member!r}, url={url}, archive={download_path})."
) from exc
if not is_windows:
dest.chmod(0o755)
return dest
def _download_file(url: str, dest: Path) -> None:
dest.parent.mkdir(parents=True, exist_ok=True)
dest.unlink(missing_ok=True)
with urlopen(url, timeout=DOWNLOAD_TIMEOUT_SECS) as response, open(dest, "wb") as out:
shutil.copyfileobj(response, out)
def extract_archive(
archive_path: Path,
archive_format: str,
archive_member: str | None,
dest: Path,
) -> None:
dest.parent.mkdir(parents=True, exist_ok=True)
if archive_format == "zst":
output_path = archive_path.parent / dest.name
subprocess.check_call(
["zstd", "-f", "-d", str(archive_path), "-o", str(output_path)]
)
shutil.move(str(output_path), dest)
return
if archive_format == "tar.gz":
if not archive_member:
raise RuntimeError("Missing 'path' for tar.gz archive in DotSlash manifest.")
with tarfile.open(archive_path, "r:gz") as tar:
try:
member = tar.getmember(archive_member)
except KeyError as exc:
raise RuntimeError(
f"Entry '{archive_member}' not found in archive {archive_path}."
) from exc
tar.extract(member, path=archive_path.parent, filter="data")
extracted = archive_path.parent / archive_member
shutil.move(str(extracted), dest)
return
if archive_format == "zip":
if not archive_member:
raise RuntimeError("Missing 'path' for zip archive in DotSlash manifest.")
with zipfile.ZipFile(archive_path) as archive:
try:
with archive.open(archive_member) as src, open(dest, "wb") as out:
shutil.copyfileobj(src, out)
except KeyError as exc:
raise RuntimeError(
f"Entry '{archive_member}' not found in archive {archive_path}."
) from exc
return
raise RuntimeError(f"Unsupported archive format '{archive_format}'.")
def _load_manifest(manifest_path: Path) -> dict:
cmd = ["dotslash", "--", "parse", str(manifest_path)]
stdout = subprocess.check_output(cmd, text=True)
try:
manifest = json.loads(stdout)
except json.JSONDecodeError as exc:
raise RuntimeError(f"Invalid DotSlash manifest output from {manifest_path}.") from exc
if not isinstance(manifest, dict):
raise RuntimeError(
f"Unexpected DotSlash manifest structure for {manifest_path}: {type(manifest)!r}"
)
return manifest
if __name__ == "__main__":
import sys
sys.exit(main())

View File

@@ -1,5 +1,5 @@
[target.'cfg(all(windows, target_env = "msvc"))']
rustflags = ["-C", "link-arg=/STACK:8388608", "-C", "target-feature=+crt-static"]
rustflags = ["-C", "link-arg=/STACK:8388608"]
# MSVC emits a warning about code that may trip "Cortex-A53 MPCore processor bug #843419" (see
# https://developer.arm.com/documentation/epm048406/latest) which is sometimes emitted by LLVM.

View File

@@ -1,12 +1,6 @@
[profile.default]
# Retry once so one transient failure does not fail full-CI outright.
# Fanout keeps the full-CI shards moving without treating every >30s test as
# stuck. Keep this aligned with the broader timeout budget we give sharded CI.
slow-timeout = { period = "30s", terminate-after = 2 }
retries = 1
[profile.default.junit]
path = "junit.xml"
# Do not increase, fix your test instead
slow-timeout = { period = "15s", terminate-after = 2 }
[test-groups.app_server_protocol_codegen]
max-threads = 1
@@ -20,9 +14,6 @@ max-threads = 1
[test-groups.windows_sandbox_legacy_sessions]
max-threads = 1
[test-groups.windows_process_heavy]
max-threads = 2
[[profile.default.overrides]]
# Do not add new tests here
filter = 'test(rmcp_client) | test(humanlike_typing_1000_chars_appears_live_no_placeholder)'
@@ -53,18 +44,3 @@ test-group = 'core_apply_patch_cli_integration'
# Serialize them to avoid exhausting Windows session/global desktop resources in CI.
filter = 'package(codex-windows-sandbox) & test(legacy_)'
test-group = 'windows_sandbox_legacy_sessions'
[[profile.default.overrides]]
# This Codex-home startup path still exceeded the broader Windows-heavy ceiling
# in both Windows full-CI lanes after contention was reduced.
platform = 'cfg(windows)'
filter = 'test(start_thread_uses_all_default_environments_from_codex_home)'
slow-timeout = { period = "1m", terminate-after = 2 }
[[profile.default.overrides]]
# These Windows-heavy tests spawn subprocesses, session files, or JSON-RPC
# clients and have been the dominant source of 30s full-CI timeouts.
platform = 'cfg(windows)'
filter = 'test(suite::resume::) | test(suite::cli_stream::) | test(suite::auth_env::) | test(start_thread_uses_all_default_environments_from_codex_home) | test(connect_stdio_command_initializes_json_rpc_client_on_windows)'
test-group = 'windows_process_heavy'
slow-timeout = { period = "45s", terminate-after = 2 }

View File

@@ -17,7 +17,7 @@ jobs:
working-directory: codex-rs
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
- name: Install cargo-audit
uses: taiki-e/install-action@v2
with:

1278
codex-rs/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@ members = [
"app-server-client",
"app-server-protocol",
"app-server-test-client",
"debug-client",
"apply-patch",
"arg0",
"feedback",
@@ -21,7 +22,7 @@ members = [
"install-context",
"codex-backend-openapi-models",
"code-mode",
"cloud-config",
"cloud-requirements",
"cloud-tasks",
"cloud-tasks-client",
"cloud-tasks-mock-client",
@@ -37,7 +38,6 @@ members = [
"core-plugins",
"core-skills",
"hooks",
"http-state",
"secrets",
"exec",
"file-system",
@@ -45,11 +45,8 @@ members = [
"execpolicy",
"execpolicy-legacy",
"ext/extension-api",
"ext/goal",
"ext/guardian",
"ext/image-generation",
"ext/memories",
"ext/web-search",
"external-agent-migration",
"external-agent-sessions",
"keyring-store",
@@ -60,6 +57,7 @@ members = [
"login",
"codex-mcp",
"mcp-server",
"memories/mcp",
"memories/read",
"memories/write",
"model-provider-info",
@@ -69,7 +67,6 @@ members = [
"process-hardening",
"protocol",
"realtime-webrtc",
"prompts",
"rollout",
"rollout-trace",
"rmcp-client",
@@ -150,7 +147,7 @@ codex-chatgpt = { path = "chatgpt" }
codex-cli = { path = "cli" }
codex-client = { path = "codex-client" }
codex-collaboration-mode-templates = { path = "collaboration-mode-templates" }
codex-cloud-config = { path = "cloud-config" }
codex-cloud-requirements = { path = "cloud-requirements" }
codex-cloud-tasks-client = { path = "cloud-tasks-client" }
codex-cloud-tasks-mock-client = { path = "cloud-tasks-mock-client" }
codex-code-mode = { path = "code-mode" }
@@ -165,9 +162,7 @@ codex-file-system = { path = "file-system" }
codex-exec-server = { path = "exec-server" }
codex-execpolicy = { path = "execpolicy" }
codex-extension-api = { path = "ext/extension-api" }
codex-goal-extension = { path = "ext/goal" }
codex-guardian = { path = "ext/guardian" }
codex-image-generation-extension = { path = "ext/image-generation" }
codex-external-agent-migration = { path = "external-agent-migration" }
codex-external-agent-sessions = { path = "external-agent-sessions" }
codex-experimental-api-macros = { path = "codex-experimental-api-macros" }
@@ -178,14 +173,12 @@ codex-file-search = { path = "file-search" }
codex-file-watcher = { path = "file-watcher" }
codex-git-utils = { path = "git-utils" }
codex-hooks = { path = "hooks" }
codex-http-state = { path = "http-state" }
codex-keyring-store = { path = "keyring-store" }
codex-linux-sandbox = { path = "linux-sandbox" }
codex-lmstudio = { path = "lmstudio" }
codex-login = { path = "login" }
codex-message-history = { path = "message-history" }
codex-memories-extension = { path = "ext/memories" }
codex-web-search-extension = { path = "ext/web-search" }
codex-memories-read = { path = "memories/read" }
codex-memories-write = { path = "memories/write" }
codex-mcp = { path = "codex-mcp" }
@@ -200,7 +193,6 @@ codex-model-provider = { path = "model-provider" }
codex-process-hardening = { path = "process-hardening" }
codex-protocol = { path = "protocol" }
codex-realtime-webrtc = { path = "realtime-webrtc" }
codex-prompts = { path = "prompts" }
codex-responses-api-proxy = { path = "responses-api-proxy" }
codex-response-debug-context = { path = "response-debug-context" }
codex-rmcp-client = { path = "rmcp-client" }
@@ -281,7 +273,6 @@ deno_core_icudata = "0.77.0"
derive_more = "2"
diffy = "0.4.2"
dirs = "6"
divan = "0.1.21"
dns-lookup = "3.0.1"
dotenvy = "0.15.7"
dunce = "1.0.4"
@@ -308,7 +299,6 @@ indexmap = "2.12.0"
insta = "1.46.3"
inventory = "0.3.19"
itertools = "0.14.0"
jsonptr = { version = "0.7.1", default-features = false }
jsonwebtoken = "9.3.1"
keyring = { version = "3.6", default-features = false }
landlock = "0.4.4"
@@ -347,7 +337,7 @@ rcgen = { version = "0.14.7", default-features = false, features = [
regex = "1.12.3"
regex-lite = "0.1.8"
reqwest = { version = "0.12", features = ["cookies"] }
rmcp = { version = "1.7.0", default-features = false }
rmcp = { version = "0.15.0", default-features = false }
runfiles = { git = "https://github.com/dzbarsky/rules_rust", rev = "b56cbaa8465e74127f1ea216f813cd377295ad81" }
rustls = { version = "0.23", default-features = false, features = [
"ring",
@@ -371,14 +361,13 @@ sha2 = "0.10"
shlex = "1.3.0"
similar = "2.7.0"
socket2 = "0.6.1"
sqlx = { version = "0.9.0", default-features = false, features = [
sqlx = { version = "0.8.6", default-features = false, features = [
"chrono",
"json",
"macros",
"migrate",
"runtime-tokio",
"tls-rustls",
"sqlite-bundled",
"runtime-tokio-rustls",
"sqlite",
"time",
"uuid",
] }
@@ -422,7 +411,7 @@ unicode-width = "0.2"
url = "2"
urlencoding = "2.1"
uuid = "1"
v8 = "=147.4.0"
v8 = "=146.4.0"
vt100 = "0.16.2"
walkdir = "2.5.0"
webbrowser = "1.0"
@@ -481,7 +470,7 @@ unwrap_used = "deny"
[workspace.metadata.cargo-shear]
ignored = [
"codex-agent-graph-store",
"codex-goal-extension",
"codex-memories-extension",
"icu_provider",
"openssl-sys",
"codex-v8-poc",

View File

@@ -55,20 +55,25 @@ Use `codex exec --ephemeral ...` to run without persisting session rollout files
### Experimenting with the Codex Sandbox
To test to see what happens when a command is run under the sandbox provided by Codex, use the `sandbox` subcommand in Codex CLI:
To test to see what happens when a command is run under the sandbox provided by Codex, we provide the following subcommands in Codex CLI:
```
# Uses the sandbox implementation for the current host OS:
# Seatbelt on macOS, the Linux sandbox on Linux, and Windows restricted token on Windows.
codex sandbox [COMMAND]...
# macOS
codex sandbox macos [--log-denials] [COMMAND]...
# macOS-only diagnostic option
codex sandbox --log-denials [COMMAND]...
# Linux
codex sandbox linux [COMMAND]...
# Windows
codex sandbox windows [COMMAND]...
# Legacy aliases
codex debug seatbelt [--log-denials] [COMMAND]...
codex debug landlock [COMMAND]...
```
`codex sandbox` also accepts `--profile NAME` (`-p NAME`) to layer
`$CODEX_HOME/NAME.config.toml` onto the base user config for the sandboxed
command.
To try a writable legacy sandbox mode with these commands, pass an explicit config override such
as `-c 'sandbox_mode="workspace-write"'`.
### Selecting a sandbox policy via `--sandbox`
@@ -85,6 +90,7 @@ codex --sandbox workspace-write
codex --sandbox danger-full-access
```
The same setting can be persisted in `~/.codex/config.toml` via the top-level `sandbox_mode = "MODE"` key, e.g. `sandbox_mode = "workspace-write"`.
In `workspace-write`, Codex also includes `~/.codex/memories` in its writable roots so memory maintenance does not require an extra approval.
## Code Organization

View File

@@ -138,6 +138,7 @@ use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::HookEventName;
use codex_protocol::protocol::HookRunStatus;
use codex_protocol::protocol::HookSource;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use codex_protocol::protocol::ThreadSource;
@@ -160,13 +161,11 @@ fn sample_thread_with_metadata(
ephemeral: bool,
source: AppServerSessionSource,
thread_source: Option<AppServerThreadSource>,
parent_thread_id: Option<String>,
) -> Thread {
Thread {
id: thread_id.to_string(),
session_id: format!("session-{thread_id}"),
forked_from_id: None,
parent_thread_id,
preview: "first prompt".to_string(),
ephemeral,
model_provider: "openai".to_string(),
@@ -197,17 +196,16 @@ fn sample_thread_start_response(
ephemeral,
AppServerSessionSource::Exec,
Some(AppServerThreadSource::User),
/*parent_thread_id*/ None,
),
model: model.to_string(),
model_provider: "openai".to_string(),
service_tier: None,
cwd: test_path_buf("/tmp").abs(),
runtime_workspace_roots: Vec::new(),
instruction_sources: Vec::new(),
approval_policy: AppServerAskForApproval::OnFailure,
approvals_reviewer: AppServerApprovalsReviewer::User,
sandbox: AppServerSandboxPolicy::DangerFullAccess,
permission_profile: None,
active_permission_profile: None,
reasoning_effort: None,
})
@@ -243,7 +241,6 @@ fn sample_thread_resume_response(
model,
AppServerSessionSource::Exec,
Some(AppServerThreadSource::User),
/*parent_thread_id*/ None,
)
}
@@ -253,28 +250,20 @@ fn sample_thread_resume_response_with_source(
model: &str,
source: AppServerSessionSource,
thread_source: Option<AppServerThreadSource>,
parent_thread_id: Option<String>,
) -> ClientResponsePayload {
ClientResponsePayload::ThreadResume(ThreadResumeResponse {
thread: sample_thread_with_metadata(
thread_id,
ephemeral,
source,
thread_source,
parent_thread_id,
),
thread: sample_thread_with_metadata(thread_id, ephemeral, source, thread_source),
model: model.to_string(),
model_provider: "openai".to_string(),
service_tier: None,
cwd: test_path_buf("/tmp").abs(),
runtime_workspace_roots: Vec::new(),
instruction_sources: Vec::new(),
approval_policy: AppServerAskForApproval::OnFailure,
approvals_reviewer: AppServerApprovalsReviewer::User,
sandbox: AppServerSandboxPolicy::DangerFullAccess,
permission_profile: None,
active_permission_profile: None,
reasoning_effort: None,
initial_turns_page: None,
})
}
@@ -283,7 +272,6 @@ fn sample_turn_start_request(thread_id: &str, request_id: i64) -> ClientRequest
request_id: RequestId::Integer(request_id),
params: TurnStartParams {
thread_id: thread_id.to_string(),
client_user_message_id: None,
input: vec![
UserInput::Text {
text: "hello".to_string(),
@@ -291,7 +279,6 @@ fn sample_turn_start_request(thread_id: &str, request_id: i64) -> ClientRequest
},
UserInput::Image {
url: "https://example.com/a.png".to_string(),
detail: None,
},
],
..Default::default()
@@ -379,7 +366,9 @@ fn sample_turn_resolved_config(thread_id: &str, turn_id: &str) -> TurnResolvedCo
session_source: SessionSource::Exec,
model: "gpt-5".to_string(),
model_provider: "openai".to_string(),
permission_profile: CorePermissionProfile::read_only(),
permission_profile: CorePermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::new_read_only_policy(),
),
permission_profile_cwd: PathBuf::from("/tmp"),
reasoning_effort: None,
reasoning_summary: None,
@@ -403,7 +392,6 @@ fn sample_turn_steer_request(
params: TurnSteerParams {
thread_id: thread_id.to_string(),
expected_turn_id: expected_turn_id.to_string(),
client_user_message_id: None,
input: vec![
UserInput::Text {
text: "more".to_string(),
@@ -411,11 +399,9 @@ fn sample_turn_steer_request(
},
UserInput::LocalImage {
path: "/tmp/a.png".into(),
detail: None,
},
],
responsesapi_client_metadata: None,
additional_context: None,
},
}
}
@@ -1227,7 +1213,6 @@ fn compaction_event_serializes_expected_shape() {
completed_at: 106,
duration_ms: Some(6543),
},
"session-thread-1".to_string(),
sample_app_server_client_metadata(),
sample_runtime_metadata(),
Some(ThreadSource::User),
@@ -1244,7 +1229,6 @@ fn compaction_event_serializes_expected_shape() {
"event_type": "codex_compaction_event",
"event_params": {
"thread_id": "thread-1",
"session_id": "session-thread-1",
"turn_id": "turn-1",
"app_server_client": {
"product_client_id": DEFAULT_ORIGINATOR,
@@ -1279,14 +1263,6 @@ fn compaction_event_serializes_expected_shape() {
);
}
#[test]
fn compaction_implementation_serializes_remote_v2() {
let payload = serde_json::to_value(CompactionImplementation::ResponsesCompactionV2)
.expect("serialize compaction implementation");
assert_eq!(payload, json!("responses_compaction_v2"));
}
#[test]
fn app_used_dedupe_is_keyed_by_turn_and_connector() {
let (sender, _receiver) = mpsc::channel(1);
@@ -1323,7 +1299,6 @@ fn thread_initialized_event_serializes_expected_shape() {
event_type: "codex_thread_initialized",
event_params: ThreadInitializedEventParams {
thread_id: "thread-0".to_string(),
session_id: "session-thread-0".to_string(),
app_server_client: CodexAppServerClientMetadata {
product_client_id: DEFAULT_ORIGINATOR.to_string(),
client_name: Some("codex-tui".to_string()),
@@ -1355,7 +1330,6 @@ fn thread_initialized_event_serializes_expected_shape() {
"event_type": "codex_thread_initialized",
"event_params": {
"thread_id": "thread-0",
"session_id": "session-thread-0",
"app_server_client": {
"product_client_id": DEFAULT_ORIGINATOR,
"client_name": "codex-tui",
@@ -1623,7 +1597,6 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize
let payload = serde_json::to_value(&events).expect("serialize events");
assert_eq!(payload.as_array().expect("events array").len(), 1);
assert_eq!(payload[0]["event_type"], "codex_thread_initialized");
assert_eq!(payload[0]["event_params"]["session_id"], "session-thread-1");
assert_eq!(
payload[0]["event_params"]["app_server_client"]["product_client_id"],
DEFAULT_ORIGINATOR
@@ -1766,7 +1739,6 @@ async fn compaction_event_ingests_custom_fact() {
agent_role: None,
}),
Some(AppServerThreadSource::Subagent),
Some(parent_thread_id.to_string()),
)),
},
&mut events,
@@ -1801,7 +1773,6 @@ async fn compaction_event_ingests_custom_fact() {
let payload = serde_json::to_value(&events).expect("serialize events");
assert_eq!(payload.as_array().expect("events array").len(), 1);
assert_eq!(payload[0]["event_type"], "codex_compaction_event");
assert_eq!(payload[0]["event_params"]["session_id"], "session-thread-1");
assert_eq!(payload[0]["event_params"]["thread_id"], "thread-1");
assert_eq!(payload[0]["event_params"]["turn_id"], "turn-compact");
assert_eq!(
@@ -1926,10 +1897,6 @@ async fn guardian_review_event_ingests_custom_fact_with_optional_target_item() {
let payload = serde_json::to_value(&events).expect("serialize events");
assert_eq!(payload.as_array().expect("events array").len(), 1);
assert_eq!(payload[0]["event_type"], "codex_guardian_review");
assert_eq!(
payload[0]["event_params"]["session_id"],
"session-thread-guardian"
);
assert_eq!(payload[0]["event_params"]["thread_id"], "thread-guardian");
assert_eq!(payload[0]["event_params"]["turn_id"], "turn-guardian");
assert_eq!(payload[0]["event_params"]["review_id"], "review-guardian");
@@ -2422,7 +2389,6 @@ async fn item_review_summaries_do_not_cross_threads_with_reused_item_ids() {
fn subagent_thread_started_review_serializes_expected_shape() {
let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request(
SubAgentThreadStartedInput {
session_id: "session-root".to_string(),
thread_id: "thread-review".to_string(),
parent_thread_id: None,
product_client_id: "codex-tui".to_string(),
@@ -2466,9 +2432,8 @@ fn subagent_thread_started_thread_spawn_serializes_parent_thread_id() {
.expect("valid thread id");
let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request(
SubAgentThreadStartedInput {
session_id: "session-root".to_string(),
thread_id: "thread-spawn".to_string(),
parent_thread_id: Some(parent_thread_id.to_string()),
parent_thread_id: None,
product_client_id: "codex-tui".to_string(),
client_name: "codex-tui".to_string(),
client_version: "1.0.0".to_string(),
@@ -2486,21 +2451,18 @@ fn subagent_thread_started_thread_spawn_serializes_parent_thread_id() {
));
let payload = serde_json::to_value(&event).expect("serialize thread spawn subagent event");
assert_eq!(payload["event_params"]["thread_id"], "thread-spawn");
assert_eq!(payload["event_params"]["thread_source"], "subagent");
assert_eq!(payload["event_params"]["subagent_source"], "thread_spawn");
assert_eq!(
payload["event_params"]["parent_thread_id"],
"11111111-1111-1111-1111-111111111111"
);
assert_eq!(payload["event_params"]["session_id"], "session-root");
}
#[test]
fn subagent_thread_started_memory_consolidation_serializes_expected_shape() {
let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request(
SubAgentThreadStartedInput {
session_id: "session-root".to_string(),
thread_id: "thread-memory".to_string(),
parent_thread_id: None,
product_client_id: "codex-tui".to_string(),
@@ -2526,7 +2488,6 @@ fn subagent_thread_started_memory_consolidation_serializes_expected_shape() {
fn subagent_thread_started_other_serializes_expected_shape() {
let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request(
SubAgentThreadStartedInput {
session_id: "session-root".to_string(),
thread_id: "thread-guardian".to_string(),
parent_thread_id: None,
product_client_id: "codex-tui".to_string(),
@@ -2546,14 +2507,10 @@ fn subagent_thread_started_other_serializes_expected_shape() {
#[test]
fn subagent_thread_started_other_serializes_explicit_parent_thread_id() {
let parent_thread_id =
codex_protocol::ThreadId::from_string("33333333-3333-4333-8333-333333333333")
.expect("valid thread id");
let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request(
SubAgentThreadStartedInput {
session_id: "session-root".to_string(),
thread_id: "thread-guardian".to_string(),
parent_thread_id: Some(parent_thread_id.to_string()),
parent_thread_id: Some("parent-thread-guardian".to_string()),
product_client_id: "codex-tui".to_string(),
client_name: "codex-tui".to_string(),
client_version: "1.0.0".to_string(),
@@ -2568,7 +2525,7 @@ fn subagent_thread_started_other_serializes_explicit_parent_thread_id() {
assert_eq!(payload["event_params"]["subagent_source"], "guardian");
assert_eq!(
payload["event_params"]["parent_thread_id"],
"33333333-3333-4333-8333-333333333333"
"parent-thread-guardian"
);
}
@@ -2581,7 +2538,6 @@ async fn subagent_thread_started_publishes_without_initialize() {
.ingest(
AnalyticsFact::Custom(CustomAnalyticsFact::SubAgentThreadStarted(
SubAgentThreadStartedInput {
session_id: "session-root".to_string(),
thread_id: "thread-review".to_string(),
parent_thread_id: None,
product_client_id: "codex-tui".to_string(),
@@ -2655,9 +2611,8 @@ async fn subagent_thread_started_inherits_parent_connection_for_new_thread() {
.ingest(
AnalyticsFact::Custom(CustomAnalyticsFact::SubAgentThreadStarted(
SubAgentThreadStartedInput {
session_id: "session-root".to_string(),
thread_id: "thread-review".to_string(),
parent_thread_id: Some(parent_thread_id.to_string()),
parent_thread_id: None,
product_client_id: "parent-client".to_string(),
client_name: "parent-client".to_string(),
client_version: "1.0.0".to_string(),
@@ -2703,8 +2658,6 @@ async fn subagent_thread_started_inherits_parent_connection_for_new_thread() {
.await;
let payload = serde_json::to_value(&events).expect("serialize events");
assert_eq!(payload[0]["event_params"]["session_id"], "session-root");
assert_eq!(payload[0]["event_params"]["thread_id"], "thread-review");
assert_eq!(
payload[0]["event_params"]["app_server_client"]["product_client_id"],
"parent-client"
@@ -2725,7 +2678,6 @@ async fn subagent_tool_items_inherit_parent_connection_metadata() {
.ingest(
AnalyticsFact::Custom(CustomAnalyticsFact::SubAgentThreadStarted(
SubAgentThreadStartedInput {
session_id: "session-root".to_string(),
thread_id: "thread-subagent".to_string(),
parent_thread_id: Some("thread-1".to_string()),
product_client_id: "codex-tui".to_string(),
@@ -3231,7 +3183,6 @@ fn turn_event_serializes_expected_shape() {
event_type: "codex_turn_event",
event_params: crate::events::CodexTurnEventParams {
thread_id: "thread-2".to_string(),
session_id: "session-thread-2".to_string(),
turn_id: "turn-2".to_string(),
app_server_client: sample_app_server_client_metadata(),
runtime: sample_runtime_metadata(),
@@ -3282,7 +3233,6 @@ fn turn_event_serializes_expected_shape() {
"event_type": "codex_turn_event",
"event_params": {
"thread_id": "thread-2",
"session_id": "session-thread-2",
"turn_id": "turn-2",
"submission_type": null,
"app_server_client": {
@@ -3384,10 +3334,6 @@ async fn accepted_turn_steer_emits_expected_event() {
let payload = serde_json::to_value(&out[0]).expect("serialize turn steer event");
assert_eq!(payload["event_type"], json!("codex_turn_steer_event"));
assert_eq!(payload["event_params"]["thread_id"], json!("thread-2"));
assert_eq!(
payload["event_params"]["session_id"],
json!("session-thread-2")
);
assert_eq!(payload["event_params"]["expected_turn_id"], json!("turn-2"));
assert_eq!(payload["event_params"]["accepted_turn_id"], json!("turn-2"));
assert_eq!(payload["event_params"]["num_input_images"], json!(1));
@@ -3605,10 +3551,6 @@ async fn turn_lifecycle_emits_turn_event() {
let payload = serde_json::to_value(&out[0]).expect("serialize turn event");
assert_eq!(payload["event_type"], json!("codex_turn_event"));
assert_eq!(payload["event_params"]["thread_id"], json!("thread-2"));
assert_eq!(
payload["event_params"]["session_id"],
json!("session-thread-2")
);
assert_eq!(payload["event_params"]["turn_id"], json!("turn-2"));
assert_eq!(
payload["event_params"]["app_server_client"],
@@ -3687,7 +3629,6 @@ async fn turn_event_counts_completed_tool_items() {
status: McpToolCallStatus::Completed,
arguments: json!({}),
mcp_app_resource_uri: None,
plugin_id: None,
result: None,
error: None,
duration_ms: Some(2),

View File

@@ -12,6 +12,7 @@ use codex_app_server_protocol::ApprovalsReviewer as AppServerApprovalsReviewer;
use codex_app_server_protocol::AskForApproval as AppServerAskForApproval;
use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::ClientResponsePayload;
use codex_app_server_protocol::PermissionProfile as AppServerPermissionProfile;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::SandboxPolicy as AppServerSandboxPolicy;
use codex_app_server_protocol::SessionSource as AppServerSessionSource;
@@ -28,6 +29,7 @@ use codex_app_server_protocol::TurnStartResponse;
use codex_app_server_protocol::TurnStatus as AppServerTurnStatus;
use codex_app_server_protocol::TurnSteerParams;
use codex_app_server_protocol::TurnSteerResponse;
use codex_protocol::models::PermissionProfile as CorePermissionProfile;
use codex_utils_absolute_path::test_support::PathBufExt;
use codex_utils_absolute_path::test_support::test_path_buf;
use std::collections::HashSet;
@@ -89,7 +91,6 @@ fn sample_turn_start_request() -> ClientRequest {
request_id: RequestId::Integer(1),
params: TurnStartParams {
thread_id: "thread-1".to_string(),
client_user_message_id: None,
input: Vec::new(),
..Default::default()
},
@@ -102,10 +103,8 @@ fn sample_turn_steer_request() -> ClientRequest {
params: TurnSteerParams {
thread_id: "thread-1".to_string(),
expected_turn_id: "turn-1".to_string(),
client_user_message_id: None,
input: Vec::new(),
responsesapi_client_metadata: None,
additional_context: None,
},
}
}
@@ -124,7 +123,6 @@ fn sample_thread(thread_id: &str) -> Thread {
id: thread_id.to_string(),
session_id: format!("session-{thread_id}"),
forked_from_id: None,
parent_thread_id: None,
preview: "first prompt".to_string(),
ephemeral: false,
model_provider: "openai".to_string(),
@@ -144,6 +142,10 @@ fn sample_thread(thread_id: &str) -> Thread {
}
}
fn sample_permission_profile() -> AppServerPermissionProfile {
CorePermissionProfile::Disabled.into()
}
fn sample_thread_start_response() -> ClientResponsePayload {
ClientResponsePayload::ThreadStart(ThreadStartResponse {
thread: sample_thread("thread-1"),
@@ -151,11 +153,11 @@ fn sample_thread_start_response() -> ClientResponsePayload {
model_provider: "openai".to_string(),
service_tier: None,
cwd: test_path_buf("/tmp").abs(),
runtime_workspace_roots: Vec::new(),
instruction_sources: Vec::new(),
approval_policy: AppServerAskForApproval::OnFailure,
approvals_reviewer: AppServerApprovalsReviewer::User,
sandbox: AppServerSandboxPolicy::DangerFullAccess,
permission_profile: Some(sample_permission_profile()),
active_permission_profile: None,
reasoning_effort: None,
})
@@ -168,14 +170,13 @@ fn sample_thread_resume_response() -> ClientResponsePayload {
model_provider: "openai".to_string(),
service_tier: None,
cwd: test_path_buf("/tmp").abs(),
runtime_workspace_roots: Vec::new(),
instruction_sources: Vec::new(),
approval_policy: AppServerAskForApproval::OnFailure,
approvals_reviewer: AppServerApprovalsReviewer::User,
sandbox: AppServerSandboxPolicy::DangerFullAccess,
permission_profile: Some(sample_permission_profile()),
active_permission_profile: None,
reasoning_effort: None,
initial_turns_page: None,
})
}
@@ -186,11 +187,11 @@ fn sample_thread_fork_response() -> ClientResponsePayload {
model_provider: "openai".to_string(),
service_tier: None,
cwd: test_path_buf("/tmp").abs(),
runtime_workspace_roots: Vec::new(),
instruction_sources: Vec::new(),
approval_policy: AppServerAskForApproval::OnFailure,
approvals_reviewer: AppServerApprovalsReviewer::User,
sandbox: AppServerSandboxPolicy::DangerFullAccess,
permission_profile: Some(sample_permission_profile()),
active_permission_profile: None,
reasoning_effort: None,
})

View File

@@ -147,7 +147,6 @@ pub(crate) struct CodexRuntimeMetadata {
#[derive(Serialize)]
pub(crate) struct ThreadInitializedEventParams {
pub(crate) thread_id: String,
pub(crate) session_id: String,
pub(crate) app_server_client: CodexAppServerClientMetadata,
pub(crate) runtime: CodexRuntimeMetadata,
pub(crate) model: String,
@@ -421,7 +420,6 @@ impl GuardianReviewAnalyticsResult {
#[derive(Serialize)]
pub(crate) struct GuardianReviewEventPayload {
pub(crate) session_id: String,
pub(crate) app_server_client: CodexAppServerClientMetadata,
pub(crate) runtime: CodexRuntimeMetadata,
#[serde(flatten)]
@@ -740,7 +738,6 @@ pub(crate) struct CodexHookRunEventRequest {
#[derive(Serialize)]
pub(crate) struct CodexCompactionEventParams {
pub(crate) thread_id: String,
pub(crate) session_id: String,
pub(crate) turn_id: String,
pub(crate) app_server_client: CodexAppServerClientMetadata,
pub(crate) runtime: CodexRuntimeMetadata,
@@ -770,7 +767,6 @@ pub(crate) struct CodexCompactionEventRequest {
#[derive(Serialize)]
pub(crate) struct CodexTurnEventParams {
pub(crate) thread_id: String,
pub(crate) session_id: String,
pub(crate) turn_id: String,
// TODO(rhan-oai): Populate once queued/default submission type is plumbed from
// the turn/start callsites instead of always being reported as None.
@@ -825,7 +821,6 @@ pub(crate) struct CodexTurnEventRequest {
#[derive(Serialize)]
pub(crate) struct CodexTurnSteerEventParams {
pub(crate) thread_id: String,
pub(crate) session_id: String,
pub(crate) expected_turn_id: Option<String>,
pub(crate) accepted_turn_id: Option<String>,
pub(crate) app_server_client: CodexAppServerClientMetadata,
@@ -931,7 +926,6 @@ pub(crate) fn codex_plugin_metadata(plugin: PluginTelemetryMetadata) -> CodexPlu
pub(crate) fn codex_compaction_event_params(
input: CodexCompactionEvent,
session_id: String,
app_server_client: CodexAppServerClientMetadata,
runtime: CodexRuntimeMetadata,
thread_source: Option<ThreadSource>,
@@ -940,7 +934,6 @@ pub(crate) fn codex_compaction_event_params(
) -> CodexCompactionEventParams {
CodexCompactionEventParams {
thread_id: input.thread_id,
session_id,
turn_id: input.turn_id,
app_server_client,
runtime,
@@ -997,8 +990,6 @@ fn analytics_hook_event_name(event_name: HookEventName) -> &'static str {
HookEventName::PostCompact => "PostCompact",
HookEventName::SessionStart => "SessionStart",
HookEventName::UserPromptSubmit => "UserPromptSubmit",
HookEventName::SubagentStart => "SubagentStart",
HookEventName::SubagentStop => "SubagentStop",
HookEventName::Stop => "Stop",
}
}
@@ -1012,7 +1003,6 @@ fn analytics_hook_source(source: HookSource) -> &'static str {
HookSource::SessionFlags => "session_flags",
HookSource::Plugin => "plugin",
HookSource::CloudRequirements => "cloud_requirements",
HookSource::CloudManagedConfig => "cloud_managed_config",
HookSource::LegacyManagedConfigFile => "legacy_managed_config_file",
HookSource::LegacyManagedConfigMdm => "legacy_managed_config_mdm",
HookSource::Unknown => "unknown",
@@ -1034,7 +1024,6 @@ pub(crate) fn subagent_thread_started_event_request(
) -> ThreadInitializedEvent {
let event_params = ThreadInitializedEventParams {
thread_id: input.thread_id,
session_id: input.session_id,
app_server_client: CodexAppServerClientMetadata {
product_client_id: input.product_client_id,
client_name: Some(input.client_name),
@@ -1048,7 +1037,9 @@ pub(crate) fn subagent_thread_started_event_request(
thread_source: Some(ThreadSource::Subagent),
initialization_mode: ThreadInitializationMode::New,
subagent_source: Some(subagent_source_name(&input.subagent_source)),
parent_thread_id: input.parent_thread_id,
parent_thread_id: input
.parent_thread_id
.or_else(|| subagent_parent_thread_id(&input.subagent_source)),
created_at: input.created_at,
};
ThreadInitializedEvent {
@@ -1058,7 +1049,22 @@ pub(crate) fn subagent_thread_started_event_request(
}
pub(crate) fn subagent_source_name(subagent_source: &SubAgentSource) -> String {
subagent_source.kind().to_string()
match subagent_source {
SubAgentSource::Review => "review".to_string(),
SubAgentSource::Compact => "compact".to_string(),
SubAgentSource::ThreadSpawn { .. } => "thread_spawn".to_string(),
SubAgentSource::MemoryConsolidation => "memory_consolidation".to_string(),
SubAgentSource::Other(other) => other.clone(),
}
}
pub(crate) fn subagent_parent_thread_id(subagent_source: &SubAgentSource) -> Option<String> {
match subagent_source {
SubAgentSource::ThreadSpawn {
parent_thread_id, ..
} => Some(parent_thread_id.to_string()),
_ => None,
}
}
fn analytics_hook_status(status: HookRunStatus) -> HookRunStatus {

View File

@@ -199,7 +199,6 @@ pub struct AppInvocation {
#[derive(Clone)]
pub struct SubAgentThreadStartedInput {
pub session_id: String,
pub thread_id: String,
pub parent_thread_id: Option<String>,
pub product_client_id: String,
@@ -230,7 +229,6 @@ pub enum CompactionReason {
#[serde(rename_all = "snake_case")]
pub enum CompactionImplementation {
Responses,
ResponsesCompactionV2,
ResponsesCompact,
}

View File

@@ -55,6 +55,7 @@ use crate::events::codex_hook_run_metadata;
use crate::events::codex_plugin_metadata;
use crate::events::codex_plugin_used_metadata;
use crate::events::plugin_state_event_type;
use crate::events::subagent_parent_thread_id;
use crate::events::subagent_source_name;
use crate::events::subagent_thread_started_event_request;
use crate::facts::AnalyticsFact;
@@ -254,7 +255,6 @@ struct ItemReviewSummary {
#[derive(Clone)]
struct ThreadMetadataState {
session_id: String,
thread_source: Option<ThreadSource>,
initialization_mode: ThreadInitializationMode,
subagent_source: Option<String>,
@@ -263,24 +263,24 @@ struct ThreadMetadataState {
impl ThreadMetadataState {
fn from_thread_metadata(
session_id: String,
session_source: &SessionSource,
thread_source: Option<ThreadSource>,
parent_thread_id: Option<String>,
initialization_mode: ThreadInitializationMode,
) -> Self {
let subagent_source = match session_source {
SessionSource::SubAgent(subagent_source) => Some(subagent_source_name(subagent_source)),
let (subagent_source, parent_thread_id) = match session_source {
SessionSource::SubAgent(subagent_source) => (
Some(subagent_source_name(subagent_source)),
subagent_parent_thread_id(subagent_source),
),
SessionSource::Cli
| SessionSource::VSCode
| SessionSource::Exec
| SessionSource::Mcp
| SessionSource::Custom(_)
| SessionSource::Internal(_)
| SessionSource::Unknown => None,
| SessionSource::Unknown => (None, None),
};
Self {
session_id,
thread_source,
initialization_mode,
subagent_source,
@@ -513,7 +513,10 @@ impl AnalyticsReducer {
input: SubAgentThreadStartedInput,
out: &mut Vec<TrackEventRequest>,
) {
let parent_thread_id = input.parent_thread_id.clone();
let parent_thread_id = input
.parent_thread_id
.clone()
.or_else(|| subagent_parent_thread_id(&input.subagent_source));
let parent_connection_id = parent_thread_id
.as_ref()
.and_then(|parent_thread_id| self.threads.get(parent_thread_id))
@@ -522,7 +525,6 @@ impl AnalyticsReducer {
thread_state
.metadata
.get_or_insert_with(|| ThreadMetadataState {
session_id: input.session_id.clone(),
thread_source: Some(ThreadSource::Subagent),
initialization_mode: ThreadInitializationMode::New,
subagent_source: Some(subagent_source_name(&input.subagent_source)),
@@ -541,8 +543,8 @@ impl AnalyticsReducer {
input: GuardianReviewEventParams,
out: &mut Vec<TrackEventRequest>,
) {
let Some((connection_state, thread_metadata)) =
self.thread_context_or_warn(AnalyticsDropSite::guardian(&input))
let Some(connection_state) =
self.thread_connection_or_warn(AnalyticsDropSite::guardian(&input))
else {
return;
};
@@ -550,7 +552,6 @@ impl AnalyticsReducer {
GuardianReviewEventRequest {
event_type: "codex_guardian_review",
event_params: GuardianReviewEventPayload {
session_id: thread_metadata.session_id.clone(),
app_server_client: connection_state.app_server_client.clone(),
runtime: connection_state.runtime.clone(),
guardian_review: input,
@@ -1230,17 +1231,13 @@ impl AnalyticsReducer {
out: &mut Vec<TrackEventRequest>,
) {
let session_source: SessionSource = thread.source.into();
let session_id = thread.session_id;
let thread_id = thread.id;
let parent_thread_id = thread.parent_thread_id;
let Some(connection_state) = self.connections.get(&connection_id) else {
return;
};
let thread_metadata = ThreadMetadataState::from_thread_metadata(
session_id.clone(),
&session_source,
thread.thread_source.map(Into::into),
parent_thread_id,
initialization_mode,
);
self.threads.insert(
@@ -1255,7 +1252,6 @@ impl AnalyticsReducer {
event_type: "codex_thread_initialized",
event_params: ThreadInitializedEventParams {
thread_id,
session_id,
app_server_client: connection_state.app_server_client.clone(),
runtime: connection_state.runtime.clone(),
model,
@@ -1281,7 +1277,6 @@ impl AnalyticsReducer {
event_type: "codex_compaction_event",
event_params: codex_compaction_event_params(
input,
thread_metadata.session_id.clone(),
connection_state.app_server_client.clone(),
connection_state.runtime.clone(),
thread_metadata.thread_source,
@@ -1384,7 +1379,6 @@ impl AnalyticsReducer {
event_type: "codex_turn_steer_event",
event_params: CodexTurnSteerEventParams {
thread_id: pending_request.thread_id,
session_id: thread_metadata.session_id.clone(),
expected_turn_id: Some(pending_request.expected_turn_id),
accepted_turn_id,
app_server_client: connection_state.app_server_client.clone(),
@@ -2453,7 +2447,6 @@ fn codex_turn_event_params(
let token_usage = turn_state.token_usage.clone();
CodexTurnEventParams {
thread_id,
session_id: thread_metadata.session_id.clone(),
turn_id,
app_server_client,
runtime,

View File

@@ -176,7 +176,6 @@ pub(crate) fn server_notification_requires_delivery(notification: &ServerNotific
matches!(
notification,
ServerNotification::TurnCompleted(_)
| ServerNotification::ThreadSettingsUpdated(_)
| ServerNotification::ItemCompleted(_)
| ServerNotification::AgentMessageDelta(_)
| ServerNotification::PlanDelta(_)
@@ -1122,9 +1121,7 @@ mod tests {
websocket,
JSONRPCMessage::Response(JSONRPCResponse {
id: request.id,
result: serde_json::json!({
"userAgent": "codex_cli_rs/9.8.7-test (Test OS; x86_64) rust",
}),
result: serde_json::json!({}),
}),
)
.await;
@@ -1459,7 +1456,6 @@ mod tests {
.await
.expect("remote client should connect");
assert_eq!(client.server_version(), Some("9.8.7-test"));
let response: GetAccountResponse = client
.request_typed(ClientRequest::GetAccount {
request_id: RequestId::Integer(1),
@@ -2182,13 +2178,11 @@ mod tests {
let environment_manager = Arc::new(
EnvironmentManager::create_for_tests(
Some("ws://127.0.0.1:8765".to_string()),
Some(
ExecServerRuntimePaths::new(
std::env::current_exe().expect("current exe"),
/*codex_linux_sandbox_exe*/ None,
)
.expect("runtime paths"),
),
ExecServerRuntimePaths::new(
std::env::current_exe().expect("current exe"),
/*codex_linux_sandbox_exe*/ None,
)
.expect("runtime paths"),
)
.await,
);

View File

@@ -150,7 +150,6 @@ pub struct RemoteAppServerClient {
command_tx: mpsc::Sender<RemoteClientCommand>,
event_rx: mpsc::UnboundedReceiver<AppServerEvent>,
pending_events: VecDeque<AppServerEvent>,
server_version: Option<String>,
worker_handle: tokio::task::JoinHandle<()>,
}
@@ -181,10 +180,6 @@ impl RemoteAppServerClient {
}
}
pub fn server_version(&self) -> Option<&str> {
self.server_version.as_deref()
}
async fn connect_with_stream<S>(
channel_capacity: usize,
endpoint: String,
@@ -195,7 +190,7 @@ impl RemoteAppServerClient {
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
let mut stream = stream;
let (pending_events, server_version) = initialize_remote_connection(
let pending_events = initialize_remote_connection(
&mut stream,
&endpoint,
initialize_params,
@@ -471,7 +466,6 @@ impl RemoteAppServerClient {
command_tx,
event_rx,
pending_events: pending_events.into(),
server_version,
worker_handle,
})
}
@@ -612,7 +606,6 @@ impl RemoteAppServerClient {
command_tx,
event_rx,
pending_events: _pending_events,
server_version: _server_version,
worker_handle,
} = self;
let mut worker_handle = worker_handle;
@@ -800,13 +793,12 @@ async fn initialize_remote_connection<S>(
endpoint: &str,
params: InitializeParams,
initialize_timeout: Duration,
) -> IoResult<(Vec<AppServerEvent>, Option<String>)>
) -> IoResult<Vec<AppServerEvent>>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let initialize_request_id = RequestId::String("initialize".to_string());
let mut pending_events = Vec::new();
let mut server_version = None;
write_jsonrpc_message(
stream,
JSONRPCMessage::Request(jsonrpc_request_from_client_request(
@@ -830,14 +822,6 @@ where
})?;
match message {
JSONRPCMessage::Response(response) if response.id == initialize_request_id => {
server_version = response
.result
.get("userAgent")
.and_then(serde_json::Value::as_str)
.and_then(|user_agent| {
let (_, rest) = user_agent.split_once('/')?;
rest.split_whitespace().next().map(str::to_string)
});
break Ok(());
}
JSONRPCMessage::Error(error) if error.id == initialize_request_id => {
@@ -929,7 +913,7 @@ where
)
.await?;
Ok((pending_events, server_version))
Ok(pending_events)
}
fn app_server_event_from_notification(notification: JSONRPCNotification) -> Option<AppServerEvent> {
@@ -1023,7 +1007,6 @@ mod tests {
command_tx,
event_rx,
pending_events: VecDeque::new(),
server_version: None,
worker_handle,
};

View File

@@ -1,6 +1,5 @@
mod pid;
use std::path::Path;
use std::path::PathBuf;
use serde::Serialize;
@@ -32,15 +31,3 @@ pub(crate) fn pid_backend(paths: BackendPaths) -> PidBackend {
pub(crate) fn pid_update_loop_backend(paths: BackendPaths) -> PidBackend {
PidBackend::new_update_loop(paths.codex_bin, paths.update_pid_file)
}
pub(crate) async fn append_stderr_log_tail_context(pid_file: &Path, context: &mut String) {
match pid::read_stderr_log_tail(pid_file).await {
Ok(Some(tail)) => tail.append_to_context(context),
Ok(None) => {}
Err(err) => {
context.push_str(&format!(
"\n\nFailed to read managed app-server stderr log: {err:#}"
));
}
}
}

View File

@@ -1,4 +1,3 @@
use std::io::SeekFrom;
use std::path::Path;
use std::path::PathBuf;
#[cfg(unix)]
@@ -11,8 +10,6 @@ use anyhow::bail;
use serde::Deserialize;
use serde::Serialize;
use tokio::fs;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncSeekExt;
#[cfg(unix)]
use tokio::process::Command;
use tokio::time::sleep;
@@ -21,7 +18,6 @@ const STOP_POLL_INTERVAL: Duration = Duration::from_millis(50);
const STOP_GRACE_PERIOD: Duration = Duration::from_secs(60);
const STOP_TIMEOUT: Duration = Duration::from_secs(70);
const START_TIMEOUT: Duration = Duration::from_secs(10);
const STDERR_LOG_TAIL_BYTES: u64 = 4096;
#[derive(Debug)]
#[cfg_attr(not(unix), allow(dead_code))]
@@ -39,25 +35,6 @@ struct PidRecord {
process_start_time: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PidLogTail {
pub(crate) path: PathBuf,
pub(crate) contents: String,
}
impl PidLogTail {
pub(crate) fn append_to_context(&self, context: &mut String) {
context.push_str(&format!(
"\n\nManaged app-server stderr ({}):",
self.path.display()
));
for line in self.contents.lines() {
context.push_str("\n ");
context.push_str(line);
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum PidFileState {
Missing,
@@ -152,18 +129,11 @@ impl PidBackend {
}
};
let mut command = Command::new(&self.codex_bin);
let stderr_log = match self.open_stderr_log().await {
Ok(stderr_log) => stderr_log,
Err(err) => {
let _ = fs::remove_file(&self.pid_file).await;
return Err(err);
}
};
command
.args(self.command_args())
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::from(stderr_log.into_std().await));
.stderr(Stdio::null());
#[cfg(unix)]
{
@@ -199,11 +169,8 @@ impl PidBackend {
},
Err(err) => {
let _ = self.terminate_process(pid);
let mut context =
format!("failed to record pid-managed app-server process {pid} startup");
super::append_stderr_log_tail_context(&self.pid_file, &mut context).await;
let _ = fs::remove_file(&self.pid_file).await;
return Err(err).context(context);
return Err(err);
}
};
let contents = serde_json::to_vec(&record).context("failed to serialize pid record")?;
@@ -377,23 +344,6 @@ impl PidBackend {
Ok(reservation_lock)
}
#[cfg(unix)]
async fn open_stderr_log(&self) -> Result<fs::File> {
let stderr_log_file = stderr_log_file_for_pid_file(&self.pid_file);
fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(&stderr_log_file)
.await
.with_context(|| {
format!(
"failed to open stderr log for pid-managed app server {}",
stderr_log_file.display()
)
})
}
#[cfg(unix)]
fn command_args(&self) -> Vec<&'static str> {
match self.command_kind {
@@ -426,56 +376,6 @@ impl PidBackend {
}
}
pub(crate) async fn read_stderr_log_tail(pid_file: &Path) -> Result<Option<PidLogTail>> {
let path = stderr_log_file_for_pid_file(pid_file);
let Some(contents) = read_log_tail(&path, STDERR_LOG_TAIL_BYTES).await? else {
return Ok(None);
};
Ok(Some(PidLogTail { path, contents }))
}
fn stderr_log_file_for_pid_file(pid_file: &Path) -> PathBuf {
pid_file.with_extension("stderr.log")
}
async fn read_log_tail(path: &Path, byte_limit: u64) -> Result<Option<String>> {
let mut file = match fs::File::open(path).await {
Ok(file) => file,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(err) => {
return Err(err)
.with_context(|| format!("failed to open stderr log {}", path.display()));
}
};
let len = file
.metadata()
.await
.with_context(|| format!("failed to inspect stderr log {}", path.display()))?
.len();
if len == 0 {
return Ok(None);
}
let start = len.saturating_sub(byte_limit);
file.seek(SeekFrom::Start(start))
.await
.with_context(|| format!("failed to seek stderr log {}", path.display()))?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)
.await
.with_context(|| format!("failed to read stderr log {}", path.display()))?;
if start > 0
&& let Some(newline_index) = bytes.iter().position(|byte| *byte == b'\n')
{
bytes.drain(..=newline_index);
}
let contents = String::from_utf8_lossy(&bytes).trim_end().to_string();
if contents.is_empty() {
return Ok(None);
}
Ok(Some(contents))
}
#[cfg(unix)]
fn process_exists(pid: u32) -> bool {
let Ok(pid) = libc::pid_t::try_from(pid) else {

View File

@@ -6,10 +6,7 @@ use tempfile::TempDir;
use super::PidBackend;
use super::PidCommandKind;
use super::PidFileState;
use super::PidLogTail;
use super::PidRecord;
use super::read_stderr_log_tail;
use super::stderr_log_file_for_pid_file;
use super::try_lock_file;
#[tokio::test]
@@ -173,24 +170,3 @@ fn app_server_remote_control_uses_runtime_flag() {
vec!["app-server", "--remote-control", "--listen", "unix://"]
);
}
#[tokio::test]
async fn read_stderr_log_tail_returns_recent_complete_lines() {
let temp_dir = TempDir::new().expect("temp dir");
let pid_file = temp_dir.path().join("app-server.pid");
let log_file = stderr_log_file_for_pid_file(&pid_file);
let contents = format!("{}\nrecent error\nusage", "x".repeat(4100));
tokio::fs::write(&log_file, contents)
.await
.expect("write stderr log");
assert_eq!(
read_stderr_log_tail(&pid_file)
.await
.expect("read stderr log"),
Some(PidLogTail {
path: log_file,
contents: "recent error\nusage".to_string(),
})
);
}

View File

@@ -5,7 +5,6 @@ use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
use codex_app_server_protocol::ClientInfo;
use codex_app_server_protocol::InitializeCapabilities;
use codex_app_server_protocol::InitializeParams;
use codex_app_server_protocol::InitializeResponse;
use codex_app_server_protocol::JSONRPCMessage;
@@ -15,16 +14,12 @@ use codex_app_server_protocol::RequestId;
use codex_uds::UnixStream;
use futures::SinkExt;
use futures::StreamExt;
use tokio::io::AsyncRead;
use tokio::io::AsyncWrite;
use tokio::time::timeout;
use tokio_tungstenite::WebSocketStream;
use tokio_tungstenite::client_async;
use tokio_tungstenite::tungstenite::Message;
pub(crate) const CONTROL_SOCKET_RESPONSE_TIMEOUT: Duration = Duration::from_secs(2);
const PROBE_TIMEOUT: Duration = Duration::from_secs(2);
const CLIENT_NAME: &str = "codex_app_server_daemon";
const INITIALIZE_REQUEST_ID: RequestId = RequestId::Integer(1);
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ProbeInfo {
@@ -32,7 +27,7 @@ pub(crate) struct ProbeInfo {
}
pub(crate) async fn probe(socket_path: &Path) -> Result<ProbeInfo> {
timeout(CONTROL_SOCKET_RESPONSE_TIMEOUT, probe_inner(socket_path))
timeout(PROBE_TIMEOUT, probe_inner(socket_path))
.await
.with_context(|| {
format!(
@@ -43,42 +38,15 @@ pub(crate) async fn probe(socket_path: &Path) -> Result<ProbeInfo> {
}
async fn probe_inner(socket_path: &Path) -> Result<ProbeInfo> {
let mut websocket = connect(socket_path).await?;
let initialize_response = initialize(&mut websocket, /*experimental_api*/ false).await?;
let initialized = JSONRPCMessage::Notification(JSONRPCNotification {
method: "initialized".to_string(),
params: None,
});
send_message(&mut websocket, &initialized)
.await
.context("failed to send initialized notification")?;
websocket.close(None).await.ok();
Ok(ProbeInfo {
app_server_version: parse_version_from_user_agent(&initialize_response.user_agent)?,
})
}
pub(crate) async fn connect(socket_path: &Path) -> Result<WebSocketStream<UnixStream>> {
let stream = UnixStream::connect(socket_path)
.await
.with_context(|| format!("failed to connect to {}", socket_path.display()))?;
let (websocket, _response) = client_async("ws://localhost/", stream)
let (mut websocket, _response) = client_async("ws://localhost/", stream)
.await
.with_context(|| format!("failed to upgrade {}", socket_path.display()))?;
Ok(websocket)
}
pub(crate) async fn initialize<S>(
websocket: &mut WebSocketStream<S>,
experimental_api: bool,
) -> Result<InitializeResponse>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let initialize = JSONRPCMessage::Request(JSONRPCRequest {
id: INITIALIZE_REQUEST_ID,
id: RequestId::Integer(1),
method: "initialize".to_string(),
params: Some(serde_json::to_value(InitializeParams {
client_info: ClientInfo {
@@ -86,63 +54,45 @@ where
title: Some("Codex App Server Daemon".to_string()),
version: env!("CARGO_PKG_VERSION").to_string(),
},
capabilities: if experimental_api {
Some(InitializeCapabilities {
experimental_api: true,
..Default::default()
})
} else {
None
},
capabilities: None,
})?),
trace: None,
});
send_message(websocket, &initialize)
websocket
.send(Message::Text(serde_json::to_string(&initialize)?.into()))
.await
.context("failed to send initialize request")?;
let response = loop {
let message = timeout(CONTROL_SOCKET_RESPONSE_TIMEOUT, read_message(websocket))
let frame = websocket
.next()
.await
.context("timed out waiting for initialize response")??;
.ok_or_else(|| anyhow!("app-server closed before initialize response"))??;
let Message::Text(payload) = frame else {
continue;
};
let message = serde_json::from_str::<JSONRPCMessage>(&payload)?;
if let JSONRPCMessage::Response(response) = message
&& response.id == INITIALIZE_REQUEST_ID
&& response.id == RequestId::Integer(1)
{
break response;
}
};
serde_json::from_value::<InitializeResponse>(response.result)
.context("failed to parse initialize response")
}
let initialize_response = serde_json::from_value::<InitializeResponse>(response.result)?;
pub(crate) async fn send_message<S>(
websocket: &mut WebSocketStream<S>,
message: &JSONRPCMessage,
) -> Result<()>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let initialized = JSONRPCMessage::Notification(JSONRPCNotification {
method: "initialized".to_string(),
params: None,
});
websocket
.send(Message::Text(serde_json::to_string(message)?.into()))
.await?;
Ok(())
}
.send(Message::Text(serde_json::to_string(&initialized)?.into()))
.await
.context("failed to send initialized notification")?;
websocket.close(None).await.ok();
pub(crate) async fn read_message<S>(websocket: &mut WebSocketStream<S>) -> Result<JSONRPCMessage>
where
S: AsyncRead + AsyncWrite + Unpin,
{
loop {
let frame = websocket
.next()
.await
.ok_or_else(|| anyhow!("app-server closed the control socket"))??;
let Message::Text(payload) = frame else {
continue;
};
return serde_json::from_str::<JSONRPCMessage>(&payload)
.context("failed to parse app-server JSON-RPC message");
}
Ok(ProbeInfo {
app_server_version: parse_version_from_user_agent(&initialize_response.user_agent)?,
})
}
fn parse_version_from_user_agent(user_agent: &str) -> Result<String> {

View File

@@ -1,7 +1,6 @@
mod backend;
mod client;
mod managed_install;
mod remote_control_client;
mod settings;
mod update_loop;
@@ -14,7 +13,6 @@ use anyhow::Result;
use anyhow::anyhow;
pub use backend::BackendKind;
use backend::BackendPaths;
use codex_app_server_protocol::RemoteControlConnectionStatus;
use codex_app_server_transport::app_server_control_socket_path;
use codex_utils_home_dir::find_codex_home;
use managed_install::managed_codex_bin;
@@ -60,8 +58,6 @@ pub struct LifecycleOutput {
pub backend: Option<BackendKind>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pid: Option<u32>,
pub managed_codex_path: PathBuf,
pub managed_codex_version: Option<String>,
pub socket_path: PathBuf,
#[serde(skip_serializing_if = "Option::is_none")]
pub cli_version: Option<String>,
@@ -74,12 +70,6 @@ pub struct BootstrapOptions {
pub remote_control_enabled: bool,
}
/// Passively probes an existing app-server socket and returns its reported
/// app-server version.
pub async fn probe_app_server_version(socket_path: &Path) -> Result<String> {
Ok(client::probe(socket_path).await?.app_server_version)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum BootstrapStatus {
@@ -94,7 +84,6 @@ pub struct BootstrapOutput {
pub auto_update_enabled: bool,
pub remote_control_enabled: bool,
pub managed_codex_path: PathBuf,
pub managed_codex_version: Option<String>,
pub socket_path: PathBuf,
pub cli_version: String,
pub app_server_version: String,
@@ -107,20 +96,6 @@ pub enum RemoteControlStartOutput {
Start(LifecycleOutput),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemoteControlReadyStatus {
pub status: RemoteControlConnectionStatus,
pub server_name: String,
pub environment_id: Option<String>,
pub timed_out: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemoteControlReadyOutput {
pub daemon: RemoteControlStartOutput,
pub remote_control: RemoteControlReadyStatus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RemoteControlMode {
Enabled,
@@ -204,27 +179,6 @@ pub async fn ensure_remote_control_started() -> Result<RemoteControlStartOutput>
.await
}
pub async fn ensure_remote_control_ready() -> Result<RemoteControlReadyOutput> {
ensure_supported_platform()?;
Daemon::from_environment()?
.ensure_remote_control_ready()
.await
}
pub async fn enable_remote_control_on_socket(
socket_path: &Path,
connect_timeout: Duration,
connect_retry_delay: Duration,
) -> Result<RemoteControlReadyStatus> {
ensure_supported_platform()?;
remote_control_client::enable_remote_control_with_connect_retry(
socket_path,
connect_timeout,
connect_retry_delay,
)
.await
}
pub async fn set_remote_control(mode: RemoteControlMode) -> Result<RemoteControlOutput> {
ensure_supported_platform()?;
Daemon::from_environment()?.set_remote_control(mode).await
@@ -294,39 +248,33 @@ impl Daemon {
async fn start(&self) -> Result<LifecycleOutput> {
let settings = self.load_settings().await?;
if let Ok(info) = client::probe(&self.socket_path).await {
return Ok(self
.output(
LifecycleStatus::AlreadyRunning,
self.running_backend(&settings).await?,
/*pid*/ None,
Some(info.app_server_version),
)
.await);
return Ok(self.output(
LifecycleStatus::AlreadyRunning,
self.running_backend(&settings).await?,
/*pid*/ None,
Some(info.app_server_version),
));
}
if self.running_backend_instance(&settings).await?.is_some() {
let info = self.wait_until_ready().await?;
return Ok(self
.output(
LifecycleStatus::AlreadyRunning,
Some(BackendKind::Pid),
/*pid*/ None,
Some(info.app_server_version),
)
.await);
return Ok(self.output(
LifecycleStatus::AlreadyRunning,
Some(BackendKind::Pid),
/*pid*/ None,
Some(info.app_server_version),
));
}
self.ensure_managed_codex_bin()?;
let pid = self.start_managed_backend(&settings).await?;
let info = self.wait_until_ready().await?;
Ok(self
.output(
LifecycleStatus::Started,
Some(BackendKind::Pid),
pid,
Some(info.app_server_version),
)
.await)
Ok(self.output(
LifecycleStatus::Started,
Some(BackendKind::Pid),
pid,
Some(info.app_server_version),
))
}
async fn restart(&self) -> Result<LifecycleOutput> {
@@ -346,14 +294,12 @@ impl Daemon {
let pid = self.start_managed_backend(&settings).await?;
let info = self.wait_until_ready().await?;
Ok(self
.output(
LifecycleStatus::Restarted,
Some(BackendKind::Pid),
pid,
Some(info.app_server_version),
)
.await)
Ok(self.output(
LifecycleStatus::Restarted,
Some(BackendKind::Pid),
pid,
Some(info.app_server_version),
))
}
#[cfg(unix)]
@@ -406,14 +352,12 @@ impl Daemon {
let settings = self.load_settings().await?;
if let Some(backend) = self.running_backend_instance(&settings).await? {
backend.stop().await?;
return Ok(self
.output(
LifecycleStatus::Stopped,
Some(BackendKind::Pid),
/*pid*/ None,
/*app_server_version*/ None,
)
.await);
return Ok(self.output(
LifecycleStatus::Stopped,
Some(BackendKind::Pid),
/*pid*/ None,
/*app_server_version*/ None,
));
}
if client::probe(&self.socket_path).await.is_ok() {
@@ -422,27 +366,23 @@ impl Daemon {
));
}
Ok(self
.output(
LifecycleStatus::NotRunning,
/*backend*/ None,
/*pid*/ None,
/*app_server_version*/ None,
)
.await)
Ok(self.output(
LifecycleStatus::NotRunning,
/*backend*/ None,
/*pid*/ None,
/*app_server_version*/ None,
))
}
async fn version(&self) -> Result<LifecycleOutput> {
let settings = self.load_settings().await?;
let info = client::probe(&self.socket_path).await?;
Ok(self
.output(
LifecycleStatus::Running,
self.running_backend(&settings).await?,
/*pid*/ None,
Some(info.app_server_version),
)
.await)
Ok(self.output(
LifecycleStatus::Running,
self.running_backend(&settings).await?,
/*pid*/ None,
Some(info.app_server_version),
))
}
async fn wait_until_ready(&self) -> Result<client::ProbeInfo> {
@@ -455,34 +395,17 @@ impl Daemon {
sleep(START_POLL_INTERVAL).await;
}
Err(err) => {
let context = self.app_server_not_ready_context().await;
return Err(err).context(context);
return Err(err).with_context(|| {
format!(
"app server did not become ready on {}",
self.socket_path.display()
)
});
}
}
}
}
async fn app_server_not_ready_context(&self) -> String {
let mut context = format!(
"app server did not become ready on {}",
self.socket_path.display()
);
self.append_daemon_app_server_context(&mut context).await;
backend::append_stderr_log_tail_context(&self.pid_file, &mut context).await;
context
}
async fn append_daemon_app_server_context(&self, context: &mut String) {
let managed_codex_version = self
.managed_codex_version_best_effort()
.await
.unwrap_or_else(|| "unknown".to_string());
context.push_str(&format!(
"\n\nDaemon used app-server:\n path: {}\n version: {managed_codex_version}",
self.managed_codex_bin.display()
));
}
async fn bootstrap(&self, options: BootstrapOptions) -> Result<BootstrapOutput> {
let _operation_lock = self.acquire_operation_lock().await?;
self.bootstrap_locked(options).await
@@ -507,16 +430,6 @@ impl Daemon {
Ok(RemoteControlStartOutput::Bootstrap(output))
}
async fn ensure_remote_control_ready(&self) -> Result<RemoteControlReadyOutput> {
let daemon = self.ensure_remote_control_started().await?;
let remote_control =
remote_control_client::enable_remote_control(&self.socket_path).await?;
Ok(RemoteControlReadyOutput {
daemon,
remote_control,
})
}
async fn set_remote_control(&self, mode: RemoteControlMode) -> Result<RemoteControlOutput> {
let _operation_lock = self.acquire_operation_lock().await?;
self.set_remote_control_locked(mode).await
@@ -599,14 +512,12 @@ impl Daemon {
updater.start().await?;
let info = self.wait_until_ready().await?;
let managed_codex_version = self.managed_codex_version_best_effort().await;
Ok(BootstrapOutput {
status: BootstrapStatus::Bootstrapped,
backend: BackendKind::Pid,
auto_update_enabled: true,
remote_control_enabled: settings.remote_control_enabled,
managed_codex_path: self.managed_codex_bin.clone(),
managed_codex_version,
socket_path: self.socket_path.clone(),
cli_version: env!("CARGO_PKG_VERSION").to_string(),
app_server_version: info.app_server_version,
@@ -666,16 +577,6 @@ impl Daemon {
))
}
#[cfg(unix)]
async fn managed_codex_version_best_effort(&self) -> Option<String> {
managed_codex_version(&self.managed_codex_bin).await.ok()
}
#[cfg(not(unix))]
async fn managed_codex_version_best_effort(&self) -> Option<String> {
None
}
fn backend_paths(&self, settings: &DaemonSettings) -> BackendPaths {
self.backend_paths_with_bin(settings, &self.managed_codex_bin)
}
@@ -735,20 +636,17 @@ impl Daemon {
})
}
async fn output(
fn output(
&self,
status: LifecycleStatus,
backend: Option<BackendKind>,
pid: Option<u32>,
app_server_version: Option<String>,
) -> LifecycleOutput {
let managed_codex_version = self.managed_codex_version_best_effort().await;
LifecycleOutput {
status,
backend,
pid,
managed_codex_path: self.managed_codex_bin.clone(),
managed_codex_version,
socket_path: self.socket_path.clone(),
cli_version: Some(env!("CARGO_PKG_VERSION").to_string()),
app_server_version,
@@ -837,12 +735,10 @@ fn try_lock_file(_file: &tokio::fs::File) -> Result<bool> {
#[cfg(all(test, unix))]
mod tests {
use pretty_assertions::assert_eq;
use tempfile::TempDir;
use super::BackendKind;
use super::BootstrapOutput;
use super::BootstrapStatus;
use super::Daemon;
use super::LifecycleOutput;
use super::LifecycleStatus;
use super::RemoteControlStartOutput;
@@ -855,6 +751,22 @@ mod tests {
use super::should_reexec_updater;
use crate::client::ProbeInfo;
#[test]
fn lifecycle_status_uses_camel_case_json() {
assert_eq!(
serde_json::to_string(&LifecycleStatus::AlreadyRunning).expect("serialize"),
"\"alreadyRunning\""
);
}
#[test]
fn bootstrap_status_uses_camel_case_json() {
assert_eq!(
serde_json::to_string(&BootstrapStatus::Bootstrapped).expect("serialize"),
"\"bootstrapped\""
);
}
#[test]
fn remote_control_status_uses_camel_case_json() {
assert_eq!(
@@ -935,26 +847,12 @@ mod tests {
status: LifecycleStatus::AlreadyRunning,
backend: Some(BackendKind::Pid),
pid: None,
managed_codex_path: "codex".into(),
managed_codex_version: Some("1.2.3".to_string()),
socket_path: "codex.sock".into(),
cli_version: Some("1.2.3".to_string()),
app_server_version: Some("1.2.4".to_string()),
};
let output = RemoteControlStartOutput::Start(lifecycle_output.clone());
assert_eq!(
serde_json::to_value(&lifecycle_output).expect("serialize"),
serde_json::json!({
"status": "alreadyRunning",
"backend": "pid",
"managedCodexPath": "codex",
"managedCodexVersion": "1.2.3",
"socketPath": "codex.sock",
"cliVersion": "1.2.3",
"appServerVersion": "1.2.4",
})
);
assert_eq!(
serde_json::to_value(output).expect("serialize"),
serde_json::to_value(lifecycle_output).expect("serialize")
@@ -966,59 +864,15 @@ mod tests {
auto_update_enabled: true,
remote_control_enabled: true,
managed_codex_path: "codex".into(),
managed_codex_version: Some("1.2.3".to_string()),
socket_path: "codex.sock".into(),
cli_version: "1.2.3".to_string(),
app_server_version: "1.2.4".to_string(),
};
let output = RemoteControlStartOutput::Bootstrap(bootstrap_output.clone());
assert_eq!(
serde_json::to_value(&bootstrap_output).expect("serialize"),
serde_json::json!({
"status": "bootstrapped",
"backend": "pid",
"autoUpdateEnabled": true,
"remoteControlEnabled": true,
"managedCodexPath": "codex",
"managedCodexVersion": "1.2.3",
"socketPath": "codex.sock",
"cliVersion": "1.2.3",
"appServerVersion": "1.2.4",
})
);
assert_eq!(
serde_json::to_value(output).expect("serialize"),
serde_json::to_value(bootstrap_output).expect("serialize")
);
}
#[tokio::test]
async fn not_ready_context_reports_daemon_app_server_before_stderr() {
let temp_dir = TempDir::new().expect("temp dir");
let daemon = Daemon {
socket_path: temp_dir.path().join("app-server-control.sock"),
pid_file: temp_dir.path().join("app-server.pid"),
update_pid_file: temp_dir.path().join("app-server-updater.pid"),
operation_lock_file: temp_dir.path().join("daemon.lock"),
settings_file: temp_dir.path().join("settings.json"),
managed_codex_bin: temp_dir.path().join("missing-codex"),
};
let stderr_log = daemon.pid_file.with_extension("stderr.log");
tokio::fs::write(&stderr_log, "unexpected argument")
.await
.expect("write stderr log");
assert_eq!(
daemon.app_server_not_ready_context().await,
format!(
"app server did not become ready on {}\n\n\
Daemon used app-server:\n path: {}\n version: unknown\n\n\
Managed app-server stderr ({}):\n unexpected argument",
daemon.socket_path.display(),
daemon.managed_codex_bin.display(),
stderr_log.display()
)
);
}
}

View File

@@ -1,459 +0,0 @@
use std::path::Path;
use std::time::Duration;
use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::JSONRPCRequest;
use codex_app_server_protocol::RemoteControlConnectionStatus;
use codex_app_server_protocol::RemoteControlEnableResponse;
use codex_app_server_protocol::RemoteControlStatusChangedNotification;
use codex_app_server_protocol::RequestId;
use tokio::io::AsyncRead;
use tokio::io::AsyncWrite;
use tokio::time::Instant;
use tokio::time::sleep;
use tokio::time::timeout;
use tokio_tungstenite::WebSocketStream;
use crate::RemoteControlReadyStatus;
use crate::client;
const REMOTE_CONTROL_READY_TIMEOUT: Duration = Duration::from_secs(10);
const REMOTE_CONTROL_ENABLE_REQUEST_ID: RequestId = RequestId::Integer(2);
pub(crate) async fn enable_remote_control(socket_path: &Path) -> Result<RemoteControlReadyStatus> {
let mut websocket = client::connect(socket_path).await?;
enable_remote_control_with_timeout(&mut websocket, REMOTE_CONTROL_READY_TIMEOUT).await
}
pub(crate) async fn enable_remote_control_with_connect_retry(
socket_path: &Path,
connect_timeout: Duration,
connect_retry_delay: Duration,
) -> Result<RemoteControlReadyStatus> {
let mut websocket =
connect_with_retry(socket_path, connect_timeout, connect_retry_delay).await?;
enable_remote_control_with_timeout(&mut websocket, REMOTE_CONTROL_READY_TIMEOUT).await
}
async fn enable_remote_control_with_timeout<S>(
websocket: &mut WebSocketStream<S>,
ready_timeout: Duration,
) -> Result<RemoteControlReadyStatus>
where
S: AsyncRead + AsyncWrite + Unpin,
{
client::initialize(websocket, /*experimental_api*/ true).await?;
let initialized = JSONRPCMessage::Notification(JSONRPCNotification {
method: "initialized".to_string(),
params: None,
});
client::send_message(websocket, &initialized)
.await
.context("failed to send initialized notification")?;
let enable = JSONRPCMessage::Request(JSONRPCRequest {
id: REMOTE_CONTROL_ENABLE_REQUEST_ID,
method: "remoteControl/enable".to_string(),
params: None,
trace: None,
});
client::send_message(websocket, &enable)
.await
.context("failed to send remoteControl/enable request")?;
let mut latest = read_enable_response(websocket).await?;
if latest.status == RemoteControlConnectionStatus::Connecting {
latest = wait_for_remote_control_status(websocket, latest, ready_timeout).await?;
}
websocket.close(None).await.ok();
Ok(latest)
}
async fn connect_with_retry(
socket_path: &Path,
connect_timeout: Duration,
connect_retry_delay: Duration,
) -> Result<WebSocketStream<codex_uds::UnixStream>> {
let deadline = Instant::now() + connect_timeout;
loop {
match client::connect(socket_path).await {
Ok(websocket) => return Ok(websocket),
Err(_) if Instant::now() < deadline => {
sleep(connect_retry_delay).await;
}
Err(error) => {
return Err(error).with_context(|| {
format!(
"app server did not become ready on {}",
socket_path.display()
)
});
}
}
}
}
async fn read_enable_response<S>(
websocket: &mut WebSocketStream<S>,
) -> Result<RemoteControlReadyStatus>
where
S: AsyncRead + AsyncWrite + Unpin,
{
loop {
let message = timeout(
client::CONTROL_SOCKET_RESPONSE_TIMEOUT,
client::read_message(websocket),
)
.await
.context("timed out waiting for remoteControl/enable response")??;
match message {
JSONRPCMessage::Response(response)
if response.id == REMOTE_CONTROL_ENABLE_REQUEST_ID =>
{
let response =
serde_json::from_value::<RemoteControlEnableResponse>(response.result)
.context("failed to parse remoteControl/enable response")?;
return Ok(RemoteControlReadyStatus::from(response));
}
JSONRPCMessage::Error(err) if err.id == REMOTE_CONTROL_ENABLE_REQUEST_ID => {
return Err(anyhow!(
"remoteControl/enable failed: {}",
err.error.message
));
}
JSONRPCMessage::Notification(notification)
if remote_control_status_notification(&notification).is_some() =>
{
continue;
}
_ => {}
}
}
}
async fn wait_for_remote_control_status<S>(
websocket: &mut WebSocketStream<S>,
mut latest: RemoteControlReadyStatus,
ready_timeout: Duration,
) -> Result<RemoteControlReadyStatus>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let deadline = tokio::time::Instant::now() + ready_timeout;
while tokio::time::Instant::now() < deadline {
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
let message = match timeout(remaining, client::read_message(websocket)).await {
Ok(Ok(message)) => message,
Ok(Err(err)) => return Err(err),
Err(_) => {
latest.timed_out = true;
return Ok(latest);
}
};
let JSONRPCMessage::Notification(notification) = message else {
continue;
};
let Some(status) = remote_control_status_notification(&notification) else {
continue;
};
latest = RemoteControlReadyStatus::from(status);
if latest.status != RemoteControlConnectionStatus::Connecting {
return Ok(latest);
}
}
latest.timed_out = true;
Ok(latest)
}
fn remote_control_status_notification(
notification: &JSONRPCNotification,
) -> Option<RemoteControlStatusChangedNotification> {
if notification.method != "remoteControl/status/changed" {
return None;
}
let params = notification.params.clone()?;
serde_json::from_value(params).ok()
}
impl From<RemoteControlEnableResponse> for RemoteControlReadyStatus {
fn from(response: RemoteControlEnableResponse) -> Self {
let RemoteControlEnableResponse {
status,
server_name,
installation_id: _,
environment_id,
} = response;
Self {
status,
server_name,
environment_id,
timed_out: false,
}
}
}
impl From<RemoteControlStatusChangedNotification> for RemoteControlReadyStatus {
fn from(notification: RemoteControlStatusChangedNotification) -> Self {
let RemoteControlStatusChangedNotification {
status,
server_name,
installation_id: _,
environment_id,
} = notification;
Self {
status,
server_name,
environment_id,
timed_out: false,
}
}
}
#[cfg(all(test, unix))]
mod tests {
use anyhow::Result;
use codex_app_server_protocol::JSONRPCResponse;
use codex_uds::UnixListener;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
use tokio_tungstenite::accept_async;
use super::*;
const INITIALIZE_REQUEST_ID: RequestId = RequestId::Integer(1);
const TEST_INSTALLATION_ID: &str = "11111111-1111-4111-8111-111111111111";
const TEST_SERVER_NAME: &str = "owen-mbp";
const TEST_CODEX_HOME: &str = "/tmp/codex-home";
#[tokio::test]
async fn enable_remote_control_uses_connected_enable_response_without_later_notification()
-> Result<()> {
let status = run_enable_remote_control_scenario(EnableScenario {
initial_notification: Some(remote_control_status(
RemoteControlConnectionStatus::Connected,
Some("env_test"),
)),
enable_response: remote_control_status(
RemoteControlConnectionStatus::Connected,
Some("env_test"),
),
after_enable_notification: None,
ready_timeout: Duration::from_millis(20),
})
.await?;
assert_eq!(
status,
RemoteControlReadyStatus {
status: RemoteControlConnectionStatus::Connected,
server_name: TEST_SERVER_NAME.to_string(),
environment_id: Some("env_test".to_string()),
timed_out: false,
}
);
Ok(())
}
#[tokio::test]
async fn enable_remote_control_waits_for_connected_notification() -> Result<()> {
let status = run_enable_remote_control_scenario(EnableScenario {
initial_notification: None,
enable_response: remote_control_status(
RemoteControlConnectionStatus::Connecting,
/*environment_id*/ None,
),
after_enable_notification: Some(remote_control_status(
RemoteControlConnectionStatus::Connected,
Some("env_test"),
)),
ready_timeout: Duration::from_secs(1),
})
.await?;
assert_eq!(
status,
RemoteControlReadyStatus {
status: RemoteControlConnectionStatus::Connected,
server_name: TEST_SERVER_NAME.to_string(),
environment_id: Some("env_test".to_string()),
timed_out: false,
}
);
Ok(())
}
#[tokio::test]
async fn enable_remote_control_reports_connecting_after_timeout() -> Result<()> {
let status = run_enable_remote_control_scenario(EnableScenario {
initial_notification: None,
enable_response: remote_control_status(
RemoteControlConnectionStatus::Connecting,
/*environment_id*/ None,
),
after_enable_notification: None,
ready_timeout: Duration::from_millis(20),
})
.await?;
assert_eq!(
status,
RemoteControlReadyStatus {
status: RemoteControlConnectionStatus::Connecting,
server_name: TEST_SERVER_NAME.to_string(),
environment_id: None,
timed_out: true,
}
);
Ok(())
}
#[tokio::test]
async fn enable_remote_control_returns_errored_enable_response() -> Result<()> {
let status = run_enable_remote_control_scenario(EnableScenario {
initial_notification: None,
enable_response: remote_control_status(
RemoteControlConnectionStatus::Errored,
/*environment_id*/ None,
),
after_enable_notification: None,
ready_timeout: Duration::from_millis(20),
})
.await?;
assert_eq!(
status,
RemoteControlReadyStatus {
status: RemoteControlConnectionStatus::Errored,
server_name: TEST_SERVER_NAME.to_string(),
environment_id: None,
timed_out: false,
}
);
Ok(())
}
struct EnableScenario {
initial_notification: Option<RemoteControlStatusChangedNotification>,
enable_response: RemoteControlStatusChangedNotification,
after_enable_notification: Option<RemoteControlStatusChangedNotification>,
ready_timeout: Duration,
}
async fn run_enable_remote_control_scenario(
scenario: EnableScenario,
) -> Result<RemoteControlReadyStatus> {
let dir = TempDir::new()?;
let socket_path = dir.path().join("app-server.sock");
let listener = UnixListener::bind(&socket_path).await?;
let ready_timeout = scenario.ready_timeout;
let server_task = tokio::spawn(serve_enable_remote_control_scenario(listener, scenario));
let mut websocket = client::connect(&socket_path).await?;
let status = enable_remote_control_with_timeout(&mut websocket, ready_timeout).await?;
server_task.await??;
Ok(status)
}
async fn serve_enable_remote_control_scenario(
mut listener: UnixListener,
scenario: EnableScenario,
) -> Result<()> {
let stream = listener.accept().await?;
let mut websocket = accept_async(stream).await?;
let initialize = client::read_message(&mut websocket).await?;
let JSONRPCMessage::Request(initialize) = initialize else {
panic!("expected initialize request");
};
assert_eq!(initialize.id, INITIALIZE_REQUEST_ID);
assert_eq!(initialize.method, "initialize");
let Some(initialize_params) = initialize.params else {
panic!("expected initialize params");
};
assert_eq!(
initialize_params["capabilities"]["experimentalApi"],
serde_json::Value::Bool(true)
);
client::send_message(
&mut websocket,
&JSONRPCMessage::Response(JSONRPCResponse {
id: INITIALIZE_REQUEST_ID,
result: serde_json::json!({
"userAgent": "codex_app_server/1.2.3",
"codexHome": TEST_CODEX_HOME,
"platformFamily": "unix",
"platformOs": "macos",
}),
}),
)
.await?;
let initialized = client::read_message(&mut websocket).await?;
let JSONRPCMessage::Notification(initialized) = initialized else {
panic!("expected initialized notification");
};
assert_eq!(initialized.method, "initialized");
if let Some(status) = scenario.initial_notification {
send_remote_control_status(&mut websocket, status).await?;
}
let enable = client::read_message(&mut websocket).await?;
let JSONRPCMessage::Request(enable) = enable else {
panic!("expected remoteControl/enable request");
};
assert_eq!(enable.id, REMOTE_CONTROL_ENABLE_REQUEST_ID);
assert_eq!(enable.method, "remoteControl/enable");
client::send_message(
&mut websocket,
&JSONRPCMessage::Response(JSONRPCResponse {
id: REMOTE_CONTROL_ENABLE_REQUEST_ID,
result: serde_json::to_value(RemoteControlEnableResponse::from(
scenario.enable_response,
))?,
}),
)
.await?;
if let Some(status) = scenario.after_enable_notification {
send_remote_control_status(&mut websocket, status).await?;
} else {
tokio::time::sleep(Duration::from_millis(50)).await;
}
Ok(())
}
async fn send_remote_control_status<S>(
websocket: &mut WebSocketStream<S>,
status: RemoteControlStatusChangedNotification,
) -> Result<()>
where
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
{
client::send_message(
websocket,
&JSONRPCMessage::Notification(JSONRPCNotification {
method: "remoteControl/status/changed".to_string(),
params: Some(serde_json::to_value(status)?),
}),
)
.await
}
fn remote_control_status(
status: RemoteControlConnectionStatus,
environment_id: Option<&str>,
) -> RemoteControlStatusChangedNotification {
RemoteControlStatusChangedNotification {
status,
server_name: TEST_SERVER_NAME.to_string(),
installation_id: TEST_INSTALLATION_ID.to_string(),
environment_id: environment_id.map(str::to_string),
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -277,7 +277,7 @@
"enum": [
"read",
"write",
"deny"
"none"
],
"type": "string"
},

View File

@@ -62,7 +62,7 @@
"enum": [
"read",
"write",
"deny"
"none"
],
"type": "string"
},

View File

@@ -62,7 +62,7 @@
"enum": [
"read",
"write",
"deny"
"none"
],
"type": "string"
},

View File

@@ -29,7 +29,6 @@
"type": "object"
},
"AccountRateLimitsUpdatedNotification": {
"description": "Sparse rolling rate-limit update.\n\nClients should merge available values into the most recent `account/rateLimits/read` response or refetch that snapshot. Nullable account metadata may be unavailable in a rolling update and does not clear a previously observed value.",
"properties": {
"rateLimits": {
"$ref": "#/definitions/RateLimitSnapshot"
@@ -65,26 +64,6 @@
},
"type": "object"
},
"ActivePermissionProfile": {
"properties": {
"extends": {
"default": null,
"description": "Parent profile identifier from the selected permissions profile's `extends` setting, when present.",
"type": [
"string",
"null"
]
},
"id": {
"description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.<id>]` profile.",
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"AdditionalFileSystemPermissions": {
"properties": {
"entries": {
@@ -436,65 +415,6 @@
],
"type": "object"
},
"ApprovalsReviewer": {
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.",
"enum": [
"user",
"auto_review",
"guardian_subagent"
],
"type": "string"
},
"AskForApproval": {
"oneOf": [
{
"enum": [
"untrusted",
"on-failure",
"on-request",
"never"
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"granular": {
"properties": {
"mcp_elicitations": {
"type": "boolean"
},
"request_permissions": {
"default": false,
"type": "boolean"
},
"rules": {
"type": "boolean"
},
"sandbox_approval": {
"type": "boolean"
},
"skill_approval": {
"default": false,
"type": "boolean"
}
},
"required": [
"mcp_elicitations",
"rules",
"sandbox_approval"
],
"type": "object"
}
},
"required": [
"granular"
],
"title": "GranularAskForApproval",
"type": "object"
}
]
},
"AuthMode": {
"description": "Authentication mode for OpenAI-backed providers.",
"oneOf": [
@@ -738,22 +658,6 @@
],
"type": "string"
},
"CollaborationMode": {
"description": "Collaboration mode for a Codex session.",
"properties": {
"mode": {
"$ref": "#/definitions/ModeKind"
},
"settings": {
"$ref": "#/definitions/Settings"
}
},
"required": [
"mode",
"settings"
],
"type": "object"
},
"CommandAction": {
"oneOf": [
{
@@ -1181,7 +1085,7 @@
"enum": [
"read",
"write",
"deny"
"none"
],
"type": "string"
},
@@ -1836,8 +1740,6 @@
"postCompact",
"sessionStart",
"userPromptSubmit",
"subagentStart",
"subagentStop",
"stop"
],
"type": "string"
@@ -2003,7 +1905,6 @@
"sessionFlags",
"plugin",
"cloudRequirements",
"cloudManagedConfig",
"legacyManagedConfigFile",
"legacyManagedConfigMdm",
"unknown"
@@ -2031,15 +1932,6 @@
],
"type": "object"
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
"type": "string"
},
"ItemCompletedNotification": {
"properties": {
"completedAtMs": {
@@ -2358,14 +2250,6 @@
}
]
},
"ModeKind": {
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"default"
],
"type": "string"
},
"ModelRerouteReason": {
"enum": [
"highRiskCyberActivity"
@@ -2427,13 +2311,6 @@
],
"type": "object"
},
"NetworkAccess": {
"enum": [
"restricted",
"enabled"
],
"type": "string"
},
"NetworkApprovalProtocol": {
"enum": [
"http",
@@ -2517,14 +2394,6 @@
}
]
},
"Personality": {
"enum": [
"none",
"friendly",
"pragmatic"
],
"type": "string"
},
"PlanDeltaNotification": {
"description": "EXPERIMENTAL - proposed plan streaming deltas for plan items. Clients should not assume concatenated deltas match the completed plan item content.",
"properties": {
@@ -2678,16 +2547,6 @@
}
]
},
"individualLimit": {
"anyOf": [
{
"$ref": "#/definitions/SpendControlLimitSnapshot"
},
{
"type": "null"
}
]
},
"limitId": {
"type": [
"string",
@@ -2788,26 +2647,6 @@
],
"type": "string"
},
"ReasoningSummary": {
"description": "A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries",
"oneOf": [
{
"enum": [
"auto",
"concise",
"detailed"
],
"type": "string"
},
{
"description": "Option to disable reasoning summaries.",
"enum": [
"none"
],
"type": "string"
}
]
},
"ReasoningSummaryPartAddedNotification": {
"properties": {
"itemId": {
@@ -2909,16 +2748,12 @@
"installationId": {
"type": "string"
},
"serverName": {
"type": "string"
},
"status": {
"$ref": "#/definitions/RemoteControlConnectionStatus"
}
},
"required": [
"installationId",
"serverName",
"status"
],
"type": "object"
@@ -2960,105 +2795,6 @@
},
"type": "object"
},
"SandboxPolicy": {
"oneOf": [
{
"properties": {
"type": {
"enum": [
"dangerFullAccess"
],
"title": "DangerFullAccessSandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "DangerFullAccessSandboxPolicy",
"type": "object"
},
{
"properties": {
"networkAccess": {
"default": false,
"type": "boolean"
},
"type": {
"enum": [
"readOnly"
],
"title": "ReadOnlySandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ReadOnlySandboxPolicy",
"type": "object"
},
{
"properties": {
"networkAccess": {
"allOf": [
{
"$ref": "#/definitions/NetworkAccess"
}
],
"default": "restricted"
},
"type": {
"enum": [
"externalSandbox"
],
"title": "ExternalSandboxSandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ExternalSandboxSandboxPolicy",
"type": "object"
},
{
"properties": {
"excludeSlashTmp": {
"default": false,
"type": "boolean"
},
"excludeTmpdirEnvVar": {
"default": false,
"type": "boolean"
},
"networkAccess": {
"default": false,
"type": "boolean"
},
"type": {
"enum": [
"workspaceWrite"
],
"title": "WorkspaceWriteSandboxPolicyType",
"type": "string"
},
"writableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
}
},
"required": [
"type"
],
"title": "WorkspaceWriteSandboxPolicy",
"type": "object"
}
]
},
"ServerRequestResolvedNotification": {
"properties": {
"requestId": {
@@ -3114,63 +2850,10 @@
}
]
},
"Settings": {
"description": "Settings for a collaboration mode.",
"properties": {
"developer_instructions": {
"type": [
"string",
"null"
]
},
"model": {
"type": "string"
},
"reasoning_effort": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningEffort"
},
{
"type": "null"
}
]
}
},
"required": [
"model"
],
"type": "object"
},
"SkillsChangedNotification": {
"description": "Notification emitted when watched local skill files change.\n\nTreat this as an invalidation signal and re-run `skills/list` with the client's current parameters when refreshed skill metadata is needed.",
"type": "object"
},
"SpendControlLimitSnapshot": {
"properties": {
"limit": {
"type": "string"
},
"remainingPercent": {
"format": "int32",
"type": "integer"
},
"resetsAt": {
"format": "int64",
"type": "integer"
},
"used": {
"type": "string"
}
},
"required": [
"limit",
"remainingPercent",
"resetsAt",
"used"
],
"type": "object"
},
"SubAgentSource": {
"oneOf": [
{
@@ -3402,13 +3085,6 @@
"null"
]
},
"parentThreadId": {
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
"type": [
"string",
"null"
]
},
"path": {
"description": "[UNSTABLE] Path to the thread on disk.",
"type": [
@@ -3570,8 +3246,6 @@
"enum": [
"active",
"paused",
"blocked",
"usageLimited",
"budgetLimited",
"complete"
],
@@ -3605,12 +3279,6 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"
@@ -3913,12 +3581,6 @@
"null"
]
},
"pluginId": {
"type": [
"string",
"null"
]
},
"result": {
"anyOf": [
{
@@ -4472,102 +4134,6 @@
],
"type": "object"
},
"ThreadSettings": {
"properties": {
"activePermissionProfile": {
"anyOf": [
{
"$ref": "#/definitions/ActivePermissionProfile"
},
{
"type": "null"
}
]
},
"approvalPolicy": {
"$ref": "#/definitions/AskForApproval"
},
"approvalsReviewer": {
"$ref": "#/definitions/ApprovalsReviewer"
},
"collaborationMode": {
"$ref": "#/definitions/CollaborationMode"
},
"cwd": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"effort": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningEffort"
},
{
"type": "null"
}
]
},
"model": {
"type": "string"
},
"modelProvider": {
"type": "string"
},
"personality": {
"anyOf": [
{
"$ref": "#/definitions/Personality"
},
{
"type": "null"
}
]
},
"sandboxPolicy": {
"$ref": "#/definitions/SandboxPolicy"
},
"serviceTier": {
"type": [
"string",
"null"
]
},
"summary": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningSummary"
},
{
"type": "null"
}
]
}
},
"required": [
"approvalPolicy",
"approvalsReviewer",
"collaborationMode",
"cwd",
"model",
"modelProvider",
"sandboxPolicy"
],
"type": "object"
},
"ThreadSettingsUpdatedNotification": {
"properties": {
"threadId": {
"type": "string"
},
"threadSettings": {
"$ref": "#/definitions/ThreadSettings"
}
},
"required": [
"threadId",
"threadSettings"
],
"type": "object"
},
"ThreadSource": {
"enum": [
"user",
@@ -5023,17 +4589,6 @@
},
{
"properties": {
"detail": {
"anyOf": [
{
"$ref": "#/definitions/ImageDetail"
},
{
"type": "null"
}
],
"default": null
},
"type": {
"enum": [
"image"
@@ -5054,17 +4609,6 @@
},
{
"properties": {
"detail": {
"anyOf": [
{
"$ref": "#/definitions/ImageDetail"
},
{
"type": "null"
}
],
"default": null
},
"path": {
"type": "string"
},
@@ -5509,26 +5053,6 @@
"title": "Thread/goal/clearedNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/settings/updated"
],
"title": "Thread/settings/updatedNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadSettingsUpdatedNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/settings/updatedNotification",
"type": "object"
},
{
"properties": {
"method": {

View File

@@ -631,7 +631,7 @@
"enum": [
"read",
"write",
"deny"
"none"
],
"type": "string"
},

View File

@@ -61,16 +61,6 @@
}
]
},
"individualLimit": {
"anyOf": [
{
"$ref": "#/definitions/SpendControlLimitSnapshot"
},
{
"type": "null"
}
]
},
"limitId": {
"type": [
"string",
@@ -151,34 +141,8 @@
"usedPercent"
],
"type": "object"
},
"SpendControlLimitSnapshot": {
"properties": {
"limit": {
"type": "string"
},
"remainingPercent": {
"format": "int32",
"type": "integer"
},
"resetsAt": {
"format": "int64",
"type": "integer"
},
"used": {
"type": "string"
}
},
"required": [
"limit",
"remainingPercent",
"resetsAt",
"used"
],
"type": "object"
}
},
"description": "Sparse rolling rate-limit update.\n\nClients should merge available values into the most recent `account/rateLimits/read` response or refetch that snapshot. Nullable account metadata may be unavailable in a rolling update and does not clear a previously observed value.",
"properties": {
"rateLimits": {
"$ref": "#/definitions/RateLimitSnapshot"

View File

@@ -27,6 +27,202 @@
],
"type": "object"
},
"FileSystemAccessMode": {
"enum": [
"read",
"write",
"none"
],
"type": "string"
},
"FileSystemPath": {
"oneOf": [
{
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"path"
],
"title": "PathFileSystemPathType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "PathFileSystemPath",
"type": "object"
},
{
"properties": {
"pattern": {
"type": "string"
},
"type": {
"enum": [
"glob_pattern"
],
"title": "GlobPatternFileSystemPathType",
"type": "string"
}
},
"required": [
"pattern",
"type"
],
"title": "GlobPatternFileSystemPath",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"special"
],
"title": "SpecialFileSystemPathType",
"type": "string"
},
"value": {
"$ref": "#/definitions/FileSystemSpecialPath"
}
},
"required": [
"type",
"value"
],
"title": "SpecialFileSystemPath",
"type": "object"
}
]
},
"FileSystemSandboxEntry": {
"properties": {
"access": {
"$ref": "#/definitions/FileSystemAccessMode"
},
"path": {
"$ref": "#/definitions/FileSystemPath"
}
},
"required": [
"access",
"path"
],
"type": "object"
},
"FileSystemSpecialPath": {
"oneOf": [
{
"properties": {
"kind": {
"enum": [
"root"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "RootFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"minimal"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "MinimalFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"project_roots"
],
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind"
],
"title": "KindFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"tmpdir"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "TmpdirFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"slash_tmp"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "SlashTmpFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"unknown"
],
"type": "string"
},
"path": {
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind",
"path"
],
"type": "object"
}
]
},
"NetworkAccess": {
"enum": [
"restricted",
@@ -34,6 +230,135 @@
],
"type": "string"
},
"PermissionProfile": {
"oneOf": [
{
"description": "Codex owns sandbox construction for this profile.",
"properties": {
"fileSystem": {
"$ref": "#/definitions/PermissionProfileFileSystemPermissions"
},
"network": {
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
},
"type": {
"enum": [
"managed"
],
"title": "ManagedPermissionProfileType",
"type": "string"
}
},
"required": [
"fileSystem",
"network",
"type"
],
"title": "ManagedPermissionProfile",
"type": "object"
},
{
"description": "Do not apply an outer sandbox.",
"properties": {
"type": {
"enum": [
"disabled"
],
"title": "DisabledPermissionProfileType",
"type": "string"
}
},
"required": [
"type"
],
"title": "DisabledPermissionProfile",
"type": "object"
},
{
"description": "Filesystem isolation is enforced by an external caller.",
"properties": {
"network": {
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
},
"type": {
"enum": [
"external"
],
"title": "ExternalPermissionProfileType",
"type": "string"
}
},
"required": [
"network",
"type"
],
"title": "ExternalPermissionProfile",
"type": "object"
}
]
},
"PermissionProfileFileSystemPermissions": {
"oneOf": [
{
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/FileSystemSandboxEntry"
},
"type": "array"
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"type": {
"enum": [
"restricted"
],
"title": "RestrictedPermissionProfileFileSystemPermissionsType",
"type": "string"
}
},
"required": [
"entries",
"type"
],
"title": "RestrictedPermissionProfileFileSystemPermissions",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"unrestricted"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
"type": "string"
}
},
"required": [
"type"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
"type": "object"
}
]
},
"PermissionProfileNetworkPermissions": {
"properties": {
"enabled": {
"type": "boolean"
}
},
"required": [
"enabled"
],
"type": "object"
},
"SandboxPolicy": {
"oneOf": [
{

View File

@@ -9,6 +9,7 @@
]
},
"includeLayers": {
"default": false,
"type": "boolean"
}
},

View File

@@ -19,16 +19,6 @@
},
"AppConfig": {
"properties": {
"approvals_reviewer": {
"anyOf": [
{
"$ref": "#/definitions/ApprovalsReviewer"
},
{
"type": "null"
}
]
},
"default_tools_approval_mode": {
"anyOf": [
{
@@ -198,25 +188,6 @@
}
]
},
"AutoCompactTokenLimitScope": {
"description": "Selects which part of the active context is charged against `model_auto_compact_token_limit`.",
"oneOf": [
{
"description": "Count the full active context against the limit.",
"enum": [
"total"
],
"type": "string"
},
{
"description": "Count sampled output and later growth after the carried window prefix.",
"enum": [
"body_after_prefix"
],
"type": "string"
}
]
},
"Config": {
"additionalProperties": true,
"properties": {
@@ -257,13 +228,6 @@
"null"
]
},
"desktop": {
"additionalProperties": true,
"type": [
"object",
"null"
]
},
"developer_instructions": {
"type": [
"string",
@@ -271,13 +235,9 @@
]
},
"forced_chatgpt_workspace_id": {
"anyOf": [
{
"$ref": "#/definitions/ForcedChatgptWorkspaceIds"
},
{
"type": "null"
}
"type": [
"string",
"null"
]
},
"forced_login_method": {
@@ -309,16 +269,6 @@
"null"
]
},
"model_auto_compact_token_limit_scope": {
"anyOf": [
{
"$ref": "#/definitions/AutoCompactTokenLimitScope"
},
{
"type": "null"
}
]
},
"model_context_window": {
"format": "int64",
"type": [
@@ -362,6 +312,19 @@
}
]
},
"profile": {
"type": [
"string",
"null"
]
},
"profiles": {
"additionalProperties": {
"$ref": "#/definitions/ProfileV2"
},
"default": {},
"type": "object"
},
"review_model": {
"type": [
"string",
@@ -508,33 +471,6 @@
"title": "SystemConfigLayerSource",
"type": "object"
},
{
"description": "Enterprise-managed config layer delivered by the cloud config bundle.",
"properties": {
"id": {
"description": "Stable identifier for the delivered layer.",
"type": "string"
},
"name": {
"description": "Admin-facing name for the delivered layer. This is surfaced in diagnostics so users know which cloud layer needs administrator attention.",
"type": "string"
},
"type": {
"enum": [
"enterpriseManaged"
],
"title": "EnterpriseManagedConfigLayerSourceType",
"type": "string"
}
},
"required": [
"id",
"name",
"type"
],
"title": "EnterpriseManagedConfigLayerSource",
"type": "object"
},
{
"description": "User config layer from $CODEX_HOME/config.toml. This layer is special in that it is expected to be: - writable by the user - generally outside the workspace directory",
"properties": {
@@ -645,20 +581,6 @@
}
]
},
"ForcedChatgptWorkspaceIds": {
"anyOf": [
{
"type": "string"
},
{
"items": {
"type": "string"
},
"type": "array"
}
],
"description": "Backward-compatible API shape for ChatGPT workspace login restrictions."
},
"ForcedLoginMethod": {
"enum": [
"chatgpt",
@@ -666,6 +588,107 @@
],
"type": "string"
},
"ProfileV2": {
"additionalProperties": true,
"properties": {
"approval_policy": {
"anyOf": [
{
"$ref": "#/definitions/AskForApproval"
},
{
"type": "null"
}
]
},
"approvals_reviewer": {
"anyOf": [
{
"$ref": "#/definitions/ApprovalsReviewer"
},
{
"type": "null"
}
],
"description": "[UNSTABLE] Optional profile-level override for where approval requests are routed for review. If omitted, the enclosing config default is used."
},
"chatgpt_base_url": {
"type": [
"string",
"null"
]
},
"model": {
"type": [
"string",
"null"
]
},
"model_provider": {
"type": [
"string",
"null"
]
},
"model_reasoning_effort": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningEffort"
},
{
"type": "null"
}
]
},
"model_reasoning_summary": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningSummary"
},
{
"type": "null"
}
]
},
"model_verbosity": {
"anyOf": [
{
"$ref": "#/definitions/Verbosity"
},
{
"type": "null"
}
]
},
"service_tier": {
"type": [
"string",
"null"
]
},
"tools": {
"anyOf": [
{
"$ref": "#/definitions/ToolsV2"
},
{
"type": "null"
}
]
},
"web_search": {
"anyOf": [
{
"$ref": "#/definitions/WebSearchMode"
},
{
"type": "null"
}
]
}
},
"type": "object"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [

View File

@@ -60,25 +60,8 @@
}
]
},
"ComputerUseRequirements": {
"properties": {
"allowLockedComputerUse": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"ConfigRequirements": {
"properties": {
"allowAppshots": {
"type": [
"boolean",
"null"
]
},
"allowManagedHooksOnly": {
"type": [
"boolean",
@@ -94,15 +77,6 @@
"null"
]
},
"allowedPermissions": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"allowedSandboxModes": {
"items": {
"$ref": "#/definitions/SandboxMode"
@@ -121,25 +95,6 @@
"null"
]
},
"allowedWindowsSandboxImplementations": {
"items": {
"$ref": "#/definitions/WindowsSandboxSetupMode"
},
"type": [
"array",
"null"
]
},
"computerUse": {
"anyOf": [
{
"$ref": "#/definitions/ComputerUseRequirements"
},
{
"type": "null"
}
]
},
"enforceResidency": {
"anyOf": [
{
@@ -306,18 +261,6 @@
},
"type": "array"
},
"SubagentStart": {
"items": {
"$ref": "#/definitions/ConfiguredHookMatcherGroup"
},
"type": "array"
},
"SubagentStop": {
"items": {
"$ref": "#/definitions/ConfiguredHookMatcherGroup"
},
"type": "array"
},
"UserPromptSubmit": {
"items": {
"$ref": "#/definitions/ConfiguredHookMatcherGroup"
@@ -345,8 +288,6 @@
"PreToolUse",
"SessionStart",
"Stop",
"SubagentStart",
"SubagentStop",
"UserPromptSubmit"
],
"type": "object"
@@ -469,7 +410,7 @@
"NetworkUnixSocketPermission": {
"enum": [
"allow",
"deny"
"none"
],
"type": "string"
},
@@ -494,13 +435,6 @@
"live"
],
"type": "string"
},
"WindowsSandboxSetupMode": {
"enum": [
"elevated",
"unelevated"
],
"type": "string"
}
},
"properties": {

View File

@@ -73,33 +73,6 @@
"title": "SystemConfigLayerSource",
"type": "object"
},
{
"description": "Enterprise-managed config layer delivered by the cloud config bundle.",
"properties": {
"id": {
"description": "Stable identifier for the delivered layer.",
"type": "string"
},
"name": {
"description": "Admin-facing name for the delivered layer. This is surfaced in diagnostics so users know which cloud layer needs administrator attention.",
"type": "string"
},
"type": {
"enum": [
"enterpriseManaged"
],
"title": "EnterpriseManagedConfigLayerSourceType",
"type": "string"
}
},
"required": [
"id",
"name",
"type"
],
"title": "EnterpriseManagedConfigLayerSource",
"type": "object"
},
{
"description": "User config layer from $CODEX_HOME/config.toml. This layer is special in that it is expected to be: - writable by the user - generally outside the workspace directory",
"properties": {

View File

@@ -16,13 +16,6 @@
"integer",
"null"
]
},
"threadId": {
"description": "Optional loaded thread id. Pass this when showing feature state for an existing thread so enablement is computed from that thread's refreshed config, including project-local config for the thread's cwd.",
"type": [
"string",
"null"
]
}
},
"title": "ExperimentalFeatureListParams",

View File

@@ -39,7 +39,8 @@
}
},
"required": [
"classification"
"classification",
"includeLogs"
],
"title": "FeedbackUploadParams",
"type": "object"

View File

@@ -2,6 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"refreshToken": {
"default": false,
"description": "When `true`, requests a proactive token refresh before returning.\n\nIn managed auth mode this triggers the normal refresh-token flow. In external auth mode this flag is ignored. Clients should refresh tokens themselves and call `account/login/start` with `chatgptAuthTokens`.",
"type": "boolean"
}

View File

@@ -61,16 +61,6 @@
}
]
},
"individualLimit": {
"anyOf": [
{
"$ref": "#/definitions/SpendControlLimitSnapshot"
},
{
"type": "null"
}
]
},
"limitId": {
"type": [
"string",
@@ -151,31 +141,6 @@
"usedPercent"
],
"type": "object"
},
"SpendControlLimitSnapshot": {
"properties": {
"limit": {
"type": "string"
},
"remainingPercent": {
"format": "int32",
"type": "integer"
},
"resetsAt": {
"format": "int64",
"type": "integer"
},
"used": {
"type": "string"
}
},
"required": [
"limit",
"remainingPercent",
"resetsAt",
"used"
],
"type": "object"
}
},
"properties": {

View File

@@ -14,8 +14,6 @@
"postCompact",
"sessionStart",
"userPromptSubmit",
"subagentStart",
"subagentStop",
"stop"
],
"type": "string"
@@ -166,7 +164,6 @@
"sessionFlags",
"plugin",
"cloudRequirements",
"cloudManagedConfig",
"legacyManagedConfigFile",
"legacyManagedConfigMdm",
"unknown"

View File

@@ -14,8 +14,6 @@
"postCompact",
"sessionStart",
"userPromptSubmit",
"subagentStart",
"subagentStop",
"stop"
],
"type": "string"
@@ -166,7 +164,6 @@
"sessionFlags",
"plugin",
"cloudRequirements",
"cloudManagedConfig",
"legacyManagedConfigFile",
"legacyManagedConfigMdm",
"unknown"

View File

@@ -29,8 +29,6 @@
"postCompact",
"sessionStart",
"userPromptSubmit",
"subagentStart",
"subagentStop",
"stop"
],
"type": "string"
@@ -130,7 +128,6 @@
"sessionFlags",
"plugin",
"cloudRequirements",
"cloudManagedConfig",
"legacyManagedConfigFile",
"legacyManagedConfigMdm",
"unknown"

View File

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

View File

@@ -1,13 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"state": {
"type": [
"string",
"null"
]
}
},
"title": "HttpStateGetResponse",
"type": "object"
}

View File

@@ -1,20 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"expectedState": {
"description": "When present, write only if the calling surface still stores this state.",
"type": [
"string",
"null"
]
},
"state": {
"type": "string"
}
},
"required": [
"state"
],
"title": "HttpStateSetParams",
"type": "object"
}

View File

@@ -1,13 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"written": {
"type": "boolean"
}
},
"required": [
"written"
],
"title": "HttpStateSetResponse",
"type": "object"
}

View File

@@ -285,15 +285,6 @@
],
"type": "object"
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
"type": "string"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -500,12 +491,6 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"
@@ -808,12 +793,6 @@
"null"
]
},
"pluginId": {
"type": [
"string",
"null"
]
},
"result": {
"anyOf": [
{
@@ -1200,17 +1179,6 @@
},
{
"properties": {
"detail": {
"anyOf": [
{
"$ref": "#/definitions/ImageDetail"
},
{
"type": "null"
}
],
"default": null
},
"type": {
"enum": [
"image"
@@ -1231,17 +1199,6 @@
},
{
"properties": {
"detail": {
"anyOf": [
{
"$ref": "#/definitions/ImageDetail"
},
{
"type": "null"
}
],
"default": null
},
"path": {
"type": "string"
},

View File

@@ -69,7 +69,7 @@
"enum": [
"read",
"write",
"deny"
"none"
],
"type": "string"
},

View File

@@ -62,7 +62,7 @@
"enum": [
"read",
"write",
"deny"
"none"
],
"type": "string"
},

View File

@@ -285,15 +285,6 @@
],
"type": "object"
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
"type": "string"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -500,12 +491,6 @@
"oneOf": [
{
"properties": {
"clientId": {
"type": [
"string",
"null"
]
},
"content": {
"items": {
"$ref": "#/definitions/UserInput"
@@ -808,12 +793,6 @@
"null"
]
},
"pluginId": {
"type": [
"string",
"null"
]
},
"result": {
"anyOf": [
{
@@ -1200,17 +1179,6 @@
},
{
"properties": {
"detail": {
"anyOf": [
{
"$ref": "#/definitions/ImageDetail"
},
{
"type": "null"
}
],
"default": null
},
"type": {
"enum": [
"image"
@@ -1231,17 +1199,6 @@
},
{
"properties": {
"detail": {
"anyOf": [
{
"$ref": "#/definitions/ImageDetail"
},
{
"type": "null"
}
],
"default": null
},
"path": {
"type": "string"
},

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