mirror of
https://github.com/openai/codex.git
synced 2026-04-04 12:54:44 +00:00
Compare commits
62 Commits
codex-exp-
...
codex/extr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d52e8716b | ||
|
|
f4caf7003c | ||
|
|
d432ae4286 | ||
|
|
bf77fd9b99 | ||
|
|
1db6f3dba3 | ||
|
|
e5f4d1fef5 | ||
|
|
461ba012fc | ||
|
|
b3a4da84da | ||
|
|
b1570d6c23 | ||
|
|
cc192763e1 | ||
|
|
f7201e5a9f | ||
|
|
fa2a2f0be9 | ||
|
|
96a86710c3 | ||
|
|
2e22885e79 | ||
|
|
35f8b87a5b | ||
|
|
a3e59e9e85 | ||
|
|
0a344e4fab | ||
|
|
2aa4873802 | ||
|
|
ded7854f09 | ||
|
|
403b397e4e | ||
|
|
6b8175c734 | ||
|
|
9e695fe830 | ||
|
|
2bee37fe69 | ||
|
|
2254ec4f30 | ||
|
|
27977d6716 | ||
|
|
69750a0b5a | ||
|
|
7eb19e5319 | ||
|
|
668330acc1 | ||
|
|
60cd0cf75e | ||
|
|
fe287ac467 | ||
|
|
1d210f639e | ||
|
|
b87ba0a3cc | ||
|
|
1837038f4e | ||
|
|
267499bed8 | ||
|
|
5ec121ba12 | ||
|
|
859c58f07d | ||
|
|
2cf4d5ef35 | ||
|
|
dee03da508 | ||
|
|
32d2df5c1e | ||
|
|
70cdb17703 | ||
|
|
db5781a088 | ||
|
|
01df50cf42 | ||
|
|
10eb3ec7fc | ||
|
|
42e932d7bf | ||
|
|
b14689df3b | ||
|
|
20f2a216df | ||
|
|
903660edba | ||
|
|
4fd2774614 | ||
|
|
825d09373d | ||
|
|
81996fcde6 | ||
|
|
dcd5e08269 | ||
|
|
56d0c6bf67 | ||
|
|
3590e181fa | ||
|
|
b306885bd8 | ||
|
|
bb30432421 | ||
|
|
ebbbc52ce4 | ||
|
|
7b37a0350f | ||
|
|
86982ca1f9 | ||
|
|
e5de13644d | ||
|
|
5cada46ddf | ||
|
|
88e5382fc4 | ||
|
|
392347d436 |
24
.github/dotslash-argument-comment-lint-config.json
vendored
Normal file
24
.github/dotslash-argument-comment-lint-config.json
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"outputs": {
|
||||
"argument-comment-lint": {
|
||||
"platforms": {
|
||||
"macos-aarch64": {
|
||||
"regex": "^argument-comment-lint-aarch64-apple-darwin\\.tar\\.gz$",
|
||||
"path": "argument-comment-lint/bin/argument-comment-lint"
|
||||
},
|
||||
"linux-x86_64": {
|
||||
"regex": "^argument-comment-lint-x86_64-unknown-linux-gnu\\.tar\\.gz$",
|
||||
"path": "argument-comment-lint/bin/argument-comment-lint"
|
||||
},
|
||||
"linux-aarch64": {
|
||||
"regex": "^argument-comment-lint-aarch64-unknown-linux-gnu\\.tar\\.gz$",
|
||||
"path": "argument-comment-lint/bin/argument-comment-lint"
|
||||
},
|
||||
"windows-x86_64": {
|
||||
"regex": "^argument-comment-lint-x86_64-pc-windows-msvc\\.zip$",
|
||||
"path": "argument-comment-lint/bin/argument-comment-lint.exe"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
287
.github/scripts/rusty_v8_bazel.py
vendored
Normal file
287
.github/scripts/rusty_v8_bazel.py
vendored
Normal file
@@ -0,0 +1,287 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import gzip
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
MUSL_RUNTIME_ARCHIVE_LABELS = [
|
||||
"@llvm//runtimes/libcxx:libcxx.static",
|
||||
"@llvm//runtimes/libcxx:libcxxabi.static",
|
||||
]
|
||||
LLVM_AR_LABEL = "@llvm//tools:llvm-ar"
|
||||
LLVM_RANLIB_LABEL = "@llvm//tools:llvm-ranlib"
|
||||
|
||||
|
||||
def bazel_execroot() -> Path:
|
||||
result = subprocess.run(
|
||||
["bazel", "info", "execution_root"],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return Path(result.stdout.strip())
|
||||
|
||||
|
||||
def bazel_output_base() -> Path:
|
||||
result = subprocess.run(
|
||||
["bazel", "info", "output_base"],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return Path(result.stdout.strip())
|
||||
|
||||
|
||||
def bazel_output_path(path: str) -> Path:
|
||||
if path.startswith("external/"):
|
||||
return bazel_output_base() / path
|
||||
return bazel_execroot() / path
|
||||
|
||||
|
||||
def bazel_output_files(
|
||||
platform: str,
|
||||
labels: list[str],
|
||||
compilation_mode: str = "fastbuild",
|
||||
) -> list[Path]:
|
||||
expression = "set(" + " ".join(labels) + ")"
|
||||
result = subprocess.run(
|
||||
[
|
||||
"bazel",
|
||||
"cquery",
|
||||
"-c",
|
||||
compilation_mode,
|
||||
f"--platforms=@llvm//platforms:{platform}",
|
||||
"--output=files",
|
||||
expression,
|
||||
],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return [bazel_output_path(line.strip()) for line in result.stdout.splitlines() if line.strip()]
|
||||
|
||||
|
||||
def bazel_build(
|
||||
platform: str,
|
||||
labels: list[str],
|
||||
compilation_mode: str = "fastbuild",
|
||||
) -> None:
|
||||
subprocess.run(
|
||||
[
|
||||
"bazel",
|
||||
"build",
|
||||
"-c",
|
||||
compilation_mode,
|
||||
f"--platforms=@llvm//platforms:{platform}",
|
||||
*labels,
|
||||
],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
def ensure_bazel_output_files(
|
||||
platform: str,
|
||||
labels: list[str],
|
||||
compilation_mode: str = "fastbuild",
|
||||
) -> list[Path]:
|
||||
outputs = bazel_output_files(platform, labels, compilation_mode)
|
||||
if all(path.exists() for path in outputs):
|
||||
return outputs
|
||||
|
||||
bazel_build(platform, labels, compilation_mode)
|
||||
outputs = bazel_output_files(platform, labels, compilation_mode)
|
||||
missing = [str(path) for path in outputs if not path.exists()]
|
||||
if missing:
|
||||
raise SystemExit(f"missing built outputs for {labels}: {missing}")
|
||||
return outputs
|
||||
|
||||
|
||||
def release_pair_label(target: str) -> str:
|
||||
target_suffix = target.replace("-", "_")
|
||||
return f"//third_party/v8:rusty_v8_release_pair_{target_suffix}"
|
||||
|
||||
|
||||
def resolved_v8_crate_version() -> str:
|
||||
cargo_lock = tomllib.loads((ROOT / "codex-rs" / "Cargo.lock").read_text())
|
||||
versions = sorted(
|
||||
{
|
||||
package["version"]
|
||||
for package in cargo_lock["package"]
|
||||
if package["name"] == "v8"
|
||||
}
|
||||
)
|
||||
if len(versions) == 1:
|
||||
return versions[0]
|
||||
if len(versions) > 1:
|
||||
raise SystemExit(f"expected exactly one resolved v8 version, found: {versions}")
|
||||
|
||||
module_bazel = (ROOT / "MODULE.bazel").read_text()
|
||||
matches = sorted(
|
||||
set(
|
||||
re.findall(
|
||||
r'https://static\.crates\.io/crates/v8/v8-([0-9]+\.[0-9]+\.[0-9]+)\.crate',
|
||||
module_bazel,
|
||||
)
|
||||
)
|
||||
)
|
||||
if len(matches) != 1:
|
||||
raise SystemExit(
|
||||
"expected exactly one pinned v8 crate version in MODULE.bazel, "
|
||||
f"found: {matches}"
|
||||
)
|
||||
return matches[0]
|
||||
|
||||
|
||||
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 is_musl_archive_target(target: str, source_path: Path) -> bool:
|
||||
return target.endswith("-unknown-linux-musl") and source_path.suffix == ".a"
|
||||
|
||||
|
||||
def single_bazel_output_file(
|
||||
platform: str,
|
||||
label: str,
|
||||
compilation_mode: str = "fastbuild",
|
||||
) -> Path:
|
||||
outputs = ensure_bazel_output_files(platform, [label], compilation_mode)
|
||||
if len(outputs) != 1:
|
||||
raise SystemExit(f"expected exactly one output for {label}, found {outputs}")
|
||||
return outputs[0]
|
||||
|
||||
|
||||
def merged_musl_archive(
|
||||
platform: str,
|
||||
lib_path: Path,
|
||||
compilation_mode: str = "fastbuild",
|
||||
) -> Path:
|
||||
llvm_ar = single_bazel_output_file(platform, LLVM_AR_LABEL, compilation_mode)
|
||||
llvm_ranlib = single_bazel_output_file(platform, LLVM_RANLIB_LABEL, compilation_mode)
|
||||
runtime_archives = [
|
||||
single_bazel_output_file(platform, label, compilation_mode)
|
||||
for label in MUSL_RUNTIME_ARCHIVE_LABELS
|
||||
]
|
||||
|
||||
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
|
||||
|
||||
|
||||
def stage_release_pair(
|
||||
platform: str,
|
||||
target: str,
|
||||
output_dir: Path,
|
||||
compilation_mode: str = "fastbuild",
|
||||
) -> None:
|
||||
outputs = ensure_bazel_output_files(
|
||||
platform,
|
||||
[release_pair_label(target)],
|
||||
compilation_mode,
|
||||
)
|
||||
|
||||
try:
|
||||
lib_path = next(path for path in outputs if path.suffix in {".a", ".lib"})
|
||||
except StopIteration as exc:
|
||||
raise SystemExit(f"missing static library output for {target}") from exc
|
||||
|
||||
try:
|
||||
binding_path = next(path for path in outputs if path.suffix == ".rs")
|
||||
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)
|
||||
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)
|
||||
|
||||
print(staged_library)
|
||||
print(staged_binding)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
stage_release_pair_parser = subparsers.add_parser("stage-release-pair")
|
||||
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(
|
||||
"--compilation-mode",
|
||||
default="fastbuild",
|
||||
choices=["fastbuild", "opt", "dbg"],
|
||||
)
|
||||
|
||||
subparsers.add_parser("resolved-v8-crate-version")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
if args.command == "stage-release-pair":
|
||||
stage_release_pair(
|
||||
platform=args.platform,
|
||||
target=args.target,
|
||||
output_dir=Path(args.output_dir),
|
||||
compilation_mode=args.compilation_mode,
|
||||
)
|
||||
return 0
|
||||
if args.command == "resolved-v8-crate-version":
|
||||
print(resolved_v8_crate_version())
|
||||
return 0
|
||||
raise SystemExit(f"unsupported command: {args.command}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
12
.github/workflows/bazel.yml
vendored
12
.github/workflows/bazel.yml
vendored
@@ -156,7 +156,6 @@ jobs:
|
||||
|
||||
bazel_args=(
|
||||
test
|
||||
//...
|
||||
--test_verbose_timeout_warnings
|
||||
--build_metadata=REPO_URL=https://github.com/openai/codex.git
|
||||
--build_metadata=COMMIT_SHA=$(git rev-parse HEAD)
|
||||
@@ -164,6 +163,13 @@ jobs:
|
||||
--build_metadata=VISIBILITY=PUBLIC
|
||||
)
|
||||
|
||||
bazel_targets=(
|
||||
//...
|
||||
# Keep V8 out of the ordinary Bazel CI path. Only the dedicated
|
||||
# canary and release workflows should build `third_party/v8`.
|
||||
-//third_party/v8:all
|
||||
)
|
||||
|
||||
if [[ "${RUNNER_OS:-}" != "Windows" ]]; then
|
||||
# Bazel test sandboxes on macOS may resolve an older Homebrew `node`
|
||||
# before the `actions/setup-node` runtime on PATH.
|
||||
@@ -183,6 +189,8 @@ jobs:
|
||||
--bazelrc=.github/workflows/ci.bazelrc \
|
||||
"${bazel_args[@]}" \
|
||||
"--remote_header=x-buildbuddy-api-key=$BUILDBUDDY_API_KEY" \
|
||||
-- \
|
||||
"${bazel_targets[@]}" \
|
||||
2>&1 | tee "$bazel_console_log"
|
||||
bazel_status=${PIPESTATUS[0]}
|
||||
set -e
|
||||
@@ -210,6 +218,8 @@ jobs:
|
||||
"${bazel_args[@]}" \
|
||||
--remote_cache= \
|
||||
--remote_executor= \
|
||||
-- \
|
||||
"${bazel_targets[@]}" \
|
||||
2>&1 | tee "$bazel_console_log"
|
||||
bazel_status=${PIPESTATUS[0]}
|
||||
set -e
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Use a rust-release version that includes all native binaries.
|
||||
CODEX_VERSION=0.74.0
|
||||
CODEX_VERSION=0.115.0
|
||||
OUTPUT_DIR="${RUNNER_TEMP}"
|
||||
python3 ./scripts/stage_npm_packages.py \
|
||||
--release-version "$CODEX_VERSION" \
|
||||
|
||||
81
.github/workflows/rust-ci.yml
vendored
81
.github/workflows/rust-ci.yml
vendored
@@ -91,17 +91,13 @@ jobs:
|
||||
- name: cargo shear
|
||||
run: cargo shear
|
||||
|
||||
argument_comment_lint:
|
||||
name: Argument comment lint
|
||||
argument_comment_lint_package:
|
||||
name: Argument comment lint package
|
||||
runs-on: ubuntu-24.04
|
||||
needs: changed
|
||||
if: ${{ needs.changed.outputs.argument_comment_lint == 'true' || needs.changed.outputs.workflows == 'true' || github.event_name == 'push' }}
|
||||
if: ${{ needs.changed.outputs.argument_comment_lint_package == 'true' || github.event_name == 'push' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install Linux sandbox build dependencies
|
||||
run: |
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get update
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
|
||||
- uses: dtolnay/rust-toolchain@1.93.0
|
||||
with:
|
||||
toolchain: nightly-2025-09-18
|
||||
@@ -120,14 +116,46 @@ jobs:
|
||||
- name: Install cargo-dylint tooling
|
||||
if: ${{ steps.cargo_dylint_cache.outputs.cache-hit != 'true' }}
|
||||
run: cargo install --locked cargo-dylint dylint-link
|
||||
- name: Check source wrapper syntax
|
||||
run: bash -n tools/argument-comment-lint/run.sh
|
||||
- name: Test argument comment lint package
|
||||
if: ${{ needs.changed.outputs.argument_comment_lint_package == 'true' || github.event_name == 'push' }}
|
||||
working-directory: tools/argument-comment-lint
|
||||
run: cargo test
|
||||
- name: Run argument comment lint on codex-rs
|
||||
|
||||
argument_comment_lint_prebuilt:
|
||||
name: Argument comment lint - ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.runs_on || matrix.runner }}
|
||||
needs: changed
|
||||
if: ${{ needs.changed.outputs.argument_comment_lint == 'true' || needs.changed.outputs.workflows == 'true' || github.event_name == 'push' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: Linux
|
||||
runner: ubuntu-24.04
|
||||
- name: macOS
|
||||
runner: macos-15-xlarge
|
||||
- name: Windows
|
||||
runner: windows-x64
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-windows-x64
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install Linux sandbox build dependencies
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
shell: bash
|
||||
run: |
|
||||
bash -n tools/argument-comment-lint/run.sh
|
||||
./tools/argument-comment-lint/run.sh
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get update
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
|
||||
- uses: dtolnay/rust-toolchain@1.93.0
|
||||
with:
|
||||
toolchain: nightly-2025-09-18
|
||||
components: llvm-tools-preview, rustc-dev, rust-src
|
||||
- uses: facebook/install-dotslash@v2
|
||||
- name: Run argument comment lint on codex-rs
|
||||
shell: bash
|
||||
run: ./tools/argument-comment-lint/run-prebuilt-linter.sh
|
||||
|
||||
# --- CI to validate on different os/targets --------------------------------
|
||||
lint_build:
|
||||
@@ -141,8 +169,10 @@ jobs:
|
||||
run:
|
||||
working-directory: codex-rs
|
||||
env:
|
||||
# Speed up repeated builds across CI runs by caching compiled objects (non-Windows).
|
||||
USE_SCCACHE: ${{ startsWith(matrix.runner, 'windows') && 'false' || 'true' }}
|
||||
# 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
|
||||
# In rust-ci, representative release-profile checks use thin LTO for faster feedback.
|
||||
@@ -506,8 +536,10 @@ jobs:
|
||||
run:
|
||||
working-directory: codex-rs
|
||||
env:
|
||||
# Speed up repeated builds across CI runs by caching compiled objects (non-Windows).
|
||||
USE_SCCACHE: ${{ startsWith(matrix.runner, 'windows') && 'false' || 'true' }}
|
||||
# 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
|
||||
|
||||
@@ -704,14 +736,23 @@ jobs:
|
||||
results:
|
||||
name: CI results (required)
|
||||
needs:
|
||||
[changed, general, cargo_shear, argument_comment_lint, lint_build, tests]
|
||||
[
|
||||
changed,
|
||||
general,
|
||||
cargo_shear,
|
||||
argument_comment_lint_package,
|
||||
argument_comment_lint_prebuilt,
|
||||
lint_build,
|
||||
tests,
|
||||
]
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Summarize
|
||||
shell: bash
|
||||
run: |
|
||||
echo "arglint: ${{ needs.argument_comment_lint.result }}"
|
||||
echo "argpkg : ${{ needs.argument_comment_lint_package.result }}"
|
||||
echo "arglint: ${{ needs.argument_comment_lint_prebuilt.result }}"
|
||||
echo "general: ${{ needs.general.result }}"
|
||||
echo "shear : ${{ needs.cargo_shear.result }}"
|
||||
echo "lint : ${{ needs.lint_build.result }}"
|
||||
@@ -724,8 +765,12 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ '${{ needs.changed.outputs.argument_comment_lint_package }}' == 'true' || '${{ github.event_name }}' == 'push' ]]; then
|
||||
[[ '${{ needs.argument_comment_lint_package.result }}' == 'success' ]] || { echo 'argument_comment_lint_package failed'; exit 1; }
|
||||
fi
|
||||
|
||||
if [[ '${{ needs.changed.outputs.argument_comment_lint }}' == 'true' || '${{ needs.changed.outputs.workflows }}' == 'true' || '${{ github.event_name }}' == 'push' ]]; then
|
||||
[[ '${{ needs.argument_comment_lint.result }}' == 'success' ]] || { echo 'argument_comment_lint failed'; exit 1; }
|
||||
[[ '${{ needs.argument_comment_lint_prebuilt.result }}' == 'success' ]] || { echo 'argument_comment_lint_prebuilt failed'; exit 1; }
|
||||
fi
|
||||
|
||||
if [[ '${{ needs.changed.outputs.codex }}' == 'true' || '${{ needs.changed.outputs.workflows }}' == 'true' || '${{ github.event_name }}' == 'push' ]]; then
|
||||
|
||||
103
.github/workflows/rust-release-argument-comment-lint.yml
vendored
Normal file
103
.github/workflows/rust-release-argument-comment-lint.yml
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
name: rust-release-argument-comment-lint
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
publish:
|
||||
required: true
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
skip:
|
||||
if: ${{ !inputs.publish }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "Skipping argument-comment-lint release assets for prerelease tag"
|
||||
|
||||
build:
|
||||
if: ${{ inputs.publish }}
|
||||
name: Build - ${{ matrix.runner }} - ${{ matrix.target }}
|
||||
runs-on: ${{ matrix.runs_on || matrix.runner }}
|
||||
timeout-minutes: 60
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: macos-15-xlarge
|
||||
target: aarch64-apple-darwin
|
||||
archive_name: argument-comment-lint-aarch64-apple-darwin.tar.gz
|
||||
lib_name: libargument_comment_lint@nightly-2025-09-18-aarch64-apple-darwin.dylib
|
||||
runner_binary: argument-comment-lint
|
||||
cargo_dylint_binary: cargo-dylint
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
archive_name: argument-comment-lint-x86_64-unknown-linux-gnu.tar.gz
|
||||
lib_name: libargument_comment_lint@nightly-2025-09-18-x86_64-unknown-linux-gnu.so
|
||||
runner_binary: argument-comment-lint
|
||||
cargo_dylint_binary: cargo-dylint
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-gnu
|
||||
archive_name: argument-comment-lint-aarch64-unknown-linux-gnu.tar.gz
|
||||
lib_name: libargument_comment_lint@nightly-2025-09-18-aarch64-unknown-linux-gnu.so
|
||||
runner_binary: argument-comment-lint
|
||||
cargo_dylint_binary: cargo-dylint
|
||||
- runner: windows-x64
|
||||
target: x86_64-pc-windows-msvc
|
||||
archive_name: argument-comment-lint-x86_64-pc-windows-msvc.zip
|
||||
lib_name: argument_comment_lint@nightly-2025-09-18-x86_64-pc-windows-msvc.dll
|
||||
runner_binary: argument-comment-lint.exe
|
||||
cargo_dylint_binary: cargo-dylint.exe
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-windows-x64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: dtolnay/rust-toolchain@1.93.0
|
||||
with:
|
||||
toolchain: nightly-2025-09-18
|
||||
targets: ${{ matrix.target }}
|
||||
components: llvm-tools-preview, rustc-dev, rust-src
|
||||
|
||||
- name: Install tooling
|
||||
shell: bash
|
||||
run: |
|
||||
install_root="${RUNNER_TEMP}/argument-comment-lint-tools"
|
||||
cargo install --locked cargo-dylint --root "$install_root"
|
||||
cargo install --locked dylint-link
|
||||
echo "INSTALL_ROOT=$install_root" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Cargo build
|
||||
working-directory: tools/argument-comment-lint
|
||||
shell: bash
|
||||
run: cargo build --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Stage artifact
|
||||
shell: bash
|
||||
run: |
|
||||
dest="dist/argument-comment-lint/${{ matrix.target }}"
|
||||
mkdir -p "$dest"
|
||||
package_root="${RUNNER_TEMP}/argument-comment-lint"
|
||||
rm -rf "$package_root"
|
||||
mkdir -p "$package_root/bin" "$package_root/lib"
|
||||
|
||||
cp "tools/argument-comment-lint/target/${{ matrix.target }}/release/${{ matrix.runner_binary }}" \
|
||||
"$package_root/bin/${{ matrix.runner_binary }}"
|
||||
cp "${INSTALL_ROOT}/bin/${{ matrix.cargo_dylint_binary }}" \
|
||||
"$package_root/bin/${{ matrix.cargo_dylint_binary }}"
|
||||
cp "tools/argument-comment-lint/target/${{ matrix.target }}/release/${{ matrix.lib_name }}" \
|
||||
"$package_root/lib/${{ matrix.lib_name }}"
|
||||
|
||||
archive_path="$dest/${{ matrix.archive_name }}"
|
||||
if [[ "${{ runner.os }}" == "Windows" ]]; then
|
||||
(cd "${RUNNER_TEMP}" && 7z a "$GITHUB_WORKSPACE/$archive_path" argument-comment-lint >/dev/null)
|
||||
else
|
||||
(cd "${RUNNER_TEMP}" && tar -czf "$GITHUB_WORKSPACE/$archive_path" argument-comment-lint)
|
||||
fi
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: argument-comment-lint-${{ matrix.target }}
|
||||
path: dist/argument-comment-lint/${{ matrix.target }}/*
|
||||
15
.github/workflows/rust-release.yml
vendored
15
.github/workflows/rust-release.yml
vendored
@@ -380,11 +380,19 @@ jobs:
|
||||
publish: true
|
||||
secrets: inherit
|
||||
|
||||
argument-comment-lint-release-assets:
|
||||
name: argument-comment-lint release assets
|
||||
needs: tag-check
|
||||
uses: ./.github/workflows/rust-release-argument-comment-lint.yml
|
||||
with:
|
||||
publish: true
|
||||
|
||||
release:
|
||||
needs:
|
||||
- build
|
||||
- build-windows
|
||||
- shell-tool-mcp
|
||||
- argument-comment-lint-release-assets
|
||||
name: release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -521,6 +529,13 @@ jobs:
|
||||
tag: ${{ github.ref_name }}
|
||||
config: .github/dotslash-config.json
|
||||
|
||||
- uses: facebook/dotslash-publish-release@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag: ${{ github.ref_name }}
|
||||
config: .github/dotslash-argument-comment-lint-config.json
|
||||
|
||||
- name: Trigger developers.openai.com deploy
|
||||
# Only trigger the deploy if the release is not a pre-release.
|
||||
# The deploy is used to update the developers.openai.com website with the new config schema json file.
|
||||
|
||||
188
.github/workflows/rusty-v8-release.yml
vendored
Normal file
188
.github/workflows/rusty-v8-release.yml
vendored
Normal file
@@ -0,0 +1,188 @@
|
||||
name: rusty-v8-release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: Optional release tag. Defaults to rusty-v8-v<resolved_v8_version>.
|
||||
required: false
|
||||
type: string
|
||||
publish:
|
||||
description: Publish the staged musl artifacts to a GitHub release.
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}::${{ inputs.release_tag || github.run_id }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
metadata:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
release_tag: ${{ steps.release_tag.outputs.release_tag }}
|
||||
v8_version: ${{ steps.v8_version.outputs.version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Resolve exact v8 crate version
|
||||
id: v8_version
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version="$(python3 .github/scripts/rusty_v8_bazel.py resolved-v8-crate-version)"
|
||||
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Resolve release tag
|
||||
id: release_tag
|
||||
env:
|
||||
RELEASE_TAG_INPUT: ${{ inputs.release_tag }}
|
||||
V8_VERSION: ${{ steps.v8_version.outputs.version }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
release_tag="${RELEASE_TAG_INPUT}"
|
||||
if [[ -z "${release_tag}" ]]; then
|
||||
release_tag="rusty-v8-v${V8_VERSION}"
|
||||
fi
|
||||
|
||||
echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.target }}
|
||||
needs: metadata
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
platform: linux_amd64_musl
|
||||
target: x86_64-unknown-linux-musl
|
||||
- runner: ubuntu-24.04-arm
|
||||
platform: linux_arm64_musl
|
||||
target: aarch64-unknown-linux-musl
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Bazel
|
||||
uses: bazelbuild/setup-bazelisk@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Build Bazel V8 release pair
|
||||
env:
|
||||
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
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"
|
||||
)
|
||||
fi
|
||||
|
||||
bazel_args=(
|
||||
build
|
||||
-c
|
||||
opt
|
||||
"--platforms=@llvm//platforms:${PLATFORM}"
|
||||
"${pair_target}"
|
||||
"${extra_targets[@]}"
|
||||
--build_metadata=COMMIT_SHA=$(git rev-parse HEAD)
|
||||
)
|
||||
|
||||
bazel \
|
||||
--noexperimental_remote_repo_contents_cache \
|
||||
--bazelrc=.github/workflows/v8-ci.bazelrc \
|
||||
"${bazel_args[@]}" \
|
||||
"--remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}"
|
||||
|
||||
- name: Stage release pair
|
||||
env:
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
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 \
|
||||
--output-dir "dist/${TARGET}"
|
||||
|
||||
- name: Upload staged musl artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: rusty-v8-${{ needs.metadata.outputs.v8_version }}-${{ matrix.target }}
|
||||
path: dist/${{ matrix.target }}/*
|
||||
|
||||
publish-release:
|
||||
if: ${{ inputs.publish }}
|
||||
needs:
|
||||
- metadata
|
||||
- build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
|
||||
steps:
|
||||
- name: Ensure publishing from default branch
|
||||
if: ${{ github.ref_name != github.event.repository.default_branch }}
|
||||
env:
|
||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "Publishing is only allowed from ${DEFAULT_BRANCH}; current ref is ${GITHUB_REF_NAME}." >&2
|
||||
exit 1
|
||||
|
||||
- name: Ensure release tag is new
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_TAG: ${{ needs.metadata.outputs.release_tag }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" > /dev/null 2>&1; then
|
||||
echo "Release tag ${RELEASE_TAG} already exists; musl artifact tags are immutable." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: dist
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ needs.metadata.outputs.release_tag }}
|
||||
name: ${{ needs.metadata.outputs.release_tag }}
|
||||
files: dist/**
|
||||
# Keep V8 artifact releases out of Codex's normal "latest release" channel.
|
||||
prerelease: true
|
||||
132
.github/workflows/v8-canary.yml
vendored
Normal file
132
.github/workflows/v8-canary.yml
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
name: v8-canary
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/scripts/rusty_v8_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/v8_*.patch"
|
||||
- "third_party/v8/**"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- ".github/scripts/rusty_v8_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/v8_*.patch"
|
||||
- "third_party/v8/**"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}::${{ github.event.pull_request.number > 0 && format('pr-{0}', github.event.pull_request.number) || github.ref_name }}
|
||||
cancel-in-progress: ${{ github.ref_name != 'main' }}
|
||||
|
||||
jobs:
|
||||
metadata:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
v8_version: ${{ steps.v8_version.outputs.version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Resolve exact v8 crate version
|
||||
id: v8_version
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version="$(python3 .github/scripts/rusty_v8_bazel.py resolved-v8-crate-version)"
|
||||
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.target }}
|
||||
needs: metadata
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
platform: linux_amd64_musl
|
||||
target: x86_64-unknown-linux-musl
|
||||
- runner: ubuntu-24.04-arm
|
||||
platform: linux_arm64_musl
|
||||
target: aarch64-unknown-linux-musl
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Bazel
|
||||
uses: bazelbuild/setup-bazelisk@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Build Bazel V8 release pair
|
||||
env:
|
||||
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
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"
|
||||
)
|
||||
|
||||
bazel_args=(
|
||||
build
|
||||
"--platforms=@llvm//platforms:${PLATFORM}"
|
||||
"${pair_target}"
|
||||
"${extra_targets[@]}"
|
||||
--build_metadata=COMMIT_SHA=$(git rev-parse HEAD)
|
||||
)
|
||||
|
||||
bazel \
|
||||
--noexperimental_remote_repo_contents_cache \
|
||||
--bazelrc=.github/workflows/v8-ci.bazelrc \
|
||||
"${bazel_args[@]}" \
|
||||
"--remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}"
|
||||
|
||||
- name: Stage release pair
|
||||
env:
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
TARGET: ${{ matrix.target }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
python3 .github/scripts/rusty_v8_bazel.py stage-release-pair \
|
||||
--platform "${PLATFORM}" \
|
||||
--target "${TARGET}" \
|
||||
--output-dir "dist/${TARGET}"
|
||||
|
||||
- name: Upload staged musl artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: v8-canary-${{ needs.metadata.outputs.v8_version }}-${{ matrix.target }}
|
||||
path: dist/${{ matrix.target }}/*
|
||||
5
.github/workflows/v8-ci.bazelrc
vendored
Normal file
5
.github/workflows/v8-ci.bazelrc
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
import %workspace%/.github/workflows/ci.bazelrc
|
||||
|
||||
common --build_metadata=REPO_URL=https://github.com/openai/codex.git
|
||||
common --build_metadata=ROLE=CI
|
||||
common --build_metadata=VISIBILITY=PUBLIC
|
||||
@@ -48,6 +48,8 @@ Run `just fmt` (in `codex-rs` directory) automatically after you have finished m
|
||||
|
||||
Before finalizing a large change to `codex-rs`, run `just fix -p <project>` (in `codex-rs` directory) to fix any linter issues in the code. Prefer scoping with `-p` to avoid slow workspace‑wide Clippy builds; only run `just fix` without `-p` if you changed shared crates. Do not re-run tests after running `fix` or `fmt`.
|
||||
|
||||
Also run `just argument-comment-lint` to ensure the codebase is clean of comment lint errors.
|
||||
|
||||
## TUI style conventions
|
||||
|
||||
See `codex-rs/tui/styles.md`.
|
||||
|
||||
26
MODULE.bazel
26
MODULE.bazel
@@ -1,5 +1,6 @@
|
||||
module(name = "codex")
|
||||
|
||||
bazel_dep(name = "bazel_skylib", version = "1.8.2")
|
||||
bazel_dep(name = "platforms", version = "1.0.0")
|
||||
bazel_dep(name = "llvm", version = "0.6.7")
|
||||
|
||||
@@ -132,6 +133,8 @@ crate.annotation(
|
||||
workspace_cargo_toml = "rust/runfiles/Cargo.toml",
|
||||
)
|
||||
|
||||
http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
||||
|
||||
llvm = use_extension("@llvm//extensions:llvm.bzl", "llvm")
|
||||
use_repo(llvm, "llvm-project")
|
||||
|
||||
@@ -174,6 +177,29 @@ crate.annotation(
|
||||
|
||||
inject_repo(crate, "alsa_lib")
|
||||
|
||||
bazel_dep(name = "v8", version = "14.6.202.9")
|
||||
archive_override(
|
||||
module_name = "v8",
|
||||
integrity = "sha256-JphDwLAzsd9KvgRZ7eQvNtPU6qGd3XjFt/a/1QITAJU=",
|
||||
patch_strip = 3,
|
||||
patches = [
|
||||
"//patches:v8_module_deps.patch",
|
||||
"//patches:v8_bazel_rules.patch",
|
||||
"//patches:v8_source_portability.patch",
|
||||
],
|
||||
strip_prefix = "v8-14.6.202.9",
|
||||
urls = ["https://github.com/v8/v8/archive/refs/tags/14.6.202.9.tar.gz"],
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "v8_crate_146_4_0",
|
||||
build_file = "//third_party/v8:v8_crate.BUILD.bazel",
|
||||
sha256 = "d97bcac5cdc5a195a4813f1855a6bc658f240452aac36caa12fd6c6f16026ab1",
|
||||
strip_prefix = "v8-146.4.0",
|
||||
type = "tar.gz",
|
||||
urls = ["https://static.crates.io/crates/v8/v8-146.4.0.crate"],
|
||||
)
|
||||
|
||||
use_repo(crate, "crates")
|
||||
|
||||
bazel_dep(name = "libcap", version = "2.27.bcr.1")
|
||||
|
||||
6
MODULE.bazel.lock
generated
6
MODULE.bazel.lock
generated
@@ -12,6 +12,7 @@
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20240116.2/MODULE.bazel": "73939767a4686cd9a520d16af5ab440071ed75cec1a876bf2fcfaf1f71987a16",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20250127.1/MODULE.bazel": "c4a89e7ceb9bf1e25cf84a9f830ff6b817b72874088bf5141b314726e46a57c1",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20250512.1/MODULE.bazel": "d209fdb6f36ffaf61c509fcc81b19e81b411a999a934a032e10cd009a0226215",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20250814.0/MODULE.bazel": "c43c16ca2c432566cdb78913964497259903ebe8fb7d9b57b38e9f1425b427b8",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20250814.1/MODULE.bazel": "51f2312901470cdab0dbdf3b88c40cd21c62a7ed58a3de45b365ddc5b11bcab2",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20250814.1/source.json": "cea3901d7e299da7320700abbaafe57a65d039f10d0d7ea601c4a66938ea4b0c",
|
||||
"https://bcr.bazel.build/modules/alsa_lib/1.2.9.bcr.4/MODULE.bazel": "66842efc2b50b7c12274a5218d468119a5d6f9dc46a5164d9496fb517f64aba6",
|
||||
@@ -104,6 +105,7 @@
|
||||
"https://bcr.bazel.build/modules/platforms/1.0.0/source.json": "f4ff1fd412e0246fd38c82328eb209130ead81d62dcd5a9e40910f867f733d96",
|
||||
"https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7",
|
||||
"https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c",
|
||||
"https://bcr.bazel.build/modules/protobuf/27.1/MODULE.bazel": "703a7b614728bb06647f965264967a8ef1c39e09e8f167b3ca0bb1fd80449c0d",
|
||||
"https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df",
|
||||
"https://bcr.bazel.build/modules/protobuf/29.0-rc3/MODULE.bazel": "33c2dfa286578573afc55a7acaea3cada4122b9631007c594bf0729f41c8de92",
|
||||
"https://bcr.bazel.build/modules/protobuf/29.1/MODULE.bazel": "557c3457560ff49e122ed76c0bc3397a64af9574691cb8201b4e46d4ab2ecb95",
|
||||
@@ -167,6 +169,7 @@
|
||||
"https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3",
|
||||
"https://bcr.bazel.build/modules/rules_kotlin/1.9.6/source.json": "2faa4794364282db7c06600b7e5e34867a564ae91bda7cae7c29c64e9466b7d5",
|
||||
"https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0",
|
||||
"https://bcr.bazel.build/modules/rules_license/0.0.4/MODULE.bazel": "6a88dd22800cf1f9f79ba32cacad0d3a423ed28efa2c2ed5582eaa78dd3ac1e5",
|
||||
"https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d",
|
||||
"https://bcr.bazel.build/modules/rules_license/1.0.0/MODULE.bazel": "a7fda60eefdf3d8c827262ba499957e4df06f659330bbe6cdbdb975b768bb65c",
|
||||
"https://bcr.bazel.build/modules/rules_license/1.0.0/source.json": "a52c89e54cc311196e478f8382df91c15f7a2bfdf4c6cd0e2675cc2ff0b56efb",
|
||||
@@ -181,6 +184,7 @@
|
||||
"https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7",
|
||||
"https://bcr.bazel.build/modules/rules_proto/6.0.0-rc1/MODULE.bazel": "1e5b502e2e1a9e825eef74476a5a1ee524a92297085015a052510b09a1a09483",
|
||||
"https://bcr.bazel.build/modules/rules_proto/6.0.2/MODULE.bazel": "ce916b775a62b90b61888052a416ccdda405212b6aaeb39522f7dc53431a5e73",
|
||||
"https://bcr.bazel.build/modules/rules_proto/7.0.2/MODULE.bazel": "bf81793bd6d2ad89a37a40693e56c61b0ee30f7a7fdbaf3eabbf5f39de47dea2",
|
||||
"https://bcr.bazel.build/modules/rules_proto/7.1.0/MODULE.bazel": "002d62d9108f75bb807cd56245d45648f38275cb3a99dcd45dfb864c5d74cb96",
|
||||
"https://bcr.bazel.build/modules/rules_proto/7.1.0/source.json": "39f89066c12c24097854e8f57ab8558929f9c8d474d34b2c00ac04630ad8940e",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f",
|
||||
@@ -190,6 +194,7 @@
|
||||
"https://bcr.bazel.build/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.33.2/MODULE.bazel": "3e036c4ad8d804a4dad897d333d8dce200d943df4827cb849840055be8d2e937",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c",
|
||||
"https://bcr.bazel.build/modules/rules_python/1.0.0/MODULE.bazel": "898a3d999c22caa585eb062b600f88654bf92efb204fa346fb55f6f8edffca43",
|
||||
"https://bcr.bazel.build/modules/rules_python/1.3.0/MODULE.bazel": "8361d57eafb67c09b75bf4bbe6be360e1b8f4f18118ab48037f2bd50aa2ccb13",
|
||||
"https://bcr.bazel.build/modules/rules_python/1.4.1/MODULE.bazel": "8991ad45bdc25018301d6b7e1d3626afc3c8af8aaf4bc04f23d0b99c938b73a6",
|
||||
"https://bcr.bazel.build/modules/rules_python/1.6.0/MODULE.bazel": "7e04ad8f8d5bea40451cf80b1bd8262552aa73f841415d20db96b7241bd027d8",
|
||||
@@ -1295,6 +1300,7 @@
|
||||
"strum_0.26.3": "{\"dependencies\":[{\"features\":[\"macros\"],\"name\":\"phf\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"strum_macros\",\"optional\":true,\"req\":\"^0.26.3\"},{\"kind\":\"dev\",\"name\":\"strum_macros\",\"req\":\"^0.26\"}],\"features\":{\"default\":[\"std\"],\"derive\":[\"strum_macros\"],\"std\":[]}}",
|
||||
"strum_0.27.2": "{\"dependencies\":[{\"features\":[\"macros\"],\"name\":\"phf\",\"optional\":true,\"req\":\"^0.12\"},{\"name\":\"strum_macros\",\"optional\":true,\"req\":\"^0.27\"}],\"features\":{\"default\":[\"std\"],\"derive\":[\"strum_macros\"],\"std\":[]}}",
|
||||
"strum_macros_0.26.4": "{\"dependencies\":[{\"name\":\"heck\",\"req\":\"^0.5.0\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"strum\",\"req\":\"^0.26\"},{\"features\":[\"parsing\",\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2.0\"}],\"features\":{}}",
|
||||
"strum_macros_0.27.2": "{\"dependencies\":[{\"name\":\"heck\",\"req\":\"^0.5.0\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"features\":[\"parsing\"],\"name\":\"syn\",\"req\":\"^2.0\"}],\"features\":{}}",
|
||||
"strum_macros_0.28.0": "{\"dependencies\":[{\"name\":\"heck\",\"req\":\"^0.5.0\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"features\":[\"parsing\"],\"name\":\"syn\",\"req\":\"^2.0\"}],\"features\":{}}",
|
||||
"subtle_2.6.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"}],\"features\":{\"const-generics\":[],\"core_hint_black_box\":[],\"default\":[\"std\",\"i128\"],\"i128\":[],\"nightly\":[],\"std\":[]}}",
|
||||
"supports-color_2.1.0": "{\"dependencies\":[{\"name\":\"is-terminal\",\"req\":\"^0.4.0\"},{\"name\":\"is_ci\",\"req\":\"^1.1.1\"}],\"features\":{}}",
|
||||
|
||||
160
codex-rs/Cargo.lock
generated
160
codex-rs/Cargo.lock
generated
@@ -409,6 +409,8 @@ dependencies = [
|
||||
"chrono",
|
||||
"codex-app-server-protocol",
|
||||
"codex-core",
|
||||
"codex-features",
|
||||
"codex-models",
|
||||
"codex-protocol",
|
||||
"codex-utils-cargo-bin",
|
||||
"core_test_support",
|
||||
@@ -1427,10 +1429,12 @@ dependencies = [
|
||||
"codex-chatgpt",
|
||||
"codex-cloud-requirements",
|
||||
"codex-core",
|
||||
"codex-environment",
|
||||
"codex-exec-server",
|
||||
"codex-features",
|
||||
"codex-feedback",
|
||||
"codex-file-search",
|
||||
"codex-login",
|
||||
"codex-models",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"codex-rmcp-client",
|
||||
@@ -1474,7 +1478,9 @@ dependencies = [
|
||||
"codex-app-server-protocol",
|
||||
"codex-arg0",
|
||||
"codex-core",
|
||||
"codex-features",
|
||||
"codex-feedback",
|
||||
"codex-models",
|
||||
"codex-protocol",
|
||||
"futures",
|
||||
"pretty_assertions",
|
||||
@@ -1657,6 +1663,7 @@ dependencies = [
|
||||
"codex-core",
|
||||
"codex-exec",
|
||||
"codex-execpolicy",
|
||||
"codex-features",
|
||||
"codex-login",
|
||||
"codex-mcp-server",
|
||||
"codex-protocol",
|
||||
@@ -1664,6 +1671,7 @@ dependencies = [
|
||||
"codex-rmcp-client",
|
||||
"codex-state",
|
||||
"codex-stdio-to-uds",
|
||||
"codex-terminal-detection",
|
||||
"codex-tui",
|
||||
"codex-tui-app-server",
|
||||
"codex-utils-cargo-bin",
|
||||
@@ -1840,15 +1848,16 @@ dependencies = [
|
||||
"codex-arg0",
|
||||
"codex-artifacts",
|
||||
"codex-async-utils",
|
||||
"codex-client",
|
||||
"codex-config",
|
||||
"codex-connectors",
|
||||
"codex-environment",
|
||||
"codex-exec-server",
|
||||
"codex-execpolicy",
|
||||
"codex-features",
|
||||
"codex-file-search",
|
||||
"codex-git",
|
||||
"codex-hooks",
|
||||
"codex-keyring-store",
|
||||
"codex-login",
|
||||
"codex-models",
|
||||
"codex-network-proxy",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
@@ -1858,6 +1867,7 @@ dependencies = [
|
||||
"codex-shell-escalation",
|
||||
"codex-skills",
|
||||
"codex-state",
|
||||
"codex-terminal-detection",
|
||||
"codex-test-macros",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-cache",
|
||||
@@ -1884,7 +1894,6 @@ dependencies = [
|
||||
"image",
|
||||
"indexmap 2.13.0",
|
||||
"insta",
|
||||
"keyring",
|
||||
"landlock",
|
||||
"libc",
|
||||
"maplit",
|
||||
@@ -1893,7 +1902,6 @@ dependencies = [
|
||||
"openssl-sys",
|
||||
"opentelemetry",
|
||||
"opentelemetry_sdk",
|
||||
"os_info",
|
||||
"predicates",
|
||||
"pretty_assertions",
|
||||
"rand 0.9.2",
|
||||
@@ -1907,7 +1915,6 @@ dependencies = [
|
||||
"serde_yaml",
|
||||
"serial_test",
|
||||
"sha1",
|
||||
"sha2",
|
||||
"shlex",
|
||||
"similar",
|
||||
"tempfile",
|
||||
@@ -1947,17 +1954,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-environment"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"codex-utils-absolute-path",
|
||||
"pretty_assertions",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-exec"
|
||||
version = "0.0.0"
|
||||
@@ -1972,6 +1968,7 @@ dependencies = [
|
||||
"codex-cloud-requirements",
|
||||
"codex-core",
|
||||
"codex-feedback",
|
||||
"codex-models",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
@@ -2003,6 +2000,30 @@ dependencies = [
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-exec-server"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"clap",
|
||||
"codex-app-server-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-cargo-bin",
|
||||
"codex-utils-pty",
|
||||
"futures",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-execpolicy"
|
||||
version = "0.0.0"
|
||||
@@ -2049,6 +2070,20 @@ dependencies = [
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-features"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-login",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"pretty_assertions",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"toml 0.9.11+spec-1.1.0",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-feedback"
|
||||
version = "0.0.0"
|
||||
@@ -2145,6 +2180,7 @@ name = "codex-lmstudio"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-core",
|
||||
"codex-models",
|
||||
"reqwest",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
@@ -2158,19 +2194,30 @@ name = "codex-login"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"codex-app-server-protocol",
|
||||
"codex-client",
|
||||
"codex-core",
|
||||
"codex-config",
|
||||
"codex-keyring-store",
|
||||
"codex-protocol",
|
||||
"codex-terminal-detection",
|
||||
"core_test_support",
|
||||
"keyring",
|
||||
"once_cell",
|
||||
"os_info",
|
||||
"pretty_assertions",
|
||||
"rand 0.9.2",
|
||||
"regex-lite",
|
||||
"reqwest",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serial_test",
|
||||
"sha2",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tiny_http",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -2187,6 +2234,8 @@ dependencies = [
|
||||
"anyhow",
|
||||
"codex-arg0",
|
||||
"codex-core",
|
||||
"codex-features",
|
||||
"codex-models",
|
||||
"codex-protocol",
|
||||
"codex-shell-command",
|
||||
"codex-utils-cli",
|
||||
@@ -2207,6 +2256,31 @@ dependencies = [
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-models"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"codex-api",
|
||||
"codex-login",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"http 1.4.0",
|
||||
"maplit",
|
||||
"pretty_assertions",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"toml 0.9.11+spec-1.1.0",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-network-proxy"
|
||||
version = "0.0.0"
|
||||
@@ -2246,6 +2320,7 @@ dependencies = [
|
||||
"async-stream",
|
||||
"bytes",
|
||||
"codex-core",
|
||||
"codex-models",
|
||||
"futures",
|
||||
"pretty_assertions",
|
||||
"reqwest",
|
||||
@@ -2262,6 +2337,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"codex-api",
|
||||
"codex-app-server-protocol",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-string",
|
||||
@@ -2327,8 +2403,8 @@ dependencies = [
|
||||
"icu_decimal",
|
||||
"icu_locale_core",
|
||||
"icu_provider",
|
||||
"mime_guess",
|
||||
"pretty_assertions",
|
||||
"quick-xml",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2477,6 +2553,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"strum 0.27.2",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
@@ -2494,6 +2571,14 @@ dependencies = [
|
||||
"uds_windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-terminal-detection"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"pretty_assertions",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-test-macros"
|
||||
version = "0.0.0"
|
||||
@@ -2514,6 +2599,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"codex-ansi-escape",
|
||||
"codex-app-server-client",
|
||||
"codex-app-server-protocol",
|
||||
"codex-arg0",
|
||||
"codex-backend-client",
|
||||
@@ -2522,13 +2608,16 @@ dependencies = [
|
||||
"codex-client",
|
||||
"codex-cloud-requirements",
|
||||
"codex-core",
|
||||
"codex-features",
|
||||
"codex-feedback",
|
||||
"codex-file-search",
|
||||
"codex-login",
|
||||
"codex-models",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"codex-shell-command",
|
||||
"codex-state",
|
||||
"codex-terminal-detection",
|
||||
"codex-tui-app-server",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-approval-presets",
|
||||
@@ -2613,13 +2702,16 @@ dependencies = [
|
||||
"codex-client",
|
||||
"codex-cloud-requirements",
|
||||
"codex-core",
|
||||
"codex-features",
|
||||
"codex-feedback",
|
||||
"codex-file-search",
|
||||
"codex-login",
|
||||
"codex-models",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"codex-shell-command",
|
||||
"codex-state",
|
||||
"codex-terminal-detection",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-approval-presets",
|
||||
"codex-utils-cargo-bin",
|
||||
@@ -2758,6 +2850,7 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"codex-utils-cache",
|
||||
"image",
|
||||
"mime_guess",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
]
|
||||
@@ -2777,6 +2870,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-core",
|
||||
"codex-lmstudio",
|
||||
"codex-models",
|
||||
"codex-ollama",
|
||||
]
|
||||
|
||||
@@ -2819,6 +2913,7 @@ name = "codex-utils-sandbox-summary"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-core",
|
||||
"codex-models",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
"pretty_assertions",
|
||||
@@ -3061,13 +3156,18 @@ dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
"base64 0.22.1",
|
||||
"codex-arg0",
|
||||
"codex-core",
|
||||
"codex-features",
|
||||
"codex-models",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-cargo-bin",
|
||||
"ctor 0.6.3",
|
||||
"futures",
|
||||
"notify",
|
||||
"opentelemetry",
|
||||
"opentelemetry_sdk",
|
||||
"pretty_assertions",
|
||||
"regex-lite",
|
||||
"reqwest",
|
||||
@@ -3076,6 +3176,9 @@ dependencies = [
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tracing",
|
||||
"tracing-opentelemetry",
|
||||
"tracing-subscriber",
|
||||
"walkdir",
|
||||
"wiremock",
|
||||
"zstd",
|
||||
@@ -5796,6 +5899,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"codex-core",
|
||||
"codex-mcp-server",
|
||||
"codex-terminal-detection",
|
||||
"codex-utils-cargo-bin",
|
||||
"core_test_support",
|
||||
"os_info",
|
||||
@@ -7248,6 +7352,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9367,6 +9472,9 @@ name = "strum"
|
||||
version = "0.27.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
||||
dependencies = [
|
||||
"strum_macros 0.27.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
@@ -9381,6 +9489,18 @@ dependencies = [
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.27.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.28.0"
|
||||
|
||||
@@ -11,6 +11,7 @@ members = [
|
||||
"apply-patch",
|
||||
"arg0",
|
||||
"feedback",
|
||||
"features",
|
||||
"codex-backend-openapi-models",
|
||||
"cloud-requirements",
|
||||
"cloud-tasks",
|
||||
@@ -22,10 +23,10 @@ members = [
|
||||
"shell-escalation",
|
||||
"skills",
|
||||
"core",
|
||||
"environment",
|
||||
"hooks",
|
||||
"secrets",
|
||||
"exec",
|
||||
"exec-server",
|
||||
"execpolicy",
|
||||
"execpolicy-legacy",
|
||||
"keyring-store",
|
||||
@@ -33,6 +34,7 @@ members = [
|
||||
"linux-sandbox",
|
||||
"lmstudio",
|
||||
"login",
|
||||
"models",
|
||||
"mcp-server",
|
||||
"network-proxy",
|
||||
"ollama",
|
||||
@@ -66,6 +68,7 @@ members = [
|
||||
"codex-client",
|
||||
"codex-api",
|
||||
"state",
|
||||
"terminal-detection",
|
||||
"codex-experimental-api-macros",
|
||||
"test-macros",
|
||||
"package-manager",
|
||||
@@ -104,11 +107,12 @@ codex-cloud-requirements = { path = "cloud-requirements" }
|
||||
codex-connectors = { path = "connectors" }
|
||||
codex-config = { path = "config" }
|
||||
codex-core = { path = "core" }
|
||||
codex-environment = { path = "environment" }
|
||||
codex-exec = { path = "exec" }
|
||||
codex-exec-server = { path = "exec-server" }
|
||||
codex-execpolicy = { path = "execpolicy" }
|
||||
codex-experimental-api-macros = { path = "codex-experimental-api-macros" }
|
||||
codex-feedback = { path = "feedback" }
|
||||
codex-features = { path = "features" }
|
||||
codex-file-search = { path = "file-search" }
|
||||
codex-git = { path = "utils/git" }
|
||||
codex-hooks = { path = "hooks" }
|
||||
@@ -116,6 +120,7 @@ codex-keyring-store = { path = "keyring-store" }
|
||||
codex-linux-sandbox = { path = "linux-sandbox" }
|
||||
codex-lmstudio = { path = "lmstudio" }
|
||||
codex-login = { path = "login" }
|
||||
codex-models = { path = "models" }
|
||||
codex-mcp-server = { path = "mcp-server" }
|
||||
codex-network-proxy = { path = "network-proxy" }
|
||||
codex-ollama = { path = "ollama" }
|
||||
@@ -131,6 +136,7 @@ codex-skills = { path = "skills" }
|
||||
codex-state = { path = "state" }
|
||||
codex-stdio-to-uds = { path = "stdio-to-uds" }
|
||||
codex-test-macros = { path = "test-macros" }
|
||||
codex-terminal-detection = { path = "terminal-detection" }
|
||||
codex-tui = { path = "tui" }
|
||||
codex-tui-app-server = { path = "tui_app_server" }
|
||||
codex-utils-absolute-path = { path = "utils/absolute-path" }
|
||||
@@ -232,6 +238,7 @@ portable-pty = "0.9.0"
|
||||
predicates = "3"
|
||||
pretty_assertions = "1.4.1"
|
||||
pulldown-cmark = "0.10"
|
||||
quick-xml = "0.38.4"
|
||||
rand = "0.9"
|
||||
ratatui = "0.29.0"
|
||||
ratatui-macros = "0.6.0"
|
||||
|
||||
@@ -16,7 +16,9 @@ codex-app-server = { workspace = true }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-arg0 = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-features = { workspace = true }
|
||||
codex-feedback = { workspace = true }
|
||||
codex-models = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
@@ -46,8 +46,9 @@ use codex_core::ThreadManager;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config_loader::CloudRequirementsLoader;
|
||||
use codex_core::config_loader::LoaderOverrides;
|
||||
use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig;
|
||||
use codex_features::Feature;
|
||||
use codex_feedback::CodexFeedback;
|
||||
use codex_models::CollaborationModesConfig;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use serde::de::DeserializeOwned;
|
||||
use tokio::sync::mpsc;
|
||||
@@ -215,7 +216,7 @@ impl InProcessClientStartArgs {
|
||||
default_mode_request_user_input: self
|
||||
.config
|
||||
.features
|
||||
.enabled(codex_core::features::Feature::DefaultModeRequestUserInput),
|
||||
.enabled(Feature::DefaultModeRequestUserInput),
|
||||
},
|
||||
));
|
||||
|
||||
@@ -1033,10 +1034,6 @@ mod tests {
|
||||
for (session_source, expected_source) in [
|
||||
(SessionSource::Exec, ApiSessionSource::Exec),
|
||||
(SessionSource::Cli, ApiSessionSource::Cli),
|
||||
(
|
||||
SessionSource::Custom("atlas".to_string()),
|
||||
ApiSessionSource::Custom("atlas".to_string()),
|
||||
),
|
||||
] {
|
||||
let client = start_test_client(session_source).await;
|
||||
let parsed: ThreadStartResponse = client
|
||||
@@ -1488,7 +1485,7 @@ mod tests {
|
||||
CollaborationModesConfig {
|
||||
default_mode_request_user_input: config
|
||||
.features
|
||||
.enabled(codex_core::features::Feature::DefaultModeRequestUserInput),
|
||||
.enabled(Feature::DefaultModeRequestUserInput),
|
||||
},
|
||||
));
|
||||
event_tx
|
||||
|
||||
@@ -2881,6 +2881,22 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadShellCommandParams": {
|
||||
"properties": {
|
||||
"command": {
|
||||
"description": "Shell command string evaluated by the thread's configured shell. Unlike `command/exec`, this intentionally preserves shell syntax such as pipes, redirects, and quoting. This runs unsandboxed with full access rather than inheriting the thread sandbox policy.",
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"threadId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSortKey": {
|
||||
"enum": [
|
||||
"created_at",
|
||||
@@ -2894,7 +2910,6 @@
|
||||
"vscode",
|
||||
"exec",
|
||||
"appServer",
|
||||
"custom",
|
||||
"subAgent",
|
||||
"subAgentReview",
|
||||
"subAgentCompact",
|
||||
@@ -3587,6 +3602,30 @@
|
||||
"title": "Thread/compact/startRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/shellCommand"
|
||||
],
|
||||
"title": "Thread/shellCommandRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadShellCommandParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/shellCommandRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"FuzzyFileSearchMatchType": {
|
||||
"enum": [
|
||||
"file",
|
||||
"directory"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"FuzzyFileSearchResult": {
|
||||
"description": "Superset of [`codex_file_search::FileMatch`]",
|
||||
"properties": {
|
||||
@@ -18,6 +25,9 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"match_type": {
|
||||
"$ref": "#/definitions/FuzzyFileSearchMatchType"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -32,6 +42,7 @@
|
||||
},
|
||||
"required": [
|
||||
"file_name",
|
||||
"match_type",
|
||||
"path",
|
||||
"root",
|
||||
"score"
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"FuzzyFileSearchMatchType": {
|
||||
"enum": [
|
||||
"file",
|
||||
"directory"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"FuzzyFileSearchResult": {
|
||||
"description": "Superset of [`codex_file_search::FileMatch`]",
|
||||
"properties": {
|
||||
@@ -18,6 +25,9 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"match_type": {
|
||||
"$ref": "#/definitions/FuzzyFileSearchMatchType"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -32,6 +42,7 @@
|
||||
},
|
||||
"required": [
|
||||
"file_name",
|
||||
"match_type",
|
||||
"path",
|
||||
"root",
|
||||
"score"
|
||||
|
||||
@@ -745,6 +745,15 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
@@ -964,6 +973,13 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"FuzzyFileSearchMatchType": {
|
||||
"enum": [
|
||||
"file",
|
||||
"directory"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"FuzzyFileSearchResult": {
|
||||
"description": "Superset of [`codex_file_search::FileMatch`]",
|
||||
"properties": {
|
||||
@@ -981,6 +997,9 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"match_type": {
|
||||
"$ref": "#/definitions/FuzzyFileSearchMatchType"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -995,6 +1014,7 @@
|
||||
},
|
||||
"required": [
|
||||
"file_name",
|
||||
"match_type",
|
||||
"path",
|
||||
"root",
|
||||
"score"
|
||||
@@ -1181,6 +1201,21 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunId": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunId",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HookRunStatus": {
|
||||
"enum": [
|
||||
"running",
|
||||
@@ -1400,6 +1435,36 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpServerStartupState": {
|
||||
"enum": [
|
||||
"starting",
|
||||
"ready",
|
||||
"failed",
|
||||
"cancelled"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpServerStatusUpdatedNotification": {
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/McpServerStartupState"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"status"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -2237,6 +2302,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -2392,6 +2484,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
@@ -2717,6 +2817,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"savedPath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -4144,6 +4250,26 @@
|
||||
"title": "McpServer/oauthLogin/completedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"mcpServer/startupStatus/updated"
|
||||
],
|
||||
"title": "McpServer/startupStatus/updatedNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/McpServerStatusUpdatedNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "McpServer/startupStatus/updatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
|
||||
@@ -499,6 +499,30 @@
|
||||
"title": "Thread/compact/startRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/shellCommand"
|
||||
],
|
||||
"title": "Thread/shellCommandRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/ThreadShellCommandParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/shellCommandRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -2034,6 +2058,13 @@
|
||||
"title": "FileChangeRequestApprovalResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"FuzzyFileSearchMatchType": {
|
||||
"enum": [
|
||||
"file",
|
||||
"directory"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"FuzzyFileSearchParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -2093,6 +2124,9 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"match_type": {
|
||||
"$ref": "#/definitions/FuzzyFileSearchMatchType"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -2107,6 +2141,7 @@
|
||||
},
|
||||
"required": [
|
||||
"file_name",
|
||||
"match_type",
|
||||
"path",
|
||||
"root",
|
||||
"score"
|
||||
@@ -3938,6 +3973,26 @@
|
||||
"title": "McpServer/oauthLogin/completedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"mcpServer/startupStatus/updated"
|
||||
],
|
||||
"title": "McpServer/startupStatus/updatedNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/McpServerStatusUpdatedNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "McpServer/startupStatus/updatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
@@ -5197,11 +5252,15 @@
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"needsAuth": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
"name",
|
||||
"needsAuth"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
@@ -6110,6 +6169,15 @@
|
||||
"title": "CommandExecutionOutputDeltaNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
@@ -7927,6 +7995,21 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunId": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunId",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HookRunStatus": {
|
||||
"enum": [
|
||||
"running",
|
||||
@@ -8507,6 +8590,15 @@
|
||||
"title": "McpServerRefreshResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"McpServerStartupState": {
|
||||
"enum": [
|
||||
"starting",
|
||||
"ready",
|
||||
"failed",
|
||||
"cancelled"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpServerStatus": {
|
||||
"properties": {
|
||||
"authStatus": {
|
||||
@@ -8543,6 +8635,29 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpServerStatusUpdatedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/v2/McpServerStartupState"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"status"
|
||||
],
|
||||
"title": "McpServerStatusUpdatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -9340,6 +9455,13 @@
|
||||
"PluginListResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"featuredPluginIds": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"marketplaces": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/PluginMarketplaceEntry"
|
||||
@@ -11903,6 +12025,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -12058,6 +12207,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/v2/CommandExecutionStatus"
|
||||
},
|
||||
@@ -12383,6 +12540,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"savedPath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -13097,6 +13260,29 @@
|
||||
"title": "ThreadSetNameResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadShellCommandParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"command": {
|
||||
"description": "Shell command string evaluated by the thread's configured shell. Unlike `command/exec`, this intentionally preserves shell syntax such as pipes, redirects, and quoting. This runs unsandboxed with full access rather than inheriting the thread sandbox policy.",
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadShellCommandParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadShellCommandResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ThreadShellCommandResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSortKey": {
|
||||
"enum": [
|
||||
"created_at",
|
||||
@@ -13110,7 +13296,6 @@
|
||||
"vscode",
|
||||
"exec",
|
||||
"appServer",
|
||||
"custom",
|
||||
"subAgent",
|
||||
"subAgentReview",
|
||||
"subAgentCompact",
|
||||
|
||||
@@ -492,11 +492,15 @@
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"needsAuth": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
"name",
|
||||
"needsAuth"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
@@ -1026,6 +1030,30 @@
|
||||
"title": "Thread/compact/startRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/shellCommand"
|
||||
],
|
||||
"title": "Thread/shellCommandRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadShellCommandParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/shellCommandRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -2754,6 +2782,15 @@
|
||||
"title": "CommandExecutionOutputDeltaNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
@@ -4327,6 +4364,13 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"FuzzyFileSearchMatchType": {
|
||||
"enum": [
|
||||
"file",
|
||||
"directory"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"FuzzyFileSearchParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -4370,6 +4414,9 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"match_type": {
|
||||
"$ref": "#/definitions/FuzzyFileSearchMatchType"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -4384,6 +4431,7 @@
|
||||
},
|
||||
"required": [
|
||||
"file_name",
|
||||
"match_type",
|
||||
"path",
|
||||
"root",
|
||||
"score"
|
||||
@@ -4671,6 +4719,21 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunId": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunId",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HookRunStatus": {
|
||||
"enum": [
|
||||
"running",
|
||||
@@ -5295,6 +5358,15 @@
|
||||
"title": "McpServerRefreshResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"McpServerStartupState": {
|
||||
"enum": [
|
||||
"starting",
|
||||
"ready",
|
||||
"failed",
|
||||
"cancelled"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpServerStatus": {
|
||||
"properties": {
|
||||
"authStatus": {
|
||||
@@ -5331,6 +5403,29 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpServerStatusUpdatedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/McpServerStartupState"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"status"
|
||||
],
|
||||
"title": "McpServerStatusUpdatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -6128,6 +6223,13 @@
|
||||
"PluginListResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"featuredPluginIds": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"marketplaces": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginMarketplaceEntry"
|
||||
@@ -8303,6 +8405,26 @@
|
||||
"title": "McpServer/oauthLogin/completedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"mcpServer/startupStatus/updated"
|
||||
],
|
||||
"title": "McpServer/startupStatus/updatedNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/McpServerStatusUpdatedNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "McpServer/startupStatus/updatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
@@ -9663,6 +9785,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -9818,6 +9967,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
@@ -10143,6 +10300,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"savedPath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -10857,6 +11020,29 @@
|
||||
"title": "ThreadSetNameResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadShellCommandParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"command": {
|
||||
"description": "Shell command string evaluated by the thread's configured shell. Unlike `command/exec`, this intentionally preserves shell syntax such as pipes, redirects, and quoting. This runs unsandboxed with full access rather than inheriting the thread sandbox policy.",
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadShellCommandParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadShellCommandResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ThreadShellCommandResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSortKey": {
|
||||
"enum": [
|
||||
"created_at",
|
||||
@@ -10870,7 +11056,6 @@
|
||||
"vscode",
|
||||
"exec",
|
||||
"appServer",
|
||||
"custom",
|
||||
"subAgent",
|
||||
"subAgentReview",
|
||||
"subAgentCompact",
|
||||
|
||||
@@ -177,6 +177,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
@@ -257,6 +266,21 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunId": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunId",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -487,6 +511,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -642,6 +693,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
@@ -967,6 +1026,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"savedPath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -177,6 +177,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
@@ -257,6 +266,21 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunId": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunId",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -487,6 +511,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -642,6 +693,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
@@ -967,6 +1026,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"savedPath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"McpServerStartupState": {
|
||||
"enum": [
|
||||
"starting",
|
||||
"ready",
|
||||
"failed",
|
||||
"cancelled"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/McpServerStartupState"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"status"
|
||||
],
|
||||
"title": "McpServerStatusUpdatedNotification",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -21,11 +21,15 @@
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"needsAuth": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
"name",
|
||||
"needsAuth"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
|
||||
@@ -239,6 +239,13 @@
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"featuredPluginIds": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"marketplaces": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginMarketplaceEntry"
|
||||
|
||||
@@ -25,11 +25,15 @@
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"needsAuth": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
"name",
|
||||
"needsAuth"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
|
||||
@@ -291,6 +291,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
@@ -371,6 +380,21 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunId": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunId",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -601,6 +625,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -756,6 +807,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
@@ -1081,6 +1140,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"savedPath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -353,6 +353,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
@@ -456,6 +465,21 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunId": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunId",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -1094,6 +1118,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -1249,6 +1300,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
@@ -1574,6 +1633,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"savedPath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
"vscode",
|
||||
"exec",
|
||||
"appServer",
|
||||
"custom",
|
||||
"subAgent",
|
||||
"subAgentReview",
|
||||
"subAgentCompact",
|
||||
|
||||
@@ -291,6 +291,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
@@ -394,6 +403,21 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunId": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunId",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -852,6 +876,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -1007,6 +1058,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
@@ -1332,6 +1391,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"savedPath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -291,6 +291,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
@@ -394,6 +403,21 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunId": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunId",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -852,6 +876,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -1007,6 +1058,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
@@ -1332,6 +1391,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"savedPath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -291,6 +291,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
@@ -394,6 +403,21 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunId": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunId",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -852,6 +876,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -1007,6 +1058,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
@@ -1332,6 +1391,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"savedPath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -353,6 +353,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
@@ -456,6 +465,21 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunId": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunId",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -1094,6 +1118,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -1249,6 +1300,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
@@ -1574,6 +1633,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"savedPath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -291,6 +291,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
@@ -394,6 +403,21 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunId": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunId",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -852,6 +876,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -1007,6 +1058,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
@@ -1332,6 +1391,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"savedPath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"command": {
|
||||
"description": "Shell command string evaluated by the thread's configured shell. Unlike `command/exec`, this intentionally preserves shell syntax such as pipes, redirects, and quoting. This runs unsandboxed with full access rather than inheriting the thread sandbox policy.",
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadShellCommandParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ThreadShellCommandResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -353,6 +353,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
@@ -456,6 +465,21 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunId": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunId",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -1094,6 +1118,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -1249,6 +1300,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
@@ -1574,6 +1633,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"savedPath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -291,6 +291,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
@@ -394,6 +403,21 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunId": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunId",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -852,6 +876,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -1007,6 +1058,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
@@ -1332,6 +1391,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"savedPath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -291,6 +291,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
@@ -394,6 +403,21 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunId": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunId",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -852,6 +876,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -1007,6 +1058,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
@@ -1332,6 +1391,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"savedPath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -291,6 +291,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
@@ -371,6 +380,21 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunId": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunId",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -601,6 +625,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -756,6 +807,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
@@ -1081,6 +1140,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"savedPath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -291,6 +291,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
@@ -371,6 +380,21 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunId": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunId",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -601,6 +625,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -756,6 +807,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
@@ -1081,6 +1140,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"savedPath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -291,6 +291,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
@@ -371,6 +380,21 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunId": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunId",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -601,6 +625,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -756,6 +807,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
@@ -1081,6 +1140,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"savedPath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -49,6 +49,7 @@ import type { ThreadReadParams } from "./v2/ThreadReadParams";
|
||||
import type { ThreadResumeParams } from "./v2/ThreadResumeParams";
|
||||
import type { ThreadRollbackParams } from "./v2/ThreadRollbackParams";
|
||||
import type { ThreadSetNameParams } from "./v2/ThreadSetNameParams";
|
||||
import type { ThreadShellCommandParams } from "./v2/ThreadShellCommandParams";
|
||||
import type { ThreadStartParams } from "./v2/ThreadStartParams";
|
||||
import type { ThreadUnarchiveParams } from "./v2/ThreadUnarchiveParams";
|
||||
import type { ThreadUnsubscribeParams } from "./v2/ThreadUnsubscribeParams";
|
||||
@@ -60,4 +61,4 @@ import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupSta
|
||||
/**
|
||||
* Request from the client to the server.
|
||||
*/
|
||||
export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, };
|
||||
export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, };
|
||||
|
||||
@@ -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 FuzzyFileSearchMatchType = "file" | "directory";
|
||||
@@ -1,8 +1,9 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { FuzzyFileSearchMatchType } from "./FuzzyFileSearchMatchType";
|
||||
|
||||
/**
|
||||
* Superset of [`codex_file_search::FileMatch`]
|
||||
*/
|
||||
export type FuzzyFileSearchResult = { root: string, path: string, file_name: string, score: number, indices: Array<number> | null, };
|
||||
export type FuzzyFileSearchResult = { root: string, path: string, match_type: FuzzyFileSearchMatchType, file_name: string, score: number, indices: Array<number> | null, };
|
||||
|
||||
@@ -22,6 +22,7 @@ import type { ItemGuardianApprovalReviewCompletedNotification } from "./v2/ItemG
|
||||
import type { ItemGuardianApprovalReviewStartedNotification } from "./v2/ItemGuardianApprovalReviewStartedNotification";
|
||||
import type { ItemStartedNotification } from "./v2/ItemStartedNotification";
|
||||
import type { McpServerOauthLoginCompletedNotification } from "./v2/McpServerOauthLoginCompletedNotification";
|
||||
import type { McpServerStatusUpdatedNotification } from "./v2/McpServerStatusUpdatedNotification";
|
||||
import type { McpToolCallProgressNotification } from "./v2/McpToolCallProgressNotification";
|
||||
import type { ModelReroutedNotification } from "./v2/ModelReroutedNotification";
|
||||
import type { PlanDeltaNotification } from "./v2/PlanDeltaNotification";
|
||||
@@ -54,4 +55,4 @@ import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldW
|
||||
/**
|
||||
* Notification sent from the server to the client.
|
||||
*/
|
||||
export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification };
|
||||
export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification };
|
||||
|
||||
@@ -18,6 +18,7 @@ export type { FileChange } from "./FileChange";
|
||||
export type { ForcedLoginMethod } from "./ForcedLoginMethod";
|
||||
export type { FunctionCallOutputBody } from "./FunctionCallOutputBody";
|
||||
export type { FunctionCallOutputContentItem } from "./FunctionCallOutputContentItem";
|
||||
export type { FuzzyFileSearchMatchType } from "./FuzzyFileSearchMatchType";
|
||||
export type { FuzzyFileSearchParams } from "./FuzzyFileSearchParams";
|
||||
export type { FuzzyFileSearchResponse } from "./FuzzyFileSearchResponse";
|
||||
export type { FuzzyFileSearchResult } from "./FuzzyFileSearchResult";
|
||||
|
||||
@@ -5,4 +5,4 @@
|
||||
/**
|
||||
* EXPERIMENTAL - app metadata summary for plugin responses.
|
||||
*/
|
||||
export type AppSummary = { id: string, name: string, description: string | null, installUrl: string | null, };
|
||||
export type AppSummary = { id: string, name: string, description: string | null, installUrl: string | null, needsAuth: boolean, };
|
||||
|
||||
@@ -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 CommandExecutionSource = "agent" | "userShell" | "unifiedExecStartup" | "unifiedExecInteraction";
|
||||
@@ -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 HookPromptFragment = { text: string, hookRunId: string, };
|
||||
@@ -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 McpServerStartupState = "starting" | "ready" | "failed" | "cancelled";
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { McpServerStartupState } from "./McpServerStartupState";
|
||||
|
||||
export type McpServerStatusUpdatedNotification = { name: string, status: McpServerStartupState, error: string | null, };
|
||||
@@ -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 { PluginMarketplaceEntry } from "./PluginMarketplaceEntry";
|
||||
|
||||
export type PluginListResponse = { marketplaces: Array<PluginMarketplaceEntry>, remoteSyncError: string | null, };
|
||||
export type PluginListResponse = { marketplaces: Array<PluginMarketplaceEntry>, remoteSyncError: string | null, featuredPluginIds: Array<string>, };
|
||||
|
||||
@@ -8,10 +8,12 @@ import type { CollabAgentState } from "./CollabAgentState";
|
||||
import type { CollabAgentTool } from "./CollabAgentTool";
|
||||
import type { CollabAgentToolCallStatus } from "./CollabAgentToolCallStatus";
|
||||
import type { CommandAction } from "./CommandAction";
|
||||
import type { CommandExecutionSource } from "./CommandExecutionSource";
|
||||
import type { CommandExecutionStatus } from "./CommandExecutionStatus";
|
||||
import type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem";
|
||||
import type { DynamicToolCallStatus } from "./DynamicToolCallStatus";
|
||||
import type { FileUpdateChange } from "./FileUpdateChange";
|
||||
import type { HookPromptFragment } from "./HookPromptFragment";
|
||||
import type { McpToolCallError } from "./McpToolCallError";
|
||||
import type { McpToolCallResult } from "./McpToolCallResult";
|
||||
import type { McpToolCallStatus } from "./McpToolCallStatus";
|
||||
@@ -20,7 +22,7 @@ import type { PatchApplyStatus } from "./PatchApplyStatus";
|
||||
import type { UserInput } from "./UserInput";
|
||||
import type { WebSearchAction } from "./WebSearchAction";
|
||||
|
||||
export type ThreadItem = { "type": "userMessage", id: string, content: Array<UserInput>, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, memoryCitation: MemoryCitation | null, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array<string>, content: Array<string>, } | { "type": "commandExecution", id: string,
|
||||
export type ThreadItem = { "type": "userMessage", id: string, content: Array<UserInput>, } | { "type": "hookPrompt", id: string, fragments: Array<HookPromptFragment>, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, memoryCitation: MemoryCitation | null, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array<string>, content: Array<string>, } | { "type": "commandExecution", id: string,
|
||||
/**
|
||||
* The command to be executed.
|
||||
*/
|
||||
@@ -32,7 +34,7 @@ cwd: string,
|
||||
/**
|
||||
* Identifier for the underlying PTY process (when available).
|
||||
*/
|
||||
processId: string | null, status: CommandExecutionStatus,
|
||||
processId: string | null, source: CommandExecutionSource, status: CommandExecutionStatus,
|
||||
/**
|
||||
* A best-effort parsing of the command to understand the action(s) it will perform.
|
||||
* This returns a list of CommandAction objects because a single shell command may
|
||||
@@ -95,4 +97,4 @@ reasoningEffort: ReasoningEffort | null,
|
||||
/**
|
||||
* Last known status of the target agents, when available.
|
||||
*/
|
||||
agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "webSearch", id: string, query: string, action: WebSearchAction | null, } | { "type": "imageView", id: string, path: string, } | { "type": "imageGeneration", id: string, status: string, revisedPrompt: string | null, result: string, } | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, };
|
||||
agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "webSearch", id: string, query: string, action: WebSearchAction | null, } | { "type": "imageView", id: string, path: string, } | { "type": "imageGeneration", id: string, status: string, revisedPrompt: string | null, result: string, savedPath?: string, } | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, };
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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 ThreadShellCommandParams = { threadId: string,
|
||||
/**
|
||||
* Shell command string evaluated by the thread's configured shell.
|
||||
* Unlike `command/exec`, this intentionally preserves shell syntax
|
||||
* such as pipes, redirects, and quoting. This runs unsandboxed with full
|
||||
* access rather than inheriting the thread sandbox policy.
|
||||
*/
|
||||
command: string, };
|
||||
@@ -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 ThreadShellCommandResponse = Record<string, never>;
|
||||
@@ -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 ThreadSourceKind = "cli" | "vscode" | "exec" | "appServer" | "custom" | "subAgent" | "subAgentReview" | "subAgentCompact" | "subAgentThreadSpawn" | "subAgentOther" | "unknown";
|
||||
export type ThreadSourceKind = "cli" | "vscode" | "exec" | "appServer" | "subAgent" | "subAgentReview" | "subAgentCompact" | "subAgentThreadSpawn" | "subAgentOther" | "unknown";
|
||||
|
||||
@@ -55,6 +55,7 @@ export type { CommandExecutionOutputDeltaNotification } from "./CommandExecution
|
||||
export type { CommandExecutionRequestApprovalParams } from "./CommandExecutionRequestApprovalParams";
|
||||
export type { CommandExecutionRequestApprovalResponse } from "./CommandExecutionRequestApprovalResponse";
|
||||
export type { CommandExecutionRequestApprovalSkillMetadata } from "./CommandExecutionRequestApprovalSkillMetadata";
|
||||
export type { CommandExecutionSource } from "./CommandExecutionSource";
|
||||
export type { CommandExecutionStatus } from "./CommandExecutionStatus";
|
||||
export type { Config } from "./Config";
|
||||
export type { ConfigBatchWriteParams } from "./ConfigBatchWriteParams";
|
||||
@@ -125,6 +126,7 @@ export type { HookExecutionMode } from "./HookExecutionMode";
|
||||
export type { HookHandlerType } from "./HookHandlerType";
|
||||
export type { HookOutputEntry } from "./HookOutputEntry";
|
||||
export type { HookOutputEntryKind } from "./HookOutputEntryKind";
|
||||
export type { HookPromptFragment } from "./HookPromptFragment";
|
||||
export type { HookRunStatus } from "./HookRunStatus";
|
||||
export type { HookRunSummary } from "./HookRunSummary";
|
||||
export type { HookScope } from "./HookScope";
|
||||
@@ -169,7 +171,9 @@ export type { McpServerOauthLoginCompletedNotification } from "./McpServerOauthL
|
||||
export type { McpServerOauthLoginParams } from "./McpServerOauthLoginParams";
|
||||
export type { McpServerOauthLoginResponse } from "./McpServerOauthLoginResponse";
|
||||
export type { McpServerRefreshResponse } from "./McpServerRefreshResponse";
|
||||
export type { McpServerStartupState } from "./McpServerStartupState";
|
||||
export type { McpServerStatus } from "./McpServerStatus";
|
||||
export type { McpServerStatusUpdatedNotification } from "./McpServerStatusUpdatedNotification";
|
||||
export type { McpToolCallError } from "./McpToolCallError";
|
||||
export type { McpToolCallProgressNotification } from "./McpToolCallProgressNotification";
|
||||
export type { McpToolCallResult } from "./McpToolCallResult";
|
||||
@@ -283,6 +287,8 @@ export type { ThreadRollbackParams } from "./ThreadRollbackParams";
|
||||
export type { ThreadRollbackResponse } from "./ThreadRollbackResponse";
|
||||
export type { ThreadSetNameParams } from "./ThreadSetNameParams";
|
||||
export type { ThreadSetNameResponse } from "./ThreadSetNameResponse";
|
||||
export type { ThreadShellCommandParams } from "./ThreadShellCommandParams";
|
||||
export type { ThreadShellCommandResponse } from "./ThreadShellCommandResponse";
|
||||
export type { ThreadSortKey } from "./ThreadSortKey";
|
||||
export type { ThreadSourceKind } from "./ThreadSourceKind";
|
||||
export type { ThreadStartParams } from "./ThreadStartParams";
|
||||
|
||||
@@ -267,6 +267,10 @@ client_request_definitions! {
|
||||
params: v2::ThreadCompactStartParams,
|
||||
response: v2::ThreadCompactStartResponse,
|
||||
},
|
||||
ThreadShellCommand => "thread/shellCommand" {
|
||||
params: v2::ThreadShellCommandParams,
|
||||
response: v2::ThreadShellCommandResponse,
|
||||
},
|
||||
#[experimental("thread/backgroundTerminals/clean")]
|
||||
ThreadBackgroundTerminalsClean => "thread/backgroundTerminals/clean" {
|
||||
params: v2::ThreadBackgroundTerminalsCleanParams,
|
||||
@@ -800,11 +804,20 @@ pub struct FuzzyFileSearchParams {
|
||||
pub struct FuzzyFileSearchResult {
|
||||
pub root: String,
|
||||
pub path: String,
|
||||
pub match_type: FuzzyFileSearchMatchType,
|
||||
pub file_name: String,
|
||||
pub score: u32,
|
||||
pub indices: Option<Vec<u32>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
pub enum FuzzyFileSearchMatchType {
|
||||
File,
|
||||
Directory,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
pub struct FuzzyFileSearchResponse {
|
||||
pub files: Vec<FuzzyFileSearchResult>,
|
||||
@@ -892,6 +905,7 @@ server_notification_definitions! {
|
||||
ServerRequestResolved => "serverRequest/resolved" (v2::ServerRequestResolvedNotification),
|
||||
McpToolCallProgress => "item/mcpToolCall/progress" (v2::McpToolCallProgressNotification),
|
||||
McpServerOauthLoginCompleted => "mcpServer/oauthLogin/completed" (v2::McpServerOauthLoginCompletedNotification),
|
||||
McpServerStatusUpdated => "mcpServer/startupStatus/updated" (v2::McpServerStatusUpdatedNotification),
|
||||
AccountUpdated => "account/updated" (v2::AccountUpdatedNotification),
|
||||
AccountRateLimitsUpdated => "account/rateLimits/updated" (v2::AccountRateLimitsUpdatedNotification),
|
||||
AppListUpdated => "app/list/updated" (v2::AppListUpdatedNotification),
|
||||
|
||||
@@ -18,6 +18,7 @@ use crate::protocol::v2::TurnError;
|
||||
use crate::protocol::v2::TurnStatus;
|
||||
use crate::protocol::v2::UserInput;
|
||||
use crate::protocol::v2::WebSearchAction;
|
||||
use codex_protocol::items::parse_hook_prompt_message;
|
||||
use codex_protocol::models::MessagePhase;
|
||||
use codex_protocol::protocol::AgentReasoningEvent;
|
||||
use codex_protocol::protocol::AgentReasoningRawContentEvent;
|
||||
@@ -184,12 +185,37 @@ impl ThreadHistoryBuilder {
|
||||
match item {
|
||||
RolloutItem::EventMsg(event) => self.handle_event(event),
|
||||
RolloutItem::Compacted(payload) => self.handle_compacted(payload),
|
||||
RolloutItem::TurnContext(_)
|
||||
| RolloutItem::SessionMeta(_)
|
||||
| RolloutItem::ResponseItem(_) => {}
|
||||
RolloutItem::ResponseItem(item) => self.handle_response_item(item),
|
||||
RolloutItem::TurnContext(_) | RolloutItem::SessionMeta(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_response_item(&mut self, item: &codex_protocol::models::ResponseItem) {
|
||||
let codex_protocol::models::ResponseItem::Message {
|
||||
role, content, id, ..
|
||||
} = item
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if role != "user" {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(hook_prompt) = parse_hook_prompt_message(id.as_ref(), content) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.ensure_turn().items.push(ThreadItem::HookPrompt {
|
||||
id: hook_prompt.id,
|
||||
fragments: hook_prompt
|
||||
.fragments
|
||||
.into_iter()
|
||||
.map(crate::protocol::v2::HookPromptFragment::from)
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_user_message(&mut self, payload: &UserMessageEvent) {
|
||||
// User messages should stay in explicitly opened turns. For backward
|
||||
// compatibility with older streams that did not open turns explicitly,
|
||||
@@ -281,6 +307,7 @@ impl ThreadHistoryBuilder {
|
||||
);
|
||||
}
|
||||
codex_protocol::items::TurnItem::UserMessage(_)
|
||||
| codex_protocol::items::TurnItem::HookPrompt(_)
|
||||
| codex_protocol::items::TurnItem::AgentMessage(_)
|
||||
| codex_protocol::items::TurnItem::Reasoning(_)
|
||||
| codex_protocol::items::TurnItem::WebSearch(_)
|
||||
@@ -301,6 +328,7 @@ impl ThreadHistoryBuilder {
|
||||
);
|
||||
}
|
||||
codex_protocol::items::TurnItem::UserMessage(_)
|
||||
| codex_protocol::items::TurnItem::HookPrompt(_)
|
||||
| codex_protocol::items::TurnItem::AgentMessage(_)
|
||||
| codex_protocol::items::TurnItem::Reasoning(_)
|
||||
| codex_protocol::items::TurnItem::WebSearch(_)
|
||||
@@ -341,6 +369,7 @@ impl ThreadHistoryBuilder {
|
||||
command,
|
||||
cwd: payload.cwd.clone(),
|
||||
process_id: payload.process_id.clone(),
|
||||
source: payload.source.into(),
|
||||
status: CommandExecutionStatus::InProgress,
|
||||
command_actions,
|
||||
aggregated_output: None,
|
||||
@@ -371,6 +400,7 @@ impl ThreadHistoryBuilder {
|
||||
command,
|
||||
cwd: payload.cwd.clone(),
|
||||
process_id: payload.process_id.clone(),
|
||||
source: payload.source.into(),
|
||||
status,
|
||||
command_actions,
|
||||
aggregated_output,
|
||||
@@ -539,6 +569,7 @@ impl ThreadHistoryBuilder {
|
||||
status: String::new(),
|
||||
revised_prompt: None,
|
||||
result: String::new(),
|
||||
saved_path: None,
|
||||
};
|
||||
self.upsert_item_in_current_turn(item);
|
||||
}
|
||||
@@ -549,6 +580,7 @@ impl ThreadHistoryBuilder {
|
||||
status: payload.status.clone(),
|
||||
revised_prompt: payload.revised_prompt.clone(),
|
||||
result: payload.result.clone(),
|
||||
saved_path: payload.saved_path.clone(),
|
||||
};
|
||||
self.upsert_item_in_current_turn(item);
|
||||
}
|
||||
@@ -1144,10 +1176,13 @@ impl From<&PendingTurn> for Turn {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::protocol::v2::CommandExecutionSource;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem;
|
||||
use codex_protocol::items::HookPromptFragment as CoreHookPromptFragment;
|
||||
use codex_protocol::items::TurnItem as CoreTurnItem;
|
||||
use codex_protocol::items::UserMessageItem as CoreUserMessageItem;
|
||||
use codex_protocol::items::build_hook_prompt_message;
|
||||
use codex_protocol::models::MessagePhase as CoreMessagePhase;
|
||||
use codex_protocol::models::WebSearchAction as CoreWebSearchAction;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
@@ -1352,6 +1387,61 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replays_image_generation_end_events_into_turn_history() {
|
||||
let items = vec![
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-image".into(),
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
})),
|
||||
RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
|
||||
message: "generate an image".into(),
|
||||
images: None,
|
||||
text_elements: Vec::new(),
|
||||
local_images: Vec::new(),
|
||||
})),
|
||||
RolloutItem::EventMsg(EventMsg::ImageGenerationEnd(ImageGenerationEndEvent {
|
||||
call_id: "ig_123".into(),
|
||||
status: "completed".into(),
|
||||
revised_prompt: Some("final prompt".into()),
|
||||
result: "Zm9v".into(),
|
||||
saved_path: Some("/tmp/ig_123.png".into()),
|
||||
})),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-image".into(),
|
||||
last_agent_message: None,
|
||||
})),
|
||||
];
|
||||
|
||||
let turns = build_turns_from_rollout_items(&items);
|
||||
assert_eq!(turns.len(), 1);
|
||||
assert_eq!(
|
||||
turns[0],
|
||||
Turn {
|
||||
id: "turn-image".into(),
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
items: vec![
|
||||
ThreadItem::UserMessage {
|
||||
id: "item-1".into(),
|
||||
content: vec![UserInput::Text {
|
||||
text: "generate an image".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
},
|
||||
ThreadItem::ImageGeneration {
|
||||
id: "ig_123".into(),
|
||||
status: "completed".into(),
|
||||
revised_prompt: Some("final prompt".into()),
|
||||
result: "Zm9v".into(),
|
||||
saved_path: Some("/tmp/ig_123.png".into()),
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn splits_reasoning_when_interleaved() {
|
||||
let events = vec![
|
||||
@@ -1745,6 +1835,7 @@ mod tests {
|
||||
command: "echo 'hello world'".into(),
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
process_id: Some("pid-1".into()),
|
||||
source: CommandExecutionSource::Agent,
|
||||
status: CommandExecutionStatus::Completed,
|
||||
command_actions: vec![CommandAction::Unknown {
|
||||
command: "echo hello world".into(),
|
||||
@@ -1893,6 +1984,7 @@ mod tests {
|
||||
command: "ls".into(),
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
process_id: Some("pid-2".into()),
|
||||
source: CommandExecutionSource::Agent,
|
||||
status: CommandExecutionStatus::Declined,
|
||||
command_actions: vec![CommandAction::Unknown {
|
||||
command: "ls".into(),
|
||||
@@ -1987,6 +2079,7 @@ mod tests {
|
||||
command: "echo done".into(),
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
process_id: Some("pid-42".into()),
|
||||
source: CommandExecutionSource::Agent,
|
||||
status: CommandExecutionStatus::Completed,
|
||||
command_actions: vec![CommandAction::Unknown {
|
||||
command: "echo done".into(),
|
||||
@@ -2639,4 +2732,80 @@ mod tests {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuilds_hook_prompt_items_from_rollout_response_items() {
|
||||
let hook_prompt = build_hook_prompt_message(&[
|
||||
CoreHookPromptFragment::from_single_hook("Retry with tests.", "hook-run-1"),
|
||||
CoreHookPromptFragment::from_single_hook("Then summarize cleanly.", "hook-run-2"),
|
||||
])
|
||||
.expect("hook prompt message");
|
||||
let items = vec![
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
})),
|
||||
RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
|
||||
message: "hello".into(),
|
||||
images: None,
|
||||
text_elements: Vec::new(),
|
||||
local_images: Vec::new(),
|
||||
})),
|
||||
RolloutItem::ResponseItem(hook_prompt),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
last_agent_message: None,
|
||||
})),
|
||||
];
|
||||
|
||||
let turns = build_turns_from_rollout_items(&items);
|
||||
|
||||
assert_eq!(turns.len(), 1);
|
||||
assert_eq!(turns[0].items.len(), 2);
|
||||
assert_eq!(
|
||||
turns[0].items[1],
|
||||
ThreadItem::HookPrompt {
|
||||
id: turns[0].items[1].id().to_string(),
|
||||
fragments: vec![
|
||||
crate::protocol::v2::HookPromptFragment {
|
||||
text: "Retry with tests.".into(),
|
||||
hook_run_id: "hook-run-1".into(),
|
||||
},
|
||||
crate::protocol::v2::HookPromptFragment {
|
||||
text: "Then summarize cleanly.".into(),
|
||||
hook_run_id: "hook-run-2".into(),
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_plain_user_response_items_in_rollout_replay() {
|
||||
let items = vec![
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
})),
|
||||
RolloutItem::ResponseItem(codex_protocol::models::ResponseItem::Message {
|
||||
id: Some("msg-1".into()),
|
||||
role: "user".into(),
|
||||
content: vec![codex_protocol::models::ContentItem::InputText {
|
||||
text: "plain text".into(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
last_agent_message: None,
|
||||
})),
|
||||
];
|
||||
|
||||
let turns = build_turns_from_rollout_items(&items);
|
||||
assert_eq!(turns.len(), 1);
|
||||
assert!(turns[0].items.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ use codex_protocol::protocol::AgentStatus as CoreAgentStatus;
|
||||
use codex_protocol::protocol::AskForApproval as CoreAskForApproval;
|
||||
use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo;
|
||||
use codex_protocol::protocol::CreditsSnapshot as CoreCreditsSnapshot;
|
||||
use codex_protocol::protocol::ExecCommandSource as CoreExecCommandSource;
|
||||
use codex_protocol::protocol::ExecCommandStatus as CoreExecCommandStatus;
|
||||
use codex_protocol::protocol::GranularApprovalConfig as CoreGranularApprovalConfig;
|
||||
use codex_protocol::protocol::GuardianRiskLevel as CoreGuardianRiskLevel;
|
||||
@@ -92,6 +93,7 @@ use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value as JsonValue;
|
||||
use serde_with::serde_as;
|
||||
use thiserror::Error;
|
||||
use ts_rs::TS;
|
||||
|
||||
@@ -2033,6 +2035,7 @@ pub struct AppSummary {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub install_url: Option<String>,
|
||||
pub needs_auth: bool,
|
||||
}
|
||||
|
||||
impl From<AppInfo> for AppSummary {
|
||||
@@ -2042,6 +2045,7 @@ impl From<AppInfo> for AppSummary {
|
||||
name: value.name,
|
||||
description: value.description,
|
||||
install_url: value.install_url,
|
||||
needs_auth: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2874,6 +2878,23 @@ pub struct ThreadCompactStartParams {
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadCompactStartResponse {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadShellCommandParams {
|
||||
pub thread_id: String,
|
||||
/// Shell command string evaluated by the thread's configured shell.
|
||||
/// Unlike `command/exec`, this intentionally preserves shell syntax
|
||||
/// such as pipes, redirects, and quoting. This runs unsandboxed with full
|
||||
/// access rather than inheriting the thread sandbox policy.
|
||||
pub command: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadShellCommandResponse {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -2954,7 +2975,6 @@ pub enum ThreadSourceKind {
|
||||
VsCode,
|
||||
Exec,
|
||||
AppServer,
|
||||
Custom,
|
||||
SubAgent,
|
||||
SubAgentReview,
|
||||
SubAgentCompact,
|
||||
@@ -3097,6 +3117,8 @@ pub struct PluginListParams {
|
||||
pub struct PluginListResponse {
|
||||
pub marketplaces: Vec<PluginMarketplaceEntry>,
|
||||
pub remote_sync_error: Option<String>,
|
||||
#[serde(default)]
|
||||
pub featured_plugin_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
@@ -4107,6 +4129,12 @@ pub enum ThreadItem {
|
||||
UserMessage { id: String, content: Vec<UserInput> },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
HookPrompt {
|
||||
id: String,
|
||||
fragments: Vec<HookPromptFragment>,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
AgentMessage {
|
||||
id: String,
|
||||
text: String,
|
||||
@@ -4139,6 +4167,8 @@ pub enum ThreadItem {
|
||||
cwd: PathBuf,
|
||||
/// Identifier for the underlying PTY process (when available).
|
||||
process_id: Option<String>,
|
||||
#[serde(default)]
|
||||
source: CommandExecutionSource,
|
||||
status: CommandExecutionStatus,
|
||||
/// A best-effort parsing of the command to understand the action(s) it will perform.
|
||||
/// This returns a list of CommandAction objects because a single shell command may
|
||||
@@ -4226,6 +4256,9 @@ pub enum ThreadItem {
|
||||
status: String,
|
||||
revised_prompt: Option<String>,
|
||||
result: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
saved_path: Option<String>,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
@@ -4238,10 +4271,19 @@ pub enum ThreadItem {
|
||||
ContextCompaction { id: String },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase", export_to = "v2/")]
|
||||
pub struct HookPromptFragment {
|
||||
pub text: String,
|
||||
pub hook_run_id: String,
|
||||
}
|
||||
|
||||
impl ThreadItem {
|
||||
pub fn id(&self) -> &str {
|
||||
match self {
|
||||
ThreadItem::UserMessage { id, .. }
|
||||
| ThreadItem::HookPrompt { id, .. }
|
||||
| ThreadItem::AgentMessage { id, .. }
|
||||
| ThreadItem::Plan { id, .. }
|
||||
| ThreadItem::Reasoning { id, .. }
|
||||
@@ -4351,6 +4393,14 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
id: user.id,
|
||||
content: user.content.into_iter().map(UserInput::from).collect(),
|
||||
},
|
||||
CoreTurnItem::HookPrompt(hook_prompt) => ThreadItem::HookPrompt {
|
||||
id: hook_prompt.id,
|
||||
fragments: hook_prompt
|
||||
.fragments
|
||||
.into_iter()
|
||||
.map(HookPromptFragment::from)
|
||||
.collect(),
|
||||
},
|
||||
CoreTurnItem::AgentMessage(agent) => {
|
||||
let text = agent
|
||||
.content
|
||||
@@ -4385,6 +4435,7 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
status: image.status,
|
||||
revised_prompt: image.revised_prompt,
|
||||
result: image.result,
|
||||
saved_path: image.saved_path,
|
||||
},
|
||||
CoreTurnItem::ContextCompaction(compaction) => {
|
||||
ThreadItem::ContextCompaction { id: compaction.id }
|
||||
@@ -4393,6 +4444,15 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<codex_protocol::items::HookPromptFragment> for HookPromptFragment {
|
||||
fn from(value: codex_protocol::items::HookPromptFragment) -> Self {
|
||||
Self {
|
||||
text: value.text,
|
||||
hook_run_id: value.hook_run_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -4419,6 +4479,17 @@ impl From<&CoreExecCommandStatus> for CommandExecutionStatus {
|
||||
}
|
||||
}
|
||||
|
||||
v2_enum_from_core! {
|
||||
#[derive(Default)]
|
||||
pub enum CommandExecutionSource from CoreExecCommandSource {
|
||||
#[default]
|
||||
Agent,
|
||||
UserShell,
|
||||
UnifiedExecStartup,
|
||||
UnifiedExecInteraction,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -4865,6 +4936,7 @@ pub struct TerminalInteractionNotification {
|
||||
pub stdin: String,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -4934,6 +5006,25 @@ pub struct McpServerOauthLoginCompletedNotification {
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum McpServerStartupState {
|
||||
Starting,
|
||||
Ready,
|
||||
Failed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct McpServerStatusUpdatedNotification {
|
||||
pub name: String,
|
||||
pub status: McpServerStartupState,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -6315,6 +6406,40 @@ mod tests {
|
||||
assert_eq!(decoded, params);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_shell_command_params_round_trip() {
|
||||
let params = ThreadShellCommandParams {
|
||||
thread_id: "thr_123".to_string(),
|
||||
command: "printf 'hello world\\n'".to_string(),
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(¶ms).expect("serialize thread/shellCommand params");
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({
|
||||
"threadId": "thr_123",
|
||||
"command": "printf 'hello world\\n'",
|
||||
})
|
||||
);
|
||||
|
||||
let decoded = serde_json::from_value::<ThreadShellCommandParams>(value)
|
||||
.expect("deserialize thread/shellCommand params");
|
||||
assert_eq!(decoded, params);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_shell_command_response_round_trip() {
|
||||
let response = ThreadShellCommandResponse {};
|
||||
|
||||
let value =
|
||||
serde_json::to_value(&response).expect("serialize thread/shellCommand response");
|
||||
assert_eq!(value, json!({}));
|
||||
|
||||
let decoded = serde_json::from_value::<ThreadShellCommandResponse>(value)
|
||||
.expect("deserialize thread/shellCommand response");
|
||||
assert_eq!(decoded, response);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_exec_params_default_optional_streaming_flags() {
|
||||
let params = serde_json::from_value::<CommandExecParams>(json!({
|
||||
@@ -6609,6 +6734,32 @@ mod tests {
|
||||
assert_eq!(decoded, notification);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_execution_output_delta_round_trips() {
|
||||
let notification = CommandExecutionOutputDeltaNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
item_id: "item-1".to_string(),
|
||||
delta: "\u{fffd}a\n".to_string(),
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(¬ification)
|
||||
.expect("serialize item/commandExecution/outputDelta notification");
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({
|
||||
"threadId": "thread-1",
|
||||
"turnId": "turn-1",
|
||||
"itemId": "item-1",
|
||||
"delta": "\u{fffd}a\n",
|
||||
})
|
||||
);
|
||||
|
||||
let decoded = serde_json::from_value::<CommandExecutionOutputDeltaNotification>(value)
|
||||
.expect("deserialize round-trip");
|
||||
assert_eq!(decoded, notification);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_policy_round_trips_external_sandbox_network_access() {
|
||||
let v2_policy = SandboxPolicy::ExternalSandbox {
|
||||
|
||||
@@ -32,7 +32,8 @@ axum = { workspace = true, default-features = false, features = [
|
||||
codex-arg0 = { workspace = true }
|
||||
codex-cloud-requirements = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-environment = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-features = { workspace = true }
|
||||
codex-otel = { workspace = true }
|
||||
codex-shell-command = { workspace = true }
|
||||
codex-utils-cli = { workspace = true }
|
||||
@@ -41,6 +42,7 @@ codex-backend-client = { workspace = true }
|
||||
codex-file-search = { workspace = true }
|
||||
codex-chatgpt = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-models = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-feedback = { workspace = true }
|
||||
|
||||
@@ -115,10 +115,7 @@ Example with notification opt-out:
|
||||
},
|
||||
"capabilities": {
|
||||
"experimentalApi": true,
|
||||
"optOutNotificationMethods": [
|
||||
"thread/started",
|
||||
"item/agentMessage/delta"
|
||||
]
|
||||
"optOutNotificationMethods": ["thread/started", "item/agentMessage/delta"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,6 +136,7 @@ Example with notification opt-out:
|
||||
- `thread/name/set` — set or update a thread’s user-facing name for either a loaded thread or a persisted rollout; returns `{}` on success and emits `thread/name/updated` to initialized, opted-in clients. Thread names are not required to be unique; name lookups resolve to the most recently updated thread.
|
||||
- `thread/unarchive` — move an archived rollout file back into the sessions directory; returns the restored `thread` on success and emits `thread/unarchived`.
|
||||
- `thread/compact/start` — trigger conversation history compaction for a thread; returns `{}` immediately while progress streams through standard turn/item notifications.
|
||||
- `thread/shellCommand` — run a user-initiated `!` shell command against a thread; this runs unsandboxed with full access rather than inheriting the thread sandbox policy. Returns `{}` immediately while progress streams through standard turn/item notifications and any active turn receives the formatted output in its message stream.
|
||||
- `thread/backgroundTerminals/clean` — terminate all running background terminals for a thread (experimental; requires `capabilities.experimentalApi`); returns `{}` when the cleanup request is accepted.
|
||||
- `thread/rollback` — drop the last N turns from the agent’s in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success.
|
||||
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode".
|
||||
@@ -165,12 +163,12 @@ Example with notification opt-out:
|
||||
- `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`.
|
||||
- `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). 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`).
|
||||
- `plugin/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata. `interface.category` uses the marketplace category when present; otherwise it falls back to the plugin manifest category. Pass `forceRemoteSync: true` to refresh curated plugin state before listing (**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/apps/MCP server names (**under development; do not call from production clients yet**).
|
||||
- `plugin/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata 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. Pass `forceRemoteSync: true` to refresh curated plugin state before listing (**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/apps/MCP server names. Plugin app summaries also include `needsAuth` when the server can determine connector accessibility (**under development; do not call from production clients yet**).
|
||||
- `skills/changed` — notification emitted when watched local skill files change.
|
||||
- `app/list` — list available apps.
|
||||
- `skills/config/write` — write user-level skill config by path.
|
||||
- `plugin/install` — install a plugin from a discovered marketplace entry, rejecting marketplace entries marked unavailable for install, and return the effective plugin auth policy plus any apps that still need auth (**under development; do not call from production clients yet**).
|
||||
- `plugin/install` — install a plugin from a discovered marketplace entry, rejecting marketplace entries marked unavailable for install, install MCPs if any, and return the effective plugin auth policy plus any apps that still need auth (**under development; do not call from production clients yet**).
|
||||
- `plugin/uninstall` — uninstall a plugin by id by removing its cached files and clearing its user-level config entry (**under development; do not call from production clients yet**).
|
||||
- `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; returns an `authorization_url` and later emits `mcpServer/oauthLogin/completed` once the browser flow finishes.
|
||||
- `tool/requestUserInput` — prompt the user with 1–3 short questions for a tool call and return their answers (experimental).
|
||||
@@ -228,7 +226,11 @@ Start a fresh thread when you need a new Codex conversation.
|
||||
|
||||
Valid `personality` values are `"friendly"`, `"pragmatic"`, and `"none"`. When `"none"` is selected, the personality placeholder is replaced with an empty string.
|
||||
|
||||
To continue a stored session, call `thread/resume` with the `thread.id` you previously recorded. The response shape matches `thread/start`, and no additional notifications are emitted. You can also pass the same configuration overrides supported by `thread/start`, including `approvalsReviewer`:
|
||||
To continue a stored session, call `thread/resume` with the `thread.id` you previously recorded. The response shape matches `thread/start`, and no additional notifications are emitted. You can also pass the same configuration overrides supported by `thread/start`, including `approvalsReviewer`.
|
||||
|
||||
By default, resume uses the latest persisted `model` and `reasoningEffort` values associated with the thread. Supplying any of `model`, `modelProvider`, `config.model`, or `config.model_reasoning_effort` disables that persisted fallback and uses the explicit overrides plus normal config resolution instead.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{ "method": "thread/resume", "id": 11, "params": {
|
||||
@@ -256,7 +258,7 @@ Experimental API: `thread/start`, `thread/resume`, and `thread/fork` accept `per
|
||||
- `limit` — server defaults to a reasonable page size if unset.
|
||||
- `sortKey` — `created_at` (default) or `updated_at`.
|
||||
- `modelProviders` — restrict results to specific providers; unset, null, or an empty array will include all providers.
|
||||
- `sourceKinds` — restrict results to specific sources; omit or pass `[]` for interactive sessions only (`cli`, `vscode`, and custom product sources).
|
||||
- `sourceKinds` — restrict results to specific sources; omit or pass `[]` for interactive sessions only (`cli`, `vscode`).
|
||||
- `archived` — when `true`, list archived threads only. When `false` or `null`, list non-archived threads (default).
|
||||
- `cwd` — restrict results to threads whose session cwd exactly matches this path.
|
||||
- `searchTerm` — restrict results to threads whose extracted title contains this substring (case-sensitive).
|
||||
@@ -301,10 +303,13 @@ When `nextCursor` is `null`, you’ve reached the final page.
|
||||
- `thread/start`, `thread/fork`, and detached review threads do not emit a separate initial `thread/status/changed`; their `thread/started` notification already carries the current `thread.status`.
|
||||
|
||||
```json
|
||||
{ "method": "thread/status/changed", "params": {
|
||||
{
|
||||
"method": "thread/status/changed",
|
||||
"params": {
|
||||
"threadId": "thr_123",
|
||||
"status": { "type": "active", "activeFlags": [] }
|
||||
} }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example: Unsubscribe from a loaded thread
|
||||
@@ -411,6 +416,31 @@ While compaction is running, the thread is effectively in a turn so clients shou
|
||||
{ "id": 25, "result": {} }
|
||||
```
|
||||
|
||||
### Example: Run a thread shell command
|
||||
|
||||
Use `thread/shellCommand` for the TUI `!` workflow. The request returns immediately with `{}`.
|
||||
This API runs unsandboxed with full access; it does not inherit the thread
|
||||
sandbox policy.
|
||||
|
||||
If the thread already has an active turn, the command runs as an auxiliary action on that turn. In that case, progress is emitted as standard `item/*` notifications on the existing turn and the formatted output is injected into the turn’s message stream:
|
||||
|
||||
- `item/started` with `item: { "type": "commandExecution", "source": "userShell", ... }`
|
||||
- zero or more `item/commandExecution/outputDelta`
|
||||
- `item/completed` with the same `commandExecution` item id
|
||||
|
||||
If the thread does not already have an active turn, the server starts a standalone turn for the shell command. In that case clients should expect:
|
||||
|
||||
- `turn/started`
|
||||
- `item/started` with `item: { "type": "commandExecution", "source": "userShell", ... }`
|
||||
- zero or more `item/commandExecution/outputDelta`
|
||||
- `item/completed` with the same `commandExecution` item id
|
||||
- `turn/completed`
|
||||
|
||||
```json
|
||||
{ "method": "thread/shellCommand", "id": 26, "params": { "threadId": "thr_b", "command": "git status --short" } }
|
||||
{ "id": 26, "result": {} }
|
||||
```
|
||||
|
||||
### Example: Start a turn (send user input)
|
||||
|
||||
Turns attach user input (text or images) to a thread and trigger Codex generation. The `input` field is a list of discriminated unions:
|
||||
@@ -806,6 +836,10 @@ Because audio is intentionally separate from `ThreadItem`, clients can opt out o
|
||||
|
||||
- `windowsSandbox/setupCompleted` — `{ mode, success, error }` after a `windowsSandbox/setupStart` request finishes.
|
||||
|
||||
### MCP server startup events
|
||||
|
||||
- `mcpServer/startupStatus/updated` — `{ name, status, error }` when app-server observes an MCP server startup transition. `status` is one of `starting`, `ready`, `failed`, or `cancelled`. `error` is `null` except for `failed`.
|
||||
|
||||
### Turn events
|
||||
|
||||
The app-server streams JSON-RPC notifications while a turn is running. Each turn emits `turn/started` when it begins running and ends with `turn/completed` (final `turn` status). Token usage events stream separately via `thread/tokenUsage/updated`. Clients subscribe to the events they care about, rendering each item incrementally as updates arrive. The per-item lifecycle is always: `item/started` → zero or more item-specific deltas → `item/completed`.
|
||||
@@ -953,10 +987,7 @@ The built-in `request_permissions` tool sends an `item/permissions/requestApprov
|
||||
"reason": "Select a workspace root",
|
||||
"permissions": {
|
||||
"fileSystem": {
|
||||
"write": [
|
||||
"/Users/me/project",
|
||||
"/Users/me/shared"
|
||||
]
|
||||
"write": ["/Users/me/project", "/Users/me/shared"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -972,9 +1003,7 @@ The client responds with `result.permissions`, which should be the granted subse
|
||||
"scope": "session",
|
||||
"permissions": {
|
||||
"fileSystem": {
|
||||
"write": [
|
||||
"/Users/me/project"
|
||||
]
|
||||
"write": ["/Users/me/project"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1233,6 +1262,7 @@ Codex supports these authentication modes. The current mode is surfaced in `acco
|
||||
- `account/rateLimits/read` — fetch ChatGPT rate limits; updates arrive via `account/rateLimits/updated` (notify).
|
||||
- `account/rateLimits/updated` (notify) — emitted whenever a user's ChatGPT rate limits change.
|
||||
- `mcpServer/oauthLogin/completed` (notify) — emitted after a `mcpServer/oauth/login` flow finishes for a server; payload includes `{ name, success, error? }`.
|
||||
- `mcpServer/startupStatus/updated` (notify) — emitted when a configured MCP server's startup status changes for a loaded thread; payload includes `{ name, status, error }` where `status` is `starting`, `ready`, `failed`, or `cancelled`.
|
||||
|
||||
### 1) Check auth state
|
||||
|
||||
|
||||
@@ -107,6 +107,7 @@ fn app_server_request_span_template(
|
||||
app_server.api_version = "v2",
|
||||
app_server.client_name = field::Empty,
|
||||
app_server.client_version = field::Empty,
|
||||
turn.id = field::Empty,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ use codex_app_server_protocol::CommandExecutionOutputDeltaNotification;
|
||||
use codex_app_server_protocol::CommandExecutionRequestApprovalParams;
|
||||
use codex_app_server_protocol::CommandExecutionRequestApprovalResponse;
|
||||
use codex_app_server_protocol::CommandExecutionRequestApprovalSkillMetadata;
|
||||
use codex_app_server_protocol::CommandExecutionSource;
|
||||
use codex_app_server_protocol::CommandExecutionStatus;
|
||||
use codex_app_server_protocol::ContextCompactedNotification;
|
||||
use codex_app_server_protocol::DeprecationNoticeNotification;
|
||||
@@ -56,6 +57,8 @@ use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::McpServerElicitationAction;
|
||||
use codex_app_server_protocol::McpServerElicitationRequestParams;
|
||||
use codex_app_server_protocol::McpServerElicitationRequestResponse;
|
||||
use codex_app_server_protocol::McpServerStartupState;
|
||||
use codex_app_server_protocol::McpServerStatusUpdatedNotification;
|
||||
use codex_app_server_protocol::McpToolCallError;
|
||||
use codex_app_server_protocol::McpToolCallResult;
|
||||
use codex_app_server_protocol::McpToolCallStatus;
|
||||
@@ -110,6 +113,7 @@ use codex_core::sandboxing::intersect_permission_profiles;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem;
|
||||
use codex_protocol::dynamic_tools::DynamicToolResponse as CoreDynamicToolResponse;
|
||||
use codex_protocol::items::parse_hook_prompt_message;
|
||||
use codex_protocol::plan_tool::UpdatePlanArgs;
|
||||
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
|
||||
use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo;
|
||||
@@ -308,6 +312,34 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
.await;
|
||||
}
|
||||
}
|
||||
EventMsg::McpStartupUpdate(update) => {
|
||||
if let ApiVersion::V2 = api_version {
|
||||
let (status, error) = match update.status {
|
||||
codex_protocol::protocol::McpStartupStatus::Starting => {
|
||||
(McpServerStartupState::Starting, None)
|
||||
}
|
||||
codex_protocol::protocol::McpStartupStatus::Ready => {
|
||||
(McpServerStartupState::Ready, None)
|
||||
}
|
||||
codex_protocol::protocol::McpStartupStatus::Failed { error } => {
|
||||
(McpServerStartupState::Failed, Some(error))
|
||||
}
|
||||
codex_protocol::protocol::McpStartupStatus::Cancelled => {
|
||||
(McpServerStartupState::Cancelled, None)
|
||||
}
|
||||
};
|
||||
let notification = McpServerStatusUpdatedNotification {
|
||||
name: update.server,
|
||||
status,
|
||||
error,
|
||||
};
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::McpServerStatusUpdated(
|
||||
notification,
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
EventMsg::Warning(_warning_event) => {}
|
||||
EventMsg::GuardianAssessment(assessment) => {
|
||||
if let ApiVersion::V2 = api_version {
|
||||
@@ -1483,6 +1515,14 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
.await;
|
||||
}
|
||||
EventMsg::RawResponseItem(raw_response_item_event) => {
|
||||
maybe_emit_hook_prompt_item_completed(
|
||||
api_version,
|
||||
conversation_id,
|
||||
&event_turn_id,
|
||||
&raw_response_item_event.item,
|
||||
&outgoing,
|
||||
)
|
||||
.await;
|
||||
maybe_emit_raw_response_item_completed(
|
||||
api_version,
|
||||
conversation_id,
|
||||
@@ -1563,6 +1603,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
command,
|
||||
cwd,
|
||||
process_id,
|
||||
source: exec_command_begin_event.source.into(),
|
||||
status: CommandExecutionStatus::InProgress,
|
||||
command_actions,
|
||||
aggregated_output: None,
|
||||
@@ -1580,7 +1621,6 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
}
|
||||
EventMsg::ExecCommandOutputDelta(exec_command_output_delta_event) => {
|
||||
let item_id = exec_command_output_delta_event.call_id.clone();
|
||||
let delta = String::from_utf8_lossy(&exec_command_output_delta_event.chunk).to_string();
|
||||
// The underlying EventMsg::ExecCommandOutputDelta is used for shell, unified_exec,
|
||||
// and apply_patch tool calls. We represent apply_patch with the FileChange item, and
|
||||
// everything else with the CommandExecution item.
|
||||
@@ -1592,6 +1632,8 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
state.turn_summary.file_change_started.contains(&item_id)
|
||||
};
|
||||
if is_file_change {
|
||||
let delta =
|
||||
String::from_utf8_lossy(&exec_command_output_delta_event.chunk).to_string();
|
||||
let notification = FileChangeOutputDeltaNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: event_turn_id.clone(),
|
||||
@@ -1608,7 +1650,8 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: event_turn_id.clone(),
|
||||
item_id,
|
||||
delta,
|
||||
delta: String::from_utf8_lossy(&exec_command_output_delta_event.chunk)
|
||||
.to_string(),
|
||||
};
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::CommandExecutionOutputDelta(
|
||||
@@ -1641,6 +1684,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
aggregated_output,
|
||||
exit_code,
|
||||
duration,
|
||||
source,
|
||||
status,
|
||||
..
|
||||
} = exec_command_end_event;
|
||||
@@ -1672,6 +1716,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
command: shlex_join(&command),
|
||||
cwd,
|
||||
process_id,
|
||||
source: source.into(),
|
||||
status,
|
||||
command_actions,
|
||||
aggregated_output,
|
||||
@@ -1935,6 +1980,7 @@ async fn complete_command_execution_item(
|
||||
command: String,
|
||||
cwd: PathBuf,
|
||||
process_id: Option<String>,
|
||||
source: CommandExecutionSource,
|
||||
command_actions: Vec<V2ParsedCommand>,
|
||||
status: CommandExecutionStatus,
|
||||
outgoing: &ThreadScopedOutgoingMessageSender,
|
||||
@@ -1944,6 +1990,7 @@ async fn complete_command_execution_item(
|
||||
command,
|
||||
cwd,
|
||||
process_id,
|
||||
source,
|
||||
status,
|
||||
command_actions,
|
||||
aggregated_output: None,
|
||||
@@ -1981,6 +2028,49 @@ async fn maybe_emit_raw_response_item_completed(
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn maybe_emit_hook_prompt_item_completed(
|
||||
api_version: ApiVersion,
|
||||
conversation_id: ThreadId,
|
||||
turn_id: &str,
|
||||
item: &codex_protocol::models::ResponseItem,
|
||||
outgoing: &ThreadScopedOutgoingMessageSender,
|
||||
) {
|
||||
let ApiVersion::V2 = api_version else {
|
||||
return;
|
||||
};
|
||||
|
||||
let codex_protocol::models::ResponseItem::Message {
|
||||
role, content, id, ..
|
||||
} = item
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if role != "user" {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(hook_prompt) = parse_hook_prompt_message(id.as_ref(), content) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let notification = ItemCompletedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: turn_id.to_string(),
|
||||
item: ThreadItem::HookPrompt {
|
||||
id: hook_prompt.id,
|
||||
fragments: hook_prompt
|
||||
.fragments
|
||||
.into_iter()
|
||||
.map(codex_app_server_protocol::HookPromptFragment::from)
|
||||
.collect(),
|
||||
},
|
||||
};
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ItemCompleted(notification))
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn find_and_remove_turn_summary(
|
||||
_conversation_id: ThreadId,
|
||||
thread_state: &Arc<Mutex<ThreadState>>,
|
||||
@@ -2607,6 +2697,7 @@ async fn on_command_execution_request_approval_response(
|
||||
completion_item.command,
|
||||
completion_item.cwd,
|
||||
/*process_id*/ None,
|
||||
CommandExecutionSource::Agent,
|
||||
completion_item.command_actions,
|
||||
status,
|
||||
&outgoing,
|
||||
@@ -2751,6 +2842,8 @@ mod tests {
|
||||
use codex_app_server_protocol::GuardianApprovalReviewStatus;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::TurnPlanStepStatus;
|
||||
use codex_protocol::items::HookPromptFragment;
|
||||
use codex_protocol::items::build_hook_prompt_message;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions;
|
||||
use codex_protocol::models::NetworkPermissions as CoreNetworkPermissions;
|
||||
@@ -3785,4 +3878,59 @@ mod tests {
|
||||
assert!(rx.try_recv().is_err(), "no messages expected");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_hook_prompt_raw_response_emits_item_completed() -> Result<()> {
|
||||
let (tx, mut rx) = mpsc::channel(CHANNEL_CAPACITY);
|
||||
let outgoing = Arc::new(OutgoingMessageSender::new(tx));
|
||||
let conversation_id = ThreadId::new();
|
||||
let outgoing = ThreadScopedOutgoingMessageSender::new(
|
||||
outgoing,
|
||||
vec![ConnectionId(1)],
|
||||
conversation_id,
|
||||
);
|
||||
let item = build_hook_prompt_message(&[
|
||||
HookPromptFragment::from_single_hook("Retry with tests.", "hook-run-1"),
|
||||
HookPromptFragment::from_single_hook("Then summarize cleanly.", "hook-run-2"),
|
||||
])
|
||||
.expect("hook prompt message");
|
||||
|
||||
maybe_emit_hook_prompt_item_completed(
|
||||
ApiVersion::V2,
|
||||
conversation_id,
|
||||
"turn-1",
|
||||
&item,
|
||||
&outgoing,
|
||||
)
|
||||
.await;
|
||||
|
||||
let msg = recv_broadcast_message(&mut rx).await?;
|
||||
match msg {
|
||||
OutgoingMessage::AppServerNotification(ServerNotification::ItemCompleted(
|
||||
notification,
|
||||
)) => {
|
||||
assert_eq!(notification.thread_id, conversation_id.to_string());
|
||||
assert_eq!(notification.turn_id, "turn-1");
|
||||
assert_eq!(
|
||||
notification.item,
|
||||
ThreadItem::HookPrompt {
|
||||
id: notification.item.id().to_string(),
|
||||
fragments: vec![
|
||||
codex_app_server_protocol::HookPromptFragment {
|
||||
text: "Retry with tests.".into(),
|
||||
hook_run_id: "hook-run-1".into(),
|
||||
},
|
||||
codex_app_server_protocol::HookPromptFragment {
|
||||
text: "Then summarize cleanly.".into(),
|
||||
hook_run_id: "hook-run-2".into(),
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
other => bail!("unexpected message: {other:?}"),
|
||||
}
|
||||
assert!(rx.try_recv().is_err(), "no extra messages expected");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +146,8 @@ use codex_app_server_protocol::ThreadResumeResponse;
|
||||
use codex_app_server_protocol::ThreadRollbackParams;
|
||||
use codex_app_server_protocol::ThreadSetNameParams;
|
||||
use codex_app_server_protocol::ThreadSetNameResponse;
|
||||
use codex_app_server_protocol::ThreadShellCommandParams;
|
||||
use codex_app_server_protocol::ThreadShellCommandResponse;
|
||||
use codex_app_server_protocol::ThreadSortKey;
|
||||
use codex_app_server_protocol::ThreadSourceKind;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
@@ -189,7 +191,6 @@ use codex_core::ThreadSortKey as CoreThreadSortKey;
|
||||
use codex_core::auth::AuthMode as CoreAuthMode;
|
||||
use codex_core::auth::CLIENT_ID;
|
||||
use codex_core::auth::login_with_api_key;
|
||||
use codex_core::auth::login_with_chatgpt_auth_tokens;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use codex_core::config::NetworkProxyAuditMetadata;
|
||||
@@ -202,12 +203,10 @@ use codex_core::config_loader::CloudRequirementsLoader;
|
||||
use codex_core::default_client::set_default_client_residency_requirement;
|
||||
use codex_core::error::CodexErr;
|
||||
use codex_core::error::Result as CodexResult;
|
||||
use codex_core::exec::ExecCapturePolicy;
|
||||
use codex_core::exec::ExecExpiration;
|
||||
use codex_core::exec::ExecParams;
|
||||
use codex_core::exec_env::create_env;
|
||||
use codex_core::features::FEATURES;
|
||||
use codex_core::features::Feature;
|
||||
use codex_core::features::Stage;
|
||||
use codex_core::find_archived_thread_path_by_id_str;
|
||||
use codex_core::find_thread_name_by_id;
|
||||
use codex_core::find_thread_names_by_ids;
|
||||
@@ -217,15 +216,16 @@ use codex_core::mcp::auth::discover_supported_scopes;
|
||||
use codex_core::mcp::auth::resolve_oauth_scopes;
|
||||
use codex_core::mcp::collect_mcp_snapshot;
|
||||
use codex_core::mcp::group_tools_by_server;
|
||||
use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig;
|
||||
use codex_core::parse_cursor;
|
||||
use codex_core::plugins::MarketplaceError;
|
||||
use codex_core::plugins::MarketplacePluginSource;
|
||||
use codex_core::plugins::OPENAI_CURATED_MARKETPLACE_NAME;
|
||||
use codex_core::plugins::PluginInstallError as CorePluginInstallError;
|
||||
use codex_core::plugins::PluginInstallRequest;
|
||||
use codex_core::plugins::PluginReadRequest;
|
||||
use codex_core::plugins::PluginUninstallError as CorePluginUninstallError;
|
||||
use codex_core::plugins::load_plugin_apps;
|
||||
use codex_core::plugins::load_plugin_mcp_servers;
|
||||
use codex_core::read_head_for_summary;
|
||||
use codex_core::read_session_meta_line;
|
||||
use codex_core::rollout_date_parts;
|
||||
@@ -236,10 +236,15 @@ use codex_core::state_db::reconcile_rollout;
|
||||
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
|
||||
use codex_core::windows_sandbox::WindowsSandboxSetupMode as CoreWindowsSandboxSetupMode;
|
||||
use codex_core::windows_sandbox::WindowsSandboxSetupRequest;
|
||||
use codex_features::FEATURES;
|
||||
use codex_features::Feature;
|
||||
use codex_features::Stage;
|
||||
use codex_feedback::CodexFeedback;
|
||||
use codex_login::ServerOptions as LoginServerOptions;
|
||||
use codex_login::ShutdownHandle;
|
||||
use codex_login::auth::login_with_chatgpt_auth_tokens;
|
||||
use codex_login::run_login_server;
|
||||
use codex_models::CollaborationModesConfig;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::ForcedLoginMethod;
|
||||
@@ -271,6 +276,7 @@ use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS;
|
||||
use codex_protocol::user_input::UserInput as CoreInputItem;
|
||||
use codex_rmcp_client::perform_oauth_login_return_url;
|
||||
use codex_state::StateRuntime;
|
||||
use codex_state::ThreadMetadata;
|
||||
use codex_state::ThreadMetadataBuilder;
|
||||
use codex_state::log_db::LogDbLayer;
|
||||
use codex_utils_json_to_toml::json_to_toml;
|
||||
@@ -307,6 +313,7 @@ use codex_app_server_protocol::ServerRequest;
|
||||
|
||||
mod apps_list_helpers;
|
||||
mod plugin_app_helpers;
|
||||
mod plugin_mcp_oauth;
|
||||
|
||||
use crate::filters::compute_source_filters;
|
||||
use crate::filters::source_kind_matches;
|
||||
@@ -418,16 +425,16 @@ impl CodexMessageProcessor {
|
||||
self.thread_manager.skills_manager().clear_cache();
|
||||
}
|
||||
|
||||
pub(crate) async fn maybe_start_curated_repo_sync_for_latest_config(&self) {
|
||||
pub(crate) async fn maybe_start_plugin_startup_tasks_for_latest_config(&self) {
|
||||
match self.load_latest_config(/*fallback_cwd*/ None).await {
|
||||
Ok(config) => self
|
||||
.thread_manager
|
||||
.plugins_manager()
|
||||
.maybe_start_curated_repo_sync_for_config(
|
||||
.maybe_start_plugin_startup_tasks_for_config(
|
||||
&config,
|
||||
&self.thread_manager.session_source(),
|
||||
self.thread_manager.auth_manager(),
|
||||
),
|
||||
Err(err) => warn!("failed to load latest config for curated plugin sync: {err:?}"),
|
||||
Err(err) => warn!("failed to load latest config for plugin startup tasks: {err:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -693,6 +700,10 @@ impl CodexMessageProcessor {
|
||||
self.thread_read(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::ThreadShellCommand { request_id, params } => {
|
||||
self.thread_shell_command(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::SkillsList { request_id, params } => {
|
||||
self.skills_list(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
@@ -1403,7 +1414,7 @@ impl CodexMessageProcessor {
|
||||
let account = match self.auth_manager.auth_cached() {
|
||||
Some(auth) => match auth.auth_mode() {
|
||||
CoreAuthMode::ApiKey => Some(Account::ApiKey {}),
|
||||
CoreAuthMode::Chatgpt => {
|
||||
CoreAuthMode::Chatgpt | CoreAuthMode::ChatgptAuthTokens => {
|
||||
let email = auth.get_account_email();
|
||||
let plan_type = auth.account_plan_type();
|
||||
|
||||
@@ -1664,11 +1675,17 @@ impl CodexMessageProcessor {
|
||||
None => ExecExpiration::DefaultTimeout,
|
||||
}
|
||||
};
|
||||
let capture_policy = if disable_output_cap {
|
||||
ExecCapturePolicy::FullBuffer
|
||||
} else {
|
||||
ExecCapturePolicy::ShellTool
|
||||
};
|
||||
let sandbox_cwd = self.config.cwd.clone();
|
||||
let exec_params = ExecParams {
|
||||
command,
|
||||
cwd: cwd.clone(),
|
||||
expiration,
|
||||
capture_policy,
|
||||
env,
|
||||
network: started_network_proxy
|
||||
.as_ref()
|
||||
@@ -2972,6 +2989,58 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
async fn thread_shell_command(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadShellCommandParams,
|
||||
) {
|
||||
let ThreadShellCommandParams { thread_id, command } = params;
|
||||
let command = command.trim().to_string();
|
||||
if command.is_empty() {
|
||||
self.outgoing
|
||||
.send_error(
|
||||
request_id,
|
||||
JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: "command must not be empty".to_string(),
|
||||
data: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
let (_, thread) = match self.load_thread(&thread_id).await {
|
||||
Ok(v) => v,
|
||||
Err(error) => {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match self
|
||||
.submit_core_op(
|
||||
&request_id,
|
||||
thread.as_ref(),
|
||||
Op::RunUserShellCommand { command },
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
self.outgoing
|
||||
.send_response(request_id, ThreadShellCommandResponse {})
|
||||
.await;
|
||||
}
|
||||
Err(err) => {
|
||||
self.send_internal_error(
|
||||
request_id,
|
||||
format!("failed to start shell command: {err}"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn thread_list(&self, request_id: ConnectionRequestId, params: ThreadListParams) {
|
||||
let ThreadListParams {
|
||||
cursor,
|
||||
@@ -3349,7 +3418,7 @@ impl CodexMessageProcessor {
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
sandbox,
|
||||
config: request_overrides,
|
||||
config: mut request_overrides,
|
||||
base_instructions,
|
||||
developer_instructions,
|
||||
personality,
|
||||
@@ -3375,7 +3444,7 @@ impl CodexMessageProcessor {
|
||||
};
|
||||
|
||||
let history_cwd = thread_history.session_cwd();
|
||||
let typesafe_overrides = self.build_thread_config_overrides(
|
||||
let mut typesafe_overrides = self.build_thread_config_overrides(
|
||||
model,
|
||||
model_provider,
|
||||
service_tier,
|
||||
@@ -3387,6 +3456,13 @@ impl CodexMessageProcessor {
|
||||
developer_instructions,
|
||||
personality,
|
||||
);
|
||||
let persisted_resume_metadata = self
|
||||
.load_and_apply_persisted_resume_metadata(
|
||||
&thread_history,
|
||||
&mut request_overrides,
|
||||
&mut typesafe_overrides,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Derive a Config using the same logic as new conversation, honoring overrides if provided.
|
||||
let cloud_requirements = self.current_cloud_requirements();
|
||||
@@ -3450,18 +3526,22 @@ impl CodexMessageProcessor {
|
||||
"thread",
|
||||
);
|
||||
|
||||
let Some(mut thread) = self
|
||||
let mut thread = match self
|
||||
.load_thread_from_resume_source_or_send_internal(
|
||||
request_id.clone(),
|
||||
thread_id,
|
||||
thread.as_ref(),
|
||||
&response_history,
|
||||
rollout_path.as_path(),
|
||||
fallback_model_provider.as_str(),
|
||||
persisted_resume_metadata.as_ref(),
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
{
|
||||
Ok(thread) => thread,
|
||||
Err(message) => {
|
||||
self.send_internal_error(request_id, message).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
self.thread_watch_manager
|
||||
@@ -3504,6 +3584,25 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_and_apply_persisted_resume_metadata(
|
||||
&self,
|
||||
thread_history: &InitialHistory,
|
||||
request_overrides: &mut Option<HashMap<String, serde_json::Value>>,
|
||||
typesafe_overrides: &mut ConfigOverrides,
|
||||
) -> Option<ThreadMetadata> {
|
||||
let InitialHistory::Resumed(resumed_history) = thread_history else {
|
||||
return None;
|
||||
};
|
||||
let state_db_ctx = get_state_db(&self.config).await?;
|
||||
let persisted_metadata = state_db_ctx
|
||||
.get_thread(resumed_history.conversation_id)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()?;
|
||||
merge_persisted_resume_metadata(request_overrides, typesafe_overrides, &persisted_metadata);
|
||||
Some(persisted_metadata)
|
||||
}
|
||||
|
||||
async fn resume_running_thread(
|
||||
&mut self,
|
||||
request_id: ConnectionRequestId,
|
||||
@@ -3620,6 +3719,7 @@ impl CodexMessageProcessor {
|
||||
existing_thread_id,
|
||||
rollout_path.as_path(),
|
||||
config_snapshot.model_provider_id.as_str(),
|
||||
/*persisted_metadata*/ None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -3751,13 +3851,13 @@ impl CodexMessageProcessor {
|
||||
|
||||
async fn load_thread_from_resume_source_or_send_internal(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
thread_id: ThreadId,
|
||||
thread: &CodexThread,
|
||||
thread_history: &InitialHistory,
|
||||
rollout_path: &Path,
|
||||
fallback_provider: &str,
|
||||
) -> Option<Thread> {
|
||||
persisted_resume_metadata: Option<&ThreadMetadata>,
|
||||
) -> std::result::Result<Thread, String> {
|
||||
let thread = match thread_history {
|
||||
InitialHistory::Resumed(resumed) => {
|
||||
load_thread_summary_for_rollout(
|
||||
@@ -3765,6 +3865,7 @@ impl CodexMessageProcessor {
|
||||
resumed.conversation_id,
|
||||
resumed.rollout_path.as_path(),
|
||||
fallback_provider,
|
||||
persisted_resume_metadata,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -3782,28 +3883,18 @@ impl CodexMessageProcessor {
|
||||
"failed to build resume response for thread {thread_id}: initial history missing"
|
||||
)),
|
||||
};
|
||||
let mut thread = match thread {
|
||||
Ok(thread) => thread,
|
||||
Err(message) => {
|
||||
self.send_internal_error(request_id, message).await;
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let mut thread = thread?;
|
||||
thread.id = thread_id.to_string();
|
||||
thread.path = Some(rollout_path.to_path_buf());
|
||||
let history_items = thread_history.get_rollout_items();
|
||||
if let Err(message) = populate_thread_turns(
|
||||
populate_thread_turns(
|
||||
&mut thread,
|
||||
ThreadTurnSource::HistoryItems(&history_items),
|
||||
/*active_turn*/ None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
self.send_internal_error(request_id, message).await;
|
||||
return None;
|
||||
}
|
||||
.await?;
|
||||
self.attach_thread_name(thread_id, &mut thread).await;
|
||||
Some(thread)
|
||||
Ok(thread)
|
||||
}
|
||||
|
||||
async fn attach_thread_name(&self, thread_id: ThreadId, thread: &mut Thread) {
|
||||
@@ -4505,20 +4596,28 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
};
|
||||
|
||||
let configured_servers = self
|
||||
.thread_manager
|
||||
.mcp_manager()
|
||||
.configured_servers(&config);
|
||||
if let Err(error) = self.queue_mcp_server_refresh_for_config(&config).await {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
|
||||
let response = McpServerRefreshResponse {};
|
||||
self.outgoing.send_response(request_id, response).await;
|
||||
}
|
||||
|
||||
async fn queue_mcp_server_refresh_for_config(
|
||||
&self,
|
||||
config: &Config,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let configured_servers = self.thread_manager.mcp_manager().configured_servers(config);
|
||||
let mcp_servers = match serde_json::to_value(configured_servers) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
let error = JSONRPCErrorError {
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to serialize MCP servers: {err}"),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4526,15 +4625,13 @@ impl CodexMessageProcessor {
|
||||
match serde_json::to_value(config.mcp_oauth_credentials_store_mode) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
let error = JSONRPCErrorError {
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!(
|
||||
"failed to serialize MCP OAuth credentials store mode: {err}"
|
||||
),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4547,8 +4644,7 @@ impl CodexMessageProcessor {
|
||||
// active turn to avoid work for threads that never resume.
|
||||
let thread_manager = Arc::clone(&self.thread_manager);
|
||||
thread_manager.refresh_mcp_servers(refresh_config).await;
|
||||
let response = McpServerRefreshResponse {};
|
||||
self.outgoing.send_response(request_id, response).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mcp_server_oauth_login(
|
||||
@@ -4822,6 +4918,7 @@ impl CodexMessageProcessor {
|
||||
MarketplaceError::InvalidMarketplaceFile { .. }
|
||||
| MarketplaceError::PluginNotFound { .. }
|
||||
| MarketplaceError::PluginNotAvailable { .. }
|
||||
| MarketplaceError::PluginsDisabled
|
||||
| MarketplaceError::InvalidPlugin(_) => {
|
||||
self.send_invalid_request_error(request_id, err.to_string())
|
||||
.await;
|
||||
@@ -5305,7 +5402,6 @@ impl CodexMessageProcessor {
|
||||
force_reload,
|
||||
per_cwd_extra_user_roots,
|
||||
} = params;
|
||||
let session_source = self.thread_manager.session_source();
|
||||
let cwds = if cwds.is_empty() {
|
||||
vec![self.config.cwd.clone()]
|
||||
} else {
|
||||
@@ -5344,18 +5440,22 @@ impl CodexMessageProcessor {
|
||||
.extend(valid_extra_roots);
|
||||
}
|
||||
|
||||
let config = match self.load_latest_config(/*fallback_cwd*/ None).await {
|
||||
Ok(config) => config,
|
||||
Err(error) => {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let skills_manager = self.thread_manager.skills_manager();
|
||||
let mut data = Vec::new();
|
||||
for cwd in cwds {
|
||||
let extra_roots = extra_roots_by_cwd
|
||||
.get(&cwd)
|
||||
.map_or(&[][..], std::vec::Vec::as_slice);
|
||||
let outcome = codex_core::skills::filter_skill_load_outcome_for_session_source(
|
||||
skills_manager
|
||||
.skills_for_cwd_with_extra_user_roots(&cwd, force_reload, extra_roots)
|
||||
.await,
|
||||
&session_source,
|
||||
);
|
||||
let outcome = skills_manager
|
||||
.skills_for_cwd_with_extra_user_roots(&cwd, &config, force_reload, extra_roots)
|
||||
.await;
|
||||
let errors = errors_to_info(&outcome.errors);
|
||||
let skills = skills_to_info(&outcome.skills, &outcome.disabled_paths);
|
||||
data.push(codex_app_server_protocol::SkillsListEntry {
|
||||
@@ -5371,7 +5471,6 @@ impl CodexMessageProcessor {
|
||||
|
||||
async fn plugin_list(&self, request_id: ConnectionRequestId, params: PluginListParams) {
|
||||
let plugins_manager = self.thread_manager.plugins_manager();
|
||||
let session_source = self.thread_manager.session_source();
|
||||
let PluginListParams {
|
||||
cwds,
|
||||
force_remote_sync,
|
||||
@@ -5386,11 +5485,11 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
};
|
||||
let mut remote_sync_error = None;
|
||||
let auth = self.auth_manager.auth().await;
|
||||
|
||||
if force_remote_sync {
|
||||
let auth = self.auth_manager.auth().await;
|
||||
match plugins_manager
|
||||
.sync_plugins_from_remote(&config, auth.as_ref())
|
||||
.sync_plugins_from_remote(&config, auth.as_ref(), /*additive_only*/ false)
|
||||
.await
|
||||
{
|
||||
Ok(sync_result) => {
|
||||
@@ -5420,18 +5519,23 @@ impl CodexMessageProcessor {
|
||||
};
|
||||
}
|
||||
|
||||
let config_for_marketplace_listing = config.clone();
|
||||
let plugins_manager_for_marketplace_listing = plugins_manager.clone();
|
||||
let data = match tokio::task::spawn_blocking(move || {
|
||||
let marketplaces = plugins_manager.list_marketplaces_for_config(&config, &roots)?;
|
||||
let marketplaces = plugins_manager_for_marketplace_listing
|
||||
.list_marketplaces_for_config(&config_for_marketplace_listing, &roots)?;
|
||||
Ok::<Vec<PluginMarketplaceEntry>, MarketplaceError>(
|
||||
marketplaces
|
||||
.into_iter()
|
||||
.filter_map(|marketplace| {
|
||||
let plugins = marketplace
|
||||
.map(|marketplace| PluginMarketplaceEntry {
|
||||
name: marketplace.name,
|
||||
path: marketplace.path,
|
||||
interface: marketplace.interface.map(|interface| MarketplaceInterface {
|
||||
display_name: interface.display_name,
|
||||
}),
|
||||
plugins: marketplace
|
||||
.plugins
|
||||
.into_iter()
|
||||
.filter(|plugin| {
|
||||
session_source.matches_product_restriction(&plugin.policy.products)
|
||||
})
|
||||
.map(|plugin| PluginSummary {
|
||||
id: plugin.id,
|
||||
installed: plugin.installed,
|
||||
@@ -5442,18 +5546,7 @@ impl CodexMessageProcessor {
|
||||
auth_policy: plugin.policy.authentication.into(),
|
||||
interface: plugin.interface.map(plugin_interface_to_info),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(!plugins.is_empty()).then_some(PluginMarketplaceEntry {
|
||||
name: marketplace.name,
|
||||
path: marketplace.path,
|
||||
interface: marketplace.interface.map(|interface| {
|
||||
MarketplaceInterface {
|
||||
display_name: interface.display_name,
|
||||
}
|
||||
}),
|
||||
plugins,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
@@ -5476,12 +5569,34 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
};
|
||||
|
||||
let featured_plugin_ids = if data
|
||||
.iter()
|
||||
.any(|marketplace| marketplace.name == OPENAI_CURATED_MARKETPLACE_NAME)
|
||||
{
|
||||
match plugins_manager
|
||||
.featured_plugin_ids_for_config(&config, auth.as_ref())
|
||||
.await
|
||||
{
|
||||
Ok(featured_plugin_ids) => featured_plugin_ids,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
error = %err,
|
||||
"plugin/list featured plugin fetch failed; returning empty featured ids"
|
||||
);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
self.outgoing
|
||||
.send_response(
|
||||
request_id,
|
||||
PluginListResponse {
|
||||
marketplaces: data,
|
||||
remote_sync_error,
|
||||
featured_plugin_ids,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
@@ -5528,13 +5643,19 @@ impl CodexMessageProcessor {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let session_source = self.thread_manager.session_source();
|
||||
let plugin_skills = codex_core::skills::filter_skills_for_session_source(
|
||||
outcome.plugin.skills,
|
||||
&session_source,
|
||||
);
|
||||
let app_summaries =
|
||||
plugin_app_helpers::load_plugin_app_summaries(&config, &outcome.plugin.apps).await;
|
||||
let visible_skills = outcome
|
||||
.plugin
|
||||
.skills
|
||||
.iter()
|
||||
.filter(|skill| {
|
||||
skill.matches_product_restriction_for_product(
|
||||
self.thread_manager.session_source().restriction_product(),
|
||||
)
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let plugin = PluginDetail {
|
||||
marketplace_name: outcome.marketplace_name,
|
||||
marketplace_path: outcome.marketplace_path,
|
||||
@@ -5549,7 +5670,7 @@ impl CodexMessageProcessor {
|
||||
interface: outcome.plugin.interface.map(plugin_interface_to_info),
|
||||
},
|
||||
description: outcome.plugin.description,
|
||||
skills: plugin_skills_to_info(&plugin_skills),
|
||||
skills: plugin_skills_to_info(&visible_skills),
|
||||
apps: app_summaries,
|
||||
mcp_servers: outcome.plugin.mcp_server_names,
|
||||
};
|
||||
@@ -5635,6 +5756,22 @@ impl CodexMessageProcessor {
|
||||
self.config.as_ref().clone()
|
||||
}
|
||||
};
|
||||
|
||||
self.clear_plugin_related_caches();
|
||||
|
||||
let plugin_mcp_servers = load_plugin_mcp_servers(result.installed_path.as_path());
|
||||
|
||||
if !plugin_mcp_servers.is_empty() {
|
||||
if let Err(err) = self.queue_mcp_server_refresh_for_config(&config).await {
|
||||
warn!(
|
||||
plugin = result.plugin_id.as_key(),
|
||||
"failed to queue MCP refresh after plugin install: {err:?}"
|
||||
);
|
||||
}
|
||||
self.start_plugin_mcp_oauth_logins(&config, plugin_mcp_servers)
|
||||
.await;
|
||||
}
|
||||
|
||||
let plugin_apps = load_plugin_apps(result.installed_path.as_path());
|
||||
let apps_needing_auth = if plugin_apps.is_empty()
|
||||
|| !config.features.apps_enabled(Some(&self.auth_manager)).await
|
||||
@@ -5695,7 +5832,6 @@ impl CodexMessageProcessor {
|
||||
)
|
||||
};
|
||||
|
||||
self.clear_plugin_related_caches();
|
||||
self.outgoing
|
||||
.send_response(
|
||||
request_id,
|
||||
@@ -5917,6 +6053,9 @@ impl CodexMessageProcessor {
|
||||
|
||||
match turn_id {
|
||||
Ok(turn_id) => {
|
||||
self.outgoing
|
||||
.record_request_turn_id(&request_id, &turn_id)
|
||||
.await;
|
||||
let turn = Turn {
|
||||
id: turn_id.clone(),
|
||||
items: vec![],
|
||||
@@ -5969,6 +6108,9 @@ impl CodexMessageProcessor {
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
self.outgoing
|
||||
.record_request_turn_id(&request_id, ¶ms.expected_turn_id)
|
||||
.await;
|
||||
if let Err(error) = Self::validate_v2_input_limit(¶ms.input) {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
@@ -6449,7 +6591,10 @@ impl CodexMessageProcessor {
|
||||
request_id: ConnectionRequestId,
|
||||
params: TurnInterruptParams,
|
||||
) {
|
||||
let TurnInterruptParams { thread_id, .. } = params;
|
||||
let TurnInterruptParams { thread_id, turn_id } = params;
|
||||
self.outgoing
|
||||
.record_request_turn_id(&request_id, &turn_id)
|
||||
.await;
|
||||
|
||||
let (thread_uuid, thread) = match self.load_thread(&thread_id).await {
|
||||
Ok(v) => v,
|
||||
@@ -7413,6 +7558,36 @@ fn collect_resume_override_mismatches(
|
||||
mismatch_details
|
||||
}
|
||||
|
||||
fn merge_persisted_resume_metadata(
|
||||
request_overrides: &mut Option<HashMap<String, serde_json::Value>>,
|
||||
typesafe_overrides: &mut ConfigOverrides,
|
||||
persisted_metadata: &ThreadMetadata,
|
||||
) {
|
||||
if has_model_resume_override(request_overrides.as_ref(), typesafe_overrides) {
|
||||
return;
|
||||
}
|
||||
|
||||
typesafe_overrides.model = persisted_metadata.model.clone();
|
||||
|
||||
if let Some(reasoning_effort) = persisted_metadata.reasoning_effort {
|
||||
request_overrides.get_or_insert_with(HashMap::new).insert(
|
||||
"model_reasoning_effort".to_string(),
|
||||
serde_json::Value::String(reasoning_effort.to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn has_model_resume_override(
|
||||
request_overrides: Option<&HashMap<String, serde_json::Value>>,
|
||||
typesafe_overrides: &ConfigOverrides,
|
||||
) -> bool {
|
||||
typesafe_overrides.model.is_some()
|
||||
|| typesafe_overrides.model_provider.is_some()
|
||||
|| request_overrides.is_some_and(|overrides| overrides.contains_key("model"))
|
||||
|| request_overrides
|
||||
.is_some_and(|overrides| overrides.contains_key("model_reasoning_effort"))
|
||||
}
|
||||
|
||||
fn skills_to_info(
|
||||
skills: &[codex_core::skills::SkillMetadata],
|
||||
disabled_paths: &std::collections::HashSet<PathBuf>,
|
||||
@@ -7727,26 +7902,7 @@ async fn read_summary_from_state_db_context_by_thread_id(
|
||||
Ok(Some(metadata)) => metadata,
|
||||
Ok(None) | Err(_) => return None,
|
||||
};
|
||||
Some(summary_from_state_db_metadata(
|
||||
metadata.id,
|
||||
metadata.rollout_path,
|
||||
metadata.first_user_message,
|
||||
metadata
|
||||
.created_at
|
||||
.to_rfc3339_opts(SecondsFormat::Secs, true),
|
||||
metadata
|
||||
.updated_at
|
||||
.to_rfc3339_opts(SecondsFormat::Secs, true),
|
||||
metadata.model_provider,
|
||||
metadata.cwd,
|
||||
metadata.cli_version,
|
||||
metadata.source,
|
||||
metadata.agent_nickname,
|
||||
metadata.agent_role,
|
||||
metadata.git_sha,
|
||||
metadata.git_branch,
|
||||
metadata.git_origin_url,
|
||||
))
|
||||
Some(summary_from_thread_metadata(&metadata))
|
||||
}
|
||||
|
||||
async fn summary_from_thread_list_item(
|
||||
@@ -7857,6 +8013,29 @@ fn summary_from_state_db_metadata(
|
||||
}
|
||||
}
|
||||
|
||||
fn summary_from_thread_metadata(metadata: &ThreadMetadata) -> ConversationSummary {
|
||||
summary_from_state_db_metadata(
|
||||
metadata.id,
|
||||
metadata.rollout_path.clone(),
|
||||
metadata.first_user_message.clone(),
|
||||
metadata
|
||||
.created_at
|
||||
.to_rfc3339_opts(SecondsFormat::Secs, true),
|
||||
metadata
|
||||
.updated_at
|
||||
.to_rfc3339_opts(SecondsFormat::Secs, true),
|
||||
metadata.model_provider.clone(),
|
||||
metadata.cwd.clone(),
|
||||
metadata.cli_version.clone(),
|
||||
metadata.source.clone(),
|
||||
metadata.agent_nickname.clone(),
|
||||
metadata.agent_role.clone(),
|
||||
metadata.git_sha.clone(),
|
||||
metadata.git_branch.clone(),
|
||||
metadata.git_origin_url.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) async fn read_summary_from_rollout(
|
||||
path: &Path,
|
||||
fallback_provider: &str,
|
||||
@@ -8004,6 +8183,7 @@ async fn load_thread_summary_for_rollout(
|
||||
thread_id: ThreadId,
|
||||
rollout_path: &Path,
|
||||
fallback_provider: &str,
|
||||
persisted_metadata: Option<&ThreadMetadata>,
|
||||
) -> std::result::Result<Thread, String> {
|
||||
let mut thread = read_summary_from_rollout(rollout_path, fallback_provider)
|
||||
.await
|
||||
@@ -8014,7 +8194,12 @@ async fn load_thread_summary_for_rollout(
|
||||
rollout_path.display()
|
||||
)
|
||||
})?;
|
||||
if let Some(summary) = read_summary_from_state_db_by_thread_id(config, thread_id).await {
|
||||
if let Some(persisted_metadata) = persisted_metadata {
|
||||
merge_mutable_thread_metadata(
|
||||
&mut thread,
|
||||
summary_to_thread(summary_from_thread_metadata(persisted_metadata)),
|
||||
);
|
||||
} else if let Some(summary) = read_summary_from_state_db_by_thread_id(config, thread_id).await {
|
||||
merge_mutable_thread_metadata(&mut thread, summary_to_thread(summary));
|
||||
}
|
||||
Ok(thread)
|
||||
@@ -8166,6 +8351,7 @@ mod tests {
|
||||
use anyhow::Result;
|
||||
use codex_app_server_protocol::ServerRequestPayload;
|
||||
use codex_app_server_protocol::ToolRequestUserInputParams;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -8297,6 +8483,178 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
fn test_thread_metadata(
|
||||
model: Option<&str>,
|
||||
reasoning_effort: Option<ReasoningEffort>,
|
||||
) -> Result<ThreadMetadata> {
|
||||
let thread_id = ThreadId::from_string("3f941c35-29b3-493b-b0a4-e25800d9aeb0")?;
|
||||
let mut builder = ThreadMetadataBuilder::new(
|
||||
thread_id,
|
||||
PathBuf::from("/tmp/rollout.jsonl"),
|
||||
Utc::now(),
|
||||
codex_protocol::protocol::SessionSource::default(),
|
||||
);
|
||||
builder.model_provider = Some("mock_provider".to_string());
|
||||
let mut metadata = builder.build("mock_provider");
|
||||
metadata.model = model.map(ToString::to_string);
|
||||
metadata.reasoning_effort = reasoning_effort;
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_persisted_resume_metadata_prefers_persisted_model_and_reasoning_effort() -> Result<()>
|
||||
{
|
||||
let mut request_overrides = None;
|
||||
let mut typesafe_overrides = ConfigOverrides::default();
|
||||
let persisted_metadata =
|
||||
test_thread_metadata(Some("gpt-5.1-codex-max"), Some(ReasoningEffort::High))?;
|
||||
|
||||
merge_persisted_resume_metadata(
|
||||
&mut request_overrides,
|
||||
&mut typesafe_overrides,
|
||||
&persisted_metadata,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
typesafe_overrides.model,
|
||||
Some("gpt-5.1-codex-max".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
request_overrides,
|
||||
Some(HashMap::from([(
|
||||
"model_reasoning_effort".to_string(),
|
||||
serde_json::Value::String("high".to_string()),
|
||||
)]))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_persisted_resume_metadata_preserves_explicit_overrides() -> Result<()> {
|
||||
let mut request_overrides = Some(HashMap::from([(
|
||||
"model_reasoning_effort".to_string(),
|
||||
serde_json::Value::String("low".to_string()),
|
||||
)]));
|
||||
let mut typesafe_overrides = ConfigOverrides {
|
||||
model: Some("gpt-5.2-codex".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let persisted_metadata =
|
||||
test_thread_metadata(Some("gpt-5.1-codex-max"), Some(ReasoningEffort::High))?;
|
||||
|
||||
merge_persisted_resume_metadata(
|
||||
&mut request_overrides,
|
||||
&mut typesafe_overrides,
|
||||
&persisted_metadata,
|
||||
);
|
||||
|
||||
assert_eq!(typesafe_overrides.model, Some("gpt-5.2-codex".to_string()));
|
||||
assert_eq!(
|
||||
request_overrides,
|
||||
Some(HashMap::from([(
|
||||
"model_reasoning_effort".to_string(),
|
||||
serde_json::Value::String("low".to_string()),
|
||||
)]))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_persisted_resume_metadata_skips_persisted_values_when_model_overridden() -> Result<()>
|
||||
{
|
||||
let mut request_overrides = Some(HashMap::from([(
|
||||
"model".to_string(),
|
||||
serde_json::Value::String("gpt-5.2-codex".to_string()),
|
||||
)]));
|
||||
let mut typesafe_overrides = ConfigOverrides::default();
|
||||
let persisted_metadata =
|
||||
test_thread_metadata(Some("gpt-5.1-codex-max"), Some(ReasoningEffort::High))?;
|
||||
|
||||
merge_persisted_resume_metadata(
|
||||
&mut request_overrides,
|
||||
&mut typesafe_overrides,
|
||||
&persisted_metadata,
|
||||
);
|
||||
|
||||
assert_eq!(typesafe_overrides.model, None);
|
||||
assert_eq!(
|
||||
request_overrides,
|
||||
Some(HashMap::from([(
|
||||
"model".to_string(),
|
||||
serde_json::Value::String("gpt-5.2-codex".to_string()),
|
||||
)]))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_persisted_resume_metadata_skips_persisted_values_when_provider_overridden()
|
||||
-> Result<()> {
|
||||
let mut request_overrides = None;
|
||||
let mut typesafe_overrides = ConfigOverrides {
|
||||
model_provider: Some("oss".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let persisted_metadata =
|
||||
test_thread_metadata(Some("gpt-5.1-codex-max"), Some(ReasoningEffort::High))?;
|
||||
|
||||
merge_persisted_resume_metadata(
|
||||
&mut request_overrides,
|
||||
&mut typesafe_overrides,
|
||||
&persisted_metadata,
|
||||
);
|
||||
|
||||
assert_eq!(typesafe_overrides.model, None);
|
||||
assert_eq!(typesafe_overrides.model_provider, Some("oss".to_string()));
|
||||
assert_eq!(request_overrides, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_persisted_resume_metadata_skips_persisted_values_when_reasoning_effort_overridden()
|
||||
-> Result<()> {
|
||||
let mut request_overrides = Some(HashMap::from([(
|
||||
"model_reasoning_effort".to_string(),
|
||||
serde_json::Value::String("low".to_string()),
|
||||
)]));
|
||||
let mut typesafe_overrides = ConfigOverrides::default();
|
||||
let persisted_metadata =
|
||||
test_thread_metadata(Some("gpt-5.1-codex-max"), Some(ReasoningEffort::High))?;
|
||||
|
||||
merge_persisted_resume_metadata(
|
||||
&mut request_overrides,
|
||||
&mut typesafe_overrides,
|
||||
&persisted_metadata,
|
||||
);
|
||||
|
||||
assert_eq!(typesafe_overrides.model, None);
|
||||
assert_eq!(
|
||||
request_overrides,
|
||||
Some(HashMap::from([(
|
||||
"model_reasoning_effort".to_string(),
|
||||
serde_json::Value::String("low".to_string()),
|
||||
)]))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_persisted_resume_metadata_skips_missing_values() -> Result<()> {
|
||||
let mut request_overrides = None;
|
||||
let mut typesafe_overrides = ConfigOverrides::default();
|
||||
let persisted_metadata = test_thread_metadata(None, None)?;
|
||||
|
||||
merge_persisted_resume_metadata(
|
||||
&mut request_overrides,
|
||||
&mut typesafe_overrides,
|
||||
&persisted_metadata,
|
||||
);
|
||||
|
||||
assert_eq!(typesafe_overrides.model, None);
|
||||
assert_eq!(request_overrides, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_conversation_summary_prefers_plain_user_messages() -> Result<()> {
|
||||
let conversation_id = ThreadId::from_string("3f941c35-29b3-493b-b0a4-e25800d9aeb0")?;
|
||||
|
||||
@@ -26,9 +26,47 @@ pub(super) async fn load_plugin_app_summaries(
|
||||
}
|
||||
};
|
||||
|
||||
connectors::connectors_for_plugin_apps(connectors, plugin_apps)
|
||||
let plugin_connectors = connectors::connectors_for_plugin_apps(connectors, plugin_apps);
|
||||
|
||||
let accessible_connectors =
|
||||
match connectors::list_accessible_connectors_from_mcp_tools_with_options_and_status(
|
||||
config, /*force_refetch*/ false,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(status) if status.codex_apps_ready => status.connectors,
|
||||
Ok(_) => {
|
||||
return plugin_connectors
|
||||
.into_iter()
|
||||
.map(AppSummary::from)
|
||||
.collect();
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("failed to load app auth state for plugin/read: {err:#}");
|
||||
return plugin_connectors
|
||||
.into_iter()
|
||||
.map(AppSummary::from)
|
||||
.collect();
|
||||
}
|
||||
};
|
||||
|
||||
let accessible_ids = accessible_connectors
|
||||
.iter()
|
||||
.map(|connector| connector.id.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
plugin_connectors
|
||||
.into_iter()
|
||||
.map(AppSummary::from)
|
||||
.map(|connector| {
|
||||
let needs_auth = !accessible_ids.contains(connector.id.as_str());
|
||||
AppSummary {
|
||||
id: connector.id,
|
||||
name: connector.name,
|
||||
description: connector.description,
|
||||
install_url: connector.install_url,
|
||||
needs_auth,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -58,7 +96,13 @@ pub(super) fn plugin_apps_needing_auth(
|
||||
&& !accessible_ids.contains(connector.id.as_str())
|
||||
})
|
||||
.cloned()
|
||||
.map(AppSummary::from)
|
||||
.map(|connector| AppSummary {
|
||||
id: connector.id,
|
||||
name: connector.name,
|
||||
description: connector.description,
|
||||
install_url: connector.install_url,
|
||||
needs_auth: true,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_app_server_protocol::McpServerOauthLoginCompletedNotification;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::types::McpServerConfig;
|
||||
use codex_core::mcp::auth::McpOAuthLoginSupport;
|
||||
use codex_core::mcp::auth::oauth_login_support;
|
||||
use codex_core::mcp::auth::resolve_oauth_scopes;
|
||||
use codex_core::mcp::auth::should_retry_without_scopes;
|
||||
use codex_rmcp_client::perform_oauth_login;
|
||||
use tracing::warn;
|
||||
|
||||
use super::CodexMessageProcessor;
|
||||
|
||||
impl CodexMessageProcessor {
|
||||
pub(super) async fn start_plugin_mcp_oauth_logins(
|
||||
&self,
|
||||
config: &Config,
|
||||
plugin_mcp_servers: HashMap<String, McpServerConfig>,
|
||||
) {
|
||||
for (name, server) in plugin_mcp_servers {
|
||||
let oauth_config = match oauth_login_support(&server.transport).await {
|
||||
McpOAuthLoginSupport::Supported(config) => config,
|
||||
McpOAuthLoginSupport::Unsupported => continue,
|
||||
McpOAuthLoginSupport::Unknown(err) => {
|
||||
warn!(
|
||||
"MCP server may or may not require login for plugin install {name}: {err}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let resolved_scopes = resolve_oauth_scopes(
|
||||
/*explicit_scopes*/ None,
|
||||
server.scopes.clone(),
|
||||
oauth_config.discovered_scopes.clone(),
|
||||
);
|
||||
|
||||
let store_mode = config.mcp_oauth_credentials_store_mode;
|
||||
let callback_port = config.mcp_oauth_callback_port;
|
||||
let callback_url = config.mcp_oauth_callback_url.clone();
|
||||
let outgoing = Arc::clone(&self.outgoing);
|
||||
let notification_name = name.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let first_attempt = perform_oauth_login(
|
||||
&name,
|
||||
&oauth_config.url,
|
||||
store_mode,
|
||||
oauth_config.http_headers.clone(),
|
||||
oauth_config.env_http_headers.clone(),
|
||||
&resolved_scopes.scopes,
|
||||
server.oauth_resource.as_deref(),
|
||||
callback_port,
|
||||
callback_url.as_deref(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let final_result = match first_attempt {
|
||||
Err(err) if should_retry_without_scopes(&resolved_scopes, &err) => {
|
||||
perform_oauth_login(
|
||||
&name,
|
||||
&oauth_config.url,
|
||||
store_mode,
|
||||
oauth_config.http_headers,
|
||||
oauth_config.env_http_headers,
|
||||
&[],
|
||||
server.oauth_resource.as_deref(),
|
||||
callback_port,
|
||||
callback_url.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
result => result,
|
||||
};
|
||||
|
||||
let (success, error) = match final_result {
|
||||
Ok(()) => (true, None),
|
||||
Err(err) => (false, Some(err.to_string())),
|
||||
};
|
||||
|
||||
let notification = ServerNotification::McpServerOauthLoginCompleted(
|
||||
McpServerOauthLoginCompletedNotification {
|
||||
name: notification_name,
|
||||
success,
|
||||
error,
|
||||
},
|
||||
);
|
||||
outgoing.send_server_notification(notification).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -733,6 +733,7 @@ mod tests {
|
||||
env: HashMap::new(),
|
||||
network: None,
|
||||
expiration: ExecExpiration::DefaultTimeout,
|
||||
capture_policy: codex_core::exec::ExecCapturePolicy::ShellTool,
|
||||
sandbox: SandboxType::WindowsRestrictedToken,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
@@ -845,6 +846,7 @@ mod tests {
|
||||
env: HashMap::new(),
|
||||
network: None,
|
||||
expiration: ExecExpiration::Cancellation(CancellationToken::new()),
|
||||
capture_policy: codex_core::exec::ExecCapturePolicy::ShellTool,
|
||||
sandbox: SandboxType::None,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
use codex_app_server_protocol::ThreadSourceKind;
|
||||
use codex_core::INTERACTIVE_SESSION_SOURCES;
|
||||
use codex_protocol::protocol::SessionSource as CoreSessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource as CoreSubAgentSource;
|
||||
|
||||
fn interactive_source_kinds() -> Vec<ThreadSourceKind> {
|
||||
vec![
|
||||
ThreadSourceKind::Cli,
|
||||
ThreadSourceKind::VsCode,
|
||||
ThreadSourceKind::Custom,
|
||||
]
|
||||
}
|
||||
|
||||
pub(crate) fn compute_source_filters(
|
||||
source_kinds: Option<Vec<ThreadSourceKind>>,
|
||||
) -> (Vec<CoreSessionSource>, Option<Vec<ThreadSourceKind>>) {
|
||||
let Some(source_kinds) = source_kinds else {
|
||||
return (Vec::new(), Some(interactive_source_kinds()));
|
||||
return (INTERACTIVE_SESSION_SOURCES.to_vec(), None);
|
||||
};
|
||||
|
||||
if source_kinds.is_empty() {
|
||||
return (Vec::new(), Some(interactive_source_kinds()));
|
||||
return (INTERACTIVE_SESSION_SOURCES.to_vec(), None);
|
||||
}
|
||||
|
||||
let requires_post_filter = source_kinds.iter().any(|kind| {
|
||||
@@ -26,7 +19,6 @@ pub(crate) fn compute_source_filters(
|
||||
kind,
|
||||
ThreadSourceKind::Exec
|
||||
| ThreadSourceKind::AppServer
|
||||
| ThreadSourceKind::Custom
|
||||
| ThreadSourceKind::SubAgent
|
||||
| ThreadSourceKind::SubAgentReview
|
||||
| ThreadSourceKind::SubAgentCompact
|
||||
@@ -46,7 +38,6 @@ pub(crate) fn compute_source_filters(
|
||||
ThreadSourceKind::VsCode => Some(CoreSessionSource::VSCode),
|
||||
ThreadSourceKind::Exec
|
||||
| ThreadSourceKind::AppServer
|
||||
| ThreadSourceKind::Custom
|
||||
| ThreadSourceKind::SubAgent
|
||||
| ThreadSourceKind::SubAgentReview
|
||||
| ThreadSourceKind::SubAgentCompact
|
||||
@@ -65,7 +56,6 @@ pub(crate) fn source_kind_matches(source: &CoreSessionSource, filter: &[ThreadSo
|
||||
ThreadSourceKind::VsCode => matches!(source, CoreSessionSource::VSCode),
|
||||
ThreadSourceKind::Exec => matches!(source, CoreSessionSource::Exec),
|
||||
ThreadSourceKind::AppServer => matches!(source, CoreSessionSource::Mcp),
|
||||
ThreadSourceKind::Custom => matches!(source, CoreSessionSource::Custom(_)),
|
||||
ThreadSourceKind::SubAgent => matches!(source, CoreSessionSource::SubAgent(_)),
|
||||
ThreadSourceKind::SubAgentReview => {
|
||||
matches!(
|
||||
@@ -102,16 +92,16 @@ mod tests {
|
||||
fn compute_source_filters_defaults_to_interactive_sources() {
|
||||
let (allowed_sources, filter) = compute_source_filters(None);
|
||||
|
||||
assert_eq!(allowed_sources, Vec::new());
|
||||
assert_eq!(filter, Some(interactive_source_kinds()));
|
||||
assert_eq!(allowed_sources, INTERACTIVE_SESSION_SOURCES.to_vec());
|
||||
assert_eq!(filter, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_source_filters_empty_means_interactive_sources() {
|
||||
let (allowed_sources, filter) = compute_source_filters(Some(Vec::new()));
|
||||
|
||||
assert_eq!(allowed_sources, Vec::new());
|
||||
assert_eq!(filter, Some(interactive_source_kinds()));
|
||||
assert_eq!(allowed_sources, INTERACTIVE_SESSION_SOURCES.to_vec());
|
||||
assert_eq!(filter, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -135,15 +125,6 @@ mod tests {
|
||||
assert_eq!(filter, Some(source_kinds));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_source_filters_custom_requires_post_filtering() {
|
||||
let source_kinds = vec![ThreadSourceKind::Custom];
|
||||
let (allowed_sources, filter) = compute_source_filters(Some(source_kinds.clone()));
|
||||
|
||||
assert_eq!(allowed_sources, Vec::new());
|
||||
assert_eq!(filter, Some(source_kinds));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_kind_matches_distinguishes_subagent_variants() {
|
||||
let parent_thread_id =
|
||||
@@ -173,12 +154,4 @@ mod tests {
|
||||
&[ThreadSourceKind::SubAgentReview]
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_kind_matches_custom_sources() {
|
||||
let custom = CoreSessionSource::Custom("atlas".to_string());
|
||||
|
||||
assert!(source_kind_matches(&custom, &[ThreadSourceKind::Custom]));
|
||||
assert!(!source_kind_matches(&custom, &[ThreadSourceKind::Cli]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@ use codex_app_server_protocol::FsRemoveResponse;
|
||||
use codex_app_server_protocol::FsWriteFileParams;
|
||||
use codex_app_server_protocol::FsWriteFileResponse;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_environment::CopyOptions;
|
||||
use codex_environment::CreateDirectoryOptions;
|
||||
use codex_environment::Environment;
|
||||
use codex_environment::ExecutorFileSystem;
|
||||
use codex_environment::RemoveOptions;
|
||||
use codex_exec_server::CopyOptions;
|
||||
use codex_exec_server::CreateDirectoryOptions;
|
||||
use codex_exec_server::Environment;
|
||||
use codex_exec_server::ExecutorFileSystem;
|
||||
use codex_exec_server::RemoveOptions;
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -34,7 +34,7 @@ pub(crate) struct FsApi {
|
||||
impl Default for FsApi {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
file_system: Arc::new(Environment.get_filesystem()),
|
||||
file_system: Environment::default().get_filesystem(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::sync::Mutex;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use codex_app_server_protocol::FuzzyFileSearchMatchType;
|
||||
use codex_app_server_protocol::FuzzyFileSearchResult;
|
||||
use codex_app_server_protocol::FuzzyFileSearchSessionCompletedNotification;
|
||||
use codex_app_server_protocol::FuzzyFileSearchSessionUpdatedNotification;
|
||||
@@ -60,6 +61,10 @@ pub(crate) async fn run_fuzzy_file_search(
|
||||
FuzzyFileSearchResult {
|
||||
root: m.root.to_string_lossy().to_string(),
|
||||
path: m.path.to_string_lossy().to_string(),
|
||||
match_type: match m.match_type {
|
||||
file_search::MatchType::File => FuzzyFileSearchMatchType::File,
|
||||
file_search::MatchType::Directory => FuzzyFileSearchMatchType::Directory,
|
||||
},
|
||||
file_name: file_name.to_string_lossy().to_string(),
|
||||
score: m.score,
|
||||
indices: m.indices,
|
||||
@@ -231,6 +236,10 @@ fn collect_files(snapshot: &file_search::FileSearchSnapshot) -> Vec<FuzzyFileSea
|
||||
FuzzyFileSearchResult {
|
||||
root: m.root.to_string_lossy().to_string(),
|
||||
path: m.path.to_string_lossy().to_string(),
|
||||
match_type: match m.match_type {
|
||||
file_search::MatchType::File => FuzzyFileSearchMatchType::File,
|
||||
file_search::MatchType::Directory => FuzzyFileSearchMatchType::Directory,
|
||||
},
|
||||
file_name: file_name.to_string_lossy().to_string(),
|
||||
score: m.score,
|
||||
indices: m.indices.clone(),
|
||||
|
||||
@@ -808,10 +808,6 @@ mod tests {
|
||||
for (requested_source, expected_source) in [
|
||||
(SessionSource::Cli, ApiSessionSource::Cli),
|
||||
(SessionSource::Exec, ApiSessionSource::Exec),
|
||||
(
|
||||
SessionSource::Custom("atlas".to_string()),
|
||||
ApiSessionSource::Custom("atlas".to_string()),
|
||||
),
|
||||
] {
|
||||
let client = start_test_client(requested_source).await;
|
||||
let response = client
|
||||
|
||||
@@ -23,16 +23,14 @@ struct AppServerArgs {
|
||||
)]
|
||||
listen: AppServerTransport,
|
||||
|
||||
/// Session source stamped into new threads started by this app-server.
|
||||
///
|
||||
/// Known values such as `vscode`, `cli`, `exec`, and `mcp` map to built-in
|
||||
/// sources. Any other non-empty value is recorded as a custom source.
|
||||
/// Session source used to derive product restrictions and metadata.
|
||||
#[arg(
|
||||
long = "session-source",
|
||||
value_name = "SOURCE",
|
||||
default_value = "vscode"
|
||||
default_value = "vscode",
|
||||
value_parser = SessionSource::from_startup_arg
|
||||
)]
|
||||
session_source: String,
|
||||
session_source: SessionSource,
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
@@ -44,8 +42,7 @@ fn main() -> anyhow::Result<()> {
|
||||
..Default::default()
|
||||
};
|
||||
let transport = args.listen;
|
||||
let session_source = SessionSource::from_startup_arg(args.session_source.as_str())
|
||||
.map_err(|err| anyhow::anyhow!("invalid --session-source: {err}"))?;
|
||||
let session_source = args.session_source;
|
||||
|
||||
run_main_with_transport(
|
||||
arg0_paths,
|
||||
|
||||
@@ -50,10 +50,6 @@ use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_core::AnalyticsEventsClient;
|
||||
use codex_core::AuthManager;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::auth::ExternalAuthRefreshContext;
|
||||
use codex_core::auth::ExternalAuthRefreshReason;
|
||||
use codex_core::auth::ExternalAuthRefresher;
|
||||
use codex_core::auth::ExternalAuthTokens;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config_loader::CloudRequirementsLoader;
|
||||
use codex_core::config_loader::LoaderOverrides;
|
||||
@@ -62,8 +58,13 @@ use codex_core::default_client::USER_AGENT_SUFFIX;
|
||||
use codex_core::default_client::get_codex_user_agent;
|
||||
use codex_core::default_client::set_default_client_residency_requirement;
|
||||
use codex_core::default_client::set_default_originator;
|
||||
use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig;
|
||||
use codex_features::Feature;
|
||||
use codex_feedback::CodexFeedback;
|
||||
use codex_login::auth::ExternalAuthRefreshContext;
|
||||
use codex_login::auth::ExternalAuthRefreshReason;
|
||||
use codex_login::auth::ExternalAuthRefresher;
|
||||
use codex_login::auth::ExternalAuthTokens;
|
||||
use codex_models::CollaborationModesConfig;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::W3cTraceContext;
|
||||
@@ -212,7 +213,7 @@ impl MessageProcessor {
|
||||
CollaborationModesConfig {
|
||||
default_mode_request_user_input: config
|
||||
.features
|
||||
.enabled(codex_core::features::Feature::DefaultModeRequestUserInput),
|
||||
.enabled(Feature::DefaultModeRequestUserInput),
|
||||
},
|
||||
));
|
||||
(auth_manager, thread_manager)
|
||||
@@ -228,10 +229,7 @@ impl MessageProcessor {
|
||||
thread_manager
|
||||
.plugins_manager()
|
||||
.set_analytics_events_client(analytics_events_client.clone());
|
||||
// TODO(xl): Move into PluginManager once this no longer depends on config feature gating.
|
||||
thread_manager
|
||||
.plugins_manager()
|
||||
.maybe_start_curated_repo_sync_for_config(&config, &thread_manager.session_source());
|
||||
|
||||
let cloud_requirements = Arc::new(RwLock::new(cloud_requirements));
|
||||
let codex_message_processor = CodexMessageProcessor::new(CodexMessageProcessorArgs {
|
||||
auth_manager: auth_manager.clone(),
|
||||
@@ -244,6 +242,11 @@ impl MessageProcessor {
|
||||
feedback,
|
||||
log_db,
|
||||
});
|
||||
// Keep plugin startup warmups aligned at app-server startup.
|
||||
// TODO(xl): Move into PluginManager once this no longer depends on config feature gating.
|
||||
thread_manager
|
||||
.plugins_manager()
|
||||
.maybe_start_plugin_startup_tasks_for_config(&config, auth_manager.clone());
|
||||
let config_api = ConfigApi::new(
|
||||
config.codex_home.clone(),
|
||||
cli_overrides,
|
||||
@@ -787,7 +790,7 @@ impl MessageProcessor {
|
||||
Ok(response) => {
|
||||
self.codex_message_processor.clear_plugin_related_caches();
|
||||
self.codex_message_processor
|
||||
.maybe_start_curated_repo_sync_for_latest_config()
|
||||
.maybe_start_plugin_startup_tasks_for_latest_config()
|
||||
.await;
|
||||
self.outgoing.send_response(request_id, response).await;
|
||||
}
|
||||
@@ -804,7 +807,7 @@ impl MessageProcessor {
|
||||
Ok(response) => {
|
||||
self.codex_message_processor.clear_plugin_related_caches();
|
||||
self.codex_message_processor
|
||||
.maybe_start_curated_repo_sync_for_latest_config()
|
||||
.maybe_start_plugin_startup_tasks_for_latest_config()
|
||||
.await;
|
||||
self.outgoing.send_response(request_id, response).await;
|
||||
}
|
||||
|
||||
@@ -580,7 +580,7 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> {
|
||||
parent_span_id: remote_parent_span_id,
|
||||
context: remote_trace,
|
||||
} = RemoteTrace::new("00000000000000000000000000000077", "0000000000000088");
|
||||
let _: TurnStartResponse = harness
|
||||
let turn_start_response: TurnStartResponse = harness
|
||||
.request(
|
||||
ClientRequest::TurnStart {
|
||||
request_id: RequestId::Integer(3),
|
||||
@@ -628,6 +628,10 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> {
|
||||
assert_eq!(server_request_span.parent_span_id, remote_parent_span_id);
|
||||
assert!(server_request_span.parent_span_is_remote);
|
||||
assert_eq!(server_request_span.span_context.trace_id(), remote_trace_id);
|
||||
assert_eq!(
|
||||
span_attr(server_request_span, "turn.id"),
|
||||
Some(turn_start_response.turn.id.as_str())
|
||||
);
|
||||
assert_span_descends_from(&spans, core_turn_span, server_request_span);
|
||||
harness.shutdown().await;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use codex_app_server_protocol::Model;
|
||||
use codex_app_server_protocol::ModelUpgradeInfo;
|
||||
use codex_app_server_protocol::ReasoningEffortOption;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::models_manager::manager::RefreshStrategy;
|
||||
use codex_models::RefreshStrategy;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::openai_models::ReasoningEffortPreset;
|
||||
|
||||
|
||||
@@ -75,6 +75,10 @@ impl RequestContext {
|
||||
pub(crate) fn span(&self) -> Span {
|
||||
self.span.clone()
|
||||
}
|
||||
|
||||
fn record_turn_id(&self, turn_id: &str) {
|
||||
self.span.record("turn.id", turn_id);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -217,6 +221,17 @@ impl OutgoingMessageSender {
|
||||
.and_then(RequestContext::request_trace)
|
||||
}
|
||||
|
||||
pub(crate) async fn record_request_turn_id(
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
turn_id: &str,
|
||||
) {
|
||||
let request_contexts = self.request_contexts.lock().await;
|
||||
if let Some(request_context) = request_contexts.get(request_id) {
|
||||
request_context.record_turn_id(turn_id);
|
||||
}
|
||||
}
|
||||
|
||||
async fn take_request_context(
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
|
||||
@@ -13,6 +13,8 @@ base64 = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-features = { workspace = true }
|
||||
codex-models = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-utils-cargo-bin = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use codex_core::features::FEATURES;
|
||||
use codex_core::features::Feature;
|
||||
use codex_features::FEATURES;
|
||||
use codex_features::Feature;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ use codex_app_server_protocol::ThreadRealtimeStopParams;
|
||||
use codex_app_server_protocol::ThreadResumeParams;
|
||||
use codex_app_server_protocol::ThreadRollbackParams;
|
||||
use codex_app_server_protocol::ThreadSetNameParams;
|
||||
use codex_app_server_protocol::ThreadShellCommandParams;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadUnarchiveParams;
|
||||
use codex_app_server_protocol::ThreadUnsubscribeParams;
|
||||
@@ -114,7 +115,7 @@ impl McpProcess {
|
||||
Self::new_with_env_and_args(codex_home, env_overrides, &[]).await
|
||||
}
|
||||
|
||||
pub async fn new_with_env_and_args(
|
||||
async fn new_with_env_and_args(
|
||||
codex_home: &Path,
|
||||
env_overrides: &[(&str, Option<&str>)],
|
||||
args: &[&str],
|
||||
@@ -399,6 +400,15 @@ impl McpProcess {
|
||||
self.send_request("thread/compact/start", params).await
|
||||
}
|
||||
|
||||
/// Send a `thread/shellCommand` JSON-RPC request.
|
||||
pub async fn send_thread_shell_command_request(
|
||||
&mut self,
|
||||
params: ThreadShellCommandParams,
|
||||
) -> anyhow::Result<i64> {
|
||||
let params = Some(serde_json::to_value(params)?);
|
||||
self.send_request("thread/shellCommand", params).await
|
||||
}
|
||||
|
||||
/// Send a `thread/rollback` JSON-RPC request.
|
||||
pub async fn send_thread_rollback_request(
|
||||
&mut self,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use chrono::DateTime;
|
||||
use chrono::Utc;
|
||||
use codex_core::test_support::all_model_presets;
|
||||
use codex_models::client_version_to_whole;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::openai_models::ConfigShellToolType;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
@@ -84,7 +85,7 @@ pub fn write_models_cache_with_models(
|
||||
let cache_path = codex_home.join("models_cache.json");
|
||||
// DateTime<Utc> serializes to RFC3339 format by default with serde
|
||||
let fetched_at: DateTime<Utc> = Utc::now();
|
||||
let client_version = codex_core::models_manager::client_version_to_whole();
|
||||
let client_version = client_version_to_whole();
|
||||
let cache = json!({
|
||||
"fetched_at": fetched_at,
|
||||
"etag": null,
|
||||
|
||||
@@ -257,6 +257,7 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() -> Result<()> {
|
||||
{
|
||||
"root": root_path.clone(),
|
||||
"path": "abexy",
|
||||
"match_type": "file",
|
||||
"file_name": "abexy",
|
||||
"score": 84,
|
||||
"indices": [0, 1, 2],
|
||||
@@ -264,6 +265,7 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() -> Result<()> {
|
||||
{
|
||||
"root": root_path.clone(),
|
||||
"path": sub_abce_rel,
|
||||
"match_type": "file",
|
||||
"file_name": "abce",
|
||||
"score": expected_score,
|
||||
"indices": [4, 5, 7],
|
||||
@@ -271,6 +273,7 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() -> Result<()> {
|
||||
{
|
||||
"root": root_path.clone(),
|
||||
"path": "abcde",
|
||||
"match_type": "file",
|
||||
"file_name": "abcde",
|
||||
"score": 71,
|
||||
"indices": [0, 1, 4],
|
||||
|
||||
@@ -10,8 +10,8 @@ use codex_app_server_protocol::ExperimentalFeatureStage;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core::features::FEATURES;
|
||||
use codex_core::features::Stage;
|
||||
use codex_features::FEATURES;
|
||||
use codex_features::Stage;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
@@ -38,6 +38,7 @@ mod thread_name_websocket;
|
||||
mod thread_read;
|
||||
mod thread_resume;
|
||||
mod thread_rollback;
|
||||
mod thread_shell_command;
|
||||
mod thread_start;
|
||||
mod thread_status;
|
||||
mod thread_unarchive;
|
||||
|
||||
@@ -18,8 +18,8 @@ use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use codex_core::features::FEATURES;
|
||||
use codex_core::features::Feature;
|
||||
use codex_features::FEATURES;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Settings;
|
||||
|
||||
@@ -25,8 +25,6 @@ use codex_app_server_protocol::PluginAuthPolicy;
|
||||
use codex_app_server_protocol::PluginInstallParams;
|
||||
use codex_app_server_protocol::PluginInstallResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::SkillsListParams;
|
||||
use codex_app_server_protocol::SkillsListResponse;
|
||||
use codex_core::auth::AuthCredentialsStoreMode;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -148,6 +146,56 @@ async fn plugin_install_returns_invalid_request_for_not_available_plugin() -> Re
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_install_returns_invalid_request_for_disallowed_product_plugin() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let repo_root = TempDir::new()?;
|
||||
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
|
||||
std::fs::write(
|
||||
repo_root.path().join(".agents/plugins/marketplace.json"),
|
||||
r#"{
|
||||
"name": "debug",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "sample-plugin",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./sample-plugin"
|
||||
},
|
||||
"policy": {
|
||||
"products": ["CHATGPT"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)?;
|
||||
write_plugin_source(repo_root.path(), "sample-plugin", &[])?;
|
||||
let marketplace_path =
|
||||
AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?;
|
||||
|
||||
let mut mcp =
|
||||
McpProcess::new_with_args(codex_home.path(), &["--session-source", "atlas"]).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_install_request(PluginInstallParams {
|
||||
marketplace_path,
|
||||
plugin_name: "sample-plugin".to_string(),
|
||||
force_remote_sync: false,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let err = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
assert_eq!(err.error.code, -32600);
|
||||
assert!(err.error.message.contains("not available for install"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_install_force_remote_sync_enables_remote_plugin_before_local_install() -> Result<()>
|
||||
{
|
||||
@@ -263,21 +311,22 @@ async fn plugin_install_tracks_analytics_event() -> Result<()> {
|
||||
let response: PluginInstallResponse = to_response(response)?;
|
||||
assert_eq!(response.apps_needing_auth, Vec::<AppSummary>::new());
|
||||
|
||||
let payloads = timeout(DEFAULT_TIMEOUT, async {
|
||||
let payload = timeout(DEFAULT_TIMEOUT, async {
|
||||
loop {
|
||||
let Some(requests) = analytics_server.received_requests().await else {
|
||||
tokio::time::sleep(Duration::from_millis(25)).await;
|
||||
continue;
|
||||
};
|
||||
if !requests.is_empty() {
|
||||
break requests;
|
||||
if let Some(request) = requests.iter().find(|request| {
|
||||
request.method == "POST" && request.url.path() == "/codex/analytics-events/events"
|
||||
}) {
|
||||
break request.body.clone();
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(25)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
let payload: serde_json::Value =
|
||||
serde_json::from_slice(&payloads[0].body).expect("analytics payload");
|
||||
let payload: serde_json::Value = serde_json::from_slice(&payload).expect("analytics payload");
|
||||
assert_eq!(
|
||||
payload,
|
||||
json!({
|
||||
@@ -386,6 +435,7 @@ async fn plugin_install_returns_apps_needing_auth() -> Result<()> {
|
||||
name: "Alpha".to_string(),
|
||||
description: Some("Alpha connector".to_string()),
|
||||
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
|
||||
needs_auth: true,
|
||||
}],
|
||||
}
|
||||
);
|
||||
@@ -469,6 +519,7 @@ async fn plugin_install_filters_disallowed_apps_needing_auth() -> Result<()> {
|
||||
name: "Alpha".to_string(),
|
||||
description: Some("Alpha connector".to_string()),
|
||||
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
|
||||
needs_auth: true,
|
||||
}],
|
||||
}
|
||||
);
|
||||
@@ -479,10 +530,13 @@ async fn plugin_install_filters_disallowed_apps_needing_auth() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_install_filters_product_restricted_plugin_skills() -> Result<()> {
|
||||
async fn plugin_install_makes_bundled_mcp_servers_available_to_followup_requests() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("config.toml"),
|
||||
"[features]\nplugins = true\n",
|
||||
)?;
|
||||
let repo_root = TempDir::new()?;
|
||||
write_plugins_enabled_config(codex_home.path())?;
|
||||
write_plugin_marketplace(
|
||||
repo_root.path(),
|
||||
"debug",
|
||||
@@ -492,26 +546,20 @@ async fn plugin_install_filters_product_restricted_plugin_skills() -> Result<()>
|
||||
None,
|
||||
)?;
|
||||
write_plugin_source(repo_root.path(), "sample-plugin", &[])?;
|
||||
|
||||
let plugin_root = repo_root.path().join("sample-plugin");
|
||||
write_plugin_skill(
|
||||
&plugin_root,
|
||||
"all-products",
|
||||
"Visible to every product",
|
||||
&[],
|
||||
std::fs::write(
|
||||
repo_root.path().join("sample-plugin/.mcp.json"),
|
||||
r#"{
|
||||
"mcpServers": {
|
||||
"sample-mcp": {
|
||||
"command": "echo"
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
)?;
|
||||
write_plugin_skill(
|
||||
&plugin_root,
|
||||
"chatgpt-only",
|
||||
"Visible to ChatGPT",
|
||||
&["CHATGPT"],
|
||||
)?;
|
||||
write_plugin_skill(&plugin_root, "atlas-only", "Visible to Atlas", &["ATLAS"])?;
|
||||
let marketplace_path =
|
||||
AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?;
|
||||
|
||||
let mut mcp =
|
||||
McpProcess::new_with_args(codex_home.path(), &["--session-source", "chatgpt"]).await?;
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -521,7 +569,6 @@ async fn plugin_install_filters_product_restricted_plugin_skills() -> Result<()>
|
||||
force_remote_sync: false,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
@@ -529,37 +576,28 @@ async fn plugin_install_filters_product_restricted_plugin_skills() -> Result<()>
|
||||
.await??;
|
||||
let response: PluginInstallResponse = to_response(response)?;
|
||||
assert_eq!(response.apps_needing_auth, Vec::<AppSummary>::new());
|
||||
let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?;
|
||||
assert!(!config.contains("[mcp_servers.sample-mcp]"));
|
||||
assert!(!config.contains("command = \"echo\""));
|
||||
|
||||
let request_id = mcp
|
||||
.send_skills_list_request(SkillsListParams {
|
||||
cwds: vec![codex_home.path().to_path_buf()],
|
||||
force_reload: true,
|
||||
per_cwd_extra_user_roots: None,
|
||||
})
|
||||
.send_raw_request(
|
||||
"mcpServer/oauth/login",
|
||||
Some(json!({
|
||||
"name": "sample-mcp",
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
let err = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: SkillsListResponse = to_response(response)?;
|
||||
|
||||
let mut skills = response
|
||||
.data
|
||||
.into_iter()
|
||||
.flat_map(|entry| entry.skills.into_iter())
|
||||
.map(|skill| skill.name)
|
||||
.filter(|name| name.starts_with("sample-plugin:"))
|
||||
.collect::<Vec<_>>();
|
||||
skills.sort_unstable();
|
||||
|
||||
assert_eq!(err.error.code, -32600);
|
||||
assert_eq!(
|
||||
skills,
|
||||
vec![
|
||||
"sample-plugin:all-products".to_string(),
|
||||
"sample-plugin:chatgpt-only".to_string(),
|
||||
]
|
||||
err.error.message,
|
||||
"OAuth login is only supported for streamable HTTP servers."
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -735,16 +773,6 @@ plugins = true
|
||||
)
|
||||
}
|
||||
|
||||
fn write_plugins_enabled_config(codex_home: &std::path::Path) -> std::io::Result<()> {
|
||||
std::fs::write(
|
||||
codex_home.join("config.toml"),
|
||||
r#"
|
||||
[features]
|
||||
plugins = true
|
||||
"#,
|
||||
)
|
||||
}
|
||||
|
||||
fn write_plugin_marketplace(
|
||||
repo_root: &std::path::Path,
|
||||
marketplace_name: &str,
|
||||
@@ -814,32 +842,3 @@ fn write_plugin_source(
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_plugin_skill(
|
||||
plugin_root: &std::path::Path,
|
||||
skill_name: &str,
|
||||
description: &str,
|
||||
products: &[&str],
|
||||
) -> Result<()> {
|
||||
let skill_dir = plugin_root.join("skills").join(skill_name);
|
||||
std::fs::create_dir_all(&skill_dir)?;
|
||||
std::fs::write(
|
||||
skill_dir.join("SKILL.md"),
|
||||
format!("---\ndescription: {description}\n---\n\n# {skill_name}\n"),
|
||||
)?;
|
||||
|
||||
if !products.is_empty() {
|
||||
let products = products
|
||||
.iter()
|
||||
.map(|product| format!(" - {product}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
std::fs::create_dir_all(skill_dir.join("agents"))?;
|
||||
std::fs::write(
|
||||
skill_dir.join("agents/openai.yaml"),
|
||||
format!("policy:\n products:\n{products}\n"),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use app_test_support::ChatGptAuthFixture;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::to_response;
|
||||
@@ -27,13 +28,24 @@ use wiremock::matchers::path;
|
||||
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const TEST_CURATED_PLUGIN_SHA: &str = "0123456789abcdef0123456789abcdef01234567";
|
||||
const STARTUP_REMOTE_PLUGIN_SYNC_MARKER_FILE: &str = ".tmp/app-server-remote-plugin-sync-v1";
|
||||
|
||||
fn write_plugins_enabled_config(codex_home: &std::path::Path) -> std::io::Result<()> {
|
||||
std::fs::write(
|
||||
codex_home.join("config.toml"),
|
||||
r#"[features]
|
||||
plugins = true
|
||||
"#,
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_returns_invalid_request_for_invalid_marketplace_file() -> Result<()> {
|
||||
async fn plugin_list_skips_invalid_marketplace_file() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let repo_root = TempDir::new()?;
|
||||
std::fs::create_dir_all(repo_root.path().join(".git"))?;
|
||||
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
|
||||
write_plugins_enabled_config(codex_home.path())?;
|
||||
std::fs::write(
|
||||
repo_root.path().join(".agents/plugins/marketplace.json"),
|
||||
"{not json",
|
||||
@@ -57,14 +69,23 @@ async fn plugin_list_returns_invalid_request_for_invalid_marketplace_file() -> R
|
||||
})
|
||||
.await?;
|
||||
|
||||
let err = timeout(
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: PluginListResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(err.error.code, -32600);
|
||||
assert!(err.error.message.contains("invalid marketplace file"));
|
||||
assert!(
|
||||
response.marketplaces.iter().all(|marketplace| {
|
||||
marketplace.path
|
||||
!= AbsolutePathBuf::try_from(
|
||||
repo_root.path().join(".agents/plugins/marketplace.json"),
|
||||
)
|
||||
.expect("absolute marketplace path")
|
||||
}),
|
||||
"invalid marketplace should be skipped"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -98,6 +119,7 @@ async fn plugin_list_rejects_relative_cwds() -> Result<()> {
|
||||
async fn plugin_list_accepts_omitted_cwds() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::create_dir_all(codex_home.path().join(".agents/plugins"))?;
|
||||
write_plugins_enabled_config(codex_home.path())?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(".agents/plugins/marketplace.json"),
|
||||
r#"{
|
||||
@@ -377,179 +399,6 @@ enabled = false
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_filters_plugins_for_custom_session_source_products() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let repo_root = TempDir::new()?;
|
||||
std::fs::create_dir_all(repo_root.path().join(".git"))?;
|
||||
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
|
||||
std::fs::write(
|
||||
repo_root.path().join(".agents/plugins/marketplace.json"),
|
||||
r#"{
|
||||
"name": "codex-curated",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "all-products",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./all-products"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "chatgpt-only",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./chatgpt-only"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL",
|
||||
"products": ["CHATGPT"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "atlas-only",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./atlas-only"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL",
|
||||
"products": ["ATLAS"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "codex-only",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./codex-only"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL",
|
||||
"products": ["CODEX"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
McpProcess::new_with_args(codex_home.path(), &["--session-source", "chatgpt"]).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_list_request(PluginListParams {
|
||||
cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]),
|
||||
force_remote_sync: false,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: PluginListResponse = to_response(response)?;
|
||||
|
||||
let marketplace = response
|
||||
.marketplaces
|
||||
.into_iter()
|
||||
.find(|marketplace| marketplace.name == "codex-curated")
|
||||
.expect("expected marketplace entry");
|
||||
|
||||
assert_eq!(
|
||||
marketplace
|
||||
.plugins
|
||||
.into_iter()
|
||||
.map(|plugin| plugin.name)
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["all-products".to_string(), "chatgpt-only".to_string()]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_defaults_non_custom_session_source_to_codex_products() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let repo_root = TempDir::new()?;
|
||||
std::fs::create_dir_all(repo_root.path().join(".git"))?;
|
||||
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
|
||||
std::fs::write(
|
||||
repo_root.path().join(".agents/plugins/marketplace.json"),
|
||||
r#"{
|
||||
"name": "codex-curated",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "all-products",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./all-products"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "chatgpt-only",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./chatgpt-only"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL",
|
||||
"products": ["CHATGPT"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "codex-only",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./codex-only"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL",
|
||||
"products": ["CODEX"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_list_request(PluginListParams {
|
||||
cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]),
|
||||
force_remote_sync: false,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: PluginListResponse = to_response(response)?;
|
||||
|
||||
let marketplace = response
|
||||
.marketplaces
|
||||
.into_iter()
|
||||
.find(|marketplace| marketplace.name == "codex-curated")
|
||||
.expect("expected marketplace entry");
|
||||
|
||||
assert_eq!(
|
||||
marketplace
|
||||
.plugins
|
||||
.into_iter()
|
||||
.map(|plugin| plugin.name)
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["all-products".to_string(), "codex-only".to_string()]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_returns_plugin_interface_with_absolute_asset_paths() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -558,6 +407,7 @@ async fn plugin_list_returns_plugin_interface_with_absolute_asset_paths() -> Res
|
||||
std::fs::create_dir_all(repo_root.path().join(".git"))?;
|
||||
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
|
||||
std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;
|
||||
write_plugins_enabled_config(codex_home.path())?;
|
||||
std::fs::write(
|
||||
repo_root.path().join(".agents/plugins/marketplace.json"),
|
||||
r#"{
|
||||
@@ -691,6 +541,7 @@ async fn plugin_list_accepts_legacy_string_default_prompt() -> Result<()> {
|
||||
std::fs::create_dir_all(repo_root.path().join(".git"))?;
|
||||
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
|
||||
std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;
|
||||
write_plugins_enabled_config(codex_home.path())?;
|
||||
std::fs::write(
|
||||
repo_root.path().join(".agents/plugins/marketplace.json"),
|
||||
r#"{
|
||||
@@ -825,6 +676,16 @@ async fn plugin_list_force_remote_sync_reconciles_curated_plugin_state() -> Resu
|
||||
))
|
||||
.mount(&server)
|
||||
.await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/backend-api/plugins/featured"))
|
||||
.and(header("authorization", "Bearer chatgpt-token"))
|
||||
.and(header("chatgpt-account-id", "account-123"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(r#"["linear@openai-curated","calendar@openai-curated"]"#),
|
||||
)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
@@ -843,6 +704,13 @@ async fn plugin_list_force_remote_sync_reconciles_curated_plugin_state() -> Resu
|
||||
.await??;
|
||||
let response: PluginListResponse = to_response(response)?;
|
||||
assert_eq!(response.remote_sync_error, None);
|
||||
assert_eq!(
|
||||
response.featured_plugin_ids,
|
||||
vec![
|
||||
"linear@openai-curated".to_string(),
|
||||
"calendar@openai-curated".to_string(),
|
||||
]
|
||||
);
|
||||
|
||||
let curated_marketplace = response
|
||||
.marketplaces
|
||||
@@ -888,6 +756,220 @@ async fn plugin_list_force_remote_sync_reconciles_curated_plugin_state() -> Resu
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn app_server_startup_remote_plugin_sync_runs_once() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let server = MockServer::start().await;
|
||||
write_plugin_sync_config(codex_home.path(), &format!("{}/backend-api/", server.uri()))?;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("chatgpt-token")
|
||||
.account_id("account-123")
|
||||
.chatgpt_user_id("user-123")
|
||||
.chatgpt_account_id("account-123"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
write_openai_curated_marketplace(codex_home.path(), &["linear"])?;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/backend-api/plugins/list"))
|
||||
.and(header("authorization", "Bearer chatgpt-token"))
|
||||
.and(header("chatgpt-account-id", "account-123"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string(
|
||||
r#"[
|
||||
{"id":"1","name":"linear","marketplace_name":"openai-curated","version":"1.0.0","enabled":true}
|
||||
]"#,
|
||||
))
|
||||
.mount(&server)
|
||||
.await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/backend-api/plugins/featured"))
|
||||
.and(header("authorization", "Bearer chatgpt-token"))
|
||||
.and(header("chatgpt-account-id", "account-123"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string(r#"["linear@openai-curated"]"#))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let marker_path = codex_home
|
||||
.path()
|
||||
.join(STARTUP_REMOTE_PLUGIN_SYNC_MARKER_FILE);
|
||||
|
||||
{
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
wait_for_path_exists(&marker_path).await?;
|
||||
wait_for_remote_plugin_request_count(&server, "/plugins/list", 1).await?;
|
||||
let request_id = mcp
|
||||
.send_plugin_list_request(PluginListParams {
|
||||
cwds: None,
|
||||
force_remote_sync: false,
|
||||
})
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: PluginListResponse = to_response(response)?;
|
||||
let curated_marketplace = response
|
||||
.marketplaces
|
||||
.into_iter()
|
||||
.find(|marketplace| marketplace.name == "openai-curated")
|
||||
.expect("expected openai-curated marketplace entry");
|
||||
assert_eq!(
|
||||
curated_marketplace
|
||||
.plugins
|
||||
.into_iter()
|
||||
.map(|plugin| (plugin.id, plugin.installed, plugin.enabled))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![("linear@openai-curated".to_string(), true, true)]
|
||||
);
|
||||
wait_for_remote_plugin_request_count(&server, "/plugins/list", 1).await?;
|
||||
}
|
||||
|
||||
let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?;
|
||||
assert!(config.contains(r#"[plugins."linear@openai-curated"]"#));
|
||||
|
||||
{
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(250)).await;
|
||||
wait_for_remote_plugin_request_count(&server, "/plugins/list", 1).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_fetches_featured_plugin_ids_without_chatgpt_auth() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let server = MockServer::start().await;
|
||||
write_plugin_sync_config(codex_home.path(), &format!("{}/backend-api/", server.uri()))?;
|
||||
write_openai_curated_marketplace(codex_home.path(), &["linear", "gmail"])?;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/backend-api/plugins/featured"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string(r#"["linear@openai-curated"]"#))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_list_request(PluginListParams {
|
||||
cwds: None,
|
||||
force_remote_sync: false,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: PluginListResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(
|
||||
response.featured_plugin_ids,
|
||||
vec!["linear@openai-curated".to_string()]
|
||||
);
|
||||
assert_eq!(response.remote_sync_error, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_uses_warmed_featured_plugin_ids_cache_on_first_request() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let server = MockServer::start().await;
|
||||
write_plugin_sync_config(codex_home.path(), &format!("{}/backend-api/", server.uri()))?;
|
||||
write_openai_curated_marketplace(codex_home.path(), &["linear", "gmail"])?;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/backend-api/plugins/featured"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string(r#"["linear@openai-curated"]"#))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
wait_for_featured_plugin_request_count(&server, 1).await?;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_list_request(PluginListParams {
|
||||
cwds: None,
|
||||
force_remote_sync: false,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: PluginListResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(
|
||||
response.featured_plugin_ids,
|
||||
vec!["linear@openai-curated".to_string()]
|
||||
);
|
||||
assert_eq!(response.remote_sync_error, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn wait_for_featured_plugin_request_count(
|
||||
server: &MockServer,
|
||||
expected_count: usize,
|
||||
) -> Result<()> {
|
||||
wait_for_remote_plugin_request_count(server, "/plugins/featured", expected_count).await
|
||||
}
|
||||
|
||||
async fn wait_for_remote_plugin_request_count(
|
||||
server: &MockServer,
|
||||
path_suffix: &str,
|
||||
expected_count: usize,
|
||||
) -> Result<()> {
|
||||
timeout(DEFAULT_TIMEOUT, async {
|
||||
loop {
|
||||
let Some(requests) = server.received_requests().await else {
|
||||
bail!("wiremock did not record requests");
|
||||
};
|
||||
let request_count = requests
|
||||
.iter()
|
||||
.filter(|request| {
|
||||
request.method == "GET" && request.url.path().ends_with(path_suffix)
|
||||
})
|
||||
.count();
|
||||
if request_count == expected_count {
|
||||
return Ok::<(), anyhow::Error>(());
|
||||
}
|
||||
if request_count > expected_count {
|
||||
bail!(
|
||||
"expected exactly {expected_count} {path_suffix} requests, got {request_count}"
|
||||
);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn wait_for_path_exists(path: &std::path::Path) -> Result<()> {
|
||||
timeout(DEFAULT_TIMEOUT, async {
|
||||
loop {
|
||||
if path.exists() {
|
||||
return Ok::<(), anyhow::Error>(());
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_installed_plugin(
|
||||
codex_home: &TempDir,
|
||||
marketplace_name: &str,
|
||||
|
||||
@@ -1,17 +1,46 @@
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex as StdMutex;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use app_test_support::ChatGptAuthFixture;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::to_response;
|
||||
use app_test_support::write_chatgpt_auth;
|
||||
use axum::Json;
|
||||
use axum::Router;
|
||||
use axum::extract::State;
|
||||
use axum::http::HeaderMap;
|
||||
use axum::http::StatusCode;
|
||||
use axum::http::Uri;
|
||||
use axum::http::header::AUTHORIZATION;
|
||||
use axum::routing::get;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::PluginAuthPolicy;
|
||||
use codex_app_server_protocol::PluginInstallPolicy;
|
||||
use codex_app_server_protocol::PluginReadParams;
|
||||
use codex_app_server_protocol::PluginReadResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_core::auth::AuthCredentialsStoreMode;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rmcp::handler::server::ServerHandler;
|
||||
use rmcp::model::JsonObject;
|
||||
use rmcp::model::ListToolsResult;
|
||||
use rmcp::model::Meta;
|
||||
use rmcp::model::ServerCapabilities;
|
||||
use rmcp::model::ServerInfo;
|
||||
use rmcp::model::Tool;
|
||||
use rmcp::model::ToolAnnotations;
|
||||
use rmcp::transport::StreamableHttpServerConfig;
|
||||
use rmcp::transport::StreamableHttpService;
|
||||
use rmcp::transport::streamable_http_server::session::local::LocalSessionManager;
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
@@ -25,6 +54,7 @@ async fn plugin_read_returns_plugin_details_with_bundle_contents() -> Result<()>
|
||||
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
|
||||
std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;
|
||||
std::fs::create_dir_all(plugin_root.join("skills/thread-summarizer"))?;
|
||||
std::fs::create_dir_all(plugin_root.join("skills/chatgpt-only"))?;
|
||||
std::fs::write(
|
||||
repo_root.path().join(".agents/plugins/marketplace.json"),
|
||||
r#"{
|
||||
@@ -79,6 +109,32 @@ description: Summarize email threads
|
||||
---
|
||||
|
||||
# Thread Summarizer
|
||||
"#,
|
||||
)?;
|
||||
std::fs::write(
|
||||
plugin_root.join("skills/chatgpt-only/SKILL.md"),
|
||||
r#"---
|
||||
name: chatgpt-only
|
||||
description: Visible only for ChatGPT
|
||||
---
|
||||
|
||||
# ChatGPT Only
|
||||
"#,
|
||||
)?;
|
||||
std::fs::create_dir_all(plugin_root.join("skills/thread-summarizer/agents"))?;
|
||||
std::fs::write(
|
||||
plugin_root.join("skills/thread-summarizer/agents/openai.yaml"),
|
||||
r#"policy:
|
||||
products:
|
||||
- CODEX
|
||||
"#,
|
||||
)?;
|
||||
std::fs::create_dir_all(plugin_root.join("skills/chatgpt-only/agents"))?;
|
||||
std::fs::write(
|
||||
plugin_root.join("skills/chatgpt-only/agents/openai.yaml"),
|
||||
r#"policy:
|
||||
products:
|
||||
- CHATGPT
|
||||
"#,
|
||||
)?;
|
||||
std::fs::write(
|
||||
@@ -195,11 +251,103 @@ enabled = true
|
||||
response.plugin.apps[0].install_url.as_deref(),
|
||||
Some("https://chatgpt.com/apps/gmail/gmail")
|
||||
);
|
||||
assert_eq!(response.plugin.apps[0].needs_auth, true);
|
||||
assert_eq!(response.plugin.mcp_servers.len(), 1);
|
||||
assert_eq!(response.plugin.mcp_servers[0], "demo");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_read_returns_app_needs_auth() -> Result<()> {
|
||||
let connectors = vec![
|
||||
AppInfo {
|
||||
id: "alpha".to_string(),
|
||||
name: "Alpha".to_string(),
|
||||
description: Some("Alpha connector".to_string()),
|
||||
logo_url: Some("https://example.com/alpha.png".to_string()),
|
||||
logo_url_dark: None,
|
||||
distribution_channel: Some("featured".to_string()),
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: None,
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
AppInfo {
|
||||
id: "beta".to_string(),
|
||||
name: "Beta".to_string(),
|
||||
description: Some("Beta connector".to_string()),
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: None,
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
];
|
||||
let tools = vec![connector_tool("beta", "Beta App")?];
|
||||
let (server_url, server_handle) = start_apps_server(connectors, tools).await?;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
write_connectors_config(codex_home.path(), &server_url)?;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("chatgpt-token")
|
||||
.account_id("account-123")
|
||||
.chatgpt_user_id("user-123")
|
||||
.chatgpt_account_id("account-123"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let repo_root = TempDir::new()?;
|
||||
write_plugin_marketplace(
|
||||
repo_root.path(),
|
||||
"debug",
|
||||
"sample-plugin",
|
||||
"./sample-plugin",
|
||||
)?;
|
||||
write_plugin_source(repo_root.path(), "sample-plugin", &["alpha", "beta"])?;
|
||||
let marketplace_path =
|
||||
AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_read_request(PluginReadParams {
|
||||
marketplace_path,
|
||||
plugin_name: "sample-plugin".to_string(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: PluginReadResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(
|
||||
response
|
||||
.plugin
|
||||
.apps
|
||||
.iter()
|
||||
.map(|app| (app.id.as_str(), app.needs_auth))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![("alpha", true), ("beta", false)]
|
||||
);
|
||||
|
||||
server_handle.abort();
|
||||
let _ = server_handle.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_read_accepts_legacy_string_default_prompt() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -232,6 +380,7 @@ async fn plugin_read_accepts_legacy_string_default_prompt() -> Result<()> {
|
||||
}
|
||||
}"##,
|
||||
)?;
|
||||
write_plugins_enabled_config(&codex_home)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
@@ -285,6 +434,7 @@ async fn plugin_read_returns_invalid_request_when_plugin_is_missing() -> Result<
|
||||
]
|
||||
}"#,
|
||||
)?;
|
||||
write_plugins_enabled_config(&codex_home)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
@@ -336,6 +486,7 @@ async fn plugin_read_returns_invalid_request_when_plugin_manifest_is_missing() -
|
||||
]
|
||||
}"#,
|
||||
)?;
|
||||
write_plugins_enabled_config(&codex_home)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
@@ -382,3 +533,211 @@ fn write_installed_plugin(
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_plugins_enabled_config(codex_home: &TempDir) -> Result<()> {
|
||||
std::fs::write(
|
||||
codex_home.path().join("config.toml"),
|
||||
r#"[features]
|
||||
plugins = true
|
||||
"#,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppsServerState {
|
||||
response: Arc<StdMutex<serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct PluginReadMcpServer {
|
||||
tools: Arc<StdMutex<Vec<Tool>>>,
|
||||
}
|
||||
|
||||
impl ServerHandler for PluginReadMcpServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo {
|
||||
capabilities: ServerCapabilities::builder().enable_tools().build(),
|
||||
..ServerInfo::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn list_tools(
|
||||
&self,
|
||||
_request: Option<rmcp::model::PaginatedRequestParams>,
|
||||
_context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
|
||||
) -> impl std::future::Future<Output = Result<ListToolsResult, rmcp::ErrorData>> + Send + '_
|
||||
{
|
||||
let tools = self.tools.clone();
|
||||
async move {
|
||||
let tools = tools
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone();
|
||||
Ok(ListToolsResult {
|
||||
tools,
|
||||
next_cursor: None,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_apps_server(
|
||||
connectors: Vec<AppInfo>,
|
||||
tools: Vec<Tool>,
|
||||
) -> Result<(String, JoinHandle<()>)> {
|
||||
let state = Arc::new(AppsServerState {
|
||||
response: Arc::new(StdMutex::new(
|
||||
json!({ "apps": connectors, "next_token": null }),
|
||||
)),
|
||||
});
|
||||
let tools = Arc::new(StdMutex::new(tools));
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||
let addr = listener.local_addr()?;
|
||||
let mcp_service = StreamableHttpService::new(
|
||||
{
|
||||
let tools = tools.clone();
|
||||
move || {
|
||||
Ok(PluginReadMcpServer {
|
||||
tools: tools.clone(),
|
||||
})
|
||||
}
|
||||
},
|
||||
Arc::new(LocalSessionManager::default()),
|
||||
StreamableHttpServerConfig::default(),
|
||||
);
|
||||
let router = Router::new()
|
||||
.route("/connectors/directory/list", get(list_directory_connectors))
|
||||
.route(
|
||||
"/connectors/directory/list_workspace",
|
||||
get(list_directory_connectors),
|
||||
)
|
||||
.with_state(state)
|
||||
.nest_service("/api/codex/apps", mcp_service);
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let _ = axum::serve(listener, router).await;
|
||||
});
|
||||
|
||||
Ok((format!("http://{addr}"), handle))
|
||||
}
|
||||
|
||||
async fn list_directory_connectors(
|
||||
State(state): State<Arc<AppsServerState>>,
|
||||
headers: HeaderMap,
|
||||
uri: Uri,
|
||||
) -> Result<impl axum::response::IntoResponse, StatusCode> {
|
||||
let bearer_ok = headers
|
||||
.get(AUTHORIZATION)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.is_some_and(|value| value == "Bearer chatgpt-token");
|
||||
let account_ok = headers
|
||||
.get("chatgpt-account-id")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.is_some_and(|value| value == "account-123");
|
||||
let external_logos_ok = uri
|
||||
.query()
|
||||
.is_some_and(|query| query.split('&').any(|pair| pair == "external_logos=true"));
|
||||
|
||||
if !bearer_ok || !account_ok {
|
||||
Err(StatusCode::UNAUTHORIZED)
|
||||
} else if !external_logos_ok {
|
||||
Err(StatusCode::BAD_REQUEST)
|
||||
} else {
|
||||
let response = state
|
||||
.response
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone();
|
||||
Ok(Json(response))
|
||||
}
|
||||
}
|
||||
|
||||
fn connector_tool(connector_id: &str, connector_name: &str) -> Result<Tool> {
|
||||
let schema: JsonObject = serde_json::from_value(json!({
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}))?;
|
||||
let mut tool = Tool::new(
|
||||
Cow::Owned(format!("connector_{connector_id}")),
|
||||
Cow::Borrowed("Connector test tool"),
|
||||
Arc::new(schema),
|
||||
);
|
||||
tool.annotations = Some(ToolAnnotations::new().read_only(true));
|
||||
|
||||
let mut meta = Meta::new();
|
||||
meta.0
|
||||
.insert("connector_id".to_string(), json!(connector_id));
|
||||
meta.0
|
||||
.insert("connector_name".to_string(), json!(connector_name));
|
||||
tool.meta = Some(meta);
|
||||
Ok(tool)
|
||||
}
|
||||
|
||||
fn write_connectors_config(codex_home: &std::path::Path, base_url: &str) -> std::io::Result<()> {
|
||||
std::fs::write(
|
||||
codex_home.join("config.toml"),
|
||||
format!(
|
||||
r#"
|
||||
chatgpt_base_url = "{base_url}"
|
||||
mcp_oauth_credentials_store = "file"
|
||||
|
||||
[features]
|
||||
plugins = true
|
||||
connectors = true
|
||||
"#
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn write_plugin_marketplace(
|
||||
repo_root: &std::path::Path,
|
||||
marketplace_name: &str,
|
||||
plugin_name: &str,
|
||||
source_path: &str,
|
||||
) -> std::io::Result<()> {
|
||||
std::fs::create_dir_all(repo_root.join(".git"))?;
|
||||
std::fs::create_dir_all(repo_root.join(".agents/plugins"))?;
|
||||
std::fs::write(
|
||||
repo_root.join(".agents/plugins/marketplace.json"),
|
||||
format!(
|
||||
r#"{{
|
||||
"name": "{marketplace_name}",
|
||||
"plugins": [
|
||||
{{
|
||||
"name": "{plugin_name}",
|
||||
"source": {{
|
||||
"source": "local",
|
||||
"path": "{source_path}"
|
||||
}}
|
||||
}}
|
||||
]
|
||||
}}"#
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn write_plugin_source(
|
||||
repo_root: &std::path::Path,
|
||||
plugin_name: &str,
|
||||
app_ids: &[&str],
|
||||
) -> Result<()> {
|
||||
let plugin_root = repo_root.join(plugin_name);
|
||||
std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;
|
||||
std::fs::write(
|
||||
plugin_root.join(".codex-plugin/plugin.json"),
|
||||
format!(r#"{{"name":"{plugin_name}"}}"#),
|
||||
)?;
|
||||
|
||||
let apps = app_ids
|
||||
.iter()
|
||||
.map(|app_id| ((*app_id).to_string(), json!({ "id": app_id })))
|
||||
.collect::<serde_json::Map<_, _>>();
|
||||
std::fs::write(
|
||||
plugin_root.join(".app.json"),
|
||||
serde_json::to_vec_pretty(&json!({ "apps": apps }))?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -183,21 +183,22 @@ async fn plugin_uninstall_tracks_analytics_event() -> Result<()> {
|
||||
let response: PluginUninstallResponse = to_response(response)?;
|
||||
assert_eq!(response, PluginUninstallResponse {});
|
||||
|
||||
let payloads = timeout(DEFAULT_TIMEOUT, async {
|
||||
let payload = timeout(DEFAULT_TIMEOUT, async {
|
||||
loop {
|
||||
let Some(requests) = analytics_server.received_requests().await else {
|
||||
tokio::time::sleep(Duration::from_millis(25)).await;
|
||||
continue;
|
||||
};
|
||||
if !requests.is_empty() {
|
||||
break requests;
|
||||
if let Some(request) = requests.iter().find(|request| {
|
||||
request.method == "POST" && request.url.path() == "/codex/analytics-events/events"
|
||||
}) {
|
||||
break request.body.clone();
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(25)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
let payload: serde_json::Value =
|
||||
serde_json::from_slice(&payloads[0].body).expect("analytics payload");
|
||||
let payload: serde_json::Value = serde_json::from_slice(&payload).expect("analytics payload");
|
||||
assert_eq!(
|
||||
payload,
|
||||
json!({
|
||||
|
||||
@@ -23,8 +23,8 @@ use codex_app_server_protocol::ThreadRealtimeStopParams;
|
||||
use codex_app_server_protocol::ThreadRealtimeStopResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_core::features::FEATURES;
|
||||
use codex_core::features::Feature;
|
||||
use codex_features::FEATURES;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::protocol::RealtimeConversationVersion;
|
||||
use core_test_support::responses::start_websocket_server;
|
||||
use core_test_support::skip_if_no_network;
|
||||
|
||||
439
codex-rs/app-server/tests/suite/v2/thread_shell_command.rs
Normal file
439
codex-rs/app-server/tests/suite/v2/thread_shell_command.rs
Normal file
@@ -0,0 +1,439 @@
|
||||
use anyhow::Result;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::create_final_assistant_message_sse_response;
|
||||
use app_test_support::create_mock_responses_server_sequence;
|
||||
use app_test_support::create_shell_command_sse_response;
|
||||
use app_test_support::format_with_current_shell_display;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::CommandExecutionApprovalDecision;
|
||||
use codex_app_server_protocol::CommandExecutionOutputDeltaNotification;
|
||||
use codex_app_server_protocol::CommandExecutionRequestApprovalResponse;
|
||||
use codex_app_server_protocol::CommandExecutionSource;
|
||||
use codex_app_server_protocol::CommandExecutionStatus;
|
||||
use codex_app_server_protocol::ItemCompletedNotification;
|
||||
use codex_app_server_protocol::ItemStartedNotification;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadReadParams;
|
||||
use codex_app_server_protocol::ThreadReadResponse;
|
||||
use codex_app_server_protocol::ThreadShellCommandParams;
|
||||
use codex_app_server_protocol::ThreadShellCommandResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnCompletedNotification;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use codex_features::FEATURES;
|
||||
use codex_features::Feature;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_shell_command_runs_as_standalone_turn_and_persists_history() -> Result<()> {
|
||||
let tmp = TempDir::new()?;
|
||||
let codex_home = tmp.path().join("codex_home");
|
||||
std::fs::create_dir(&codex_home)?;
|
||||
let workspace = tmp.path().join("workspace");
|
||||
std::fs::create_dir(&workspace)?;
|
||||
|
||||
let server = create_mock_responses_server_sequence(vec![]).await;
|
||||
create_config_toml(
|
||||
codex_home.as_path(),
|
||||
&server.uri(),
|
||||
"never",
|
||||
&BTreeMap::default(),
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.as_path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let start_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
persist_extended_history: true,
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let start_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(start_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
|
||||
|
||||
let shell_id = mcp
|
||||
.send_thread_shell_command_request(ThreadShellCommandParams {
|
||||
thread_id: thread.id.clone(),
|
||||
command: "printf 'hello from bang\\n'".to_string(),
|
||||
})
|
||||
.await?;
|
||||
let shell_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(shell_id)),
|
||||
)
|
||||
.await??;
|
||||
let _: ThreadShellCommandResponse = to_response::<ThreadShellCommandResponse>(shell_resp)?;
|
||||
|
||||
let started = wait_for_command_execution_started(&mut mcp, None).await?;
|
||||
let ThreadItem::CommandExecution {
|
||||
id, source, status, ..
|
||||
} = &started.item
|
||||
else {
|
||||
unreachable!("helper returns command execution item");
|
||||
};
|
||||
let command_id = id.clone();
|
||||
assert_eq!(source, &CommandExecutionSource::UserShell);
|
||||
assert_eq!(status, &CommandExecutionStatus::InProgress);
|
||||
|
||||
let delta = wait_for_command_execution_output_delta(&mut mcp, &command_id).await?;
|
||||
assert_eq!(delta.delta, "hello from bang\n");
|
||||
|
||||
let completed = wait_for_command_execution_completed(&mut mcp, Some(&command_id)).await?;
|
||||
let ThreadItem::CommandExecution {
|
||||
id,
|
||||
source,
|
||||
status,
|
||||
aggregated_output,
|
||||
exit_code,
|
||||
..
|
||||
} = &completed.item
|
||||
else {
|
||||
unreachable!("helper returns command execution item");
|
||||
};
|
||||
assert_eq!(id, &command_id);
|
||||
assert_eq!(source, &CommandExecutionSource::UserShell);
|
||||
assert_eq!(status, &CommandExecutionStatus::Completed);
|
||||
assert_eq!(aggregated_output.as_deref(), Some("hello from bang\n"));
|
||||
assert_eq!(*exit_code, Some(0));
|
||||
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let read_id = mcp
|
||||
.send_thread_read_request(ThreadReadParams {
|
||||
thread_id: thread.id,
|
||||
include_turns: true,
|
||||
})
|
||||
.await?;
|
||||
let read_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(read_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadReadResponse { thread } = to_response::<ThreadReadResponse>(read_resp)?;
|
||||
assert_eq!(thread.turns.len(), 1);
|
||||
let ThreadItem::CommandExecution {
|
||||
source,
|
||||
status,
|
||||
aggregated_output,
|
||||
..
|
||||
} = thread.turns[0]
|
||||
.items
|
||||
.iter()
|
||||
.find(|item| matches!(item, ThreadItem::CommandExecution { .. }))
|
||||
.expect("expected persisted command execution item")
|
||||
else {
|
||||
unreachable!("matched command execution item");
|
||||
};
|
||||
assert_eq!(source, &CommandExecutionSource::UserShell);
|
||||
assert_eq!(status, &CommandExecutionStatus::Completed);
|
||||
assert_eq!(aggregated_output.as_deref(), Some("hello from bang\n"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_shell_command_uses_existing_active_turn() -> Result<()> {
|
||||
let tmp = TempDir::new()?;
|
||||
let codex_home = tmp.path().join("codex_home");
|
||||
std::fs::create_dir(&codex_home)?;
|
||||
let workspace = tmp.path().join("workspace");
|
||||
std::fs::create_dir(&workspace)?;
|
||||
|
||||
let responses = vec![
|
||||
create_shell_command_sse_response(
|
||||
vec![
|
||||
"python3".to_string(),
|
||||
"-c".to_string(),
|
||||
"print(42)".to_string(),
|
||||
],
|
||||
None,
|
||||
Some(5000),
|
||||
"call-approve",
|
||||
)?,
|
||||
create_final_assistant_message_sse_response("done")?,
|
||||
];
|
||||
let server = create_mock_responses_server_sequence(responses).await;
|
||||
create_config_toml(
|
||||
codex_home.as_path(),
|
||||
&server.uri(),
|
||||
"untrusted",
|
||||
&BTreeMap::default(),
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.as_path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let start_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
persist_extended_history: true,
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let start_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(start_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
|
||||
|
||||
let turn_id = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "run python".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
cwd: Some(workspace.clone()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let turn_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_id)),
|
||||
)
|
||||
.await??;
|
||||
let TurnStartResponse { turn } = to_response::<TurnStartResponse>(turn_resp)?;
|
||||
|
||||
let agent_started = wait_for_command_execution_started(&mut mcp, Some("call-approve")).await?;
|
||||
let ThreadItem::CommandExecution {
|
||||
command, source, ..
|
||||
} = &agent_started.item
|
||||
else {
|
||||
unreachable!("helper returns command execution item");
|
||||
};
|
||||
assert_eq!(source, &CommandExecutionSource::Agent);
|
||||
assert_eq!(
|
||||
command,
|
||||
&format_with_current_shell_display("python3 -c 'print(42)'")
|
||||
);
|
||||
|
||||
let server_req = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_request_message(),
|
||||
)
|
||||
.await??;
|
||||
let ServerRequest::CommandExecutionRequestApproval { request_id, .. } = server_req else {
|
||||
panic!("expected approval request");
|
||||
};
|
||||
|
||||
let shell_id = mcp
|
||||
.send_thread_shell_command_request(ThreadShellCommandParams {
|
||||
thread_id: thread.id.clone(),
|
||||
command: "printf 'active turn bang\\n'".to_string(),
|
||||
})
|
||||
.await?;
|
||||
let shell_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(shell_id)),
|
||||
)
|
||||
.await??;
|
||||
let _: ThreadShellCommandResponse = to_response::<ThreadShellCommandResponse>(shell_resp)?;
|
||||
|
||||
let started =
|
||||
wait_for_command_execution_started_by_source(&mut mcp, CommandExecutionSource::UserShell)
|
||||
.await?;
|
||||
assert_eq!(started.turn_id, turn.id);
|
||||
let command_id = match &started.item {
|
||||
ThreadItem::CommandExecution { id, .. } => id.clone(),
|
||||
_ => unreachable!("helper returns command execution item"),
|
||||
};
|
||||
let completed = wait_for_command_execution_completed(&mut mcp, Some(&command_id)).await?;
|
||||
assert_eq!(completed.turn_id, turn.id);
|
||||
let ThreadItem::CommandExecution {
|
||||
source,
|
||||
aggregated_output,
|
||||
..
|
||||
} = &completed.item
|
||||
else {
|
||||
unreachable!("helper returns command execution item");
|
||||
};
|
||||
assert_eq!(source, &CommandExecutionSource::UserShell);
|
||||
assert_eq!(aggregated_output.as_deref(), Some("active turn bang\n"));
|
||||
|
||||
mcp.send_response(
|
||||
request_id,
|
||||
serde_json::to_value(CommandExecutionRequestApprovalResponse {
|
||||
decision: CommandExecutionApprovalDecision::Decline,
|
||||
})?,
|
||||
)
|
||||
.await?;
|
||||
let _: TurnCompletedNotification = serde_json::from_value(
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??
|
||||
.params
|
||||
.expect("turn/completed params"),
|
||||
)?;
|
||||
|
||||
let read_id = mcp
|
||||
.send_thread_read_request(ThreadReadParams {
|
||||
thread_id: thread.id,
|
||||
include_turns: true,
|
||||
})
|
||||
.await?;
|
||||
let read_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(read_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadReadResponse { thread } = to_response::<ThreadReadResponse>(read_resp)?;
|
||||
assert_eq!(thread.turns.len(), 1);
|
||||
assert!(
|
||||
thread.turns[0].items.iter().any(|item| {
|
||||
matches!(
|
||||
item,
|
||||
ThreadItem::CommandExecution {
|
||||
source: CommandExecutionSource::UserShell,
|
||||
aggregated_output,
|
||||
..
|
||||
} if aggregated_output.as_deref() == Some("active turn bang\n")
|
||||
)
|
||||
}),
|
||||
"expected active-turn shell command to be persisted on the existing turn"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn wait_for_command_execution_started(
|
||||
mcp: &mut McpProcess,
|
||||
expected_id: Option<&str>,
|
||||
) -> Result<ItemStartedNotification> {
|
||||
loop {
|
||||
let notif = mcp
|
||||
.read_stream_until_notification_message("item/started")
|
||||
.await?;
|
||||
let started: ItemStartedNotification = serde_json::from_value(
|
||||
notif
|
||||
.params
|
||||
.ok_or_else(|| anyhow::anyhow!("missing item/started params"))?,
|
||||
)?;
|
||||
let ThreadItem::CommandExecution { id, .. } = &started.item else {
|
||||
continue;
|
||||
};
|
||||
if expected_id.is_none() || expected_id == Some(id.as_str()) {
|
||||
return Ok(started);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_command_execution_started_by_source(
|
||||
mcp: &mut McpProcess,
|
||||
expected_source: CommandExecutionSource,
|
||||
) -> Result<ItemStartedNotification> {
|
||||
loop {
|
||||
let started = wait_for_command_execution_started(mcp, None).await?;
|
||||
let ThreadItem::CommandExecution { source, .. } = &started.item else {
|
||||
continue;
|
||||
};
|
||||
if source == &expected_source {
|
||||
return Ok(started);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_command_execution_completed(
|
||||
mcp: &mut McpProcess,
|
||||
expected_id: Option<&str>,
|
||||
) -> Result<ItemCompletedNotification> {
|
||||
loop {
|
||||
let notif = mcp
|
||||
.read_stream_until_notification_message("item/completed")
|
||||
.await?;
|
||||
let completed: ItemCompletedNotification = serde_json::from_value(
|
||||
notif
|
||||
.params
|
||||
.ok_or_else(|| anyhow::anyhow!("missing item/completed params"))?,
|
||||
)?;
|
||||
let ThreadItem::CommandExecution { id, .. } = &completed.item else {
|
||||
continue;
|
||||
};
|
||||
if expected_id.is_none() || expected_id == Some(id.as_str()) {
|
||||
return Ok(completed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_command_execution_output_delta(
|
||||
mcp: &mut McpProcess,
|
||||
item_id: &str,
|
||||
) -> Result<CommandExecutionOutputDeltaNotification> {
|
||||
loop {
|
||||
let notif = mcp
|
||||
.read_stream_until_notification_message("item/commandExecution/outputDelta")
|
||||
.await?;
|
||||
let delta: CommandExecutionOutputDeltaNotification = serde_json::from_value(
|
||||
notif
|
||||
.params
|
||||
.ok_or_else(|| anyhow::anyhow!("missing output delta params"))?,
|
||||
)?;
|
||||
if delta.item_id == item_id {
|
||||
return Ok(delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_config_toml(
|
||||
codex_home: &Path,
|
||||
server_uri: &str,
|
||||
approval_policy: &str,
|
||||
feature_flags: &BTreeMap<Feature, bool>,
|
||||
) -> std::io::Result<()> {
|
||||
let feature_entries = feature_flags
|
||||
.iter()
|
||||
.map(|(feature, enabled)| {
|
||||
let key = FEATURES
|
||||
.iter()
|
||||
.find(|spec| spec.id == *feature)
|
||||
.map(|spec| spec.key)
|
||||
.unwrap_or_else(|| panic!("missing feature key for {feature:?}"));
|
||||
format!("{key} = {enabled}")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
std::fs::write(
|
||||
codex_home.join("config.toml"),
|
||||
format!(
|
||||
r#"
|
||||
model = "mock-model"
|
||||
approval_policy = "{approval_policy}"
|
||||
sandbox_mode = "read-only"
|
||||
|
||||
model_provider = "mock_provider"
|
||||
|
||||
[features]
|
||||
{feature_entries}
|
||||
|
||||
[model_providers.mock_provider]
|
||||
name = "Mock provider for test"
|
||||
base_url = "{server_uri}/v1"
|
||||
wire_api = "responses"
|
||||
request_max_retries = 0
|
||||
stream_max_retries = 0
|
||||
"#
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -7,7 +7,10 @@ use app_test_support::write_chatgpt_auth;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::McpServerStartupState;
|
||||
use codex_app_server_protocol::McpServerStatusUpdatedNotification;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::ThreadStartedNotification;
|
||||
@@ -328,6 +331,103 @@ async fn thread_start_fails_when_required_mcp_server_fails_to_initialize() -> Re
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_emits_mcp_server_status_updated_notifications() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml_with_optional_broken_mcp(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let req_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams::default())
|
||||
.await?;
|
||||
|
||||
let _: ThreadStartResponse = to_response(
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(req_id)),
|
||||
)
|
||||
.await??,
|
||||
)?;
|
||||
|
||||
let starting = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_matching_notification(
|
||||
"mcpServer/startupStatus/updated starting",
|
||||
|notification| {
|
||||
notification.method == "mcpServer/startupStatus/updated"
|
||||
&& notification
|
||||
.params
|
||||
.as_ref()
|
||||
.and_then(|params| params.get("name"))
|
||||
.and_then(Value::as_str)
|
||||
== Some("optional_broken")
|
||||
&& notification
|
||||
.params
|
||||
.as_ref()
|
||||
.and_then(|params| params.get("status"))
|
||||
.and_then(Value::as_str)
|
||||
== Some("starting")
|
||||
},
|
||||
),
|
||||
)
|
||||
.await??;
|
||||
let starting: ServerNotification = starting.try_into()?;
|
||||
let ServerNotification::McpServerStatusUpdated(starting) = starting else {
|
||||
anyhow::bail!("unexpected notification variant");
|
||||
};
|
||||
assert_eq!(
|
||||
starting,
|
||||
McpServerStatusUpdatedNotification {
|
||||
name: "optional_broken".to_string(),
|
||||
status: McpServerStartupState::Starting,
|
||||
error: None,
|
||||
}
|
||||
);
|
||||
|
||||
let failed = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_matching_notification(
|
||||
"mcpServer/startupStatus/updated failed",
|
||||
|notification| {
|
||||
notification.method == "mcpServer/startupStatus/updated"
|
||||
&& notification
|
||||
.params
|
||||
.as_ref()
|
||||
.and_then(|params| params.get("name"))
|
||||
.and_then(Value::as_str)
|
||||
== Some("optional_broken")
|
||||
&& notification
|
||||
.params
|
||||
.as_ref()
|
||||
.and_then(|params| params.get("status"))
|
||||
.and_then(Value::as_str)
|
||||
== Some("failed")
|
||||
},
|
||||
),
|
||||
)
|
||||
.await??;
|
||||
let failed: ServerNotification = failed.try_into()?;
|
||||
let ServerNotification::McpServerStatusUpdated(failed) = failed else {
|
||||
anyhow::bail!("unexpected notification variant");
|
||||
};
|
||||
assert_eq!(failed.name, "optional_broken");
|
||||
assert_eq!(failed.status, McpServerStartupState::Failed);
|
||||
assert!(
|
||||
failed
|
||||
.error
|
||||
.as_deref()
|
||||
.is_some_and(|error| error.contains("MCP client for `optional_broken` failed to start")),
|
||||
"unexpected MCP startup error: {:?}",
|
||||
failed.error
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_surfaces_cloud_requirements_load_errors() -> Result<()> {
|
||||
let server = MockServer::start().await;
|
||||
@@ -491,3 +591,32 @@ required = true
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn create_config_toml_with_optional_broken_mcp(
|
||||
codex_home: &Path,
|
||||
server_uri: &str,
|
||||
) -> std::io::Result<()> {
|
||||
let config_toml = codex_home.join("config.toml");
|
||||
std::fs::write(
|
||||
config_toml,
|
||||
format!(
|
||||
r#"
|
||||
model = "mock-model"
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "read-only"
|
||||
|
||||
model_provider = "mock_provider"
|
||||
|
||||
[model_providers.mock_provider]
|
||||
name = "Mock provider for test"
|
||||
base_url = "{server_uri}/v1"
|
||||
wire_api = "responses"
|
||||
request_max_retries = 0
|
||||
stream_max_retries = 0
|
||||
|
||||
[mcp_servers.optional_broken]
|
||||
command = "codex-definitely-not-a-real-binary"
|
||||
"#
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ use codex_app_server::INPUT_TOO_LARGE_ERROR_CODE;
|
||||
use codex_app_server::INVALID_PARAMS_ERROR_CODE;
|
||||
use codex_app_server_protocol::ByteRange;
|
||||
use codex_app_server_protocol::ClientInfo;
|
||||
use codex_app_server_protocol::CollabAgentState;
|
||||
use codex_app_server_protocol::CollabAgentStatus;
|
||||
use codex_app_server_protocol::CollabAgentTool;
|
||||
use codex_app_server_protocol::CollabAgentToolCallStatus;
|
||||
@@ -45,9 +44,9 @@ use codex_app_server_protocol::TurnStartedNotification;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use codex_core::config::ConfigToml;
|
||||
use codex_core::features::FEATURES;
|
||||
use codex_core::features::Feature;
|
||||
use codex_core::personality_migration::PERSONALITY_MIGRATION_FILENAME;
|
||||
use codex_features::FEATURES;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Personality;
|
||||
@@ -1826,16 +1825,18 @@ async fn turn_start_emits_spawn_agent_item_with_model_metadata_v2() -> Result<()
|
||||
assert_eq!(prompt, Some(CHILD_PROMPT.to_string()));
|
||||
assert_eq!(model, Some(REQUESTED_MODEL.to_string()));
|
||||
assert_eq!(reasoning_effort, Some(REQUESTED_REASONING_EFFORT));
|
||||
assert_eq!(
|
||||
agents_states,
|
||||
HashMap::from([(
|
||||
receiver_thread_id,
|
||||
CollabAgentState {
|
||||
status: CollabAgentStatus::PendingInit,
|
||||
message: None,
|
||||
},
|
||||
)])
|
||||
let agent_state = agents_states
|
||||
.get(&receiver_thread_id)
|
||||
.expect("spawn completion should include child agent state");
|
||||
assert!(
|
||||
matches!(
|
||||
agent_state.status,
|
||||
CollabAgentStatus::PendingInit | CollabAgentStatus::Running
|
||||
),
|
||||
"child agent should still be initializing or already running, got {:?}",
|
||||
agent_state.status
|
||||
);
|
||||
assert_eq!(agent_state.message, None);
|
||||
|
||||
let turn_completed = timeout(DEFAULT_READ_TIMEOUT, async {
|
||||
loop {
|
||||
@@ -2008,16 +2009,18 @@ config_file = "./custom-role.toml"
|
||||
assert_eq!(prompt, Some(CHILD_PROMPT.to_string()));
|
||||
assert_eq!(model, Some(ROLE_MODEL.to_string()));
|
||||
assert_eq!(reasoning_effort, Some(ROLE_REASONING_EFFORT));
|
||||
assert_eq!(
|
||||
agents_states,
|
||||
HashMap::from([(
|
||||
receiver_thread_id,
|
||||
CollabAgentState {
|
||||
status: CollabAgentStatus::PendingInit,
|
||||
message: None,
|
||||
},
|
||||
)])
|
||||
let agent_state = agents_states
|
||||
.get(&receiver_thread_id)
|
||||
.expect("spawn completion should include child agent state");
|
||||
assert!(
|
||||
matches!(
|
||||
agent_state.status,
|
||||
CollabAgentStatus::PendingInit | CollabAgentStatus::Running
|
||||
),
|
||||
"child agent should still be initializing or already running, got {:?}",
|
||||
agent_state.status
|
||||
);
|
||||
assert_eq!(agent_state.message, None);
|
||||
|
||||
let turn_completed = timeout(DEFAULT_READ_TIMEOUT, async {
|
||||
loop {
|
||||
|
||||
@@ -30,8 +30,8 @@ use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use codex_core::features::FEATURES;
|
||||
use codex_core::features::Feature;
|
||||
use codex_features::FEATURES;
|
||||
use codex_features::Feature;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -30,6 +30,7 @@ codex-config = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-exec = { workspace = true }
|
||||
codex-execpolicy = { workspace = true }
|
||||
codex-features = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-mcp-server = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
@@ -37,6 +38,7 @@ codex-responses-api-proxy = { workspace = true }
|
||||
codex-rmcp-client = { workspace = true }
|
||||
codex-state = { workspace = true }
|
||||
codex-stdio-to-uds = { workspace = true }
|
||||
codex-terminal-detection = { workspace = true }
|
||||
codex-tui = { workspace = true }
|
||||
codex-tui-app-server = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
|
||||
@@ -170,7 +170,7 @@ async fn run_command_under_sandbox(
|
||||
command_vec,
|
||||
&cwd_clone,
|
||||
env_map,
|
||||
None,
|
||||
/*timeout_ms*/ None,
|
||||
config.permissions.windows_sandbox_private_desktop,
|
||||
)
|
||||
} else {
|
||||
@@ -181,7 +181,7 @@ async fn run_command_under_sandbox(
|
||||
command_vec,
|
||||
&cwd_clone,
|
||||
env_map,
|
||||
None,
|
||||
/*timeout_ms*/ None,
|
||||
config.permissions.windows_sandbox_private_desktop,
|
||||
)
|
||||
}
|
||||
@@ -251,15 +251,15 @@ async fn run_command_under_sandbox(
|
||||
&config.permissions.file_system_sandbox_policy,
|
||||
config.permissions.network_sandbox_policy,
|
||||
sandbox_policy_cwd.as_path(),
|
||||
false,
|
||||
/*enforce_managed_network*/ false,
|
||||
network.as_ref(),
|
||||
None,
|
||||
/*extensions*/ None,
|
||||
);
|
||||
let network_policy = config.permissions.network_sandbox_policy;
|
||||
spawn_debug_sandbox_child(
|
||||
PathBuf::from("/usr/bin/sandbox-exec"),
|
||||
args,
|
||||
None,
|
||||
/*arg0*/ None,
|
||||
cwd,
|
||||
network_policy,
|
||||
env,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user