mirror of
https://github.com/openai/codex.git
synced 2026-05-22 03:54:18 +00:00
Compare commits
105 Commits
fjord/audi
...
starr/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbcb2713c3 | ||
|
|
44bfc2b301 | ||
|
|
5a4202ad90 | ||
|
|
e43a2e297f | ||
|
|
532b9c83ae | ||
|
|
c3faea0b09 | ||
|
|
1495302347 | ||
|
|
64ef6cd1e4 | ||
|
|
79f044ed34 | ||
|
|
c58c84d6ee | ||
|
|
b019a678d8 | ||
|
|
59f262a2b4 | ||
|
|
27c4c67b15 | ||
|
|
d86352d520 | ||
|
|
c53da029bc | ||
|
|
c2141c7ce0 | ||
|
|
8335b56c33 | ||
|
|
1509ae6d8d | ||
|
|
954a9c8579 | ||
|
|
40be41763c | ||
|
|
83af3abc68 | ||
|
|
5c43a64e2b | ||
|
|
7f4d7ae3a4 | ||
|
|
d661ab70ed | ||
|
|
d269aa2af9 | ||
|
|
3c76081876 | ||
|
|
05b8ce4354 | ||
|
|
ccbf0137db | ||
|
|
ae10708ae0 | ||
|
|
f0663fd4fd | ||
|
|
9289b7cea8 | ||
|
|
ef24ef127f | ||
|
|
b3ae3de405 | ||
|
|
80fdd4688f | ||
|
|
05e171094d | ||
|
|
ba57aab13a | ||
|
|
826b2182ed | ||
|
|
3009e23644 | ||
|
|
1dd9bf9a74 | ||
|
|
7cdeab33d1 | ||
|
|
a668379abf | ||
|
|
d3d38159ed | ||
|
|
9e9a62dc28 | ||
|
|
1a25d8b6e5 | ||
|
|
e811234484 | ||
|
|
84d941d07f | ||
|
|
daa11820b0 | ||
|
|
6b54ced108 | ||
|
|
17d552fb4d | ||
|
|
3fd79b7986 | ||
|
|
5696167fe8 | ||
|
|
a66712c95d | ||
|
|
afa0101ae2 | ||
|
|
a66e0e9c4b | ||
|
|
f2368b7de6 | ||
|
|
c95a70fb42 | ||
|
|
1752f374a8 | ||
|
|
732b12b1ef | ||
|
|
c69cde3547 | ||
|
|
4dbca61e20 | ||
|
|
10f7dc6eb5 | ||
|
|
139365a4bb | ||
|
|
8e52578e66 | ||
|
|
55f6bbc667 | ||
|
|
ae03d073b3 | ||
|
|
53a1f4c29e | ||
|
|
4ac3ea20a2 | ||
|
|
0d344aca9b | ||
|
|
d32cb2c6ac | ||
|
|
8c14b08dd1 | ||
|
|
b1c13b6fe5 | ||
|
|
9286ff2805 | ||
|
|
bb43044cba | ||
|
|
d335b00212 | ||
|
|
3f2b7ede0b | ||
|
|
82061660ae | ||
|
|
adca1b643f | ||
|
|
e734cb5713 | ||
|
|
deb159d9ff | ||
|
|
af6ffb6ebb | ||
|
|
fce10e009d | ||
|
|
4ca60ef9ff | ||
|
|
b631d92170 | ||
|
|
500ef67ed1 | ||
|
|
7ee7fe239f | ||
|
|
6a8173588c | ||
|
|
9531e932ef | ||
|
|
a80f07ec4a | ||
|
|
da14dd2add | ||
|
|
22dd9ad392 | ||
|
|
64ead6a83a | ||
|
|
e7bffc5a20 | ||
|
|
0a83353ca3 | ||
|
|
545ede569c | ||
|
|
f0166cadbb | ||
|
|
4c89772314 | ||
|
|
0445b290fe | ||
|
|
061a614d85 | ||
|
|
6941f5c2c5 | ||
|
|
b200dd1b6f | ||
|
|
d91bc15618 | ||
|
|
941e7f825e | ||
|
|
a280248021 | ||
|
|
108234b5eb | ||
|
|
de9c5c0226 |
5
.bazelrc
5
.bazelrc
@@ -193,5 +193,10 @@ 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
|
||||
|
||||
72
.codex/skills/update-v8-version/SKILL.md
Normal file
72
.codex/skills/update-v8-version/SKILL.md
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
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.
|
||||
4
.codex/skills/update-v8-version/agents/openai.yaml
Normal file
4
.codex/skills/update-v8-version/agents/openai.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
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."
|
||||
17
.github/actions/setup-msvc-env/action.yml
vendored
Normal file
17
.github/actions/setup-msvc-env/action.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
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 }}"'
|
||||
257
.github/actions/setup-msvc-env/setup-msvc-env.ps1
vendored
Normal file
257
.github/actions/setup-msvc-env/setup-msvc-env.ps1
vendored
Normal file
@@ -0,0 +1,257 @@
|
||||
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
|
||||
@@ -31,16 +31,14 @@ 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}"
|
||||
grep -E " (librusty_v8_release_${TARGET}[.]a[.]gz|src_binding_release_${TARGET}[.]rs)$" \
|
||||
"${checksums_source}" > "${checksums_path}"
|
||||
curl -fsSL "${base_url}/rusty_v8_release_${TARGET}.sha256" -o "${checksums_path}"
|
||||
|
||||
if [[ "$(wc -l < "${checksums_path}")" -ne 2 ]]; then
|
||||
echo "Expected exactly two checksums for ${TARGET} in ${checksums_source}" >&2
|
||||
echo "Expected exactly two checksums for ${TARGET} in ${checksums_path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
231
.github/scripts/rusty_v8_bazel.py
vendored
231
.github/scripts/rusty_v8_bazel.py
vendored
@@ -5,17 +5,18 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import gzip
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
from rusty_v8_module_bazel import (
|
||||
RustyV8ChecksumError,
|
||||
check_module_bazel,
|
||||
rusty_v8_http_file_versions,
|
||||
update_module_bazel,
|
||||
)
|
||||
|
||||
@@ -23,12 +24,16 @@ 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"
|
||||
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"
|
||||
RELEASE_ARTIFACT_PROFILE = "release"
|
||||
SANDBOX_ARTIFACT_PROFILE = "ptrcomp_sandbox_release"
|
||||
ARTIFACT_BAZEL_CONFIGS = ["rusty-v8-upstream-libcxx"]
|
||||
|
||||
|
||||
def bazel_remote_args() -> list[str]:
|
||||
buildbuddy_api_key = os.environ.get("BUILDBUDDY_API_KEY")
|
||||
if not buildbuddy_api_key:
|
||||
return []
|
||||
return [f"--remote_header=x-buildbuddy-api-key={buildbuddy_api_key}"]
|
||||
|
||||
|
||||
def bazel_execroot() -> Path:
|
||||
@@ -75,6 +80,7 @@ def bazel_output_files(
|
||||
compilation_mode,
|
||||
f"--platforms=@llvm//platforms:{platform}",
|
||||
*[f"--config={config}" for config in bazel_configs],
|
||||
*bazel_remote_args(),
|
||||
"--output=files",
|
||||
expression,
|
||||
],
|
||||
@@ -91,8 +97,10 @@ 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",
|
||||
@@ -101,6 +109,8 @@ def bazel_build(
|
||||
compilation_mode,
|
||||
f"--platforms=@llvm//platforms:{platform}",
|
||||
*[f"--config={config}" for config in bazel_configs],
|
||||
*bazel_remote_args(),
|
||||
*download_args,
|
||||
*labels,
|
||||
],
|
||||
cwd=ROOT,
|
||||
@@ -114,11 +124,15 @@ def ensure_bazel_output_files(
|
||||
compilation_mode: str = "fastbuild",
|
||||
bazel_configs: list[str] | None = None,
|
||||
) -> list[Path]:
|
||||
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)
|
||||
# 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)
|
||||
missing = [str(path) for path in outputs if not path.exists()]
|
||||
if missing:
|
||||
@@ -126,9 +140,18 @@ def ensure_bazel_output_files(
|
||||
return outputs
|
||||
|
||||
|
||||
def release_pair_label(target: str) -> str:
|
||||
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:
|
||||
target_suffix = target.replace("-", "_")
|
||||
return f"//third_party/v8:rusty_v8_release_pair_{target_suffix}"
|
||||
pair_kind = "sandbox_release_pair" if sandbox else "release_pair"
|
||||
return f"//third_party/v8:rusty_v8_{pair_kind}_{target_suffix}"
|
||||
|
||||
|
||||
def resolved_v8_crate_version() -> str:
|
||||
@@ -169,6 +192,16 @@ 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()
|
||||
|
||||
|
||||
@@ -180,66 +213,76 @@ def command_manifest_path(manifest: Path | None, version: str) -> Path:
|
||||
return ROOT / manifest
|
||||
|
||||
|
||||
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_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 is_musl_archive_target(target: str, source_path: Path) -> bool:
|
||||
return target.endswith("-unknown-linux-musl") and source_path.suffix == ".a"
|
||||
def staged_binding_name(target: str, artifact_profile: str) -> str:
|
||||
return f"src_binding_{artifact_profile}_{target}.rs"
|
||||
|
||||
|
||||
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 staged_checksums_name(target: str, artifact_profile: str) -> str:
|
||||
return f"rusty_v8_{artifact_profile}_{target}.sha256"
|
||||
|
||||
|
||||
def merged_musl_archive(
|
||||
platform: str,
|
||||
def stage_artifacts(
|
||||
target: str,
|
||||
lib_path: Path,
|
||||
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
|
||||
]
|
||||
binding_path: Path,
|
||||
output_dir: Path,
|
||||
sandbox: bool,
|
||||
) -> None:
|
||||
missing_paths = [str(path) for path in [lib_path, binding_path] if not path.exists()]
|
||||
if missing_paths:
|
||||
raise SystemExit(f"missing release outputs for {target}: {missing_paths}")
|
||||
|
||||
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",
|
||||
]
|
||||
)
|
||||
subprocess.run(
|
||||
[str(llvm_ar), "-M"],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
input=merge_commands,
|
||||
text=True,
|
||||
)
|
||||
subprocess.run([str(llvm_ranlib), str(merged_archive)], cwd=ROOT, check=True)
|
||||
return merged_archive
|
||||
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)
|
||||
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"
|
||||
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)
|
||||
|
||||
|
||||
def stage_release_pair(
|
||||
@@ -248,10 +291,12 @@ 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)],
|
||||
[release_pair_label(target, sandbox)],
|
||||
compilation_mode,
|
||||
bazel_configs,
|
||||
)
|
||||
@@ -266,39 +311,7 @@ def stage_release_pair(
|
||||
except StopIteration as exc:
|
||||
raise SystemExit(f"missing Rust binding output for {target}") from exc
|
||||
|
||||
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)
|
||||
stage_artifacts(target, lib_path, binding_path, output_dir, sandbox)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
@@ -309,6 +322,7 @@ 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",
|
||||
@@ -321,6 +335,14 @@ 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")
|
||||
@@ -353,6 +375,15 @@ 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":
|
||||
|
||||
13
.github/scripts/rusty_v8_module_bazel.py
vendored
13
.github/scripts/rusty_v8_module_bazel.py
vendored
@@ -9,6 +9,7 @@ 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):
|
||||
@@ -95,6 +96,18 @@ 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],
|
||||
|
||||
62
.github/scripts/setup-dev-drive.ps1
vendored
Normal file
62
.github/scripts/setup-dev-drive.ps1
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
# 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
|
||||
287
.github/scripts/test_rusty_v8_bazel.py
vendored
287
.github/scripts/test_rusty_v8_bazel.py
vendored
@@ -4,11 +4,270 @@ 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_remote_args_include_buildbuddy_header_when_present(self) -> None:
|
||||
with patch.dict(environ, {"BUILDBUDDY_API_KEY": "token"}, clear=False):
|
||||
self.assertEqual(
|
||||
["--remote_header=x-buildbuddy-api-key=token"],
|
||||
rusty_v8_bazel.bazel_remote_args(),
|
||||
)
|
||||
|
||||
with patch.dict(environ, {}, clear=True):
|
||||
self.assertEqual([], rusty_v8_bazel.bazel_remote_args())
|
||||
|
||||
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(
|
||||
"""\
|
||||
@@ -121,6 +380,34 @@ 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()
|
||||
|
||||
3
.github/workflows/README.md
vendored
3
.github/workflows/README.md
vendored
@@ -21,7 +21,8 @@ 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
|
||||
- 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
|
||||
- release-profile Cargo builds
|
||||
- cross-platform `argument-comment-lint`
|
||||
- Linux remote-env tests
|
||||
|
||||
464
.github/workflows/rust-ci-full-nextest-platform.yml
vendored
Normal file
464
.github/workflows/rust-ci-full-nextest-platform.yml
vendored
Normal file
@@ -0,0 +1,464 @@
|
||||
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@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.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@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.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
|
||||
306
.github/workflows/rust-ci-full.yml
vendored
306
.github/workflows/rust-ci-full.yml
vendored
@@ -521,235 +521,73 @@ jobs:
|
||||
/var/cache/apt
|
||||
key: apt-${{ matrix.runner }}-${{ matrix.target }}-v1
|
||||
|
||||
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_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
|
||||
|
||||
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_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
|
||||
|
||||
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_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
|
||||
|
||||
# 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_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
|
||||
|
||||
- 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
|
||||
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
|
||||
|
||||
# --- Gatherer job for the full post-merge workflow --------------------------
|
||||
results:
|
||||
@@ -761,7 +599,11 @@ jobs:
|
||||
argument_comment_lint_package,
|
||||
argument_comment_lint_prebuilt,
|
||||
lint_build,
|
||||
tests,
|
||||
tests_macos_aarch64,
|
||||
tests_linux_x64_remote,
|
||||
tests_linux_arm64,
|
||||
tests_windows_x64,
|
||||
tests_windows_arm64,
|
||||
]
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -774,13 +616,21 @@ jobs:
|
||||
echo "general: ${{ needs.general.result }}"
|
||||
echo "shear : ${{ needs.cargo_shear.result }}"
|
||||
echo "lint : ${{ needs.lint_build.result }}"
|
||||
echo "tests : ${{ needs.tests.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 }}"
|
||||
[[ '${{ 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.result }}' == 'success' ]] || { echo 'tests 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; }
|
||||
|
||||
- name: sccache summary note
|
||||
if: always()
|
||||
|
||||
4
.github/workflows/rust-release.yml
vendored
4
.github/workflows/rust-release.yml
vendored
@@ -533,10 +533,10 @@ jobs:
|
||||
platform_tag="macosx_10_9_x86_64"
|
||||
;;
|
||||
aarch64-unknown-linux-musl)
|
||||
platform_tag="musllinux_1_1_aarch64"
|
||||
platform_tag="manylinux_2_17_aarch64"
|
||||
;;
|
||||
x86_64-unknown-linux-musl)
|
||||
platform_tag="musllinux_1_1_x86_64"
|
||||
platform_tag="manylinux_2_17_x86_64"
|
||||
;;
|
||||
*)
|
||||
echo "No Python runtime wheel platform tag for ${{ matrix.target }}"
|
||||
|
||||
182
.github/workflows/rusty-v8-release.yml
vendored
182
.github/workflows/rusty-v8-release.yml
vendored
@@ -46,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 resolved v8 crate version ${V8_VERSION}." >&2
|
||||
echo "Tag ${release_tag} does not match expected release tag ${expected_release_tag}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.target }}
|
||||
name: Build ${{ matrix.variant }} ${{ matrix.target }}
|
||||
needs: metadata
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
@@ -64,11 +64,77 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
platform: linux_amd64_musl
|
||||
target: x86_64-unknown-linux-musl
|
||||
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
|
||||
@@ -85,61 +151,115 @@ jobs:
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Set up Rust toolchain for Cargo smoke
|
||||
uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
with:
|
||||
toolchain: "1.93.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_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"
|
||||
)
|
||||
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}"
|
||||
|
||||
bazel_args=(
|
||||
build
|
||||
-c
|
||||
opt
|
||||
"--platforms=@llvm//platforms:${PLATFORM}"
|
||||
--config=v8-release-compat
|
||||
--config=rusty-v8-upstream-libcxx
|
||||
"${pair_target}"
|
||||
"${extra_targets[@]}"
|
||||
--build_metadata=COMMIT_SHA=$(git rev-parse HEAD)
|
||||
)
|
||||
if [[ "${SANDBOX}" != "true" ]]; then
|
||||
bazel_args+=(--config=v8-release-compat)
|
||||
fi
|
||||
|
||||
bazel \
|
||||
--noexperimental_remote_repo_contents_cache \
|
||||
"${bazel_args[@]}" \
|
||||
--config=ci-v8 \
|
||||
"--config=${{ matrix.bazel_config }}" \
|
||||
"--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
|
||||
|
||||
python3 .github/scripts/rusty_v8_bazel.py stage-release-pair \
|
||||
--platform "${PLATFORM}" \
|
||||
--target "${TARGET}" \
|
||||
--compilation-mode opt \
|
||||
--bazel-config v8-release-compat \
|
||||
stage_args=(
|
||||
--platform "${PLATFORM}"
|
||||
--target "${TARGET}"
|
||||
--compilation-mode opt
|
||||
--output-dir "dist/${TARGET}"
|
||||
--bazel-config "${BAZEL_CONFIG}"
|
||||
)
|
||||
if [[ "${SANDBOX}" == "true" ]]; then
|
||||
stage_args+=(--sandbox)
|
||||
else
|
||||
stage_args+=(--bazel-config v8-release-compat)
|
||||
fi
|
||||
|
||||
- name: Upload staged musl artifacts
|
||||
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
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: rusty-v8-${{ needs.metadata.outputs.v8_version }}-${{ matrix.target }}
|
||||
name: rusty-v8-${{ needs.metadata.outputs.v8_version }}-${{ matrix.variant }}-${{ matrix.target }}
|
||||
path: dist/${{ matrix.target }}/*
|
||||
|
||||
publish-release:
|
||||
@@ -152,7 +272,8 @@ jobs:
|
||||
actions: read
|
||||
|
||||
steps:
|
||||
- name: Ensure release tag is new
|
||||
- name: Check whether release already exists
|
||||
id: release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_TAG: ${{ needs.metadata.outputs.release_tag }}
|
||||
@@ -161,8 +282,9 @@ jobs:
|
||||
set -euo pipefail
|
||||
|
||||
if gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" > /dev/null 2>&1; then
|
||||
echo "Release tag ${RELEASE_TAG} already exists; musl artifact tags are immutable." >&2
|
||||
exit 1
|
||||
echo "exists=true" >> "${GITHUB_OUTPUT}"
|
||||
else
|
||||
echo "exists=false" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
@@ -170,6 +292,7 @@ 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 }}
|
||||
@@ -177,3 +300,14 @@ 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
|
||||
|
||||
303
.github/workflows/v8-canary.yml
vendored
303
.github/workflows/v8-canary.yml
vendored
@@ -3,28 +3,36 @@ name: v8-canary
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".bazelrc"
|
||||
- ".github/actions/setup-bazel-ci/**"
|
||||
- ".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/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:
|
||||
@@ -59,7 +67,7 @@ jobs:
|
||||
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.target }}
|
||||
name: Build ${{ matrix.variant }} ${{ matrix.target }}
|
||||
needs: metadata
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
@@ -70,12 +78,77 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
platform: linux_amd64_musl
|
||||
target: x86_64-unknown-linux-musl
|
||||
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:
|
||||
@@ -92,53 +165,247 @@ jobs:
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Set up Rust toolchain for Cargo smoke
|
||||
uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
with:
|
||||
toolchain: "1.93.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_target="//third_party/v8:rusty_v8_release_pair_${target_suffix}"
|
||||
extra_targets=(
|
||||
"@llvm//runtimes/libcxx:libcxx.static"
|
||||
"@llvm//runtimes/libcxx:libcxxabi.static"
|
||||
)
|
||||
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}"
|
||||
|
||||
bazel_args=(
|
||||
build
|
||||
"--platforms=@llvm//platforms:${PLATFORM}"
|
||||
--config=v8-release-compat
|
||||
--config=rusty-v8-upstream-libcxx
|
||||
"${pair_target}"
|
||||
"${extra_targets[@]}"
|
||||
--build_metadata=COMMIT_SHA=$(git rev-parse HEAD)
|
||||
)
|
||||
if [[ "${SANDBOX}" != "true" ]]; then
|
||||
bazel_args+=(--config=v8-release-compat)
|
||||
fi
|
||||
|
||||
bazel \
|
||||
--noexperimental_remote_repo_contents_cache \
|
||||
"${bazel_args[@]}" \
|
||||
--config=ci-v8 \
|
||||
"--config=${{ matrix.bazel_config }}" \
|
||||
"--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
|
||||
|
||||
python3 .github/scripts/rusty_v8_bazel.py stage-release-pair \
|
||||
--platform "${PLATFORM}" \
|
||||
--target "${TARGET}" \
|
||||
--bazel-config v8-release-compat \
|
||||
stage_args=(
|
||||
--platform "${PLATFORM}"
|
||||
--target "${TARGET}"
|
||||
--output-dir "dist/${TARGET}"
|
||||
--bazel-config "${BAZEL_CONFIG}"
|
||||
)
|
||||
if [[ "${SANDBOX}" == "true" ]]; then
|
||||
stage_args+=(--sandbox)
|
||||
else
|
||||
stage_args+=(--bazel-config v8-release-compat)
|
||||
fi
|
||||
|
||||
- name: Upload staged musl artifacts
|
||||
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
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: v8-canary-${{ needs.metadata.outputs.v8_version }}-${{ matrix.target }}
|
||||
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@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
with:
|
||||
toolchain: "1.93.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.93.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 }}
|
||||
path: dist/${{ matrix.target }}/*
|
||||
|
||||
128
MODULE.bazel
128
MODULE.bazel
@@ -10,6 +10,7 @@ single_version_override(
|
||||
module_name = "llvm",
|
||||
patch_strip = 1,
|
||||
patches = [
|
||||
"//patches:llvm_rusty_v8_custom_libcxx.patch",
|
||||
"//patches:llvm_windows_symlink_extract.patch",
|
||||
],
|
||||
)
|
||||
@@ -77,6 +78,13 @@ 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
|
||||
@@ -407,18 +415,18 @@ crate.annotation(
|
||||
|
||||
inject_repo(crate, "alsa_lib")
|
||||
|
||||
bazel_dep(name = "v8", version = "14.6.202.9")
|
||||
bazel_dep(name = "v8", version = "14.7.173.20")
|
||||
archive_override(
|
||||
module_name = "v8",
|
||||
integrity = "sha256-JphDwLAzsd9KvgRZ7eQvNtPU6qGd3XjFt/a/1QITAJU=",
|
||||
integrity = "sha256-v/x6I4X38a2wckzUIft3Dh0SUdkuOTokwxyF7lzW8Lc=",
|
||||
patch_strip = 3,
|
||||
patches = [
|
||||
"//patches:v8_module_deps.patch",
|
||||
"//patches:v8_bazel_rules.patch",
|
||||
"//patches:v8_source_portability.patch",
|
||||
],
|
||||
strip_prefix = "v8-14.6.202.9",
|
||||
urls = ["https://github.com/v8/v8/archive/refs/tags/14.6.202.9.tar.gz"],
|
||||
strip_prefix = "v8-14.7.173.20",
|
||||
urls = ["https://github.com/v8/v8/archive/refs/tags/14.7.173.20.tar.gz"],
|
||||
)
|
||||
|
||||
http_archive(
|
||||
@@ -430,93 +438,53 @@ http_archive(
|
||||
urls = ["https://static.crates.io/crates/v8/v8-146.4.0.crate"],
|
||||
)
|
||||
|
||||
http_file(
|
||||
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/v146.4.0/librusty_v8_release_aarch64-apple-darwin.a.gz",
|
||||
],
|
||||
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_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/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",
|
||||
name = "rusty_v8_147_4_0_aarch64_pc_windows_msvc_archive",
|
||||
downloaded_file_path = "rusty_v8_release_aarch64-pc-windows-msvc.lib.gz",
|
||||
sha256 = "ed13363659c6d08583ac8fdc40493445c5767d8b94955a4d5d7bb8d5a81f6bf8",
|
||||
sha256 = "1fa3f94d9e09cff1f6bcce94c478e5cb072c0755f6a0357abadb9dd3b48d8127",
|
||||
urls = [
|
||||
"https://github.com/denoland/rusty_v8/releases/download/v146.4.0/rusty_v8_release_aarch64-pc-windows-msvc.lib.gz",
|
||||
"https://github.com/denoland/rusty_v8/releases/download/v147.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",
|
||||
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 = "90a9a2346acd3685a355e98df85c24dbe406cb124367d16259a4b5d522621862",
|
||||
sha256 = "e2827ff98b1a9d4c0343000fc5124ac30dfab3007bc0129c168c9355fc2fcd7c",
|
||||
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",
|
||||
"https://github.com/denoland/rusty_v8/releases/download/v147.4.0/rusty_v8_release_x86_64-pc-windows-msvc.lib.gz",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
49
MODULE.bazel.lock
generated
49
MODULE.bazel.lock
generated
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
[target.'cfg(all(windows, target_env = "msvc"))']
|
||||
rustflags = ["-C", "link-arg=/STACK:8388608"]
|
||||
rustflags = ["-C", "link-arg=/STACK:8388608", "-C", "target-feature=+crt-static"]
|
||||
|
||||
# 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.
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
[profile.default]
|
||||
# Do not increase, fix your test instead
|
||||
slow-timeout = { period = "15s", terminate-after = 2 }
|
||||
# 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"
|
||||
|
||||
[test-groups.app_server_protocol_codegen]
|
||||
max-threads = 1
|
||||
@@ -14,6 +20,9 @@ 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)'
|
||||
@@ -44,3 +53,18 @@ 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 }
|
||||
|
||||
165
codex-rs/Cargo.lock
generated
165
codex-rs/Cargo.lock
generated
@@ -1518,9 +1518,9 @@ checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0"
|
||||
|
||||
[[package]]
|
||||
name = "calendrical_calculations"
|
||||
version = "0.2.3"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a0b39595c6ee54a8d0900204ba4c401d0ab4eb45adaf07178e8d017541529e7"
|
||||
checksum = "5abbd6eeda6885048d357edc66748eea6e0268e3dd11f326fff5bd248d779c26"
|
||||
dependencies = [
|
||||
"core_maths",
|
||||
"displaydoc",
|
||||
@@ -2636,6 +2636,7 @@ dependencies = [
|
||||
"libc",
|
||||
"pretty_assertions",
|
||||
"reqwest",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tar",
|
||||
@@ -2748,6 +2749,7 @@ dependencies = [
|
||||
"axum",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"codex-api",
|
||||
"codex-app-server-protocol",
|
||||
"codex-client",
|
||||
"codex-file-system",
|
||||
@@ -2759,6 +2761,7 @@ dependencies = [
|
||||
"codex-utils-rustls-provider",
|
||||
"ctor 0.6.3",
|
||||
"futures",
|
||||
"http 1.4.0",
|
||||
"pretty_assertions",
|
||||
"prost 0.14.3",
|
||||
"reqwest",
|
||||
@@ -2827,6 +2830,7 @@ dependencies = [
|
||||
name = "codex-extension-api"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"codex-protocol",
|
||||
"codex-tools",
|
||||
]
|
||||
@@ -2944,10 +2948,23 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-goal-extension"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"codex-extension-api",
|
||||
"codex-protocol",
|
||||
"codex-tools",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-guardian"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"codex-core",
|
||||
"codex-extension-api",
|
||||
"codex-protocol",
|
||||
@@ -5082,9 +5099,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "diplomat"
|
||||
version = "0.14.0"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9adb46b05e2f53dcf6a7dfc242e4ce9eb60c369b6b6eb10826a01e93167f59c6"
|
||||
checksum = "7935649d00000f5c5d735448ad3dc07b9738160727017914cf42138b8e8e6611"
|
||||
dependencies = [
|
||||
"diplomat_core",
|
||||
"proc-macro2",
|
||||
@@ -5094,15 +5111,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "diplomat-runtime"
|
||||
version = "0.14.0"
|
||||
version = "0.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0569bd3caaf13829da7ee4e83dbf9197a0e1ecd72772da6d08f0b4c9285c8d29"
|
||||
checksum = "970ac38ad677632efcee6d517e783958da9bc78ec206d8d5e35b459ffc5e4864"
|
||||
|
||||
[[package]]
|
||||
name = "diplomat_core"
|
||||
version = "0.14.0"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51731530ed7f2d4495019abc7df3744f53338e69e2863a6a64ae91821c763df1"
|
||||
checksum = "9cf41b94101a4bce993febaf0098092b0bb31deaf0ecaf6e0a2562465f61b383"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -5646,9 +5663,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fixed_decimal"
|
||||
version = "0.7.1"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35eabf480f94d69182677e37571d3be065822acfafd12f2f085db44fbbcc8e57"
|
||||
checksum = "79c3c892f121fff406e5dd6b28c1b30096b95111c30701a899d4f2b18da6d1bd"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"smallvec",
|
||||
@@ -7491,9 +7508,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_calendar"
|
||||
version = "2.1.1"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f0e52e009b6b16ba9c0693578796f2dd4aaa59a7f8f920423706714a89ac4e"
|
||||
checksum = "a2b2acc6263f494f1df50685b53ff8e57869e47d5c6fe39c23d518ae9a4f3e45"
|
||||
dependencies = [
|
||||
"calendrical_calculations",
|
||||
"displaydoc",
|
||||
@@ -7507,18 +7524,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_calendar_data"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527f04223b17edfe0bd43baf14a0cb1b017830db65f3950dc00224860a9a446d"
|
||||
checksum = "118577bcf3a0fa7c6ac0a7d6e951814da84ee56b9b1f68fb4d8d10b08cefaf4d"
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
|
||||
checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"potential_utf",
|
||||
"utf8_iter",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
@@ -7526,14 +7544,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_decimal"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a38c52231bc348f9b982c1868a2af3195199623007ba2c7650f432038f5b3e8e"
|
||||
checksum = "288247df2e32aa776ac54fdd64de552149ac43cb840f2761811f0e8d09719dd4"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"fixed_decimal",
|
||||
"icu_decimal_data",
|
||||
"icu_locale",
|
||||
"icu_locale_core",
|
||||
"icu_plurals",
|
||||
"icu_provider",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
@@ -7541,15 +7561,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_decimal_data"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2905b4044eab2dd848fe84199f9195567b63ab3a93094711501363f63546fef7"
|
||||
checksum = "6f14a5ca9e8af29eef62064f269078424283d90dbaffeac5225addf62aaabc22"
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "532b11722e350ab6bf916ba6eb0efe3ee54b932666afec989465f9243fe6dd60"
|
||||
checksum = "d5a396343c7208121dc86e35623d3dfe19814a7613cfd14964994cdc9c9a2e26"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_locale_core",
|
||||
@@ -7562,9 +7582,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_core"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
|
||||
checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
@@ -7576,15 +7596,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_data"
|
||||
version = "2.1.2"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c5f1d16b4c3a2642d3a719f18f6b06070ab0aef246a6418130c955ae08aa831"
|
||||
checksum = "d5fdcc9ac77c6d74ff5cf6e65ef3181d6af32003b16fce3a77fb451d2f695993"
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
|
||||
checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_normalizer_data",
|
||||
@@ -7596,15 +7616,34 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
|
||||
checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
|
||||
|
||||
[[package]]
|
||||
name = "icu_plurals"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a50023f1d49ad5c4333380328a0d4a19e4b9d6d842ec06639affd5ba47c8103"
|
||||
dependencies = [
|
||||
"fixed_decimal",
|
||||
"icu_locale",
|
||||
"icu_plurals_data",
|
||||
"icu_provider",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_plurals_data"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8485497155dc865f901decb93ecc20d3e467df67bfeceb91e3ba34e2b11e8e1d"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "2.1.2"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
|
||||
checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_locale_core",
|
||||
@@ -7616,15 +7655,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "2.1.2"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
|
||||
checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
|
||||
checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locale_core",
|
||||
@@ -10879,9 +10918,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "resb"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a067ab3b5ca3b4dc307d0de9cf75f9f5e6ca9717b192b2f28a36c83e5de9e76"
|
||||
checksum = "22d392791f3c6802a1905a509e9d1a6039cbbcb5e9e00e5a6d3661f7c874f390"
|
||||
dependencies = [
|
||||
"potential_utf",
|
||||
"serde_core",
|
||||
@@ -12608,14 +12647,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "temporal_capi"
|
||||
version = "0.1.2"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a151e402c2bdb6a3a2a2f3f225eddaead2e7ce7dd5d3fa2090deb11b17aa4ed8"
|
||||
checksum = "8a2a1f001e756a9f5f2d175a9965c4c0b3a054f09f30de3a75ab49765f2deb36"
|
||||
dependencies = [
|
||||
"diplomat",
|
||||
"diplomat-runtime",
|
||||
"icu_calendar",
|
||||
"icu_locale",
|
||||
"icu_locale_core",
|
||||
"num-traits",
|
||||
"temporal_rs",
|
||||
"timezone_provider",
|
||||
@@ -12625,13 +12664,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "temporal_rs"
|
||||
version = "0.1.2"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88afde3bd75d2fc68d77a914bece426aa08aa7649ffd0cdd4a11c3d4d33474d1"
|
||||
checksum = "9a902a45282e5175186b21d355efc92564601efe6e2d92818dc9e333d50bd4de"
|
||||
dependencies = [
|
||||
"calendrical_calculations",
|
||||
"core_maths",
|
||||
"icu_calendar",
|
||||
"icu_locale",
|
||||
"icu_locale_core",
|
||||
"ixdtf",
|
||||
"num-traits",
|
||||
"timezone_provider",
|
||||
@@ -12848,9 +12888,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "timezone_provider"
|
||||
version = "0.1.2"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df9ba0000e9e73862f3e7ca1ff159e2ddf915c9d8bb11e38a7874760f445d993"
|
||||
checksum = "c48f9b04628a2b813051e4dfe97c65281e49625eabd09ec343190e31e399a8c2"
|
||||
dependencies = [
|
||||
"tinystr",
|
||||
"zerotrie",
|
||||
@@ -12881,9 +12921,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
|
||||
checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"serde_core",
|
||||
@@ -13697,9 +13737,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "v8"
|
||||
version = "146.4.0"
|
||||
version = "147.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d97bcac5cdc5a195a4813f1855a6bc658f240452aac36caa12fd6c6f16026ab1"
|
||||
checksum = "2df8fffd507fb18ed000673a83d937f58e60fb07f3306b2274284125b15137cd"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"bitflags 2.10.0",
|
||||
@@ -14849,9 +14889,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
|
||||
checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
|
||||
dependencies = [
|
||||
"stable_deref_trait",
|
||||
"yoke-derive",
|
||||
@@ -14860,9 +14900,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "yoke-derive"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
|
||||
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -14995,20 +15035,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.3"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
|
||||
checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.5"
|
||||
version = "0.11.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
|
||||
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"yoke",
|
||||
@@ -15018,9 +15059,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerovec-derive"
|
||||
version = "0.11.2"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
|
||||
checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -15091,9 +15132,9 @@ checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445"
|
||||
|
||||
[[package]]
|
||||
name = "zoneinfo64"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb2e5597efbe7c421da8a7fd396b20b571704e787c21a272eecf35dfe9d386f0"
|
||||
checksum = "ed6eb2607e906160c457fd573e9297e65029669906b9ac8fb1b5cd5e055f0705"
|
||||
dependencies = [
|
||||
"calendrical_calculations",
|
||||
"icu_locale_core",
|
||||
|
||||
@@ -45,6 +45,7 @@ members = [
|
||||
"execpolicy",
|
||||
"execpolicy-legacy",
|
||||
"ext/extension-api",
|
||||
"ext/goal",
|
||||
"ext/guardian",
|
||||
"ext/memories",
|
||||
"external-agent-migration",
|
||||
@@ -162,6 +163,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-external-agent-migration = { path = "external-agent-migration" }
|
||||
codex-external-agent-sessions = { path = "external-agent-sessions" }
|
||||
@@ -411,7 +413,7 @@ unicode-width = "0.2"
|
||||
url = "2"
|
||||
urlencoding = "2.1"
|
||||
uuid = "1"
|
||||
v8 = "=146.4.0"
|
||||
v8 = "=147.4.0"
|
||||
vt100 = "0.16.2"
|
||||
walkdir = "2.5.0"
|
||||
webbrowser = "1.0"
|
||||
@@ -470,6 +472,7 @@ unwrap_used = "deny"
|
||||
[workspace.metadata.cargo-shear]
|
||||
ignored = [
|
||||
"codex-agent-graph-store",
|
||||
"codex-goal-extension",
|
||||
"icu_provider",
|
||||
"openssl-sys",
|
||||
"codex-v8-poc",
|
||||
|
||||
@@ -138,7 +138,6 @@ 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;
|
||||
@@ -367,9 +366,7 @@ 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::from_legacy_sandbox_policy(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
permission_profile: CorePermissionProfile::read_only(),
|
||||
permission_profile_cwd: PathBuf::from("/tmp"),
|
||||
reasoning_effort: None,
|
||||
reasoning_summary: None,
|
||||
|
||||
@@ -990,6 +990,7 @@ fn analytics_hook_event_name(event_name: HookEventName) -> &'static str {
|
||||
HookEventName::PostCompact => "PostCompact",
|
||||
HookEventName::SessionStart => "SessionStart",
|
||||
HookEventName::UserPromptSubmit => "UserPromptSubmit",
|
||||
HookEventName::SubagentStart => "SubagentStart",
|
||||
HookEventName::Stop => "Stop",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2178,11 +2178,13 @@ mod tests {
|
||||
let environment_manager = Arc::new(
|
||||
EnvironmentManager::create_for_tests(
|
||||
Some("ws://127.0.0.1:8765".to_string()),
|
||||
ExecServerRuntimePaths::new(
|
||||
std::env::current_exe().expect("current exe"),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
)
|
||||
.expect("runtime paths"),
|
||||
Some(
|
||||
ExecServerRuntimePaths::new(
|
||||
std::env::current_exe().expect("current exe"),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
)
|
||||
.expect("runtime paths"),
|
||||
),
|
||||
)
|
||||
.await,
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod pid;
|
||||
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::Serialize;
|
||||
@@ -31,3 +32,15 @@ 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:#}"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::io::SeekFrom;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
#[cfg(unix)]
|
||||
@@ -10,6 +11,8 @@ 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;
|
||||
@@ -18,6 +21,7 @@ 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))]
|
||||
@@ -35,6 +39,25 @@ 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,
|
||||
@@ -129,11 +152,18 @@ 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::null());
|
||||
.stderr(Stdio::from(stderr_log.into_std().await));
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
@@ -169,8 +199,11 @@ 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);
|
||||
return Err(err).context(context);
|
||||
}
|
||||
};
|
||||
let contents = serde_json::to_vec(&record).context("failed to serialize pid record")?;
|
||||
@@ -344,6 +377,23 @@ 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 {
|
||||
@@ -376,6 +426,56 @@ 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 {
|
||||
|
||||
@@ -6,7 +6,10 @@ 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]
|
||||
@@ -170,3 +173,24 @@ 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(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ 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;
|
||||
@@ -14,12 +15,16 @@ 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;
|
||||
|
||||
const PROBE_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
pub(crate) const CONTROL_SOCKET_RESPONSE_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 {
|
||||
@@ -27,7 +32,7 @@ pub(crate) struct ProbeInfo {
|
||||
}
|
||||
|
||||
pub(crate) async fn probe(socket_path: &Path) -> Result<ProbeInfo> {
|
||||
timeout(PROBE_TIMEOUT, probe_inner(socket_path))
|
||||
timeout(CONTROL_SOCKET_RESPONSE_TIMEOUT, probe_inner(socket_path))
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
@@ -38,54 +43,14 @@ pub(crate) async fn probe(socket_path: &Path) -> Result<ProbeInfo> {
|
||||
}
|
||||
|
||||
async fn probe_inner(socket_path: &Path) -> Result<ProbeInfo> {
|
||||
let stream = UnixStream::connect(socket_path)
|
||||
.await
|
||||
.with_context(|| format!("failed to connect to {}", socket_path.display()))?;
|
||||
let (mut websocket, _response) = client_async("ws://localhost/", stream)
|
||||
.await
|
||||
.with_context(|| format!("failed to upgrade {}", socket_path.display()))?;
|
||||
|
||||
let initialize = JSONRPCMessage::Request(JSONRPCRequest {
|
||||
id: RequestId::Integer(1),
|
||||
method: "initialize".to_string(),
|
||||
params: Some(serde_json::to_value(InitializeParams {
|
||||
client_info: ClientInfo {
|
||||
name: CLIENT_NAME.to_string(),
|
||||
title: Some("Codex App Server Daemon".to_string()),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
},
|
||||
capabilities: None,
|
||||
})?),
|
||||
trace: None,
|
||||
});
|
||||
websocket
|
||||
.send(Message::Text(serde_json::to_string(&initialize)?.into()))
|
||||
.await
|
||||
.context("failed to send initialize request")?;
|
||||
|
||||
let response = loop {
|
||||
let frame = websocket
|
||||
.next()
|
||||
.await
|
||||
.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 == RequestId::Integer(1)
|
||||
{
|
||||
break response;
|
||||
}
|
||||
};
|
||||
let initialize_response = serde_json::from_value::<InitializeResponse>(response.result)?;
|
||||
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,
|
||||
});
|
||||
websocket
|
||||
.send(Message::Text(serde_json::to_string(&initialized)?.into()))
|
||||
send_message(&mut websocket, &initialized)
|
||||
.await
|
||||
.context("failed to send initialized notification")?;
|
||||
websocket.close(None).await.ok();
|
||||
@@ -95,6 +60,91 @@ async fn probe_inner(socket_path: &Path) -> Result<ProbeInfo> {
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
.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,
|
||||
method: "initialize".to_string(),
|
||||
params: Some(serde_json::to_value(InitializeParams {
|
||||
client_info: ClientInfo {
|
||||
name: CLIENT_NAME.to_string(),
|
||||
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
|
||||
},
|
||||
})?),
|
||||
trace: None,
|
||||
});
|
||||
send_message(websocket, &initialize)
|
||||
.await
|
||||
.context("failed to send initialize request")?;
|
||||
|
||||
let response = loop {
|
||||
let message = timeout(CONTROL_SOCKET_RESPONSE_TIMEOUT, read_message(websocket))
|
||||
.await
|
||||
.context("timed out waiting for initialize response")??;
|
||||
if let JSONRPCMessage::Response(response) = message
|
||||
&& response.id == INITIALIZE_REQUEST_ID
|
||||
{
|
||||
break response;
|
||||
}
|
||||
};
|
||||
serde_json::from_value::<InitializeResponse>(response.result)
|
||||
.context("failed to parse initialize response")
|
||||
}
|
||||
|
||||
pub(crate) async fn send_message<S>(
|
||||
websocket: &mut WebSocketStream<S>,
|
||||
message: &JSONRPCMessage,
|
||||
) -> Result<()>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
websocket
|
||||
.send(Message::Text(serde_json::to_string(message)?.into()))
|
||||
.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");
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_version_from_user_agent(user_agent: &str) -> Result<String> {
|
||||
let (_originator, rest) = user_agent
|
||||
.split_once('/')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod backend;
|
||||
mod client;
|
||||
mod managed_install;
|
||||
mod remote_control_client;
|
||||
mod settings;
|
||||
mod update_loop;
|
||||
|
||||
@@ -13,6 +14,7 @@ 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;
|
||||
@@ -58,6 +60,8 @@ 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>,
|
||||
@@ -84,6 +88,7 @@ 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,
|
||||
@@ -96,6 +101,20 @@ 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,
|
||||
@@ -179,6 +198,27 @@ 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
|
||||
@@ -248,33 +288,39 @@ 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),
|
||||
));
|
||||
return Ok(self
|
||||
.output(
|
||||
LifecycleStatus::AlreadyRunning,
|
||||
self.running_backend(&settings).await?,
|
||||
/*pid*/ None,
|
||||
Some(info.app_server_version),
|
||||
)
|
||||
.await);
|
||||
}
|
||||
|
||||
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),
|
||||
));
|
||||
return Ok(self
|
||||
.output(
|
||||
LifecycleStatus::AlreadyRunning,
|
||||
Some(BackendKind::Pid),
|
||||
/*pid*/ None,
|
||||
Some(info.app_server_version),
|
||||
)
|
||||
.await);
|
||||
}
|
||||
|
||||
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),
|
||||
))
|
||||
Ok(self
|
||||
.output(
|
||||
LifecycleStatus::Started,
|
||||
Some(BackendKind::Pid),
|
||||
pid,
|
||||
Some(info.app_server_version),
|
||||
)
|
||||
.await)
|
||||
}
|
||||
|
||||
async fn restart(&self) -> Result<LifecycleOutput> {
|
||||
@@ -294,12 +340,14 @@ 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),
|
||||
))
|
||||
Ok(self
|
||||
.output(
|
||||
LifecycleStatus::Restarted,
|
||||
Some(BackendKind::Pid),
|
||||
pid,
|
||||
Some(info.app_server_version),
|
||||
)
|
||||
.await)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
@@ -352,12 +400,14 @@ 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,
|
||||
));
|
||||
return Ok(self
|
||||
.output(
|
||||
LifecycleStatus::Stopped,
|
||||
Some(BackendKind::Pid),
|
||||
/*pid*/ None,
|
||||
/*app_server_version*/ None,
|
||||
)
|
||||
.await);
|
||||
}
|
||||
|
||||
if client::probe(&self.socket_path).await.is_ok() {
|
||||
@@ -366,23 +416,27 @@ impl Daemon {
|
||||
));
|
||||
}
|
||||
|
||||
Ok(self.output(
|
||||
LifecycleStatus::NotRunning,
|
||||
/*backend*/ None,
|
||||
/*pid*/ None,
|
||||
/*app_server_version*/ None,
|
||||
))
|
||||
Ok(self
|
||||
.output(
|
||||
LifecycleStatus::NotRunning,
|
||||
/*backend*/ None,
|
||||
/*pid*/ None,
|
||||
/*app_server_version*/ None,
|
||||
)
|
||||
.await)
|
||||
}
|
||||
|
||||
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),
|
||||
))
|
||||
Ok(self
|
||||
.output(
|
||||
LifecycleStatus::Running,
|
||||
self.running_backend(&settings).await?,
|
||||
/*pid*/ None,
|
||||
Some(info.app_server_version),
|
||||
)
|
||||
.await)
|
||||
}
|
||||
|
||||
async fn wait_until_ready(&self) -> Result<client::ProbeInfo> {
|
||||
@@ -395,17 +449,34 @@ impl Daemon {
|
||||
sleep(START_POLL_INTERVAL).await;
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err).with_context(|| {
|
||||
format!(
|
||||
"app server did not become ready on {}",
|
||||
self.socket_path.display()
|
||||
)
|
||||
});
|
||||
let context = self.app_server_not_ready_context().await;
|
||||
return Err(err).context(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -430,6 +501,16 @@ 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
|
||||
@@ -512,12 +593,14 @@ 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,
|
||||
@@ -577,6 +660,16 @@ 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)
|
||||
}
|
||||
@@ -636,17 +729,20 @@ impl Daemon {
|
||||
})
|
||||
}
|
||||
|
||||
fn output(
|
||||
async 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,
|
||||
@@ -735,10 +831,12 @@ 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;
|
||||
@@ -751,22 +849,6 @@ 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!(
|
||||
@@ -847,12 +929,26 @@ 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")
|
||||
@@ -864,15 +960,59 @@ 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()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
459
codex-rs/app-server-daemon/src/remote_control_client.rs
Normal file
459
codex-rs/app-server-daemon/src/remote_control_client.rs
Normal file
@@ -0,0 +1,459 @@
|
||||
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(¬ification).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(¬ification) 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,26 +5,6 @@
|
||||
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
||||
"type": "string"
|
||||
},
|
||||
"ActivePermissionProfile": {
|
||||
"properties": {
|
||||
"extends": {
|
||||
"default": null,
|
||||
"description": "Parent profile identifier once permissions profiles support inheritance. This is currently always `null`.",
|
||||
"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"
|
||||
},
|
||||
"AddCreditsNudgeCreditType": {
|
||||
"enum": [
|
||||
"credits",
|
||||
@@ -611,6 +591,13 @@
|
||||
"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"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -1554,6 +1541,34 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PermissionProfileListParams": {
|
||||
"properties": {
|
||||
"cursor": {
|
||||
"description": "Opaque pagination cursor returned by a previous call.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cwd": {
|
||||
"description": "Optional working directory to resolve project config layers.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"limit": {
|
||||
"description": "Optional page size; defaults to the full result set.",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"Personality": {
|
||||
"enum": [
|
||||
"none",
|
||||
@@ -1589,6 +1604,31 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginInstalledParams": {
|
||||
"properties": {
|
||||
"cwds": {
|
||||
"description": "Optional working directories used to discover repo marketplaces.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"installSuggestionPluginNames": {
|
||||
"description": "Additional uninstalled plugin names that should be returned when present locally. This is used by mention surfaces that intentionally expose install entrypoints.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"PluginListMarketplaceKind": {
|
||||
"enum": [
|
||||
"local",
|
||||
@@ -2995,7 +3035,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadForkParams": {
|
||||
"description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.",
|
||||
"description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using a non-empty path, the thread_id param will be ignored. Empty string path values are treated as absent.\n\nPrefer using thread_id whenever possible.",
|
||||
"properties": {
|
||||
"approvalPolicy": {
|
||||
"anyOf": [
|
||||
@@ -3099,6 +3139,8 @@
|
||||
"enum": [
|
||||
"active",
|
||||
"paused",
|
||||
"blocked",
|
||||
"usageLimited",
|
||||
"budgetLimited",
|
||||
"complete"
|
||||
],
|
||||
@@ -3399,7 +3441,7 @@
|
||||
]
|
||||
},
|
||||
"ThreadResumeParams": {
|
||||
"description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nThe precedence is: history > path > thread_id. If using history or path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.",
|
||||
"description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nFor non-running threads, the precedence is: history > non-empty path > thread_id. If using history or a non-empty path for a non-running thread, the thread_id param will be ignored.\n\nIf thread_id identifies a running thread, app-server rejoins that thread and treats a non-empty path as a consistency check against the active rollout path. Empty string path values are treated as absent.\n\nPrefer using thread_id whenever possible.",
|
||||
"properties": {
|
||||
"approvalPolicy": {
|
||||
"anyOf": [
|
||||
@@ -4638,6 +4680,30 @@
|
||||
"title": "Plugin/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"plugin/installed"
|
||||
],
|
||||
"title": "Plugin/installedRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/PluginInstalledParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Plugin/installedRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -5286,6 +5352,30 @@
|
||||
"title": "ExperimentalFeature/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"permissionProfile/list"
|
||||
],
|
||||
"title": "PermissionProfile/listRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/PermissionProfileListParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "PermissionProfile/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -277,7 +277,7 @@
|
||||
"enum": [
|
||||
"read",
|
||||
"write",
|
||||
"none"
|
||||
"deny"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
"enum": [
|
||||
"read",
|
||||
"write",
|
||||
"none"
|
||||
"deny"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
"enum": [
|
||||
"read",
|
||||
"write",
|
||||
"none"
|
||||
"deny"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -1085,7 +1085,7 @@
|
||||
"enum": [
|
||||
"read",
|
||||
"write",
|
||||
"none"
|
||||
"deny"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1740,6 +1740,7 @@
|
||||
"postCompact",
|
||||
"sessionStart",
|
||||
"userPromptSubmit",
|
||||
"subagentStart",
|
||||
"stop"
|
||||
],
|
||||
"type": "string"
|
||||
@@ -3257,6 +3258,8 @@
|
||||
"enum": [
|
||||
"active",
|
||||
"paused",
|
||||
"blocked",
|
||||
"usageLimited",
|
||||
"budgetLimited",
|
||||
"complete"
|
||||
],
|
||||
|
||||
@@ -631,7 +631,7 @@
|
||||
"enum": [
|
||||
"read",
|
||||
"write",
|
||||
"none"
|
||||
"deny"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -757,6 +757,30 @@
|
||||
"title": "Plugin/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"plugin/installed"
|
||||
],
|
||||
"title": "Plugin/installedRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/PluginInstalledParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Plugin/installedRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -1405,6 +1429,30 @@
|
||||
"title": "ExperimentalFeature/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"permissionProfile/list"
|
||||
],
|
||||
"title": "PermissionProfile/listRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/PermissionProfileListParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "PermissionProfile/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -6290,6 +6338,25 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"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"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AutoReviewDecisionSource": {
|
||||
"description": "[UNSTABLE] Source that produced a terminal approval auto-review decision.",
|
||||
"enum": [
|
||||
@@ -7046,6 +7113,17 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ComputerUseRequirements": {
|
||||
"properties": {
|
||||
"allowLockedComputerUse": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"Config": {
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
@@ -7138,6 +7216,16 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"model_auto_compact_token_limit_scope": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/AutoCompactTokenLimitScope"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"model_context_window": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
@@ -7582,6 +7670,16 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"computerUse": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ComputerUseRequirements"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"enforceResidency": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -8170,6 +8268,13 @@
|
||||
"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",
|
||||
@@ -8464,7 +8569,7 @@
|
||||
"enum": [
|
||||
"read",
|
||||
"write",
|
||||
"none"
|
||||
"deny"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
@@ -9564,6 +9669,7 @@
|
||||
"postCompact",
|
||||
"sessionStart",
|
||||
"userPromptSubmit",
|
||||
"subagentStart",
|
||||
"stop"
|
||||
],
|
||||
"type": "string"
|
||||
@@ -10454,6 +10560,12 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"SubagentStart": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/ConfiguredHookMatcherGroup"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"UserPromptSubmit": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/ConfiguredHookMatcherGroup"
|
||||
@@ -10481,6 +10593,7 @@
|
||||
"PreToolUse",
|
||||
"SessionStart",
|
||||
"Stop",
|
||||
"SubagentStart",
|
||||
"UserPromptSubmit"
|
||||
],
|
||||
"type": "object"
|
||||
@@ -11619,6 +11732,78 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionProfileListParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"cursor": {
|
||||
"description": "Opaque pagination cursor returned by a previous call.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cwd": {
|
||||
"description": "Optional working directory to resolve project config layers.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"limit": {
|
||||
"description": "Optional page size; defaults to the full result set.",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "PermissionProfileListParams",
|
||||
"type": "object"
|
||||
},
|
||||
"PermissionProfileListResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"data": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/PermissionProfileSummary"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"nextCursor": {
|
||||
"description": "Opaque cursor to pass to the next call to continue after the last item. If None, there are no more items to return.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"title": "PermissionProfileListResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PermissionProfileSummary": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"description": "Optional user-facing description for display in clients.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"description": "Available permission profile identifier.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"Personality": {
|
||||
"enum": [
|
||||
"none",
|
||||
@@ -11825,6 +12010,56 @@
|
||||
"title": "PluginInstallResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginInstalledParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"cwds": {
|
||||
"description": "Optional working directories used to discover repo marketplaces.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"installSuggestionPluginNames": {
|
||||
"description": "Additional uninstalled plugin names that should be returned when present locally. This is used by mention surfaces that intentionally expose install entrypoints.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "PluginInstalledParams",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginInstalledResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"marketplaceLoadErrors": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/MarketplaceLoadErrorInfo"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"marketplaces": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/PluginMarketplaceEntry"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"marketplaces"
|
||||
],
|
||||
"title": "PluginInstalledResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginInterface": {
|
||||
"properties": {
|
||||
"brandColor": {
|
||||
@@ -15252,7 +15487,7 @@
|
||||
},
|
||||
"ThreadForkParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.",
|
||||
"description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using a non-empty path, the thread_id param will be ignored. Empty string path values are treated as absent.\n\nPrefer using thread_id whenever possible.",
|
||||
"properties": {
|
||||
"approvalPolicy": {
|
||||
"anyOf": [
|
||||
@@ -15487,6 +15722,8 @@
|
||||
"enum": [
|
||||
"active",
|
||||
"paused",
|
||||
"blocked",
|
||||
"usageLimited",
|
||||
"budgetLimited",
|
||||
"complete"
|
||||
],
|
||||
@@ -16748,7 +16985,7 @@
|
||||
},
|
||||
"ThreadResumeParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nThe precedence is: history > path > thread_id. If using history or path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.",
|
||||
"description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nFor non-running threads, the precedence is: history > non-empty path > thread_id. If using history or a non-empty path for a non-running thread, the thread_id param will be ignored.\n\nIf thread_id identifies a running thread, app-server rejoins that thread and treats a non-empty path as a consistency check against the active rollout path. Empty string path values are treated as absent.\n\nPrefer using thread_id whenever possible.",
|
||||
"properties": {
|
||||
"approvalPolicy": {
|
||||
"anyOf": [
|
||||
|
||||
@@ -826,6 +826,25 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"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"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AutoReviewDecisionSource": {
|
||||
"description": "[UNSTABLE] Source that produced a terminal approval auto-review decision.",
|
||||
"enum": [
|
||||
@@ -1464,6 +1483,30 @@
|
||||
"title": "Plugin/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"plugin/installed"
|
||||
],
|
||||
"title": "Plugin/installedRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/PluginInstalledParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Plugin/installedRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -2112,6 +2155,30 @@
|
||||
"title": "ExperimentalFeature/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"permissionProfile/list"
|
||||
],
|
||||
"title": "PermissionProfile/listRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/PermissionProfileListParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "PermissionProfile/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -3435,6 +3502,17 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ComputerUseRequirements": {
|
||||
"properties": {
|
||||
"allowLockedComputerUse": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"Config": {
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
@@ -3527,6 +3605,16 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"model_auto_compact_token_limit_scope": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AutoCompactTokenLimitScope"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"model_context_window": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
@@ -3971,6 +4059,16 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"computerUse": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ComputerUseRequirements"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"enforceResidency": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -4559,6 +4657,13 @@
|
||||
"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",
|
||||
@@ -4853,7 +4958,7 @@
|
||||
"enum": [
|
||||
"read",
|
||||
"write",
|
||||
"none"
|
||||
"deny"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
@@ -6064,6 +6169,7 @@
|
||||
"postCompact",
|
||||
"sessionStart",
|
||||
"userPromptSubmit",
|
||||
"subagentStart",
|
||||
"stop"
|
||||
],
|
||||
"type": "string"
|
||||
@@ -7003,6 +7109,12 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"SubagentStart": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/ConfiguredHookMatcherGroup"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"UserPromptSubmit": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/ConfiguredHookMatcherGroup"
|
||||
@@ -7030,6 +7142,7 @@
|
||||
"PreToolUse",
|
||||
"SessionStart",
|
||||
"Stop",
|
||||
"SubagentStart",
|
||||
"UserPromptSubmit"
|
||||
],
|
||||
"type": "object"
|
||||
@@ -8168,6 +8281,78 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionProfileListParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"cursor": {
|
||||
"description": "Opaque pagination cursor returned by a previous call.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cwd": {
|
||||
"description": "Optional working directory to resolve project config layers.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"limit": {
|
||||
"description": "Optional page size; defaults to the full result set.",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "PermissionProfileListParams",
|
||||
"type": "object"
|
||||
},
|
||||
"PermissionProfileListResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"data": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PermissionProfileSummary"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"nextCursor": {
|
||||
"description": "Opaque cursor to pass to the next call to continue after the last item. If None, there are no more items to return.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"title": "PermissionProfileListResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PermissionProfileSummary": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"description": "Optional user-facing description for display in clients.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"description": "Available permission profile identifier.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"Personality": {
|
||||
"enum": [
|
||||
"none",
|
||||
@@ -8374,6 +8559,56 @@
|
||||
"title": "PluginInstallResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginInstalledParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"cwds": {
|
||||
"description": "Optional working directories used to discover repo marketplaces.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"installSuggestionPluginNames": {
|
||||
"description": "Additional uninstalled plugin names that should be returned when present locally. This is used by mention surfaces that intentionally expose install entrypoints.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "PluginInstalledParams",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginInstalledResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"marketplaceLoadErrors": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MarketplaceLoadErrorInfo"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"marketplaces": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginMarketplaceEntry"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"marketplaces"
|
||||
],
|
||||
"title": "PluginInstalledResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginInterface": {
|
||||
"properties": {
|
||||
"brandColor": {
|
||||
@@ -13076,7 +13311,7 @@
|
||||
},
|
||||
"ThreadForkParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.",
|
||||
"description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using a non-empty path, the thread_id param will be ignored. Empty string path values are treated as absent.\n\nPrefer using thread_id whenever possible.",
|
||||
"properties": {
|
||||
"approvalPolicy": {
|
||||
"anyOf": [
|
||||
@@ -13311,6 +13546,8 @@
|
||||
"enum": [
|
||||
"active",
|
||||
"paused",
|
||||
"blocked",
|
||||
"usageLimited",
|
||||
"budgetLimited",
|
||||
"complete"
|
||||
],
|
||||
@@ -14572,7 +14809,7 @@
|
||||
},
|
||||
"ThreadResumeParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nThe precedence is: history > path > thread_id. If using history or path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.",
|
||||
"description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nFor non-running threads, the precedence is: history > non-empty path > thread_id. If using history or a non-empty path for a non-running thread, the thread_id param will be ignored.\n\nIf thread_id identifies a running thread, app-server rejoins that thread and treats a non-empty path as a consistency check against the active rollout path. Empty string path values are treated as absent.\n\nPrefer using thread_id whenever possible.",
|
||||
"properties": {
|
||||
"approvalPolicy": {
|
||||
"anyOf": [
|
||||
|
||||
@@ -5,26 +5,6 @@
|
||||
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
||||
"type": "string"
|
||||
},
|
||||
"ActivePermissionProfile": {
|
||||
"properties": {
|
||||
"extends": {
|
||||
"default": null,
|
||||
"description": "Parent profile identifier once permissions profiles support inheritance. This is currently always `null`.",
|
||||
"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"
|
||||
},
|
||||
"CommandExecTerminalSize": {
|
||||
"description": "PTY size in character cells for `command/exec` PTY sessions.",
|
||||
"properties": {
|
||||
|
||||
@@ -188,6 +188,25 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"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": {
|
||||
@@ -280,6 +299,16 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"model_auto_compact_token_limit_scope": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AutoCompactTokenLimitScope"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"model_context_window": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
|
||||
@@ -60,6 +60,17 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ComputerUseRequirements": {
|
||||
"properties": {
|
||||
"allowLockedComputerUse": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ConfigRequirements": {
|
||||
"properties": {
|
||||
"allowManagedHooksOnly": {
|
||||
@@ -95,6 +106,16 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"computerUse": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ComputerUseRequirements"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"enforceResidency": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -261,6 +282,12 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"SubagentStart": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/ConfiguredHookMatcherGroup"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"UserPromptSubmit": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/ConfiguredHookMatcherGroup"
|
||||
@@ -288,6 +315,7 @@
|
||||
"PreToolUse",
|
||||
"SessionStart",
|
||||
"Stop",
|
||||
"SubagentStart",
|
||||
"UserPromptSubmit"
|
||||
],
|
||||
"type": "object"
|
||||
|
||||
@@ -16,6 +16,13 @@
|
||||
"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",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"postCompact",
|
||||
"sessionStart",
|
||||
"userPromptSubmit",
|
||||
"subagentStart",
|
||||
"stop"
|
||||
],
|
||||
"type": "string"
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"postCompact",
|
||||
"sessionStart",
|
||||
"userPromptSubmit",
|
||||
"subagentStart",
|
||||
"stop"
|
||||
],
|
||||
"type": "string"
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"postCompact",
|
||||
"sessionStart",
|
||||
"userPromptSubmit",
|
||||
"subagentStart",
|
||||
"stop"
|
||||
],
|
||||
"type": "string"
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
"enum": [
|
||||
"read",
|
||||
"write",
|
||||
"none"
|
||||
"deny"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
"enum": [
|
||||
"read",
|
||||
"write",
|
||||
"none"
|
||||
"deny"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
30
codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListParams.json
generated
Normal file
30
codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListParams.json
generated
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"cursor": {
|
||||
"description": "Opaque pagination cursor returned by a previous call.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cwd": {
|
||||
"description": "Optional working directory to resolve project config layers.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"limit": {
|
||||
"description": "Optional page size; defaults to the full result set.",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "PermissionProfileListParams",
|
||||
"type": "object"
|
||||
}
|
||||
44
codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListResponse.json
generated
Normal file
44
codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListResponse.json
generated
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"PermissionProfileSummary": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"description": "Optional user-facing description for display in clients.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"description": "Available permission profile identifier.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"data": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PermissionProfileSummary"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"nextCursor": {
|
||||
"description": "Opaque cursor to pass to the next call to continue after the last item. If None, there are no more items to return.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"title": "PermissionProfileListResponse",
|
||||
"type": "object"
|
||||
}
|
||||
33
codex-rs/app-server-protocol/schema/json/v2/PluginInstalledParams.json
generated
Normal file
33
codex-rs/app-server-protocol/schema/json/v2/PluginInstalledParams.json
generated
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"AbsolutePathBuf": {
|
||||
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"cwds": {
|
||||
"description": "Optional working directories used to discover repo marketplaces.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"installSuggestionPluginNames": {
|
||||
"description": "Additional uninstalled plugin names that should be returned when present locally. This is used by mention surfaces that intentionally expose install entrypoints.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "PluginInstalledParams",
|
||||
"type": "object"
|
||||
}
|
||||
525
codex-rs/app-server-protocol/schema/json/v2/PluginInstalledResponse.json
generated
Normal file
525
codex-rs/app-server-protocol/schema/json/v2/PluginInstalledResponse.json
generated
Normal file
@@ -0,0 +1,525 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"AbsolutePathBuf": {
|
||||
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
||||
"type": "string"
|
||||
},
|
||||
"MarketplaceInterface": {
|
||||
"properties": {
|
||||
"displayName": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MarketplaceLoadErrorInfo": {
|
||||
"properties": {
|
||||
"marketplacePath": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"marketplacePath",
|
||||
"message"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginAuthPolicy": {
|
||||
"enum": [
|
||||
"ON_INSTALL",
|
||||
"ON_USE"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginAvailability": {
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"DISABLED_BY_ADMIN"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Plugin-service currently sends `\"ENABLED\"` for available remote plugins. Codex app-server exposes `\"AVAILABLE\"` in its API; the alias keeps decoding compatible with that upstream response.",
|
||||
"enum": [
|
||||
"AVAILABLE"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PluginInstallPolicy": {
|
||||
"enum": [
|
||||
"NOT_AVAILABLE",
|
||||
"AVAILABLE",
|
||||
"INSTALLED_BY_DEFAULT"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginInterface": {
|
||||
"properties": {
|
||||
"brandColor": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"capabilities": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"category": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"composerIcon": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Local composer icon path, resolved from the installed plugin package."
|
||||
},
|
||||
"composerIconUrl": {
|
||||
"description": "Remote composer icon URL from the plugin catalog.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"defaultPrompt": {
|
||||
"description": "Starter prompts for the plugin. Capped at 3 entries with a maximum of 128 characters per entry.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"developerName": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"displayName": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"logo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Local logo path, resolved from the installed plugin package."
|
||||
},
|
||||
"logoUrl": {
|
||||
"description": "Remote logo URL from the plugin catalog.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"longDescription": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"privacyPolicyUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"screenshotUrls": {
|
||||
"description": "Remote screenshot URLs from the plugin catalog.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"screenshots": {
|
||||
"description": "Local screenshot paths, resolved from the installed plugin package.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"shortDescription": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"termsOfServiceUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"websiteUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"capabilities",
|
||||
"screenshotUrls",
|
||||
"screenshots"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginMarketplaceEntry": {
|
||||
"properties": {
|
||||
"interface": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MarketplaceInterface"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Local marketplace file path when the marketplace is backed by a local file. Remote-only catalog marketplaces do not have a local path."
|
||||
},
|
||||
"plugins": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginSummary"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"plugins"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareContext": {
|
||||
"properties": {
|
||||
"creatorAccountUserId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"creatorName": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"discoverability": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PluginShareDiscoverability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
"remoteVersion": {
|
||||
"default": null,
|
||||
"description": "Version of the remote shared plugin release when available.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sharePrincipals": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginSharePrincipal"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"shareUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"remotePluginId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareDiscoverability": {
|
||||
"enum": [
|
||||
"LISTED",
|
||||
"UNLISTED",
|
||||
"PRIVATE"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginSharePrincipal": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"principalId": {
|
||||
"type": "string"
|
||||
},
|
||||
"principalType": {
|
||||
"$ref": "#/definitions/PluginSharePrincipalType"
|
||||
},
|
||||
"role": {
|
||||
"$ref": "#/definitions/PluginSharePrincipalRole"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"principalId",
|
||||
"principalType",
|
||||
"role"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSharePrincipalRole": {
|
||||
"enum": [
|
||||
"reader",
|
||||
"editor",
|
||||
"owner"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginSharePrincipalType": {
|
||||
"enum": [
|
||||
"user",
|
||||
"group",
|
||||
"workspace"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"local"
|
||||
],
|
||||
"title": "LocalPluginSourceType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "LocalPluginSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"refName": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sha": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"git"
|
||||
],
|
||||
"title": "GitPluginSourceType",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"url"
|
||||
],
|
||||
"title": "GitPluginSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "The plugin is available in the remote catalog. Download metadata is kept server-side and is not exposed through the app-server API.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"remote"
|
||||
],
|
||||
"title": "RemotePluginSourceType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "RemotePluginSource",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PluginSummary": {
|
||||
"properties": {
|
||||
"authPolicy": {
|
||||
"$ref": "#/definitions/PluginAuthPolicy"
|
||||
},
|
||||
"availability": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PluginAvailability"
|
||||
}
|
||||
],
|
||||
"default": "AVAILABLE",
|
||||
"description": "Availability state for installing and using the plugin."
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"installPolicy": {
|
||||
"$ref": "#/definitions/PluginInstallPolicy"
|
||||
},
|
||||
"installed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"interface": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PluginInterface"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"keywords": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"localVersion": {
|
||||
"default": null,
|
||||
"description": "Version of the locally materialized plugin package when available.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"remotePluginId": {
|
||||
"description": "Backend remote plugin identifier when available.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"shareContext": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PluginShareContext"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Remote sharing context associated with this plugin when available."
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/PluginSource"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"authPolicy",
|
||||
"enabled",
|
||||
"id",
|
||||
"installPolicy",
|
||||
"installed",
|
||||
"name",
|
||||
"source"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"marketplaceLoadErrors": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MarketplaceLoadErrorInfo"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"marketplaces": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginMarketplaceEntry"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"marketplaces"
|
||||
],
|
||||
"title": "PluginInstalledResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -46,6 +46,7 @@
|
||||
"postCompact",
|
||||
"sessionStart",
|
||||
"userPromptSubmit",
|
||||
"subagentStart",
|
||||
"stop"
|
||||
],
|
||||
"type": "string"
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.",
|
||||
"description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using a non-empty path, the thread_id param will be ignored. Empty string path values are treated as absent.\n\nPrefer using thread_id whenever possible.",
|
||||
"properties": {
|
||||
"approvalPolicy": {
|
||||
"anyOf": [
|
||||
|
||||
@@ -51,6 +51,8 @@
|
||||
"enum": [
|
||||
"active",
|
||||
"paused",
|
||||
"blocked",
|
||||
"usageLimited",
|
||||
"budgetLimited",
|
||||
"complete"
|
||||
],
|
||||
|
||||
@@ -963,7 +963,7 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nThe precedence is: history > path > thread_id. If using history or path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.",
|
||||
"description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nFor non-running threads, the precedence is: history > non-empty path > thread_id. If using history or a non-empty path for a non-running thread, the thread_id param will be ignored.\n\nIf thread_id identifies a running thread, app-server rejoins that thread and treats a non-empty path as a consistency check against the active rollout path. Empty string path values are treated as absent.\n\nPrefer using thread_id whenever possible.",
|
||||
"properties": {
|
||||
"approvalPolicy": {
|
||||
"anyOf": [
|
||||
|
||||
9
codex-rs/app-server-protocol/schema/typescript/AutoCompactTokenLimitScope.ts
generated
Normal file
9
codex-rs/app-server-protocol/schema/typescript/AutoCompactTokenLimitScope.ts
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Selects which part of the active context is charged against
|
||||
* `model_auto_compact_token_limit`.
|
||||
*/
|
||||
export type AutoCompactTokenLimitScope = "total" | "body_after_prefix";
|
||||
File diff suppressed because one or more lines are too long
@@ -5,6 +5,7 @@ export type { AgentPath } from "./AgentPath";
|
||||
export type { ApplyPatchApprovalParams } from "./ApplyPatchApprovalParams";
|
||||
export type { ApplyPatchApprovalResponse } from "./ApplyPatchApprovalResponse";
|
||||
export type { AuthMode } from "./AuthMode";
|
||||
export type { AutoCompactTokenLimitScope } from "./AutoCompactTokenLimitScope";
|
||||
export type { ClientInfo } from "./ClientInfo";
|
||||
export type { ClientNotification } from "./ClientNotification";
|
||||
export type { ClientRequest } from "./ClientRequest";
|
||||
|
||||
5
codex-rs/app-server-protocol/schema/typescript/v2/ComputerUseRequirements.ts
generated
Normal file
5
codex-rs/app-server-protocol/schema/typescript/v2/ComputerUseRequirements.ts
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ComputerUseRequirements = { allowLockedComputerUse: boolean | null, };
|
||||
@@ -1,6 +1,7 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AutoCompactTokenLimitScope } from "../AutoCompactTokenLimitScope";
|
||||
import type { ForcedLoginMethod } from "../ForcedLoginMethod";
|
||||
import type { ReasoningEffort } from "../ReasoningEffort";
|
||||
import type { ReasoningSummary } from "../ReasoningSummary";
|
||||
@@ -16,7 +17,7 @@ import type { SandboxMode } from "./SandboxMode";
|
||||
import type { SandboxWorkspaceWrite } from "./SandboxWorkspaceWrite";
|
||||
import type { ToolsV2 } from "./ToolsV2";
|
||||
|
||||
export type Config = {model: string | null, review_model: string | null, model_context_window: bigint | null, model_auto_compact_token_limit: bigint | null, model_provider: string | null, approval_policy: AskForApproval | null, /**
|
||||
export type Config = {model: string | null, review_model: string | null, model_context_window: bigint | null, model_auto_compact_token_limit: bigint | null, model_auto_compact_token_limit_scope: AutoCompactTokenLimitScope | null, model_provider: string | null, approval_policy: AskForApproval | null, /**
|
||||
* [UNSTABLE] Optional default for where approval requests are routed for
|
||||
* review.
|
||||
*/
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { WebSearchMode } from "../WebSearchMode";
|
||||
import type { AskForApproval } from "./AskForApproval";
|
||||
import type { ComputerUseRequirements } from "./ComputerUseRequirements";
|
||||
import type { ResidencyRequirement } from "./ResidencyRequirement";
|
||||
import type { SandboxMode } from "./SandboxMode";
|
||||
|
||||
export type ConfigRequirements = {allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: boolean | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null};
|
||||
export type ConfigRequirements = {allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null};
|
||||
|
||||
@@ -10,4 +10,10 @@ cursor?: string | null,
|
||||
/**
|
||||
* Optional page size; defaults to a reasonable server-side value.
|
||||
*/
|
||||
limit?: number | null, };
|
||||
limit?: number | null,
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
threadId?: string | null, };
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type FileSystemAccessMode = "read" | "write" | "none";
|
||||
export type FileSystemAccessMode = "read" | "write" | "deny";
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type HookEventName = "preToolUse" | "permissionRequest" | "postToolUse" | "preCompact" | "postCompact" | "sessionStart" | "userPromptSubmit" | "stop";
|
||||
export type HookEventName = "preToolUse" | "permissionRequest" | "postToolUse" | "preCompact" | "postCompact" | "sessionStart" | "userPromptSubmit" | "subagentStart" | "stop";
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ConfiguredHookMatcherGroup } from "./ConfiguredHookMatcherGroup";
|
||||
|
||||
export type ManagedHooksRequirements = { managedDir: string | null, windowsManagedDir: string | null, PreToolUse: Array<ConfiguredHookMatcherGroup>, PermissionRequest: Array<ConfiguredHookMatcherGroup>, PostToolUse: Array<ConfiguredHookMatcherGroup>, PreCompact: Array<ConfiguredHookMatcherGroup>, PostCompact: Array<ConfiguredHookMatcherGroup>, SessionStart: Array<ConfiguredHookMatcherGroup>, UserPromptSubmit: Array<ConfiguredHookMatcherGroup>, Stop: Array<ConfiguredHookMatcherGroup>, };
|
||||
export type ManagedHooksRequirements = { managedDir: string | null, windowsManagedDir: string | null, PreToolUse: Array<ConfiguredHookMatcherGroup>, PermissionRequest: Array<ConfiguredHookMatcherGroup>, PostToolUse: Array<ConfiguredHookMatcherGroup>, PreCompact: Array<ConfiguredHookMatcherGroup>, PostCompact: Array<ConfiguredHookMatcherGroup>, SessionStart: Array<ConfiguredHookMatcherGroup>, UserPromptSubmit: Array<ConfiguredHookMatcherGroup>, SubagentStart: Array<ConfiguredHookMatcherGroup>, Stop: Array<ConfiguredHookMatcherGroup>, };
|
||||
|
||||
17
codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListParams.ts
generated
Normal file
17
codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListParams.ts
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type PermissionProfileListParams = {
|
||||
/**
|
||||
* Opaque pagination cursor returned by a previous call.
|
||||
*/
|
||||
cursor?: string | null,
|
||||
/**
|
||||
* Optional page size; defaults to the full result set.
|
||||
*/
|
||||
limit?: number | null,
|
||||
/**
|
||||
* Optional working directory to resolve project config layers.
|
||||
*/
|
||||
cwd?: string | null, };
|
||||
11
codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListResponse.ts
generated
Normal file
11
codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListResponse.ts
generated
Normal file
@@ -0,0 +1,11 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PermissionProfileSummary } from "./PermissionProfileSummary";
|
||||
|
||||
export type PermissionProfileListResponse = { data: Array<PermissionProfileSummary>,
|
||||
/**
|
||||
* Opaque cursor to pass to the next call to continue after the last item.
|
||||
* If None, there are no more items to return.
|
||||
*/
|
||||
nextCursor: string | null, };
|
||||
13
codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileSummary.ts
generated
Normal file
13
codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileSummary.ts
generated
Normal file
@@ -0,0 +1,13 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type PermissionProfileSummary = {
|
||||
/**
|
||||
* Available permission profile identifier.
|
||||
*/
|
||||
id: string,
|
||||
/**
|
||||
* Optional user-facing description for display in clients.
|
||||
*/
|
||||
description: string | null, };
|
||||
15
codex-rs/app-server-protocol/schema/typescript/v2/PluginInstalledParams.ts
generated
Normal file
15
codex-rs/app-server-protocol/schema/typescript/v2/PluginInstalledParams.ts
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
|
||||
export type PluginInstalledParams = {
|
||||
/**
|
||||
* Optional working directories used to discover repo marketplaces.
|
||||
*/
|
||||
cwds?: Array<AbsolutePathBuf> | null,
|
||||
/**
|
||||
* Additional uninstalled plugin names that should be returned when present locally.
|
||||
* This is used by mention surfaces that intentionally expose install entrypoints.
|
||||
*/
|
||||
installSuggestionPluginNames?: Array<string> | null, };
|
||||
7
codex-rs/app-server-protocol/schema/typescript/v2/PluginInstalledResponse.ts
generated
Normal file
7
codex-rs/app-server-protocol/schema/typescript/v2/PluginInstalledResponse.ts
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { MarketplaceLoadErrorInfo } from "./MarketplaceLoadErrorInfo";
|
||||
import type { PluginMarketplaceEntry } from "./PluginMarketplaceEntry";
|
||||
|
||||
export type PluginInstalledResponse = { marketplaces: Array<PluginMarketplaceEntry>, marketplaceLoadErrors: Array<MarketplaceLoadErrorInfo>, };
|
||||
@@ -12,7 +12,8 @@ import type { ThreadSource } from "./ThreadSource";
|
||||
* 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread.
|
||||
* 2. By path: load the thread from disk by path and fork it into a new thread.
|
||||
*
|
||||
* If using path, the thread_id param will be ignored.
|
||||
* If using a non-empty path, the thread_id param will be ignored.
|
||||
* Empty string path values are treated as absent.
|
||||
*
|
||||
* Prefer using thread_id whenever possible.
|
||||
*/
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ThreadGoalStatus = "active" | "paused" | "budgetLimited" | "complete";
|
||||
export type ThreadGoalStatus = "active" | "paused" | "blocked" | "usageLimited" | "budgetLimited" | "complete";
|
||||
|
||||
@@ -13,8 +13,13 @@ import type { SandboxMode } from "./SandboxMode";
|
||||
* 2. By history: instantiate the thread from memory and resume it.
|
||||
* 3. By path: load the thread from disk by path and resume it.
|
||||
*
|
||||
* The precedence is: history > path > thread_id.
|
||||
* If using history or path, the thread_id param will be ignored.
|
||||
* For non-running threads, the precedence is: history > non-empty path > thread_id.
|
||||
* If using history or a non-empty path for a non-running thread, the thread_id
|
||||
* param will be ignored.
|
||||
*
|
||||
* If thread_id identifies a running thread, app-server rejoins that thread and
|
||||
* treats a non-empty path as a consistency check against the active rollout path.
|
||||
* Empty string path values are treated as absent.
|
||||
*
|
||||
* Prefer using thread_id whenever possible.
|
||||
*/
|
||||
|
||||
@@ -62,6 +62,7 @@ export type { CommandExecutionRequestApprovalResponse } from "./CommandExecution
|
||||
export type { CommandExecutionSource } from "./CommandExecutionSource";
|
||||
export type { CommandExecutionStatus } from "./CommandExecutionStatus";
|
||||
export type { CommandMigration } from "./CommandMigration";
|
||||
export type { ComputerUseRequirements } from "./ComputerUseRequirements";
|
||||
export type { Config } from "./Config";
|
||||
export type { ConfigBatchWriteParams } from "./ConfigBatchWriteParams";
|
||||
export type { ConfigEdit } from "./ConfigEdit";
|
||||
@@ -255,6 +256,9 @@ export type { OverriddenMetadata } from "./OverriddenMetadata";
|
||||
export type { PatchApplyStatus } from "./PatchApplyStatus";
|
||||
export type { PatchChangeKind } from "./PatchChangeKind";
|
||||
export type { PermissionGrantScope } from "./PermissionGrantScope";
|
||||
export type { PermissionProfileListParams } from "./PermissionProfileListParams";
|
||||
export type { PermissionProfileListResponse } from "./PermissionProfileListResponse";
|
||||
export type { PermissionProfileSummary } from "./PermissionProfileSummary";
|
||||
export type { PermissionsRequestApprovalParams } from "./PermissionsRequestApprovalParams";
|
||||
export type { PermissionsRequestApprovalResponse } from "./PermissionsRequestApprovalResponse";
|
||||
export type { PlanDeltaNotification } from "./PlanDeltaNotification";
|
||||
@@ -265,6 +269,8 @@ export type { PluginHookSummary } from "./PluginHookSummary";
|
||||
export type { PluginInstallParams } from "./PluginInstallParams";
|
||||
export type { PluginInstallPolicy } from "./PluginInstallPolicy";
|
||||
export type { PluginInstallResponse } from "./PluginInstallResponse";
|
||||
export type { PluginInstalledParams } from "./PluginInstalledParams";
|
||||
export type { PluginInstalledResponse } from "./PluginInstalledResponse";
|
||||
export type { PluginInterface } from "./PluginInterface";
|
||||
export type { PluginListMarketplaceKind } from "./PluginListMarketplaceKind";
|
||||
export type { PluginListParams } from "./PluginListParams";
|
||||
|
||||
@@ -2747,7 +2747,6 @@ export type Config = { stableField: Keep, unstableField: string | null } & ({ [k
|
||||
let _guard = TempDirGuard(output_dir.clone());
|
||||
let path = output_dir.join("CommandExecParams.ts");
|
||||
let content = r#"import type { CommandExecTerminalSize } from "./CommandExecTerminalSize";
|
||||
import type { ActivePermissionProfile } from "./ActivePermissionProfile";
|
||||
import type { SandboxPolicy } from "./SandboxPolicy";
|
||||
|
||||
export type CommandExecParams = {/**
|
||||
@@ -2770,12 +2769,12 @@ size?: CommandExecTerminalSize | null, /**
|
||||
*/
|
||||
sandboxPolicy?: SandboxPolicy | null,
|
||||
/**
|
||||
* Optional active permissions profile for this command.
|
||||
* Optional active permissions profile id for this command.
|
||||
*
|
||||
* Defaults to the user's configured permissions when omitted. Cannot be
|
||||
* combined with `sandboxPolicy`.
|
||||
*/
|
||||
permissionProfile?: ActivePermissionProfile | null};
|
||||
permissionProfile?: string | null};
|
||||
"#;
|
||||
fs::write(&path, content)?;
|
||||
|
||||
@@ -2788,16 +2787,7 @@ permissionProfile?: ActivePermissionProfile | null};
|
||||
filter_experimental_type_fields_ts(&output_dir, &[&CUSTOM_FIELD])?;
|
||||
|
||||
let filtered = fs::read_to_string(&path)?;
|
||||
assert_eq!(
|
||||
filtered.contains("permissionProfile?: ActivePermissionProfile"),
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
filtered.contains(
|
||||
r#"import type { ActivePermissionProfile } from "./ActivePermissionProfile";"#
|
||||
),
|
||||
false
|
||||
);
|
||||
assert_eq!(filtered.contains("permissionProfile?: string"), false);
|
||||
assert_eq!(filtered.contains("sandboxPolicy?: SandboxPolicy"), true);
|
||||
assert_eq!(
|
||||
filtered.contains(r#"import type { SandboxPolicy } from "./SandboxPolicy";"#),
|
||||
|
||||
@@ -625,6 +625,11 @@ client_request_definitions! {
|
||||
serialization: None,
|
||||
response: v2::PluginListResponse,
|
||||
},
|
||||
PluginInstalled => "plugin/installed" {
|
||||
params: v2::PluginInstalledParams,
|
||||
serialization: None,
|
||||
response: v2::PluginInstalledResponse,
|
||||
},
|
||||
PluginRead => "plugin/read" {
|
||||
params: v2::PluginReadParams,
|
||||
serialization: None,
|
||||
@@ -795,6 +800,11 @@ client_request_definitions! {
|
||||
serialization: global("config"),
|
||||
response: v2::ExperimentalFeatureListResponse,
|
||||
},
|
||||
PermissionProfileList => "permissionProfile/list" {
|
||||
params: v2::PermissionProfileListParams,
|
||||
serialization: global_shared_read("config"),
|
||||
response: v2::PermissionProfileListResponse,
|
||||
},
|
||||
ExperimentalFeatureEnablementSet => "experimentalFeature/enablement/set" {
|
||||
params: v2::ExperimentalFeatureEnablementSetParams,
|
||||
serialization: global("config"),
|
||||
@@ -1550,6 +1560,7 @@ mod tests {
|
||||
use anyhow::Result;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::account::PlanType;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
use codex_protocol::protocol::RealtimeConversationVersion;
|
||||
use codex_protocol::protocol::RealtimeOutputModality;
|
||||
@@ -1719,6 +1730,15 @@ mod tests {
|
||||
};
|
||||
assert_eq!(plugin_read.serialization_scope(), None);
|
||||
|
||||
let plugin_installed = ClientRequest::PluginInstalled {
|
||||
request_id: request_id(),
|
||||
params: v2::PluginInstalledParams {
|
||||
cwds: None,
|
||||
install_suggestion_plugin_names: None,
|
||||
},
|
||||
};
|
||||
assert_eq!(plugin_installed.serialization_scope(), None);
|
||||
|
||||
let plugin_uninstall = ClientRequest::PluginUninstall {
|
||||
request_id: request_id(),
|
||||
params: v2::PluginUninstallParams {
|
||||
@@ -2707,7 +2727,33 @@ mod tests {
|
||||
"id": 8,
|
||||
"params": {
|
||||
"cursor": null,
|
||||
"limit": null
|
||||
"limit": null,
|
||||
"threadId": null
|
||||
}
|
||||
}),
|
||||
serde_json::to_value(&request)?,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_list_experimental_features_with_thread_id() -> Result<()> {
|
||||
let request = ClientRequest::ExperimentalFeatureList {
|
||||
request_id: RequestId::Integer(8),
|
||||
params: v2::ExperimentalFeatureListParams {
|
||||
cursor: Some("3".to_string()),
|
||||
limit: Some(2),
|
||||
thread_id: Some("00000000-0000-4000-8000-000000000001".to_string()),
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
json!({
|
||||
"method": "experimentalFeature/list",
|
||||
"id": 8,
|
||||
"params": {
|
||||
"cursor": "3",
|
||||
"limit": 2,
|
||||
"threadId": "00000000-0000-4000-8000-000000000001"
|
||||
}
|
||||
}),
|
||||
serde_json::to_value(&request)?,
|
||||
@@ -2954,7 +3000,7 @@ mod tests {
|
||||
env: None,
|
||||
size: None,
|
||||
sandbox_policy: None,
|
||||
permission_profile: Some(v2::ActivePermissionProfile::read_only()),
|
||||
permission_profile: Some(BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string()),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
use serde::Serialize;
|
||||
use serde::Serializer;
|
||||
|
||||
pub fn deserialize_empty_path_as_none<'de, D>(deserializer: D) -> Result<Option<PathBuf>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let path = Option::<PathBuf>::deserialize(deserializer)?;
|
||||
Ok(path.filter(|path| !path.as_os_str().is_empty()))
|
||||
}
|
||||
|
||||
pub fn deserialize_double_option<'de, T, D>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use super::ActivePermissionProfile;
|
||||
use super::SandboxPolicy;
|
||||
use codex_experimental_api_macros::ExperimentalApi;
|
||||
use schemars::JsonSchema;
|
||||
@@ -100,13 +99,13 @@ pub struct CommandExecParams {
|
||||
/// combined with `permissionProfile`.
|
||||
#[ts(optional = nullable)]
|
||||
pub sandbox_policy: Option<SandboxPolicy>,
|
||||
/// Optional active permissions profile for this command.
|
||||
/// Optional active permissions profile id for this command.
|
||||
///
|
||||
/// Defaults to the user's configured permissions when omitted. Cannot be
|
||||
/// combined with `sandboxPolicy`.
|
||||
#[experimental("command/exec.permissionProfile")]
|
||||
#[ts(optional = nullable)]
|
||||
pub permission_profile: Option<ActivePermissionProfile>,
|
||||
pub permission_profile: Option<String>,
|
||||
}
|
||||
|
||||
/// Final buffered result for `command/exec`.
|
||||
|
||||
@@ -3,6 +3,7 @@ use super::AskForApproval;
|
||||
use super::SandboxMode;
|
||||
use super::shared::default_enabled;
|
||||
use codex_experimental_api_macros::ExperimentalApi;
|
||||
use codex_protocol::config_types::AutoCompactTokenLimitScope;
|
||||
use codex_protocol::config_types::ForcedLoginMethod;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::config_types::Verbosity;
|
||||
@@ -251,6 +252,7 @@ pub struct Config {
|
||||
pub review_model: Option<String>,
|
||||
pub model_context_window: Option<i64>,
|
||||
pub model_auto_compact_token_limit: Option<i64>,
|
||||
pub model_auto_compact_token_limit_scope: Option<AutoCompactTokenLimitScope>,
|
||||
pub model_provider: Option<String>,
|
||||
#[experimental(nested)]
|
||||
pub approval_policy: Option<AskForApproval>,
|
||||
@@ -386,6 +388,7 @@ pub struct ConfigRequirements {
|
||||
pub allowed_sandbox_modes: Option<Vec<SandboxMode>>,
|
||||
pub allowed_web_search_modes: Option<Vec<WebSearchMode>>,
|
||||
pub allow_managed_hooks_only: Option<bool>,
|
||||
pub computer_use: Option<ComputerUseRequirements>,
|
||||
pub feature_requirements: Option<BTreeMap<String, bool>>,
|
||||
#[experimental("configRequirements/read.hooks")]
|
||||
pub hooks: Option<ManagedHooksRequirements>,
|
||||
@@ -394,6 +397,13 @@ pub struct ConfigRequirements {
|
||||
pub network: Option<NetworkRequirements>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ComputerUseRequirements {
|
||||
pub allow_locked_computer_use: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -421,6 +431,9 @@ pub struct ManagedHooksRequirements {
|
||||
#[serde(rename = "UserPromptSubmit")]
|
||||
#[ts(rename = "UserPromptSubmit")]
|
||||
pub user_prompt_submit: Vec<ConfiguredHookMatcherGroup>,
|
||||
#[serde(rename = "SubagentStart")]
|
||||
#[ts(rename = "SubagentStart")]
|
||||
pub subagent_start: Vec<ConfiguredHookMatcherGroup>,
|
||||
#[serde(rename = "Stop")]
|
||||
#[ts(rename = "Stop")]
|
||||
pub stop: Vec<ConfiguredHookMatcherGroup>,
|
||||
|
||||
@@ -14,6 +14,11 @@ pub struct ExperimentalFeatureListParams {
|
||||
/// Optional page size; defaults to a reasonable server-side value.
|
||||
#[ts(optional = nullable)]
|
||||
pub limit: Option<u32>,
|
||||
/// 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.
|
||||
#[ts(optional = nullable)]
|
||||
pub thread_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
|
||||
@@ -17,7 +17,7 @@ use ts_rs::TS;
|
||||
|
||||
v2_enum_from_core!(
|
||||
pub enum HookEventName from CoreHookEventName {
|
||||
PreToolUse, PermissionRequest, PostToolUse, PreCompact, PostCompact, SessionStart, UserPromptSubmit, Stop
|
||||
PreToolUse, PermissionRequest, PostToolUse, PreCompact, PostCompact, SessionStart, UserPromptSubmit, SubagentStart, Stop
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -18,9 +18,7 @@ use codex_protocol::request_permissions::RequestPermissionProfile as CoreRequest
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
use serde::Serialize;
|
||||
use serde::Serializer;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::path::PathBuf;
|
||||
use ts_rs::TS;
|
||||
@@ -180,7 +178,7 @@ v2_enum_from_core!(
|
||||
pub enum FileSystemAccessMode from CoreFileSystemAccessMode {
|
||||
Read,
|
||||
Write,
|
||||
None
|
||||
Deny
|
||||
}
|
||||
);
|
||||
|
||||
@@ -289,6 +287,41 @@ impl From<FileSystemSandboxEntry> for CoreFileSystemSandboxEntry {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PermissionProfileListParams {
|
||||
/// Opaque pagination cursor returned by a previous call.
|
||||
#[ts(optional = nullable)]
|
||||
pub cursor: Option<String>,
|
||||
/// Optional page size; defaults to the full result set.
|
||||
#[ts(optional = nullable)]
|
||||
pub limit: Option<u32>,
|
||||
/// Optional working directory to resolve project config layers.
|
||||
#[ts(optional = nullable)]
|
||||
pub cwd: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PermissionProfileSummary {
|
||||
/// Available permission profile identifier.
|
||||
pub id: String,
|
||||
/// Optional user-facing description for display in clients.
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PermissionProfileListResponse {
|
||||
pub data: Vec<PermissionProfileSummary>,
|
||||
/// Opaque cursor to pass to the next call to continue after the last item.
|
||||
/// If None, there are no more items to return.
|
||||
pub next_cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -333,102 +366,6 @@ impl From<ActivePermissionProfile> for CoreActivePermissionProfile {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PermissionProfileSelectionParams {
|
||||
id: String,
|
||||
legacy_additional_writable_roots: Vec<AbsolutePathBuf>,
|
||||
}
|
||||
|
||||
impl PermissionProfileSelectionParams {
|
||||
pub fn new(id: impl Into<String>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
legacy_additional_writable_roots: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn into_id(self) -> String {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn legacy_additional_writable_roots(&self) -> &[AbsolutePathBuf] {
|
||||
&self.legacy_additional_writable_roots
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for PermissionProfileSelectionParams {
|
||||
fn from(id: String) -> Self {
|
||||
Self::new(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for PermissionProfileSelectionParams {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.id)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for PermissionProfileSelectionParams {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum Wire {
|
||||
Id(String),
|
||||
LegacyProfile {
|
||||
#[serde(rename = "type")]
|
||||
_type: LegacyPermissionProfileSelectionType,
|
||||
id: String,
|
||||
#[serde(default)]
|
||||
modifications: Option<Vec<LegacyPermissionProfileModificationParams>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
enum LegacyPermissionProfileSelectionType {
|
||||
Profile,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
enum LegacyPermissionProfileModificationParams {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
AdditionalWritableRoot { path: AbsolutePathBuf },
|
||||
}
|
||||
|
||||
match Wire::deserialize(deserializer)? {
|
||||
Wire::Id(id) => Ok(Self::new(id)),
|
||||
Wire::LegacyProfile {
|
||||
id, modifications, ..
|
||||
} => {
|
||||
let legacy_additional_writable_roots = modifications
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|modification| match modification {
|
||||
LegacyPermissionProfileModificationParams::AdditionalWritableRoot {
|
||||
path,
|
||||
} => path,
|
||||
})
|
||||
.collect();
|
||||
Ok(Self {
|
||||
id,
|
||||
legacy_additional_writable_roots,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
||||
@@ -125,6 +125,19 @@ pub struct PluginListParams {
|
||||
pub marketplace_kinds: Option<Vec<PluginListMarketplaceKind>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginInstalledParams {
|
||||
/// Optional working directories used to discover repo marketplaces.
|
||||
#[ts(optional = nullable)]
|
||||
pub cwds: Option<Vec<AbsolutePathBuf>>,
|
||||
/// Additional uninstalled plugin names that should be returned when present locally.
|
||||
/// This is used by mention surfaces that intentionally expose install entrypoints.
|
||||
#[ts(optional = nullable)]
|
||||
pub install_suggestion_plugin_names: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum PluginListMarketplaceKind {
|
||||
@@ -150,6 +163,15 @@ pub struct PluginListResponse {
|
||||
pub featured_plugin_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginInstalledResponse {
|
||||
pub marketplaces: Vec<PluginMarketplaceEntry>,
|
||||
#[serde(default)]
|
||||
pub marketplace_load_errors: Vec<MarketplaceLoadErrorInfo>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
||||
@@ -14,6 +14,7 @@ use codex_protocol::mcp::CallToolResult;
|
||||
use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation;
|
||||
use codex_protocol::memory_citation::MemoryCitationEntry as CoreMemoryCitationEntry;
|
||||
use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
|
||||
use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions;
|
||||
use codex_protocol::models::ImageDetail;
|
||||
use codex_protocol::models::MessagePhase;
|
||||
@@ -420,7 +421,7 @@ fn additional_file_system_permissions_preserves_canonical_entries() {
|
||||
path: CoreFileSystemPath::GlobPattern {
|
||||
pattern: "**/*.env".to_string(),
|
||||
},
|
||||
access: CoreFileSystemAccessMode::None,
|
||||
access: CoreFileSystemAccessMode::Deny,
|
||||
},
|
||||
],
|
||||
glob_scan_max_depth: NonZeroUsize::new(2),
|
||||
@@ -444,7 +445,7 @@ fn additional_file_system_permissions_preserves_canonical_entries() {
|
||||
path: FileSystemPath::GlobPattern {
|
||||
pattern: "**/*.env".to_string(),
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
access: FileSystemAccessMode::Deny,
|
||||
},
|
||||
]),
|
||||
}
|
||||
@@ -614,57 +615,76 @@ fn permissions_request_approval_response_accepts_strict_auto_review() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permission_profile_selection_accepts_legacy_object_shape() {
|
||||
let additional_root = absolute_path("additional-root");
|
||||
let params = json!({
|
||||
"permissions": {
|
||||
"type": "profile",
|
||||
"id": ":workspace",
|
||||
"modifications": [
|
||||
{
|
||||
"type": "additionalWritableRoot",
|
||||
"path": additional_root,
|
||||
}
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
let start: ThreadStartParams =
|
||||
serde_json::from_value(params.clone()).expect("thread/start params deserialize");
|
||||
assert_legacy_permission_profile_selection(start.permissions, &additional_root);
|
||||
|
||||
let resume: ThreadResumeParams = serde_json::from_value(json!({
|
||||
"threadId": "thread-1",
|
||||
"permissions": params["permissions"].clone(),
|
||||
fn permission_profile_selection_uses_id_string() {
|
||||
let start: ThreadStartParams = serde_json::from_value(json!({
|
||||
"permissions": BUILT_IN_PERMISSION_PROFILE_WORKSPACE,
|
||||
}))
|
||||
.expect("thread/resume params deserialize");
|
||||
assert_legacy_permission_profile_selection(resume.permissions, &additional_root);
|
||||
|
||||
let fork: ThreadForkParams = serde_json::from_value(json!({
|
||||
"threadId": "thread-1",
|
||||
"permissions": params["permissions"].clone(),
|
||||
}))
|
||||
.expect("thread/fork params deserialize");
|
||||
assert_legacy_permission_profile_selection(fork.permissions, &additional_root);
|
||||
.expect("thread/start params deserialize");
|
||||
assert_eq!(
|
||||
start.permissions,
|
||||
Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string())
|
||||
);
|
||||
|
||||
let turn: TurnStartParams = serde_json::from_value(json!({
|
||||
"threadId": "thread-1",
|
||||
"input": [],
|
||||
"permissions": params["permissions"].clone(),
|
||||
"permissions": "dev",
|
||||
}))
|
||||
.expect("turn/start params deserialize");
|
||||
assert_legacy_permission_profile_selection(turn.permissions, &additional_root);
|
||||
assert_eq!(turn.permissions, Some("dev".to_string()));
|
||||
|
||||
let command: CommandExecParams = serde_json::from_value(json!({
|
||||
"command": ["echo", "hello"],
|
||||
"permissionProfile": "dev",
|
||||
}))
|
||||
.expect("command/exec params deserialize");
|
||||
assert_eq!(command.permission_profile, Some("dev".to_string()));
|
||||
|
||||
let resume: ThreadResumeParams = serde_json::from_value(json!({
|
||||
"threadId": "thread-1",
|
||||
"permissions": BUILT_IN_PERMISSION_PROFILE_WORKSPACE,
|
||||
}))
|
||||
.expect("thread/resume params deserialize");
|
||||
assert_eq!(
|
||||
resume.permissions,
|
||||
Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string())
|
||||
);
|
||||
|
||||
let fork: ThreadForkParams = serde_json::from_value(json!({
|
||||
"threadId": "thread-1",
|
||||
"permissions": BUILT_IN_PERMISSION_PROFILE_WORKSPACE,
|
||||
}))
|
||||
.expect("thread/fork params deserialize");
|
||||
assert_eq!(
|
||||
fork.permissions,
|
||||
Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string())
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_legacy_permission_profile_selection(
|
||||
selection: Option<PermissionProfileSelectionParams>,
|
||||
additional_root: &AbsolutePathBuf,
|
||||
) {
|
||||
let selection = selection.expect("permissions should be present");
|
||||
assert_eq!(selection.id(), ":workspace");
|
||||
#[test]
|
||||
fn thread_path_params_deserialize_empty_path_as_none() {
|
||||
let resume: ThreadResumeParams = serde_json::from_value(json!({
|
||||
"threadId": "thread-1",
|
||||
"path": "",
|
||||
}))
|
||||
.expect("thread/resume params deserialize");
|
||||
assert_eq!(resume.path, None);
|
||||
|
||||
let fork: ThreadForkParams = serde_json::from_value(json!({
|
||||
"threadId": "thread-1",
|
||||
"path": "",
|
||||
}))
|
||||
.expect("thread/fork params deserialize");
|
||||
assert_eq!(fork.path, None);
|
||||
|
||||
let resume_with_path: ThreadResumeParams = serde_json::from_value(json!({
|
||||
"threadId": "thread-1",
|
||||
"path": "/tmp/resume-thread.jsonl",
|
||||
}))
|
||||
.expect("thread/resume params deserialize");
|
||||
assert_eq!(
|
||||
selection.legacy_additional_writable_roots(),
|
||||
std::slice::from_ref(additional_root)
|
||||
resume_with_path.path,
|
||||
Some(PathBuf::from("/tmp/resume-thread.jsonl"))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1518,6 +1538,7 @@ fn config_granular_approval_policy_is_marked_experimental() {
|
||||
review_model: None,
|
||||
model_context_window: None,
|
||||
model_auto_compact_token_limit: None,
|
||||
model_auto_compact_token_limit_scope: None,
|
||||
model_provider: None,
|
||||
approval_policy: Some(AskForApproval::Granular {
|
||||
sandbox_approval: false,
|
||||
@@ -1558,6 +1579,7 @@ fn config_approvals_reviewer_is_marked_experimental() {
|
||||
review_model: None,
|
||||
model_context_window: None,
|
||||
model_auto_compact_token_limit: None,
|
||||
model_auto_compact_token_limit_scope: None,
|
||||
model_provider: None,
|
||||
approval_policy: None,
|
||||
approvals_reviewer: Some(ApprovalsReviewer::AutoReview),
|
||||
@@ -1592,6 +1614,7 @@ fn config_nested_profile_granular_approval_policy_is_marked_experimental() {
|
||||
review_model: None,
|
||||
model_context_window: None,
|
||||
model_auto_compact_token_limit: None,
|
||||
model_auto_compact_token_limit_scope: None,
|
||||
model_provider: None,
|
||||
approval_policy: None,
|
||||
approvals_reviewer: None,
|
||||
@@ -1648,6 +1671,7 @@ fn config_nested_profile_approvals_reviewer_is_marked_experimental() {
|
||||
review_model: None,
|
||||
model_context_window: None,
|
||||
model_auto_compact_token_limit: None,
|
||||
model_auto_compact_token_limit_scope: None,
|
||||
model_provider: None,
|
||||
approval_policy: None,
|
||||
approvals_reviewer: None,
|
||||
@@ -1706,6 +1730,7 @@ fn config_requirements_granular_allowed_approval_policy_is_marked_experimental()
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
computer_use: None,
|
||||
feature_requirements: None,
|
||||
hooks: None,
|
||||
enforce_residency: None,
|
||||
@@ -2790,6 +2815,27 @@ fn plugin_list_params_serializes_marketplace_kind_filter() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_installed_params_serializes_install_suggestion_names() {
|
||||
assert_eq!(
|
||||
serde_json::to_value(PluginInstalledParams {
|
||||
cwds: None,
|
||||
install_suggestion_plugin_names: Some(vec![
|
||||
"computer-use".to_string(),
|
||||
"chrome".to_string(),
|
||||
]),
|
||||
})
|
||||
.unwrap(),
|
||||
json!({
|
||||
"cwds": null,
|
||||
"installSuggestionPluginNames": [
|
||||
"computer-use",
|
||||
"chrome",
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_read_params_serialization_uses_install_source_fields() {
|
||||
let marketplace_path = if cfg!(windows) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use super::ActivePermissionProfile;
|
||||
use super::ApprovalsReviewer;
|
||||
use super::AskForApproval;
|
||||
use super::PermissionProfileSelectionParams;
|
||||
use super::SandboxMode;
|
||||
use super::SandboxPolicy;
|
||||
use super::Thread;
|
||||
@@ -122,10 +121,8 @@ pub struct ThreadStartParams {
|
||||
pub sandbox: Option<SandboxMode>,
|
||||
/// Named profile id for this thread. Cannot be combined with `sandbox`.
|
||||
#[experimental("thread/start.permissions")]
|
||||
#[schemars(with = "Option<String>")]
|
||||
#[ts(type = "string | null")]
|
||||
#[ts(optional = nullable)]
|
||||
pub permissions: Option<PermissionProfileSelectionParams>,
|
||||
pub permissions: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
pub config: Option<HashMap<String, JsonValue>>,
|
||||
#[ts(optional = nullable)]
|
||||
@@ -232,8 +229,13 @@ pub struct ThreadStartResponse {
|
||||
/// 2. By history: instantiate the thread from memory and resume it.
|
||||
/// 3. By path: load the thread from disk by path and resume it.
|
||||
///
|
||||
/// The precedence is: history > path > thread_id.
|
||||
/// If using history or path, the thread_id param will be ignored.
|
||||
/// For non-running threads, the precedence is: history > non-empty path > thread_id.
|
||||
/// If using history or a non-empty path for a non-running thread, the thread_id
|
||||
/// param will be ignored.
|
||||
///
|
||||
/// If thread_id identifies a running thread, app-server rejoins that thread and
|
||||
/// treats a non-empty path as a consistency check against the active rollout path.
|
||||
/// Empty string path values are treated as absent.
|
||||
///
|
||||
/// Prefer using thread_id whenever possible.
|
||||
pub struct ThreadResumeParams {
|
||||
@@ -247,8 +249,14 @@ pub struct ThreadResumeParams {
|
||||
pub history: Option<Vec<ResponseItem>>,
|
||||
|
||||
/// [UNSTABLE] Specify the rollout path to resume from.
|
||||
/// If specified, the thread_id param will be ignored.
|
||||
/// If specified for a non-running thread, the thread_id param will be ignored.
|
||||
/// If thread_id identifies a running thread, the path must match the active
|
||||
/// rollout path.
|
||||
#[experimental("thread/resume.path")]
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "crate::protocol::serde_helpers::deserialize_empty_path_as_none"
|
||||
)]
|
||||
#[ts(optional = nullable)]
|
||||
pub path: Option<PathBuf>,
|
||||
|
||||
@@ -284,10 +292,8 @@ pub struct ThreadResumeParams {
|
||||
/// Named profile id for the resumed thread. Cannot be combined with
|
||||
/// `sandbox`.
|
||||
#[experimental("thread/resume.permissions")]
|
||||
#[schemars(with = "Option<String>")]
|
||||
#[ts(type = "string | null")]
|
||||
#[ts(optional = nullable)]
|
||||
pub permissions: Option<PermissionProfileSelectionParams>,
|
||||
pub permissions: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
pub config: Option<HashMap<String, serde_json::Value>>,
|
||||
#[ts(optional = nullable)]
|
||||
@@ -351,7 +357,8 @@ pub struct ThreadResumeResponse {
|
||||
/// 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread.
|
||||
/// 2. By path: load the thread from disk by path and fork it into a new thread.
|
||||
///
|
||||
/// If using path, the thread_id param will be ignored.
|
||||
/// If using a non-empty path, the thread_id param will be ignored.
|
||||
/// Empty string path values are treated as absent.
|
||||
///
|
||||
/// Prefer using thread_id whenever possible.
|
||||
pub struct ThreadForkParams {
|
||||
@@ -360,6 +367,10 @@ pub struct ThreadForkParams {
|
||||
/// [UNSTABLE] Specify the rollout path to fork from.
|
||||
/// If specified, the thread_id param will be ignored.
|
||||
#[experimental("thread/fork.path")]
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "crate::protocol::serde_helpers::deserialize_empty_path_as_none"
|
||||
)]
|
||||
#[ts(optional = nullable)]
|
||||
pub path: Option<PathBuf>,
|
||||
|
||||
@@ -395,10 +406,8 @@ pub struct ThreadForkParams {
|
||||
/// Named profile id for the forked thread. Cannot be combined with
|
||||
/// `sandbox`.
|
||||
#[experimental("thread/fork.permissions")]
|
||||
#[schemars(with = "Option<String>")]
|
||||
#[ts(type = "string | null")]
|
||||
#[ts(optional = nullable)]
|
||||
pub permissions: Option<PermissionProfileSelectionParams>,
|
||||
pub permissions: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
pub config: Option<HashMap<String, serde_json::Value>>,
|
||||
#[ts(optional = nullable)]
|
||||
@@ -555,6 +564,8 @@ v2_enum_from_core! {
|
||||
pub enum ThreadGoalStatus from CoreThreadGoalStatus {
|
||||
Active,
|
||||
Paused,
|
||||
Blocked,
|
||||
UsageLimited,
|
||||
BudgetLimited,
|
||||
Complete,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use super::ApprovalsReviewer;
|
||||
use super::AskForApproval;
|
||||
use super::PermissionProfileSelectionParams;
|
||||
use super::SandboxPolicy;
|
||||
use super::Turn;
|
||||
use codex_experimental_api_macros::ExperimentalApi;
|
||||
@@ -85,10 +84,8 @@ pub struct TurnStartParams {
|
||||
/// Select a named permissions profile id for this turn and subsequent
|
||||
/// turns. Cannot be combined with `sandboxPolicy`.
|
||||
#[experimental("turn/start.permissions")]
|
||||
#[schemars(with = "Option<String>")]
|
||||
#[ts(type = "string | null")]
|
||||
#[ts(optional = nullable)]
|
||||
pub permissions: Option<PermissionProfileSelectionParams>,
|
||||
pub permissions: Option<String>,
|
||||
/// Override the model for this turn and subsequent turns.
|
||||
#[ts(optional = nullable)]
|
||||
pub model: Option<String>,
|
||||
|
||||
@@ -6,6 +6,7 @@ pub use outgoing_message::OutgoingError;
|
||||
pub use outgoing_message::OutgoingMessage;
|
||||
pub use outgoing_message::OutgoingResponse;
|
||||
pub use outgoing_message::QueuedOutgoingMessage;
|
||||
pub use transport::AppServerStartupLock;
|
||||
pub use transport::AppServerTransport;
|
||||
pub use transport::AppServerTransportParseError;
|
||||
pub use transport::CHANNEL_CAPACITY;
|
||||
@@ -14,8 +15,11 @@ pub use transport::RemoteControlHandle;
|
||||
pub use transport::RemoteControlStartConfig;
|
||||
pub use transport::RemoteControlUnavailable;
|
||||
pub use transport::TransportEvent;
|
||||
pub use transport::acquire_app_server_startup_lock;
|
||||
pub use transport::app_server_control_socket_path;
|
||||
pub use transport::app_server_startup_lock_path;
|
||||
pub use transport::auth;
|
||||
pub use transport::prepare_control_socket_path;
|
||||
pub use transport::start_control_socket_acceptor;
|
||||
pub use transport::start_remote_control;
|
||||
pub use transport::start_stdio_connection;
|
||||
|
||||
@@ -35,6 +35,9 @@ pub use remote_control::RemoteControlStartConfig;
|
||||
pub use remote_control::RemoteControlUnavailable;
|
||||
pub use remote_control::start_remote_control;
|
||||
pub use stdio::start_stdio_connection;
|
||||
pub use unix_socket::AppServerStartupLock;
|
||||
pub use unix_socket::acquire_app_server_startup_lock;
|
||||
pub use unix_socket::prepare_control_socket_path;
|
||||
pub use unix_socket::start_control_socket_acceptor;
|
||||
pub use websocket::start_websocket_acceptor;
|
||||
|
||||
@@ -42,6 +45,7 @@ const OVERLOADED_ERROR_CODE: i64 = -32001;
|
||||
|
||||
const APP_SERVER_CONTROL_SOCKET_DIR_NAME: &str = "app-server-control";
|
||||
const APP_SERVER_CONTROL_SOCKET_FILE_NAME: &str = "app-server-control.sock";
|
||||
const APP_SERVER_STARTUP_LOCK_FILE_NAME: &str = "app-server-startup.lock";
|
||||
|
||||
pub fn app_server_control_socket_path(codex_home: &Path) -> std::io::Result<AbsolutePathBuf> {
|
||||
AbsolutePathBuf::from_absolute_path(
|
||||
@@ -51,6 +55,14 @@ pub fn app_server_control_socket_path(codex_home: &Path) -> std::io::Result<Abso
|
||||
)
|
||||
}
|
||||
|
||||
pub fn app_server_startup_lock_path(codex_home: &Path) -> std::io::Result<AbsolutePathBuf> {
|
||||
AbsolutePathBuf::from_absolute_path(
|
||||
codex_home
|
||||
.join(APP_SERVER_CONTROL_SOCKET_DIR_NAME)
|
||||
.join(APP_SERVER_STARTUP_LOCK_FILE_NAME),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum AppServerTransport {
|
||||
Stdio,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::ErrorKind;
|
||||
use std::io::Result as IoResult;
|
||||
use std::path::Path;
|
||||
@@ -89,7 +90,7 @@ async fn run_control_socket_acceptor(
|
||||
info!("control socket acceptor shutting down");
|
||||
}
|
||||
|
||||
async fn prepare_control_socket_path(socket_path: &Path) -> IoResult<()> {
|
||||
pub async fn prepare_control_socket_path(socket_path: &Path) -> IoResult<()> {
|
||||
if let Some(parent) = socket_path.parent() {
|
||||
codex_uds::prepare_private_socket_directory(parent).await?;
|
||||
}
|
||||
@@ -130,6 +131,30 @@ async fn prepare_control_socket_path(socket_path: &Path) -> IoResult<()> {
|
||||
tokio::fs::remove_file(socket_path).await
|
||||
}
|
||||
|
||||
pub struct AppServerStartupLock {
|
||||
_file: std::fs::File,
|
||||
}
|
||||
|
||||
pub async fn acquire_app_server_startup_lock(
|
||||
startup_lock_path: AbsolutePathBuf,
|
||||
) -> IoResult<AppServerStartupLock> {
|
||||
if let Some(parent) = startup_lock_path.as_path().parent() {
|
||||
codex_uds::prepare_private_socket_directory(parent).await?;
|
||||
}
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(startup_lock_path.as_path())?;
|
||||
file.lock()?;
|
||||
Ok(AppServerStartupLock { _file: file })
|
||||
})
|
||||
.await
|
||||
.map_err(|err| std::io::Error::other(format!("startup lock task failed: {err}")))?
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn set_control_socket_permissions(socket_path: &Path) -> IoResult<()> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::AppServerTransport;
|
||||
use super::CHANNEL_CAPACITY;
|
||||
use super::TransportEvent;
|
||||
use super::acquire_app_server_startup_lock;
|
||||
use super::app_server_control_socket_path;
|
||||
use super::start_control_socket_acceptor;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
@@ -140,6 +141,28 @@ async fn control_socket_acceptor_upgrades_and_forwards_websocket_text_messages_a
|
||||
assert_socket_path_removed(socket_path.as_path());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn app_server_startup_lock_serializes_waiters() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("temp dir");
|
||||
let lock_path = test_startup_lock_path(temp_dir.path());
|
||||
let first_lock = acquire_app_server_startup_lock(lock_path.clone())
|
||||
.await
|
||||
.expect("first startup lock should succeed");
|
||||
let mut second_lock = tokio::spawn(acquire_app_server_startup_lock(lock_path));
|
||||
|
||||
assert!(
|
||||
timeout(Duration::from_millis(100), &mut second_lock)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
drop(first_lock);
|
||||
second_lock
|
||||
.await
|
||||
.expect("second startup lock task should join")
|
||||
.expect("second startup lock should succeed");
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn control_socket_file_is_private_after_bind() {
|
||||
@@ -185,6 +208,15 @@ fn test_socket_path(temp_dir: &Path) -> AbsolutePathBuf {
|
||||
.expect("socket path should resolve")
|
||||
}
|
||||
|
||||
fn test_startup_lock_path(temp_dir: &Path) -> AbsolutePathBuf {
|
||||
AbsolutePathBuf::from_absolute_path(
|
||||
temp_dir
|
||||
.join("app-server-control")
|
||||
.join("app-server-startup.lock"),
|
||||
)
|
||||
.expect("startup lock path should resolve")
|
||||
}
|
||||
|
||||
async fn connect_to_socket(socket_path: &Path) -> IoResult<UnixStream> {
|
||||
UnixStream::connect(socket_path).await
|
||||
}
|
||||
|
||||
@@ -188,8 +188,9 @@ Example with notification opt-out:
|
||||
- `fs/changed` — notification emitted when watched paths change, including the `watchId` and `changedPaths`.
|
||||
- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, `additionalSpeedTiers`, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata.
|
||||
- `modelProvider/capabilities/read` — read provider-level capabilities for the currently configured model provider.
|
||||
- `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. For non-beta flags, `displayName`/`description`/`announcement` are `null`.
|
||||
- `experimentalFeature/enablement/set` — patch the in-memory process-wide runtime feature enablement for the currently supported feature keys (`apps`, `memories`, `plugins`, `tool_search`, `tool_suggest`, `tool_call_mcp_elicitation`). For each feature, precedence is: cloud requirements > --enable <feature_name> > config.toml > experimentalFeature/enablement/set (new) > code default.
|
||||
- `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. Pass `threadId` when showing feature state for an existing loaded thread so `enabled` is computed from that thread's refreshed config, including project-local config for the thread's cwd; if omitted, the server uses its default config resolution context. For non-beta flags, `displayName`/`description`/`announcement` are `null`.
|
||||
- `permissionProfile/list` — beta; list available permission profile ids with optional display `description` text, using cursor pagination. Pass `cwd` when the caller needs project-local `[permissions.<id>]` entries to be included in the current catalog view.
|
||||
- `experimentalFeature/enablement/set` — patch the in-memory process-wide runtime feature enablement for the currently supported feature keys (`apps`, `memories`, `plugins`, `tool_suggest`, `tool_call_mcp_elicitation`). For each feature, precedence is: cloud requirements > --enable <feature_name> > config.toml > experimentalFeature/enablement/set (new) > code default.
|
||||
- `environment/add` — experimental; add or replace a named remote environment by `environmentId` and `execServerUrl` for later selection by `thread/start` or `turn/start`; returns `{}` and does not change the default environment.
|
||||
- `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). Built-in presets do not select a model; the Plan preset selects medium reasoning effort. This response omits built-in developer instructions; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly.
|
||||
- `skills/list` — list skills for one or more `cwd` values (optional `forceReload`).
|
||||
@@ -198,6 +199,7 @@ Example with notification opt-out:
|
||||
- `marketplace/remove` — remove a configured marketplace by name from the user marketplace config, and delete its installed marketplace root when one exists.
|
||||
- `marketplace/upgrade` — upgrade all configured Git plugin marketplaces, or one named marketplace when `marketplaceName` is provided. Returns selected marketplace names, upgraded roots, and per-marketplace errors.
|
||||
- `plugin/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata, plugin `availability` (`AVAILABLE` by default or `DISABLED_BY_ADMIN` for remote plugins blocked upstream), fail-open `marketplaceLoadErrors` entries for marketplace files that could not be parsed or loaded, and best-effort `featuredPluginIds` for the official curated marketplace. `interface.category` uses the marketplace category when present; otherwise it falls back to the plugin manifest category (**under development; do not call from production clients yet**).
|
||||
- `plugin/installed` — list installed plugin rows plus any explicitly requested local install-suggestion plugin names, without fetching the broader remote catalog. Mention surfaces can use this narrower view when they need plugin mention payloads rather than plugin-page discovery data (**under development; do not call from production clients yet**).
|
||||
- `plugin/read` — read one plugin by `marketplacePath` plus `pluginName`, returning marketplace info, a list-style `summary`, manifest descriptions/interface metadata, and bundled skills/hooks/apps/MCP server names. Returned plugin skills include their current `enabled` state after local config filtering; bundled hooks are returned as lightweight declaration summaries keyed for correlation with `hooks/list`. Plugin app summaries also include `needsAuth` when the server can determine connector accessibility (**under development; do not call from production clients yet**).
|
||||
- `plugin/skill/read` — read remote plugin skill markdown on demand by `remoteMarketplaceName`, `remotePluginId`, and `skillName`. This lets clients preview uninstalled remote plugin skills without downloading the plugin bundle.
|
||||
- `skills/changed` — notification emitted when watched local skill files change.
|
||||
@@ -222,7 +224,7 @@ Example with notification opt-out:
|
||||
- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any plugin/session `details` returned by detect. When a request includes migration items, the server emits `externalAgentConfig/import/completed` once after the full import finishes (immediately after the response when everything completed synchronously, or after background imports finish).
|
||||
- `config/value/write` — write a single config key/value to the user's config.toml on disk; dotted paths such as `desktop.someKey` use the same generic write surface.
|
||||
- `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads, including multiple `desktop.*` edits.
|
||||
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), lifecycle hook lockdown (`allowManagedHooksOnly`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`.
|
||||
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), lifecycle hook lockdown (`allowManagedHooksOnly`), computer use policy (`computerUse`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`.
|
||||
|
||||
### Example: Start or resume a thread
|
||||
|
||||
@@ -495,7 +497,7 @@ Experimental: use `memory/reset` to clear local memory artifacts and sqlite-back
|
||||
|
||||
### Example: Set and update a thread goal
|
||||
|
||||
Use `thread/goal/set` to create or update the current goal for a materialized thread. Clients can set `budgetLimited` when they stop because a token budget is exhausted or nearly exhausted; the system also sets it when accounting crosses a configured token budget.
|
||||
Use `thread/goal/set` to create or update the current goal for a materialized thread. Clients can set `budgetLimited` when they stop because a token budget is exhausted or nearly exhausted, `blocked` when progress is waiting on outside intervention, and `usageLimited` when usage availability stops further work. The system also sets `budgetLimited` when accounting crosses a configured token budget and `usageLimited` when a turn ends on a hard usage-limit error.
|
||||
|
||||
```json
|
||||
{ "method": "thread/goal/set", "id": 27, "params": {
|
||||
@@ -528,12 +530,12 @@ Use `thread/goal/set` to create or update the current goal for a materialized th
|
||||
```json
|
||||
{ "method": "thread/goal/set", "id": 28, "params": {
|
||||
"threadId": "thr_123",
|
||||
"status": "paused"
|
||||
"status": "blocked"
|
||||
} }
|
||||
{ "id": 28, "result": { "goal": {
|
||||
"threadId": "thr_123",
|
||||
"objective": "Keep improving the benchmark until p95 latency is under 120ms",
|
||||
"status": "paused",
|
||||
"status": "blocked",
|
||||
"tokenBudget": 200000,
|
||||
"tokensUsed": 10000,
|
||||
"timeUsedSeconds": 60,
|
||||
@@ -718,7 +720,7 @@ Invoke an app by including `$<app-slug>` in the text input and adding a `mention
|
||||
|
||||
### Example: Start a turn (invoke a plugin)
|
||||
|
||||
Invoke a plugin by including a UI mention token such as `@sample` in the text input and adding a `mention` input item with the exact `plugin://<plugin-name>@<marketplace-name>` path returned by `plugin/list`.
|
||||
Invoke a plugin by including a UI mention token such as `@sample` in the text input and adding a `mention` input item with the exact `plugin://<plugin-name>@<marketplace-name>` path returned by `plugin/installed` or `plugin/list`.
|
||||
|
||||
```json
|
||||
{ "method": "turn/start", "id": 35, "params": {
|
||||
@@ -831,7 +833,7 @@ Use `thread/backgroundTerminals/clean` to terminate all running background termi
|
||||
### Example: Steer an active turn
|
||||
|
||||
Use `turn/steer` to append additional user input to the currently active regular turn. This does
|
||||
not emit `turn/started` and does not accept turn context overrides.
|
||||
not emit `turn/started` and does not accept thread settings overrides.
|
||||
|
||||
```json
|
||||
{ "method": "turn/steer", "id": 32, "params": {
|
||||
@@ -926,7 +928,7 @@ Run a standalone command (argv vector) in the server’s sandbox without creatin
|
||||
"cwd": "/Users/me/project", // optional; defaults to server cwd
|
||||
"env": { "FOO": "override" }, // optional; merges into the server env and overrides matching names
|
||||
"size": { "rows": 40, "cols": 120 }, // optional; PTY size in character cells, only valid with tty=true
|
||||
"permissionProfile": { "id": ":workspace", "extends": null }, // optional; defaults to user config
|
||||
"permissionProfile": ":workspace", // optional profile id; defaults to user config
|
||||
"outputBytesCap": 1048576, // optional; per-stream capture cap
|
||||
"disableOutputCap": false, // optional; cannot be combined with outputBytesCap
|
||||
"timeoutMs": 10000, // optional; ms timeout; defaults to server timeout
|
||||
@@ -1676,7 +1678,7 @@ The server also emits `app/list/updated` notifications whenever either source (a
|
||||
}
|
||||
```
|
||||
|
||||
Invoke an app by inserting `$<app-slug>` in the text input. The slug is derived from the app name and lowercased with non-alphanumeric characters replaced by `-` (for example, "Demo App" becomes `$demo-app`). Add a `mention` input item (recommended) so the server uses the exact `app://<connector-id>` path rather than guessing by name. Plugins use the same `mention` item shape, but with `plugin://<plugin-name>@<marketplace-name>` paths from `plugin/list`.
|
||||
Invoke an app by inserting `$<app-slug>` in the text input. The slug is derived from the app name and lowercased with non-alphanumeric characters replaced by `-` (for example, "Demo App" becomes `$demo-app`). Add a `mention` input item (recommended) so the server uses the exact `app://<connector-id>` path rather than guessing by name. Plugins use the same `mention` item shape, but with `plugin://<plugin-name>@<marketplace-name>` paths from `plugin/installed` or `plugin/list`.
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
@@ -422,7 +422,7 @@ async fn detect_repo_skips_hooks_when_only_unsupported_hooks_exist() {
|
||||
fs::create_dir_all(repo_root.join(EXTERNAL_AGENT_DIR)).expect("create external agent dir");
|
||||
fs::write(
|
||||
repo_root.join(EXTERNAL_AGENT_DIR).join("settings.json"),
|
||||
r#"{"hooks":{"PreToolUse":[{"matcher":"Bash","hooks":[{"type":"command","if":"Bash(rm *)","command":"echo blocked"}]}],"SubagentStart":[{"matcher":"worker","hooks":[{"type":"command","command":"echo started"}]}]}}"#,
|
||||
r#"{"hooks":{"PreToolUse":[{"matcher":"Bash","hooks":[{"type":"command","if":"Bash(rm *)","command":"echo blocked"}]}],"UnsupportedEvent":[{"matcher":"worker","hooks":[{"type":"command","command":"echo started"}]}]}}"#,
|
||||
)
|
||||
.expect("write hooks");
|
||||
|
||||
|
||||
@@ -403,10 +403,47 @@ fn parse_key_path(path: &str) -> Result<Vec<String>, String> {
|
||||
if path.trim().is_empty() {
|
||||
return Err("keyPath must not be empty".to_string());
|
||||
}
|
||||
Ok(path
|
||||
.split('.')
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect())
|
||||
|
||||
let mut segments = Vec::new();
|
||||
let mut segment = String::new();
|
||||
let mut chars = path.chars();
|
||||
let mut quoted = false;
|
||||
|
||||
// Split on dots unless they appear inside a quoted segment. Bare segments
|
||||
// intentionally stay permissive so existing paths like `sample@catalog`
|
||||
// remain valid.
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
'"' if segment.is_empty() && !quoted => quoted = true,
|
||||
'"' if quoted => quoted = false,
|
||||
'\\' if quoted => {
|
||||
// Quoted segments may escape punctuation that would otherwise
|
||||
// participate in parsing, such as `.` or `"`.
|
||||
let Some(escaped) = chars.next() else {
|
||||
return Err("unterminated escape in keyPath".to_string());
|
||||
};
|
||||
segment.push(escaped);
|
||||
}
|
||||
'.' if !quoted => {
|
||||
if segment.is_empty() {
|
||||
return Err("keyPath segments must not be empty".to_string());
|
||||
}
|
||||
segments.push(std::mem::take(&mut segment));
|
||||
}
|
||||
'"' => return Err("invalid quoted keyPath segment".to_string()),
|
||||
_ => segment.push(ch),
|
||||
}
|
||||
}
|
||||
|
||||
if quoted {
|
||||
return Err("unterminated quoted keyPath segment".to_string());
|
||||
}
|
||||
if segment.is_empty() {
|
||||
return Err("keyPath segments must not be empty".to_string());
|
||||
}
|
||||
|
||||
segments.push(segment);
|
||||
Ok(segments)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -31,7 +31,10 @@ use crate::transport::ConnectionState;
|
||||
use crate::transport::OutboundConnectionState;
|
||||
use crate::transport::RemoteControlStartConfig;
|
||||
use crate::transport::TransportEvent;
|
||||
use crate::transport::acquire_app_server_startup_lock;
|
||||
use crate::transport::app_server_startup_lock_path;
|
||||
use crate::transport::auth::policy_from_settings;
|
||||
use crate::transport::prepare_control_socket_path;
|
||||
use crate::transport::route_outgoing_envelope;
|
||||
use crate::transport::start_control_socket_acceptor;
|
||||
use crate::transport::start_remote_control;
|
||||
@@ -400,6 +403,7 @@ pub enum PluginStartupTasks {
|
||||
pub struct AppServerRuntimeOptions {
|
||||
pub plugin_startup_tasks: PluginStartupTasks,
|
||||
pub remote_control_enabled: bool,
|
||||
pub install_shutdown_signal_handler: bool,
|
||||
}
|
||||
|
||||
impl Default for AppServerRuntimeOptions {
|
||||
@@ -407,6 +411,7 @@ impl Default for AppServerRuntimeOptions {
|
||||
Self {
|
||||
plugin_startup_tasks: PluginStartupTasks::Start,
|
||||
remote_control_enabled: false,
|
||||
install_shutdown_signal_handler: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -443,9 +448,9 @@ pub async fn run_main_with_transport_options(
|
||||
arg0_paths.codex_linux_sandbox_exe.clone(),
|
||||
)?;
|
||||
let environment_manager = if loader_overrides.ignore_user_config {
|
||||
EnvironmentManager::from_env(local_runtime_paths).await
|
||||
EnvironmentManager::from_env(Some(local_runtime_paths)).await
|
||||
} else {
|
||||
EnvironmentManager::from_codex_home(codex_home.clone(), local_runtime_paths).await
|
||||
EnvironmentManager::from_codex_home(codex_home.clone(), Some(local_runtime_paths)).await
|
||||
}
|
||||
.map(Arc::new)
|
||||
.map_err(std::io::Error::other)?;
|
||||
@@ -514,13 +519,21 @@ pub async fn run_main_with_transport_options(
|
||||
})?;
|
||||
codex_core::otel_init::record_process_start(otel.as_ref(), OTEL_SERVICE_NAME);
|
||||
codex_core::otel_init::install_sqlite_telemetry(otel.as_ref(), OTEL_SERVICE_NAME);
|
||||
let unix_socket_startup_lock = match &transport {
|
||||
AppServerTransport::UnixSocket { socket_path } => {
|
||||
let startup_lock_path = app_server_startup_lock_path(&codex_home)?;
|
||||
let startup_lock = acquire_app_server_startup_lock(startup_lock_path).await?;
|
||||
prepare_control_socket_path(socket_path.as_path()).await?;
|
||||
Some(startup_lock)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let state_db = match rollout_state_db::try_init(&config).await {
|
||||
Ok(state_db) => Some(state_db),
|
||||
Err(err) => {
|
||||
let state_db_path = codex_state::state_db_path(config.sqlite_home.as_path());
|
||||
return Err(std::io::Error::other(format!(
|
||||
"failed to initialize sqlite state db at {}: {err}",
|
||||
state_db_path.display()
|
||||
"failed to initialize sqlite state runtime under {}: {err}",
|
||||
config.sqlite_home.display()
|
||||
)));
|
||||
}
|
||||
};
|
||||
@@ -645,7 +658,8 @@ pub async fn run_main_with_transport_options(
|
||||
|
||||
let single_client_mode = matches!(&transport, AppServerTransport::Stdio);
|
||||
let shutdown_when_no_connections = single_client_mode;
|
||||
let graceful_signal_restart_enabled = !single_client_mode;
|
||||
let graceful_signal_restart_enabled =
|
||||
runtime_options.install_shutdown_signal_handler && !single_client_mode;
|
||||
let mut app_server_client_name_rx = None;
|
||||
|
||||
match &transport {
|
||||
@@ -680,6 +694,7 @@ pub async fn run_main_with_transport_options(
|
||||
}
|
||||
AppServerTransport::Off => {}
|
||||
}
|
||||
drop(unix_socket_startup_lock);
|
||||
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await;
|
||||
|
||||
@@ -17,6 +17,7 @@ const MANAGED_CONFIG_PATH_ENV_VAR: &str = "CODEX_APP_SERVER_MANAGED_CONFIG_PATH"
|
||||
const DISABLE_MANAGED_CONFIG_ENV_VAR: &str = "CODEX_APP_SERVER_DISABLE_MANAGED_CONFIG";
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(version)]
|
||||
struct AppServerArgs {
|
||||
/// Transport endpoint URL. Supported values: `stdio://` (default),
|
||||
/// `unix://`, `unix://PATH`, `ws://IP:PORT`, `off`.
|
||||
|
||||
@@ -301,6 +301,7 @@ impl MessageProcessor {
|
||||
// affect per-thread behavior, but they must not move newly started,
|
||||
// resumed, or forked threads to a different persistence backend/root.
|
||||
let thread_store = codex_core::thread_store_from_config(config.as_ref(), state_db.clone());
|
||||
let environment_manager_for_requests = Arc::clone(&environment_manager);
|
||||
let thread_manager = Arc::new_cyclic(|thread_manager| {
|
||||
ThreadManager::new(
|
||||
config.as_ref(),
|
||||
@@ -355,8 +356,12 @@ impl MessageProcessor {
|
||||
Arc::clone(&config),
|
||||
outgoing.clone(),
|
||||
config_manager.clone(),
|
||||
Arc::clone(&environment_manager_for_requests),
|
||||
);
|
||||
let process_exec_processor = ProcessExecRequestProcessor::new(
|
||||
outgoing.clone(),
|
||||
Arc::clone(&environment_manager_for_requests),
|
||||
);
|
||||
let process_exec_processor = ProcessExecRequestProcessor::new(outgoing.clone());
|
||||
let feedback_processor = FeedbackRequestProcessor::new(
|
||||
auth_manager.clone(),
|
||||
Arc::clone(&thread_manager),
|
||||
@@ -443,7 +448,6 @@ impl MessageProcessor {
|
||||
Some(on_effective_plugins_changed),
|
||||
);
|
||||
}
|
||||
let fs_watch_manager = FsWatchManager::new(outgoing.clone());
|
||||
let config_processor = ConfigRequestProcessor::new(
|
||||
outgoing.clone(),
|
||||
config_manager.clone(),
|
||||
@@ -462,11 +466,8 @@ impl MessageProcessor {
|
||||
let environment_processor =
|
||||
EnvironmentRequestProcessor::new(thread_manager.environment_manager());
|
||||
let fs_processor = FsRequestProcessor::new(
|
||||
thread_manager
|
||||
.environment_manager()
|
||||
.local_environment()
|
||||
.get_filesystem(),
|
||||
fs_watch_manager,
|
||||
Arc::clone(&environment_manager_for_requests),
|
||||
FsWatchManager::new(outgoing.clone()),
|
||||
);
|
||||
let windows_sandbox_processor = WindowsSandboxRequestProcessor::new(
|
||||
outgoing.clone(),
|
||||
@@ -1102,6 +1103,9 @@ impl MessageProcessor {
|
||||
ClientRequest::PluginList { params, .. } => {
|
||||
self.plugin_processor.plugin_list(params).await
|
||||
}
|
||||
ClientRequest::PluginInstalled { params, .. } => {
|
||||
self.plugin_processor.plugin_installed(params).await
|
||||
}
|
||||
ClientRequest::PluginRead { params, .. } => {
|
||||
self.plugin_processor.plugin_read(params).await
|
||||
}
|
||||
@@ -1145,6 +1149,9 @@ impl MessageProcessor {
|
||||
.experimental_feature_list(params)
|
||||
.await
|
||||
}
|
||||
ClientRequest::PermissionProfileList { params, .. } => {
|
||||
self.catalog_processor.permission_profile_list(params).await
|
||||
}
|
||||
ClientRequest::CollaborationModeList { params, .. } => {
|
||||
self.catalog_processor.collaboration_mode_list(params).await
|
||||
}
|
||||
|
||||
@@ -103,10 +103,14 @@ use codex_app_server_protocol::MockExperimentalMethodParams;
|
||||
use codex_app_server_protocol::MockExperimentalMethodResponse;
|
||||
use codex_app_server_protocol::ModelListParams;
|
||||
use codex_app_server_protocol::ModelListResponse;
|
||||
use codex_app_server_protocol::PermissionProfileSelectionParams;
|
||||
use codex_app_server_protocol::PermissionProfileListParams;
|
||||
use codex_app_server_protocol::PermissionProfileListResponse;
|
||||
use codex_app_server_protocol::PermissionProfileSummary;
|
||||
use codex_app_server_protocol::PluginDetail;
|
||||
use codex_app_server_protocol::PluginInstallParams;
|
||||
use codex_app_server_protocol::PluginInstallResponse;
|
||||
use codex_app_server_protocol::PluginInstalledParams;
|
||||
use codex_app_server_protocol::PluginInstalledResponse;
|
||||
use codex_app_server_protocol::PluginInterface;
|
||||
use codex_app_server_protocol::PluginListMarketplaceKind;
|
||||
use codex_app_server_protocol::PluginListParams;
|
||||
@@ -258,7 +262,7 @@ use codex_config::ConfigLayerStack;
|
||||
use codex_config::loader::project_trust_key;
|
||||
use codex_config::types::McpServerTransportConfig;
|
||||
use codex_core::CodexThread;
|
||||
use codex_core::CodexThreadTurnContextOverrides;
|
||||
use codex_core::CodexThreadSettingsOverrides;
|
||||
use codex_core::ExternalGoalPreviousStatus;
|
||||
use codex_core::ExternalGoalSet;
|
||||
use codex_core::ForkSnapshot;
|
||||
@@ -274,6 +278,7 @@ use codex_core::config::ConfigOverrides;
|
||||
use codex_core::config::NetworkProxyAuditMetadata;
|
||||
use codex_core::config::edit::ConfigEdit;
|
||||
use codex_core::config::edit::ConfigEditsBuilder;
|
||||
use codex_core::connectors::AccessibleConnectorsStatus;
|
||||
use codex_core::exec::ExecCapturePolicy;
|
||||
use codex_core::exec::ExecExpiration;
|
||||
use codex_core::exec::ExecParams;
|
||||
@@ -355,6 +360,9 @@ use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::Result as CodexResult;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
|
||||
@@ -41,11 +41,19 @@ impl AppsRequestProcessor {
|
||||
request_id: &ConnectionRequestId,
|
||||
params: AppsListParams,
|
||||
) -> Result<Option<AppsListResponse>, JSONRPCErrorError> {
|
||||
let mut config = self.load_latest_config(/*fallback_cwd*/ None).await?;
|
||||
|
||||
if let Some(thread_id) = params.thread_id.as_deref() {
|
||||
let (_, thread) = self.load_thread(thread_id).await?;
|
||||
let thread = if let Some(thread_id) = params.thread_id.as_deref() {
|
||||
let (_, loaded_thread) = self.load_thread(thread_id).await?;
|
||||
Some(loaded_thread)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let fallback_cwd = match thread.as_ref() {
|
||||
Some(thread) => Some(thread.config_snapshot().await.cwd.to_path_buf()),
|
||||
None => None,
|
||||
};
|
||||
let mut config = self.load_latest_config(fallback_cwd).await?;
|
||||
|
||||
if let Some(thread) = thread {
|
||||
let _ = config
|
||||
.features
|
||||
.set_enabled(Feature::Apps, thread.enabled(Feature::Apps));
|
||||
@@ -88,8 +96,31 @@ impl AppsRequestProcessor {
|
||||
config: Config,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
) {
|
||||
let retry_params = params.clone();
|
||||
let retry_config = config.clone();
|
||||
let retry_environment_manager = Arc::clone(&environment_manager);
|
||||
let result = Self::apps_list_response(&outgoing, params, config, environment_manager).await;
|
||||
outgoing.send_result(request_id, result).await;
|
||||
let should_retry = result
|
||||
.as_ref()
|
||||
.is_ok_and(|(_, codex_apps_ready)| !codex_apps_ready);
|
||||
outgoing
|
||||
.send_result(request_id, result.map(|(response, _)| response))
|
||||
.await;
|
||||
|
||||
if should_retry && !retry_params.force_refetch {
|
||||
let mut retry_params = retry_params;
|
||||
retry_params.force_refetch = true;
|
||||
if let Err(err) = Self::apps_list_response(
|
||||
&outgoing,
|
||||
retry_params,
|
||||
retry_config,
|
||||
retry_environment_manager,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!("failed to refresh app list after codex-apps readiness retry: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn apps_list_response(
|
||||
@@ -97,7 +128,7 @@ impl AppsRequestProcessor {
|
||||
params: AppsListParams,
|
||||
config: Config,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
) -> Result<AppsListResponse, JSONRPCErrorError> {
|
||||
) -> Result<(AppsListResponse, bool), JSONRPCErrorError> {
|
||||
let AppsListParams {
|
||||
cursor,
|
||||
limit,
|
||||
@@ -130,7 +161,6 @@ impl AppsRequestProcessor {
|
||||
&environment_manager,
|
||||
)
|
||||
.await
|
||||
.map(|status| status.connectors)
|
||||
.map_err(|err| format!("failed to load accessible apps: {err}"));
|
||||
let _ = accessible_tx.send(AppListLoadResult::Accessible(result));
|
||||
});
|
||||
@@ -146,6 +176,7 @@ impl AppsRequestProcessor {
|
||||
let app_list_deadline = tokio::time::Instant::now() + APP_LIST_LOAD_TIMEOUT;
|
||||
let mut accessible_loaded = false;
|
||||
let mut all_loaded = false;
|
||||
let mut codex_apps_ready = true;
|
||||
let mut last_notified_apps = None;
|
||||
|
||||
if accessible_connectors.is_some() || all_connectors.is_some() {
|
||||
@@ -178,9 +209,10 @@ impl AppsRequestProcessor {
|
||||
};
|
||||
|
||||
match result {
|
||||
AppListLoadResult::Accessible(Ok(connectors)) => {
|
||||
accessible_connectors = Some(connectors);
|
||||
AppListLoadResult::Accessible(Ok(status)) => {
|
||||
accessible_connectors = Some(status.connectors);
|
||||
accessible_loaded = true;
|
||||
codex_apps_ready = status.codex_apps_ready;
|
||||
}
|
||||
AppListLoadResult::Accessible(Err(err)) => {
|
||||
return Err(internal_error(err));
|
||||
@@ -222,7 +254,8 @@ impl AppsRequestProcessor {
|
||||
}
|
||||
|
||||
if accessible_loaded && all_loaded {
|
||||
return paginate_apps(merged.as_slice(), start, limit);
|
||||
let response = paginate_apps(merged.as_slice(), start, limit)?;
|
||||
return Ok((response, codex_apps_ready));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,7 +312,7 @@ impl AppsRequestProcessor {
|
||||
const APP_LIST_LOAD_TIMEOUT: Duration = Duration::from_secs(90);
|
||||
|
||||
enum AppListLoadResult {
|
||||
Accessible(Result<Vec<AppInfo>, String>),
|
||||
Accessible(Result<AccessibleConnectorsStatus, String>),
|
||||
Directory(Result<Vec<AppInfo>, String>),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use futures::StreamExt;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -155,6 +156,15 @@ impl CatalogRequestProcessor {
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
|
||||
pub(crate) async fn permission_profile_list(
|
||||
&self,
|
||||
params: PermissionProfileListParams,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.permission_profile_list_response(params)
|
||||
.await
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
|
||||
pub(crate) async fn collaboration_mode_list(
|
||||
&self,
|
||||
params: CollaborationModeListParams,
|
||||
@@ -285,8 +295,28 @@ impl CatalogRequestProcessor {
|
||||
&self,
|
||||
params: ExperimentalFeatureListParams,
|
||||
) -> Result<ExperimentalFeatureListResponse, JSONRPCErrorError> {
|
||||
let ExperimentalFeatureListParams { cursor, limit } = params;
|
||||
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
|
||||
let ExperimentalFeatureListParams {
|
||||
cursor,
|
||||
limit,
|
||||
thread_id,
|
||||
} = params;
|
||||
let config = match thread_id.as_deref() {
|
||||
Some(thread_id) => {
|
||||
let thread_id = ThreadId::from_string(thread_id)
|
||||
.map_err(|err| invalid_request(format!("invalid thread id: {err}")))?;
|
||||
let thread = self
|
||||
.thread_manager
|
||||
.get_thread(thread_id)
|
||||
.await
|
||||
.map_err(|_| invalid_request(format!("thread not found: {thread_id}")))?;
|
||||
let thread_config = thread.config().await;
|
||||
self.config_manager
|
||||
.load_latest_config_for_thread(thread_config.as_ref())
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to reload config: {err}")))?
|
||||
}
|
||||
None => self.load_latest_config(/*fallback_cwd*/ None).await?,
|
||||
};
|
||||
let auth = self.auth_manager.auth().await;
|
||||
let workspace_codex_plugins_enabled = self
|
||||
.workspace_codex_plugins_enabled(&config, auth.as_ref())
|
||||
@@ -369,6 +399,78 @@ impl CatalogRequestProcessor {
|
||||
Ok(ExperimentalFeatureListResponse { data, next_cursor })
|
||||
}
|
||||
|
||||
async fn permission_profile_list_response(
|
||||
&self,
|
||||
params: PermissionProfileListParams,
|
||||
) -> Result<PermissionProfileListResponse, JSONRPCErrorError> {
|
||||
let PermissionProfileListParams { cursor, limit, cwd } = params;
|
||||
let config_layer_stack = match cwd {
|
||||
Some(cwd) => {
|
||||
let cwd = PathBuf::from(cwd);
|
||||
let (_, config_layer_stack) = self
|
||||
.resolve_cwd_config(&cwd)
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to reload config: {err}")))?;
|
||||
config_layer_stack
|
||||
}
|
||||
None => self
|
||||
.config_manager
|
||||
.load_config_layers(/*cwd*/ None)
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to reload config: {err}")))?,
|
||||
};
|
||||
let effective_config: ConfigToml = config_layer_stack
|
||||
.effective_config()
|
||||
.try_into()
|
||||
.map_err(|err| internal_error(format!("failed to read effective config: {err}")))?;
|
||||
let mut profiles = vec![
|
||||
PermissionProfileSummary {
|
||||
id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(),
|
||||
description: None,
|
||||
},
|
||||
PermissionProfileSummary {
|
||||
id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(),
|
||||
description: None,
|
||||
},
|
||||
PermissionProfileSummary {
|
||||
id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(),
|
||||
description: None,
|
||||
},
|
||||
];
|
||||
let mut configured_profiles = effective_config
|
||||
.permissions
|
||||
.into_iter()
|
||||
.flat_map(|permissions| permissions.entries)
|
||||
.map(|(id, profile)| PermissionProfileSummary {
|
||||
id,
|
||||
description: profile.description,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
configured_profiles.sort_by(|left, right| left.id.cmp(&right.id));
|
||||
profiles.extend(configured_profiles);
|
||||
let total = profiles.len();
|
||||
let effective_limit = limit.unwrap_or(total as u32).max(1) as usize;
|
||||
let effective_limit = effective_limit.min(total);
|
||||
let start = match cursor {
|
||||
Some(cursor) => cursor
|
||||
.parse::<usize>()
|
||||
.map_err(|_| invalid_request(format!("invalid cursor: {cursor}")))?,
|
||||
None => 0,
|
||||
};
|
||||
|
||||
if start > total {
|
||||
return Err(invalid_request(format!(
|
||||
"cursor {start} exceeds total permission profiles {total}"
|
||||
)));
|
||||
}
|
||||
|
||||
let end = start.saturating_add(effective_limit).min(total);
|
||||
let data = profiles[start..end].to_vec();
|
||||
let next_cursor = (end < total).then_some(end.to_string());
|
||||
|
||||
Ok(PermissionProfileListResponse { data, next_cursor })
|
||||
}
|
||||
|
||||
async fn mock_experimental_method_inner(
|
||||
&self,
|
||||
params: MockExperimentalMethodParams,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user