mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +00:00
Merge d4740400cd into sapling-pr-archive-bolinfest
This commit is contained in:
@@ -42,9 +42,6 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
# stage_npm_packages.py requires DotSlash when staging releases.
|
||||
- uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
|
||||
|
||||
- name: Stage npm package
|
||||
id: stage_npm_package
|
||||
env:
|
||||
@@ -55,17 +52,13 @@ jobs:
|
||||
# cross-platform native payload required by the npm package layout.
|
||||
# Passing the workflow URL directly avoids relying on old rust-v*
|
||||
# branches remaining discoverable via `gh run list --branch ...`.
|
||||
CODEX_VERSION=0.125.0
|
||||
WORKFLOW_URL="https://github.com/openai/codex/actions/runs/26131514935"
|
||||
CODEX_VERSION=0.133.0-alpha.4
|
||||
WORKFLOW_URL="https://github.com/openai/codex/actions/runs/26201494185"
|
||||
OUTPUT_DIR="${RUNNER_TEMP}"
|
||||
# This reused workflow predates codex-package archive artifacts, so
|
||||
# CI synthesizes the package layout from the older per-binary
|
||||
# artifacts. Release staging must use real package archives.
|
||||
python3 ./scripts/stage_npm_packages.py \
|
||||
--release-version "$CODEX_VERSION" \
|
||||
--workflow-url "$WORKFLOW_URL" \
|
||||
--package codex \
|
||||
--allow-legacy-codex-package \
|
||||
--output-dir "$OUTPUT_DIR"
|
||||
PACK_OUTPUT="${OUTPUT_DIR}/codex-npm-${CODEX_VERSION}.tgz"
|
||||
echo "pack_output=$PACK_OUTPUT" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@openai/codex",
|
||||
"version": "0.0.0-dev",
|
||||
"description": "Codex CLI is a coding agent from OpenAI that runs locally on your computer.",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"codex": "bin/codex.js"
|
||||
@@ -10,7 +11,7 @@
|
||||
"node": ">=16"
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
"bin/codex.js",
|
||||
"vendor"
|
||||
],
|
||||
"repository": {
|
||||
|
||||
@@ -255,9 +255,6 @@ def stage_sources(staging_dir: Path, version: str, package: str) -> None:
|
||||
bin_dir = staging_dir / "bin"
|
||||
bin_dir.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(CODEX_CLI_ROOT / "bin" / "codex.js", bin_dir / "codex.js")
|
||||
rg_manifest = CODEX_CLI_ROOT / "bin" / "rg"
|
||||
if rg_manifest.exists():
|
||||
shutil.copy2(rg_manifest, bin_dir / "rg")
|
||||
|
||||
readme_src = REPO_ROOT / "README.md"
|
||||
if readme_src.exists():
|
||||
@@ -316,7 +313,7 @@ def stage_sources(staging_dir: Path, version: str, package: str) -> None:
|
||||
package_json["version"] = version
|
||||
|
||||
if package == "codex":
|
||||
package_json["files"] = ["bin"]
|
||||
package_json["files"] = ["bin/codex.js"]
|
||||
package_json["optionalDependencies"] = {
|
||||
CODEX_PLATFORM_PACKAGES[platform_package]["npm_name"]: (
|
||||
f"npm:{CODEX_NPM_NAME}@"
|
||||
|
||||
@@ -3,26 +3,20 @@
|
||||
|
||||
import argparse
|
||||
from contextlib import contextmanager
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
import zipfile
|
||||
from dataclasses import dataclass
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import Iterable, Sequence
|
||||
from urllib.parse import urlparse
|
||||
from urllib.request import urlopen
|
||||
from typing import Sequence
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
CODEX_CLI_ROOT = SCRIPT_DIR.parent
|
||||
DEFAULT_WORKFLOW_URL = "https://github.com/openai/codex/actions/runs/26131514935" # rust-v0.132.0
|
||||
DEFAULT_WORKFLOW_URL = "https://github.com/openai/codex/actions/runs/26201494185" # rust-v0.133.0-alpha.4
|
||||
VENDOR_DIR_NAME = "vendor"
|
||||
RG_MANIFEST = CODEX_CLI_ROOT / "bin" / "rg"
|
||||
BINARY_TARGETS = (
|
||||
"x86_64-unknown-linux-musl",
|
||||
"aarch64-unknown-linux-musl",
|
||||
@@ -39,57 +33,16 @@ class BinaryComponent:
|
||||
artifact_prefix: str # matches the artifact filename prefix (e.g. codex-<target>.zst)
|
||||
dest_dir: str # directory under vendor/<target>/ where the binary is installed
|
||||
binary_basename: str # executable name inside dest_dir (before optional .exe)
|
||||
targets: tuple[str, ...] | None = None # limit installation to specific targets
|
||||
|
||||
|
||||
WINDOWS_TARGETS = tuple(target for target in BINARY_TARGETS if "windows" in target)
|
||||
LINUX_TARGETS = tuple(target for target in BINARY_TARGETS if "linux" in target)
|
||||
|
||||
BINARY_COMPONENTS = {
|
||||
"bwrap": BinaryComponent(
|
||||
artifact_prefix="bwrap",
|
||||
dest_dir="codex-resources",
|
||||
binary_basename="bwrap",
|
||||
targets=LINUX_TARGETS,
|
||||
),
|
||||
"codex": BinaryComponent(
|
||||
artifact_prefix="codex",
|
||||
dest_dir="codex",
|
||||
binary_basename="codex",
|
||||
),
|
||||
"codex-responses-api-proxy": BinaryComponent(
|
||||
artifact_prefix="codex-responses-api-proxy",
|
||||
dest_dir="codex-responses-api-proxy",
|
||||
binary_basename="codex-responses-api-proxy",
|
||||
),
|
||||
"codex-windows-sandbox-setup": BinaryComponent(
|
||||
artifact_prefix="codex-windows-sandbox-setup",
|
||||
dest_dir="codex",
|
||||
binary_basename="codex-windows-sandbox-setup",
|
||||
targets=WINDOWS_TARGETS,
|
||||
),
|
||||
"codex-command-runner": BinaryComponent(
|
||||
artifact_prefix="codex-command-runner",
|
||||
dest_dir="codex",
|
||||
binary_basename="codex-command-runner",
|
||||
targets=WINDOWS_TARGETS,
|
||||
),
|
||||
}
|
||||
|
||||
RG_TARGET_PLATFORM_PAIRS: list[tuple[str, str]] = [
|
||||
("x86_64-unknown-linux-musl", "linux-x86_64"),
|
||||
("aarch64-unknown-linux-musl", "linux-aarch64"),
|
||||
("x86_64-apple-darwin", "macos-x86_64"),
|
||||
("aarch64-apple-darwin", "macos-aarch64"),
|
||||
("x86_64-pc-windows-msvc", "windows-x86_64"),
|
||||
("aarch64-pc-windows-msvc", "windows-aarch64"),
|
||||
]
|
||||
RG_TARGET_TO_PLATFORM = {target: platform for target, platform in RG_TARGET_PLATFORM_PAIRS}
|
||||
DEFAULT_RG_TARGETS = [target for target, _ in RG_TARGET_PLATFORM_PAIRS]
|
||||
|
||||
# urllib.request.urlopen() defaults to no timeout (can hang indefinitely), which is painful in CI.
|
||||
DOWNLOAD_TIMEOUT_SECS = 60
|
||||
|
||||
|
||||
def _gha_enabled() -> bool:
|
||||
# GitHub Actions supports "workflow commands" (e.g. ::group:: / ::error::) that make logs
|
||||
@@ -140,22 +93,12 @@ def parse_args() -> argparse.Namespace:
|
||||
"--component",
|
||||
dest="components",
|
||||
action="append",
|
||||
choices=tuple([CODEX_PACKAGE_COMPONENT, *BINARY_COMPONENTS, "rg"]),
|
||||
choices=tuple([CODEX_PACKAGE_COMPONENT, *BINARY_COMPONENTS]),
|
||||
help=(
|
||||
"Limit installation to the specified components."
|
||||
" May be repeated. Defaults to codex-package and codex-responses-api-proxy."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--allow-legacy-codex-package",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Allow codex-package to be synthesized from legacy per-binary artifacts "
|
||||
"when package archives are missing. Intended for CI compatibility only; "
|
||||
"release staging should not use this. Automatically enabled for the "
|
||||
"built-in default workflow."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"root",
|
||||
nargs="?",
|
||||
@@ -178,7 +121,6 @@ def main() -> int:
|
||||
components = args.components or [CODEX_PACKAGE_COMPONENT, "codex-responses-api-proxy"]
|
||||
|
||||
workflow_override = (args.workflow_url or "").strip()
|
||||
use_default_workflow = not workflow_override
|
||||
workflow_url = workflow_override or DEFAULT_WORKFLOW_URL
|
||||
|
||||
workflow_id = workflow_url.rstrip("/").split("/")[-1]
|
||||
@@ -189,28 +131,13 @@ def main() -> int:
|
||||
artifacts_dir = Path(artifacts_dir_str)
|
||||
_download_artifacts(workflow_id, artifacts_dir)
|
||||
if CODEX_PACKAGE_COMPONENT in components:
|
||||
try:
|
||||
install_codex_package_archives(artifacts_dir, vendor_dir, BINARY_TARGETS)
|
||||
except FileNotFoundError:
|
||||
if not (args.allow_legacy_codex_package or use_default_workflow):
|
||||
raise
|
||||
install_legacy_codex_package_layouts(
|
||||
artifacts_dir,
|
||||
vendor_dir,
|
||||
BINARY_TARGETS,
|
||||
manifest_path=RG_MANIFEST,
|
||||
)
|
||||
install_codex_package_archives(artifacts_dir, vendor_dir, BINARY_TARGETS)
|
||||
install_binary_components(
|
||||
artifacts_dir,
|
||||
vendor_dir,
|
||||
[BINARY_COMPONENTS[name] for name in components if name in BINARY_COMPONENTS],
|
||||
)
|
||||
|
||||
if "rg" in components:
|
||||
with _gha_group("Fetch ripgrep binaries"):
|
||||
print("Fetching ripgrep binaries...")
|
||||
fetch_rg(vendor_dir, DEFAULT_RG_TARGETS, manifest_path=RG_MANIFEST)
|
||||
|
||||
print(f"Installed native dependencies into {vendor_dir}")
|
||||
return 0
|
||||
|
||||
@@ -262,156 +189,6 @@ def _install_single_codex_package_archive(
|
||||
return dest_dir
|
||||
|
||||
|
||||
def install_legacy_codex_package_layouts(
|
||||
artifacts_dir: Path,
|
||||
vendor_dir: Path,
|
||||
targets: Sequence[str],
|
||||
*,
|
||||
manifest_path: Path,
|
||||
) -> None:
|
||||
targets = list(targets)
|
||||
print(
|
||||
"Synthesizing Codex package layouts from legacy artifacts for targets: "
|
||||
+ ", ".join(targets)
|
||||
)
|
||||
with tempfile.TemporaryDirectory(prefix="codex-legacy-package-") as legacy_vendor_dir_str:
|
||||
legacy_vendor_dir = Path(legacy_vendor_dir_str)
|
||||
install_binary_components(
|
||||
artifacts_dir,
|
||||
legacy_vendor_dir,
|
||||
[
|
||||
BINARY_COMPONENTS["codex"],
|
||||
BINARY_COMPONENTS["bwrap"],
|
||||
BINARY_COMPONENTS["codex-windows-sandbox-setup"],
|
||||
BINARY_COMPONENTS["codex-command-runner"],
|
||||
],
|
||||
)
|
||||
fetch_rg(legacy_vendor_dir, targets, manifest_path=manifest_path)
|
||||
|
||||
for target in targets:
|
||||
dest_dir = vendor_dir / target
|
||||
if dest_dir.exists():
|
||||
shutil.rmtree(dest_dir)
|
||||
_build_legacy_codex_package_layout(legacy_vendor_dir / target, dest_dir, target)
|
||||
print(f" synthesized {dest_dir}")
|
||||
|
||||
|
||||
def _build_legacy_codex_package_layout(
|
||||
legacy_target_dir: Path,
|
||||
package_dir: Path,
|
||||
target: str,
|
||||
) -> None:
|
||||
is_windows = "windows" in target
|
||||
exe_suffix = ".exe" if is_windows else ""
|
||||
package_dir.mkdir(parents=True)
|
||||
|
||||
bin_dir = package_dir / "bin"
|
||||
resources_dir = package_dir / "codex-resources"
|
||||
path_dir = package_dir / "codex-path"
|
||||
bin_dir.mkdir()
|
||||
resources_dir.mkdir()
|
||||
path_dir.mkdir()
|
||||
|
||||
shutil.copy2(
|
||||
legacy_target_dir / "codex" / f"codex{exe_suffix}",
|
||||
bin_dir / f"codex{exe_suffix}",
|
||||
)
|
||||
shutil.copy2(
|
||||
legacy_target_dir / "path" / f"rg{exe_suffix}",
|
||||
path_dir / f"rg{exe_suffix}",
|
||||
)
|
||||
|
||||
if is_windows:
|
||||
for helper in [
|
||||
"codex-command-runner.exe",
|
||||
"codex-windows-sandbox-setup.exe",
|
||||
]:
|
||||
shutil.copy2(legacy_target_dir / "codex" / helper, resources_dir / helper)
|
||||
elif "linux" in target:
|
||||
shutil.copy2(legacy_target_dir / "codex-resources" / "bwrap", resources_dir / "bwrap")
|
||||
|
||||
write_json(
|
||||
package_dir / "codex-package.json",
|
||||
{
|
||||
"layoutVersion": 1,
|
||||
"version": "unknown",
|
||||
"target": target,
|
||||
"variant": "codex",
|
||||
"entrypoint": f"bin/codex{exe_suffix}",
|
||||
"resourcesDir": "codex-resources",
|
||||
"pathDir": "codex-path",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def fetch_rg(
|
||||
vendor_dir: Path,
|
||||
targets: Sequence[str] | None = None,
|
||||
*,
|
||||
manifest_path: Path,
|
||||
) -> list[Path]:
|
||||
"""Download ripgrep binaries described by the DotSlash manifest."""
|
||||
|
||||
if targets is None:
|
||||
targets = DEFAULT_RG_TARGETS
|
||||
|
||||
if not manifest_path.exists():
|
||||
raise FileNotFoundError(f"DotSlash manifest not found: {manifest_path}")
|
||||
|
||||
manifest = _load_manifest(manifest_path)
|
||||
platforms = manifest.get("platforms", {})
|
||||
|
||||
vendor_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
targets = list(targets)
|
||||
if not targets:
|
||||
return []
|
||||
|
||||
task_configs: list[tuple[str, str, dict]] = []
|
||||
for target in targets:
|
||||
platform_key = RG_TARGET_TO_PLATFORM.get(target)
|
||||
if platform_key is None:
|
||||
raise ValueError(f"Unsupported ripgrep target '{target}'.")
|
||||
|
||||
platform_info = platforms.get(platform_key)
|
||||
if platform_info is None:
|
||||
raise RuntimeError(f"Platform '{platform_key}' not found in manifest {manifest_path}.")
|
||||
|
||||
task_configs.append((target, platform_key, platform_info))
|
||||
|
||||
results: dict[str, Path] = {}
|
||||
max_workers = min(len(task_configs), max(1, (os.cpu_count() or 1)))
|
||||
|
||||
print("Installing ripgrep binaries for targets: " + ", ".join(targets))
|
||||
|
||||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
future_map = {
|
||||
executor.submit(
|
||||
_fetch_single_rg,
|
||||
vendor_dir,
|
||||
target,
|
||||
platform_key,
|
||||
platform_info,
|
||||
manifest_path,
|
||||
): target
|
||||
for target, platform_key, platform_info in task_configs
|
||||
}
|
||||
|
||||
for future in as_completed(future_map):
|
||||
target = future_map[future]
|
||||
try:
|
||||
results[target] = future.result()
|
||||
except Exception as exc:
|
||||
_gha_error(
|
||||
title="ripgrep install failed",
|
||||
message=f"target={target} error={exc!r}",
|
||||
)
|
||||
raise RuntimeError(f"Failed to install ripgrep for target {target}.") from exc
|
||||
print(f" installed ripgrep for {target}")
|
||||
|
||||
return [results[target] for target in targets]
|
||||
|
||||
|
||||
def _download_artifacts(workflow_id: str, dest_dir: Path) -> None:
|
||||
cmd = [
|
||||
"gh",
|
||||
@@ -435,7 +212,7 @@ def install_binary_components(
|
||||
return
|
||||
|
||||
for component in selected_components:
|
||||
component_targets = list(component.targets or BINARY_TARGETS)
|
||||
component_targets = list(BINARY_TARGETS)
|
||||
|
||||
print(
|
||||
f"Installing {component.binary_basename} binaries for targets: "
|
||||
@@ -465,7 +242,7 @@ def _install_single_binary(
|
||||
component: BinaryComponent,
|
||||
) -> Path:
|
||||
artifact_subdir = artifact_dir_for_target(artifacts_dir, target)
|
||||
archive_path = legacy_binary_archive_path(artifact_subdir, component.artifact_prefix, target)
|
||||
archive_path = binary_archive_path(artifact_subdir, component.artifact_prefix, target)
|
||||
|
||||
dest_dir = vendor_dir / target / component.dest_dir
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -475,7 +252,7 @@ def _install_single_binary(
|
||||
)
|
||||
dest = dest_dir / binary_name
|
||||
dest.unlink(missing_ok=True)
|
||||
extract_archive(archive_path, "zst", None, dest)
|
||||
extract_zstd_archive(archive_path, dest)
|
||||
if "windows" not in target:
|
||||
dest.chmod(0o755)
|
||||
return dest
|
||||
@@ -487,7 +264,7 @@ def _archive_name_for_target(artifact_prefix: str, target: str) -> str:
|
||||
return f"{artifact_prefix}-{target}.zst"
|
||||
|
||||
|
||||
def legacy_binary_archive_path(artifact_dir: Path, artifact_prefix: str, target: str) -> Path:
|
||||
def binary_archive_path(artifact_dir: Path, artifact_prefix: str, target: str) -> Path:
|
||||
archive_names = [_archive_name_for_target(artifact_prefix, target)]
|
||||
if artifact_dir.name == f"{target}-unsigned":
|
||||
archive_names.append(_archive_name_for_target(artifact_prefix, f"{target}-unsigned"))
|
||||
@@ -509,142 +286,12 @@ def artifact_dir_for_target(artifacts_dir: Path, target: str) -> Path:
|
||||
return artifacts_dir / target
|
||||
|
||||
|
||||
def _fetch_single_rg(
|
||||
vendor_dir: Path,
|
||||
target: str,
|
||||
platform_key: str,
|
||||
platform_info: dict,
|
||||
manifest_path: Path,
|
||||
) -> Path:
|
||||
providers = platform_info.get("providers", [])
|
||||
if not providers:
|
||||
raise RuntimeError(f"No providers listed for platform '{platform_key}' in {manifest_path}.")
|
||||
|
||||
url = providers[0]["url"]
|
||||
archive_format = platform_info.get("format", "zst")
|
||||
archive_member = platform_info.get("path")
|
||||
digest = platform_info.get("digest")
|
||||
expected_size = platform_info.get("size")
|
||||
|
||||
dest_dir = vendor_dir / target / "path"
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
is_windows = platform_key.startswith("win")
|
||||
binary_name = "rg.exe" if is_windows else "rg"
|
||||
dest = dest_dir / binary_name
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp_dir_str:
|
||||
tmp_dir = Path(tmp_dir_str)
|
||||
archive_filename = os.path.basename(urlparse(url).path)
|
||||
download_path = tmp_dir / archive_filename
|
||||
print(
|
||||
f" downloading ripgrep for {target} ({platform_key}) from {url}",
|
||||
flush=True,
|
||||
)
|
||||
try:
|
||||
_download_file(url, download_path)
|
||||
except Exception as exc:
|
||||
_gha_error(
|
||||
title="ripgrep download failed",
|
||||
message=f"target={target} platform={platform_key} url={url} error={exc!r}",
|
||||
)
|
||||
raise RuntimeError(
|
||||
"Failed to download ripgrep "
|
||||
f"(target={target}, platform={platform_key}, format={archive_format}, "
|
||||
f"expected_size={expected_size!r}, digest={digest!r}, url={url}, dest={download_path})."
|
||||
) from exc
|
||||
|
||||
dest.unlink(missing_ok=True)
|
||||
try:
|
||||
extract_archive(download_path, archive_format, archive_member, dest)
|
||||
except Exception as exc:
|
||||
raise RuntimeError(
|
||||
"Failed to extract ripgrep "
|
||||
f"(target={target}, platform={platform_key}, format={archive_format}, "
|
||||
f"member={archive_member!r}, url={url}, archive={download_path})."
|
||||
) from exc
|
||||
|
||||
if not is_windows:
|
||||
dest.chmod(0o755)
|
||||
|
||||
return dest
|
||||
|
||||
|
||||
def _download_file(url: str, dest: Path) -> None:
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
dest.unlink(missing_ok=True)
|
||||
|
||||
with urlopen(url, timeout=DOWNLOAD_TIMEOUT_SECS) as response, open(dest, "wb") as out:
|
||||
shutil.copyfileobj(response, out)
|
||||
|
||||
|
||||
def extract_archive(
|
||||
archive_path: Path,
|
||||
archive_format: str,
|
||||
archive_member: str | None,
|
||||
dest: Path,
|
||||
) -> None:
|
||||
def extract_zstd_archive(archive_path: Path, dest: Path) -> None:
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if archive_format == "zst":
|
||||
output_path = archive_path.parent / dest.name
|
||||
subprocess.check_call(
|
||||
["zstd", "-f", "-d", str(archive_path), "-o", str(output_path)]
|
||||
)
|
||||
shutil.move(str(output_path), dest)
|
||||
return
|
||||
|
||||
if archive_format == "tar.gz":
|
||||
if not archive_member:
|
||||
raise RuntimeError("Missing 'path' for tar.gz archive in DotSlash manifest.")
|
||||
with tarfile.open(archive_path, "r:gz") as tar:
|
||||
try:
|
||||
member = tar.getmember(archive_member)
|
||||
except KeyError as exc:
|
||||
raise RuntimeError(
|
||||
f"Entry '{archive_member}' not found in archive {archive_path}."
|
||||
) from exc
|
||||
tar.extract(member, path=archive_path.parent, filter="data")
|
||||
extracted = archive_path.parent / archive_member
|
||||
shutil.move(str(extracted), dest)
|
||||
return
|
||||
|
||||
if archive_format == "zip":
|
||||
if not archive_member:
|
||||
raise RuntimeError("Missing 'path' for zip archive in DotSlash manifest.")
|
||||
with zipfile.ZipFile(archive_path) as archive:
|
||||
try:
|
||||
with archive.open(archive_member) as src, open(dest, "wb") as out:
|
||||
shutil.copyfileobj(src, out)
|
||||
except KeyError as exc:
|
||||
raise RuntimeError(
|
||||
f"Entry '{archive_member}' not found in archive {archive_path}."
|
||||
) from exc
|
||||
return
|
||||
|
||||
raise RuntimeError(f"Unsupported archive format '{archive_format}'.")
|
||||
|
||||
|
||||
def _load_manifest(manifest_path: Path) -> dict:
|
||||
cmd = ["dotslash", "--", "parse", str(manifest_path)]
|
||||
stdout = subprocess.check_output(cmd, text=True)
|
||||
try:
|
||||
manifest = json.loads(stdout)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise RuntimeError(f"Invalid DotSlash manifest output from {manifest_path}.") from exc
|
||||
|
||||
if not isinstance(manifest, dict):
|
||||
raise RuntimeError(
|
||||
f"Unexpected DotSlash manifest structure for {manifest_path}: {type(manifest)!r}"
|
||||
)
|
||||
|
||||
return manifest
|
||||
|
||||
|
||||
def write_json(path: Path, value: object) -> None:
|
||||
with open(path, "w", encoding="utf-8") as out:
|
||||
json.dump(value, out, indent=2)
|
||||
out.write("\n")
|
||||
output_path = archive_path.parent / dest.name
|
||||
subprocess.check_call(["zstd", "-f", "-d", str(archive_path), "-o", str(output_path)])
|
||||
shutil.move(str(output_path), dest)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
BUILD_SCRIPT = Path(__file__).resolve().parent / "build_npm_package.py"
|
||||
SPEC = importlib.util.spec_from_file_location("codex_build_npm_package", BUILD_SCRIPT)
|
||||
if SPEC is None or SPEC.loader is None:
|
||||
raise RuntimeError(f"Unable to load module from {BUILD_SCRIPT}")
|
||||
|
||||
build_npm_package = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(build_npm_package)
|
||||
|
||||
|
||||
class StageSourcesTest(unittest.TestCase):
|
||||
def test_codex_meta_package_omits_ripgrep_manifest(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
staging_dir = Path(temp_dir)
|
||||
|
||||
build_npm_package.stage_sources(staging_dir, "0.1.0", "codex")
|
||||
|
||||
with open(staging_dir / "package.json", encoding="utf-8") as fh:
|
||||
package_json = json.load(fh)
|
||||
|
||||
self.assertTrue((staging_dir / "bin" / "codex.js").is_file())
|
||||
self.assertFalse((staging_dir / "bin" / "rg").exists())
|
||||
self.assertEqual(package_json["files"], ["bin/codex.js"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from contextlib import redirect_stdout
|
||||
import importlib.util
|
||||
import io
|
||||
from pathlib import Path
|
||||
import tarfile
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
INSTALL_SCRIPT = Path(__file__).resolve().parent / "install_native_deps.py"
|
||||
SPEC = importlib.util.spec_from_file_location("install_native_deps", INSTALL_SCRIPT)
|
||||
if SPEC is None or SPEC.loader is None:
|
||||
raise RuntimeError(f"Unable to load module from {INSTALL_SCRIPT}")
|
||||
|
||||
install_native_deps = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(install_native_deps)
|
||||
|
||||
|
||||
class InstallCodexPackageArchivesTest(unittest.TestCase):
|
||||
def test_installs_codex_package_archive(self) -> None:
|
||||
target = "x86_64-unknown-linux-musl"
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir)
|
||||
artifact_dir = root / "artifacts" / target
|
||||
package_src = root / "package-src"
|
||||
vendor_dir = root / "vendor"
|
||||
artifact_dir.mkdir(parents=True)
|
||||
(package_src / "bin").mkdir(parents=True)
|
||||
(package_src / "bin" / "codex").write_text("codex\n", encoding="utf-8")
|
||||
(package_src / "codex-package.json").write_text("{}\n", encoding="utf-8")
|
||||
|
||||
archive_path = artifact_dir / f"codex-package-{target}.tar.gz"
|
||||
with tarfile.open(archive_path, "w:gz") as archive:
|
||||
archive.add(package_src / "bin", arcname="bin")
|
||||
archive.add(package_src / "codex-package.json", arcname="codex-package.json")
|
||||
|
||||
with redirect_stdout(io.StringIO()):
|
||||
install_native_deps.install_codex_package_archives(
|
||||
root / "artifacts",
|
||||
vendor_dir,
|
||||
[target],
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(
|
||||
path.relative_to(vendor_dir / target)
|
||||
for path in (vendor_dir / target).rglob("*")
|
||||
),
|
||||
[
|
||||
Path("bin"),
|
||||
Path("bin/codex"),
|
||||
Path("codex-package.json"),
|
||||
],
|
||||
)
|
||||
|
||||
def test_missing_codex_package_archive_errors(self) -> None:
|
||||
target = "x86_64-unknown-linux-musl"
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir)
|
||||
|
||||
with redirect_stdout(io.StringIO()):
|
||||
with self.assertRaisesRegex(
|
||||
FileNotFoundError,
|
||||
"Expected package archive not found",
|
||||
):
|
||||
install_native_deps.install_codex_package_archives(
|
||||
root / "artifacts",
|
||||
root / "vendor",
|
||||
[target],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Generated
+1
@@ -2957,6 +2957,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"codex-core",
|
||||
"codex-extension-api",
|
||||
"codex-protocol",
|
||||
"codex-state",
|
||||
|
||||
@@ -340,12 +340,11 @@ use codex_login::complete_device_code_login;
|
||||
use codex_login::login_with_api_key;
|
||||
use codex_login::request_device_code;
|
||||
use codex_login::run_login_server;
|
||||
use codex_mcp::McpRuntimeEnvironment;
|
||||
use codex_mcp::McpRuntimeContext;
|
||||
use codex_mcp::McpServerStatusSnapshot;
|
||||
use codex_mcp::McpSnapshotDetail;
|
||||
use codex_mcp::collect_mcp_server_status_snapshot_with_detail;
|
||||
use codex_mcp::discover_supported_scopes;
|
||||
use codex_mcp::effective_mcp_servers;
|
||||
use codex_mcp::read_mcp_resource as read_mcp_resource_without_thread;
|
||||
use codex_mcp::resolve_oauth_scopes;
|
||||
use codex_memories_write::clear_memory_roots_contents;
|
||||
|
||||
@@ -171,7 +171,7 @@ impl AppsRequestProcessor {
|
||||
connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
&accessible_config,
|
||||
force_refetch,
|
||||
&environment_manager,
|
||||
Arc::clone(&environment_manager),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| format!("failed to load accessible apps: {err}"));
|
||||
|
||||
@@ -220,7 +220,7 @@ impl ConfigRequestProcessor {
|
||||
connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
&config,
|
||||
/*force_refetch*/ true,
|
||||
&environment_manager,
|
||||
Arc::clone(&environment_manager),
|
||||
),
|
||||
);
|
||||
let all_connectors = match all_connectors_result {
|
||||
|
||||
@@ -205,24 +205,20 @@ impl McpRequestProcessor {
|
||||
.await;
|
||||
let auth = self.auth_manager.auth().await;
|
||||
let environment_manager = self.thread_manager.environment_manager();
|
||||
// Status listing has no turn cwd. Prefer the configured default env,
|
||||
// then configured local if present; do not manufacture a hidden local
|
||||
// env in no-local modes.
|
||||
let runtime_environment = McpRuntimeEnvironment::new(
|
||||
environment_manager.default_or_local_environment(),
|
||||
environment_manager.try_local_environment(),
|
||||
config.cwd.to_path_buf(),
|
||||
);
|
||||
// This threadless status path has no turn cwd or turn-selected
|
||||
// environment. Use config cwd only as the local stdio fallback; named
|
||||
// environment stdio MCPs must declare their own absolute cwd.
|
||||
let runtime_context =
|
||||
McpRuntimeContext::new(Arc::clone(&environment_manager), config.cwd.to_path_buf());
|
||||
|
||||
tokio::spawn(async move {
|
||||
Self::list_mcp_server_status_task(
|
||||
outgoing,
|
||||
request,
|
||||
params,
|
||||
config,
|
||||
mcp_config,
|
||||
auth,
|
||||
runtime_environment,
|
||||
runtime_context,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
@@ -233,18 +229,16 @@ impl McpRequestProcessor {
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ListMcpServerStatusParams,
|
||||
config: Config,
|
||||
mcp_config: codex_mcp::McpConfig,
|
||||
auth: Option<CodexAuth>,
|
||||
runtime_environment: McpRuntimeEnvironment,
|
||||
runtime_context: McpRuntimeContext,
|
||||
) {
|
||||
let result = Self::list_mcp_server_status_response(
|
||||
request_id.request_id.to_string(),
|
||||
params,
|
||||
config,
|
||||
mcp_config,
|
||||
auth,
|
||||
runtime_environment,
|
||||
runtime_context,
|
||||
)
|
||||
.await;
|
||||
outgoing.send_result(request_id, result).await;
|
||||
@@ -253,10 +247,9 @@ impl McpRequestProcessor {
|
||||
async fn list_mcp_server_status_response(
|
||||
request_id: String,
|
||||
params: ListMcpServerStatusParams,
|
||||
config: Config,
|
||||
mcp_config: codex_mcp::McpConfig,
|
||||
auth: Option<CodexAuth>,
|
||||
runtime_environment: McpRuntimeEnvironment,
|
||||
runtime_context: McpRuntimeContext,
|
||||
) -> Result<ListMcpServerStatusResponse, JSONRPCErrorError> {
|
||||
let detail = match params.detail.unwrap_or(McpServerStatusDetail::Full) {
|
||||
McpServerStatusDetail::Full => McpSnapshotDetail::Full,
|
||||
@@ -267,31 +260,25 @@ impl McpRequestProcessor {
|
||||
&mcp_config,
|
||||
auth.as_ref(),
|
||||
request_id,
|
||||
runtime_environment,
|
||||
runtime_context,
|
||||
detail,
|
||||
)
|
||||
.await;
|
||||
|
||||
let effective_servers = effective_mcp_servers(&mcp_config, auth.as_ref());
|
||||
let McpServerStatusSnapshot {
|
||||
tools_by_server,
|
||||
resources,
|
||||
resource_templates,
|
||||
auth_statuses,
|
||||
mut server_names,
|
||||
} = snapshot;
|
||||
|
||||
let mut server_names: Vec<String> = config
|
||||
.mcp_servers
|
||||
.keys()
|
||||
.cloned()
|
||||
// Include runtime-added/plugin MCP servers that are present in the
|
||||
// effective runtime config even when they are not user-declared in
|
||||
// `config.mcp_servers`.
|
||||
.chain(effective_servers.keys().cloned())
|
||||
.chain(auth_statuses.keys().cloned())
|
||||
.chain(resources.keys().cloned())
|
||||
.chain(resource_templates.keys().cloned())
|
||||
.collect();
|
||||
server_names.extend(
|
||||
auth_statuses
|
||||
.keys()
|
||||
.cloned()
|
||||
.chain(resources.keys().cloned())
|
||||
.chain(resource_templates.keys().cloned()),
|
||||
);
|
||||
server_names.sort();
|
||||
server_names.dedup();
|
||||
|
||||
@@ -367,21 +354,18 @@ impl McpRequestProcessor {
|
||||
.await;
|
||||
let auth = self.auth_manager.auth().await;
|
||||
let environment_manager = self.thread_manager.environment_manager();
|
||||
// Resource reads without a thread have no turn cwd. Prefer the
|
||||
// configured default env, then configured local if present; do not
|
||||
// manufacture a hidden local env in no-local modes.
|
||||
let runtime_environment = McpRuntimeEnvironment::new(
|
||||
environment_manager.default_or_local_environment(),
|
||||
environment_manager.try_local_environment(),
|
||||
config.cwd.to_path_buf(),
|
||||
);
|
||||
// This threadless resource-read path has no turn cwd or turn-selected
|
||||
// environment. Use config cwd only as the local stdio fallback; named
|
||||
// environment stdio MCPs must declare their own absolute cwd.
|
||||
let runtime_context =
|
||||
McpRuntimeContext::new(Arc::clone(&environment_manager), config.cwd.to_path_buf());
|
||||
let request_id = request_id.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let result = read_mcp_resource_without_thread(
|
||||
&mcp_config,
|
||||
auth.as_ref(),
|
||||
runtime_environment,
|
||||
runtime_context,
|
||||
&server,
|
||||
&uri,
|
||||
)
|
||||
|
||||
@@ -959,9 +959,12 @@ impl PluginRequestProcessor {
|
||||
None => None,
|
||||
};
|
||||
let environment_manager = self.thread_manager.environment_manager();
|
||||
let app_summaries =
|
||||
load_plugin_app_summaries(&config, &outcome.plugin.apps, &environment_manager)
|
||||
.await;
|
||||
let app_summaries = load_plugin_app_summaries(
|
||||
&config,
|
||||
&outcome.plugin.apps,
|
||||
Arc::clone(&environment_manager),
|
||||
)
|
||||
.await;
|
||||
let visible_skills = outcome
|
||||
.plugin
|
||||
.skills
|
||||
@@ -1037,8 +1040,12 @@ impl PluginRequestProcessor {
|
||||
.map(codex_plugin::AppConnectorId)
|
||||
.collect::<Vec<_>>();
|
||||
let environment_manager = self.thread_manager.environment_manager();
|
||||
let app_summaries =
|
||||
load_plugin_app_summaries(&config, &plugin_apps, &environment_manager).await;
|
||||
let app_summaries = load_plugin_app_summaries(
|
||||
&config,
|
||||
&plugin_apps,
|
||||
Arc::clone(&environment_manager),
|
||||
)
|
||||
.await;
|
||||
remote_plugin_detail_to_info(remote_detail, app_summaries)
|
||||
}
|
||||
};
|
||||
@@ -1506,7 +1513,7 @@ impl PluginRequestProcessor {
|
||||
connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
config,
|
||||
/*force_refetch*/ true,
|
||||
&environment_manager
|
||||
Arc::clone(&environment_manager)
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1772,7 +1779,7 @@ impl PluginRequestProcessor {
|
||||
async fn load_plugin_app_summaries(
|
||||
config: &Config,
|
||||
plugin_apps: &[codex_plugin::AppConnectorId],
|
||||
environment_manager: &EnvironmentManager,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
) -> Vec<AppSummary> {
|
||||
if plugin_apps.is_empty() {
|
||||
return Vec::new();
|
||||
|
||||
@@ -1477,7 +1477,7 @@ fn profile_v2_for_subcommand<'a>(
|
||||
subcommand: DebugSubcommand::PromptInput(_),
|
||||
}) => Ok(Some(profile_v2)),
|
||||
_ => anyhow::bail!(
|
||||
"--profile-v2 only applies to runtime commands: `codex`, `codex exec`, `codex review`, `codex resume`, `codex fork`, and `codex debug prompt-input`."
|
||||
"--profile only applies to runtime commands: `codex`, `codex exec`, `codex review`, `codex resume`, `codex fork`, and `codex debug prompt-input`."
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -2286,21 +2286,19 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn profile_v2_is_rejected_for_config_management_subcommands() {
|
||||
assert!(
|
||||
profile_v2_for_args(&["codex", "--profile-v2", "work", "features", "list"]).is_err()
|
||||
);
|
||||
assert!(profile_v2_for_args(&["codex", "--profile", "work", "features", "list"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_v2_is_allowed_for_runtime_subcommands() {
|
||||
assert_eq!(
|
||||
profile_v2_for_args(&["codex", "--profile-v2", "work", "resume"])
|
||||
profile_v2_for_args(&["codex", "--profile", "work", "resume"])
|
||||
.expect("resume supports profile-v2")
|
||||
.as_deref(),
|
||||
Some("work")
|
||||
);
|
||||
assert_eq!(
|
||||
profile_v2_for_args(&["codex", "--profile-v2", "work", "debug", "prompt-input"])
|
||||
profile_v2_for_args(&["codex", "--profile", "work", "debug", "prompt-input"])
|
||||
.expect("debug prompt-input supports profile-v2")
|
||||
.as_deref(),
|
||||
Some("work")
|
||||
@@ -2310,8 +2308,7 @@ mod tests {
|
||||
#[test]
|
||||
fn profile_v2_rejects_non_plain_names_at_parse_time() {
|
||||
assert!(
|
||||
MultitoolCli::try_parse_from(["codex", "--profile-v2", "nested/work", "resume"])
|
||||
.is_err()
|
||||
MultitoolCli::try_parse_from(["codex", "--profile", "nested/work", "resume"]).is_err()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2762,8 +2759,6 @@ mod tests {
|
||||
"-m",
|
||||
"gpt-5.1-test",
|
||||
"-p",
|
||||
"my-profile",
|
||||
"--profile-v2",
|
||||
"my-config",
|
||||
"-C",
|
||||
"/tmp",
|
||||
@@ -2776,7 +2771,7 @@ mod tests {
|
||||
|
||||
assert_eq!(interactive.model.as_deref(), Some("gpt-5.1-test"));
|
||||
assert!(interactive.oss);
|
||||
assert_eq!(interactive.config_profile.as_deref(), Some("my-profile"));
|
||||
assert_eq!(interactive.config_profile.as_deref(), None);
|
||||
assert_eq!(interactive.config_profile_v2.as_deref(), Some("my-config"));
|
||||
assert_matches!(
|
||||
interactive.sandbox_mode,
|
||||
|
||||
@@ -301,7 +301,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
|
||||
|
||||
let new_entry = McpServerConfig {
|
||||
transport: transport.clone(),
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
|
||||
@@ -27,7 +27,7 @@ use crate::rmcp_client::MCP_TOOLS_LIST_DURATION_METRIC;
|
||||
use crate::rmcp_client::ManagedClient;
|
||||
use crate::rmcp_client::StartupOutcomeError;
|
||||
use crate::rmcp_client::list_tools_for_client_uncached;
|
||||
use crate::runtime::McpRuntimeEnvironment;
|
||||
use crate::runtime::McpRuntimeContext;
|
||||
use crate::runtime::emit_duration;
|
||||
use crate::server::EffectiveMcpServer;
|
||||
use crate::server::McpServerMetadata;
|
||||
@@ -176,7 +176,7 @@ impl McpConnectionManager {
|
||||
submit_id: String,
|
||||
tx_event: Sender<Event>,
|
||||
initial_permission_profile: PermissionProfile,
|
||||
runtime_environment: McpRuntimeEnvironment,
|
||||
runtime_context: McpRuntimeContext,
|
||||
codex_home: PathBuf,
|
||||
codex_apps_tools_cache_key: CodexAppsToolsCacheKey,
|
||||
host_owned_codex_apps_enabled: bool,
|
||||
@@ -248,7 +248,7 @@ impl McpConnectionManager {
|
||||
elicitation_requests.clone(),
|
||||
codex_apps_tools_cache_context,
|
||||
Arc::clone(&tool_plugin_provenance),
|
||||
runtime_environment.clone(),
|
||||
runtime_context.clone(),
|
||||
runtime_auth_provider,
|
||||
client_elicitation_capability.clone(),
|
||||
);
|
||||
|
||||
@@ -18,6 +18,7 @@ use crate::tools::normalize_tools_for_model;
|
||||
use crate::tools::tool_with_model_visible_input_schema;
|
||||
use codex_config::Constrained;
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_protocol::ToolName;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::GranularApprovalConfig;
|
||||
@@ -864,7 +865,7 @@ async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() {
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -889,7 +890,7 @@ async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() {
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -915,9 +916,8 @@ async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() {
|
||||
String::new(),
|
||||
tx_event,
|
||||
PermissionProfile::default(),
|
||||
McpRuntimeEnvironment::new(
|
||||
/*environment*/ None,
|
||||
/*local_environment*/ None,
|
||||
McpRuntimeContext::new(
|
||||
Arc::new(EnvironmentManager::without_environments()),
|
||||
PathBuf::from("/tmp"),
|
||||
),
|
||||
codex_home.path().to_path_buf(),
|
||||
@@ -988,7 +988,7 @@ fn mcp_init_error_display_prompts_for_github_pat() {
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -1041,7 +1041,7 @@ fn mcp_init_error_display_reports_generic_errors() {
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
|
||||
@@ -3,7 +3,7 @@ pub use elicitation::ElicitationReviewRequest;
|
||||
pub use elicitation::ElicitationReviewer;
|
||||
pub use elicitation::ElicitationReviewerHandle;
|
||||
pub use rmcp_client::MCP_SANDBOX_STATE_META_CAPABILITY;
|
||||
pub use runtime::McpRuntimeEnvironment;
|
||||
pub use runtime::McpRuntimeContext;
|
||||
pub use runtime::SandboxState;
|
||||
pub use tools::ToolInfo;
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ use serde_json::Value;
|
||||
|
||||
use crate::codex_apps::codex_apps_tools_cache_key;
|
||||
use crate::connection_manager::McpConnectionManager;
|
||||
use crate::runtime::McpRuntimeEnvironment;
|
||||
use crate::runtime::McpRuntimeContext;
|
||||
use crate::server::EffectiveMcpServer;
|
||||
|
||||
pub const CODEX_APPS_MCP_SERVER_NAME: &str = "codex_apps";
|
||||
@@ -261,7 +261,7 @@ pub fn tool_plugin_provenance(config: &McpConfig) -> ToolPluginProvenance {
|
||||
pub async fn read_mcp_resource(
|
||||
config: &McpConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
runtime_environment: McpRuntimeEnvironment,
|
||||
runtime_context: McpRuntimeContext,
|
||||
server: &str,
|
||||
uri: &str,
|
||||
) -> anyhow::Result<ReadResourceResult> {
|
||||
@@ -284,7 +284,7 @@ pub async fn read_mcp_resource(
|
||||
String::new(),
|
||||
tx_event,
|
||||
PermissionProfile::default(),
|
||||
runtime_environment,
|
||||
runtime_context,
|
||||
config.codex_home.clone(),
|
||||
codex_apps_tools_cache_key(auth),
|
||||
host_owned_codex_apps_enabled,
|
||||
@@ -314,13 +314,14 @@ pub struct McpServerStatusSnapshot {
|
||||
pub resources: HashMap<String, Vec<Resource>>,
|
||||
pub resource_templates: HashMap<String, Vec<ResourceTemplate>>,
|
||||
pub auth_statuses: HashMap<String, McpAuthStatus>,
|
||||
pub server_names: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn collect_mcp_server_status_snapshot_with_detail(
|
||||
config: &McpConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
submit_id: String,
|
||||
runtime_environment: McpRuntimeEnvironment,
|
||||
runtime_context: McpRuntimeContext,
|
||||
detail: McpSnapshotDetail,
|
||||
) -> McpServerStatusSnapshot {
|
||||
let mcp_servers = effective_mcp_servers(config, auth);
|
||||
@@ -332,6 +333,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
|
||||
resources: HashMap::new(),
|
||||
resource_templates: HashMap::new(),
|
||||
auth_statuses: HashMap::new(),
|
||||
server_names: Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -342,6 +344,8 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
|
||||
)
|
||||
.await;
|
||||
|
||||
let server_names = mcp_servers.keys().cloned().collect();
|
||||
|
||||
let (tx_event, rx_event) = unbounded();
|
||||
drop(rx_event);
|
||||
|
||||
@@ -353,7 +357,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
|
||||
submit_id,
|
||||
tx_event,
|
||||
PermissionProfile::default(),
|
||||
runtime_environment,
|
||||
runtime_context,
|
||||
config.codex_home.clone(),
|
||||
codex_apps_tools_cache_key(auth),
|
||||
host_owned_codex_apps_enabled,
|
||||
@@ -367,6 +371,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
|
||||
let snapshot = collect_mcp_server_status_snapshot_from_manager(
|
||||
&mcp_connection_manager,
|
||||
auth_status_entries,
|
||||
server_names,
|
||||
detail,
|
||||
)
|
||||
.await;
|
||||
@@ -451,7 +456,7 @@ fn codex_apps_mcp_server_config(config: &McpConfig) -> McpServerConfig {
|
||||
http_headers,
|
||||
env_http_headers: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -575,6 +580,7 @@ fn convert_mcp_resource_templates(
|
||||
async fn collect_mcp_server_status_snapshot_from_manager(
|
||||
mcp_connection_manager: &McpConnectionManager,
|
||||
auth_status_entries: HashMap<String, crate::mcp::auth::McpAuthStatusEntry>,
|
||||
server_names: Vec<String>,
|
||||
detail: McpSnapshotDetail,
|
||||
) -> McpServerStatusSnapshot {
|
||||
let (tools, resources, resource_templates) = tokio::join!(
|
||||
@@ -613,6 +619,7 @@ async fn collect_mcp_server_status_snapshot_from_manager(
|
||||
resources: convert_mcp_resources(resources),
|
||||
resource_templates: convert_mcp_resource_templates(resource_templates),
|
||||
auth_statuses: auth_statuses_from_entries(&auth_status_entries),
|
||||
server_names,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -316,7 +316,7 @@ async fn effective_mcp_servers_preserve_user_servers_and_add_codex_apps() {
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -341,7 +341,7 @@ async fn effective_mcp_servers_preserve_user_servers_and_add_codex_apps() {
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
|
||||
@@ -28,7 +28,7 @@ use crate::codex_apps::write_cached_codex_apps_tools_if_needed;
|
||||
use crate::elicitation::ElicitationRequestManager;
|
||||
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use crate::mcp::ToolPluginProvenance;
|
||||
use crate::runtime::McpRuntimeEnvironment;
|
||||
use crate::runtime::McpRuntimeContext;
|
||||
use crate::runtime::emit_duration;
|
||||
use crate::server::EffectiveMcpServer;
|
||||
use crate::server::McpServerLaunch;
|
||||
@@ -142,7 +142,7 @@ impl AsyncManagedClient {
|
||||
elicitation_requests: ElicitationRequestManager,
|
||||
codex_apps_tools_cache_context: Option<CodexAppsToolsCacheContext>,
|
||||
tool_plugin_provenance: Arc<ToolPluginProvenance>,
|
||||
runtime_environment: McpRuntimeEnvironment,
|
||||
runtime_context: McpRuntimeContext,
|
||||
runtime_auth_provider: Option<SharedAuthProvider>,
|
||||
client_elicitation_capability: ElicitationCapability,
|
||||
) -> Self {
|
||||
@@ -170,7 +170,7 @@ impl AsyncManagedClient {
|
||||
&server_name,
|
||||
server.clone(),
|
||||
store_mode,
|
||||
runtime_environment,
|
||||
runtime_context,
|
||||
runtime_auth_provider,
|
||||
)
|
||||
.await?,
|
||||
@@ -560,29 +560,17 @@ async fn make_rmcp_client(
|
||||
server_name: &str,
|
||||
server: EffectiveMcpServer,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
runtime_environment: McpRuntimeEnvironment,
|
||||
runtime_context: McpRuntimeContext,
|
||||
runtime_auth_provider: Option<SharedAuthProvider>,
|
||||
) -> Result<RmcpClient, StartupOutcomeError> {
|
||||
let config = match server.launch() {
|
||||
McpServerLaunch::Configured(config) => config.as_ref().clone(),
|
||||
};
|
||||
if let Some(reason) = runtime_environment.startup_unavailable_reason(server_name, &config) {
|
||||
return Err(StartupOutcomeError::from(anyhow!(reason)));
|
||||
}
|
||||
let McpServerConfig {
|
||||
transport,
|
||||
experimental_environment,
|
||||
..
|
||||
} = config;
|
||||
let remote_environment = match experimental_environment.as_deref() {
|
||||
None | Some("local") => false,
|
||||
Some("remote") => true,
|
||||
Some(environment) => {
|
||||
return Err(StartupOutcomeError::from(anyhow!(
|
||||
"unsupported experimental_environment `{environment}` for MCP server `{server_name}`"
|
||||
)));
|
||||
}
|
||||
};
|
||||
let resolved_environment = runtime_context
|
||||
.resolve_server_environment(server_name, &config)
|
||||
.map_err(|err| StartupOutcomeError::from(anyhow!(err)))?;
|
||||
let is_local_environment = config.is_local_environment();
|
||||
let McpServerConfig { transport, .. } = config;
|
||||
|
||||
match transport {
|
||||
McpServerTransportConfig::Stdio {
|
||||
@@ -599,27 +587,24 @@ async fn make_rmcp_client(
|
||||
.map(|(key, value)| (key.into(), value.into()))
|
||||
.collect::<HashMap<_, _>>()
|
||||
});
|
||||
let launcher = if remote_environment {
|
||||
// Preflight should reject this first, but keep client startup
|
||||
// defensive if optional runtime placement is mis-threaded.
|
||||
let Some(environment) = runtime_environment.environment() else {
|
||||
return Err(StartupOutcomeError::from(anyhow!(
|
||||
"remote MCP server requires a runtime environment"
|
||||
)));
|
||||
let launcher = if is_local_environment {
|
||||
// TODO(starr): Unify local stdio MCP launch with
|
||||
// `ExecutorStdioServerLauncher` once the executor-backed path
|
||||
// preserves `LocalStdioServerLauncher` semantics.
|
||||
Arc::new(LocalStdioServerLauncher::new(
|
||||
runtime_context.local_stdio_fallback_cwd(),
|
||||
)) as Arc<dyn StdioServerLauncher>
|
||||
} else {
|
||||
let Some(environment) = resolved_environment.as_ref() else {
|
||||
unreachable!(
|
||||
"non-local stdio MCP servers resolve an environment before launch"
|
||||
);
|
||||
};
|
||||
Arc::new(ExecutorStdioServerLauncher::new(
|
||||
environment.get_exec_backend(),
|
||||
runtime_environment.fallback_cwd(),
|
||||
))
|
||||
} else {
|
||||
Arc::new(LocalStdioServerLauncher::new(
|
||||
runtime_environment.fallback_cwd(),
|
||||
)) as Arc<dyn StdioServerLauncher>
|
||||
};
|
||||
|
||||
// `RmcpClient` always sees a launched MCP stdio server. The
|
||||
// launcher hides whether that means a local child process or an
|
||||
// executor process whose stdin/stdout bytes cross the process API.
|
||||
RmcpClient::new_stdio_client(command_os, args_os, env_os, &env_vars, cwd, launcher)
|
||||
.await
|
||||
.map_err(|err| StartupOutcomeError::from(anyhow!(err)))
|
||||
@@ -630,16 +615,10 @@ async fn make_rmcp_client(
|
||||
env_http_headers,
|
||||
bearer_token_env_var,
|
||||
} => {
|
||||
let http_client: Arc<dyn HttpClient> = if remote_environment {
|
||||
let Some(environment) = runtime_environment.environment() else {
|
||||
return Err(StartupOutcomeError::from(anyhow!(
|
||||
"remote MCP server requires a runtime environment"
|
||||
)));
|
||||
};
|
||||
environment.get_http_client()
|
||||
} else {
|
||||
Arc::new(ReqwestHttpClient)
|
||||
};
|
||||
let http_client = resolved_environment.as_ref().map_or_else(
|
||||
|| Arc::new(ReqwestHttpClient) as Arc<dyn HttpClient>,
|
||||
|environment| environment.get_http_client(),
|
||||
);
|
||||
let resolved_bearer_token =
|
||||
match resolve_bearer_token(server_name, bearer_token_env_var.as_deref()) {
|
||||
Ok(token) => token,
|
||||
|
||||
+164
-133
@@ -10,6 +10,7 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_exec_server::Environment;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
|
||||
@@ -28,83 +29,87 @@ pub struct SandboxState {
|
||||
pub use_legacy_landlock: bool,
|
||||
}
|
||||
|
||||
/// Runtime placement information used when starting MCP server transports.
|
||||
/// Runtime context used when resolving per-server MCP environments.
|
||||
///
|
||||
/// `McpConfig` describes what servers exist. This value describes which
|
||||
/// selected/default environment MCP servers should use for the current caller.
|
||||
/// Keep it explicit at manager construction time so status/snapshot paths and
|
||||
/// real sessions make the same placement decision. `fallback_cwd` is not a
|
||||
/// per-server override; it is used when a stdio server omits `cwd` and the
|
||||
/// launcher needs a concrete process working directory. `local_environment`
|
||||
/// is separate because a remote selected/default environment can coexist with
|
||||
/// an explicitly configured local environment that may launch local stdio MCPs.
|
||||
/// `McpConfig` describes what servers exist. This value carries the canonical
|
||||
/// environment registry plus the local stdio fallback cwd used when a local
|
||||
/// stdio server omits its own working directory.
|
||||
#[derive(Clone)]
|
||||
pub struct McpRuntimeEnvironment {
|
||||
environment: Option<Arc<Environment>>,
|
||||
local_environment: Option<Arc<Environment>>,
|
||||
fallback_cwd: PathBuf,
|
||||
pub struct McpRuntimeContext {
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
local_stdio_fallback_cwd: PathBuf,
|
||||
}
|
||||
|
||||
impl McpRuntimeEnvironment {
|
||||
impl McpRuntimeContext {
|
||||
pub fn new(
|
||||
environment: Option<Arc<Environment>>,
|
||||
local_environment: Option<Arc<Environment>>,
|
||||
fallback_cwd: PathBuf,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
local_stdio_fallback_cwd: PathBuf,
|
||||
) -> Self {
|
||||
Self {
|
||||
environment,
|
||||
local_environment,
|
||||
fallback_cwd,
|
||||
environment_manager,
|
||||
local_stdio_fallback_cwd,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn environment(&self) -> Option<Arc<Environment>> {
|
||||
self.environment.as_ref().map(Arc::clone)
|
||||
pub(crate) fn local_stdio_fallback_cwd(&self) -> PathBuf {
|
||||
self.local_stdio_fallback_cwd.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn fallback_cwd(&self) -> PathBuf {
|
||||
self.fallback_cwd.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn startup_unavailable_reason(
|
||||
pub(crate) fn resolve_server_environment(
|
||||
&self,
|
||||
server_name: &str,
|
||||
config: &codex_config::McpServerConfig,
|
||||
) -> Option<String> {
|
||||
// This is intentionally narrower than "no env means no MCP": local
|
||||
// stdio needs a local process launcher, while local HTTP can still use
|
||||
// the ambient HTTP client with no local environment configured.
|
||||
match config.experimental_environment.as_deref() {
|
||||
None | Some("local") => {
|
||||
// Local stdio only needs an explicitly configured local
|
||||
// launcher. The selected/default MCP environment can be remote
|
||||
// when both local and remote environments are configured.
|
||||
if self.local_environment.is_none()
|
||||
&& matches!(
|
||||
config.transport,
|
||||
codex_config::McpServerTransportConfig::Stdio { .. }
|
||||
)
|
||||
{
|
||||
Some(format!(
|
||||
"local stdio MCP server `{server_name}` requires a local environment"
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
) -> Result<Option<Arc<Environment>>, String> {
|
||||
// Resolve `"local"` through the shared registry when available. Local
|
||||
// HTTP is the one current exception: it can use the ambient HTTP client
|
||||
// even when no local Environment is configured.
|
||||
if let Some(environment) = self
|
||||
.environment_manager
|
||||
.get_environment(&config.environment_id)
|
||||
{
|
||||
if !config.is_local_environment() {
|
||||
ensure_remote_stdio_cwd(server_name, config)?;
|
||||
}
|
||||
Some("remote") => match self.environment.as_ref() {
|
||||
Some(environment) if environment.is_remote() => None,
|
||||
_ => Some(format!(
|
||||
"remote MCP server `{server_name}` requires a remote environment"
|
||||
)),
|
||||
},
|
||||
Some(environment) => Some(format!(
|
||||
"unsupported experimental_environment `{environment}` for MCP server `{server_name}`"
|
||||
)),
|
||||
return Ok(Some(environment));
|
||||
}
|
||||
|
||||
if config.is_local_environment() {
|
||||
return match config.transport {
|
||||
codex_config::McpServerTransportConfig::Stdio { .. } => Err(format!(
|
||||
"local stdio MCP server `{server_name}` requires a local environment"
|
||||
)),
|
||||
codex_config::McpServerTransportConfig::StreamableHttp { .. } => Ok(None),
|
||||
};
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"MCP server `{server_name}` references unknown environment id `{}`",
|
||||
config.environment_id
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_remote_stdio_cwd(
|
||||
server_name: &str,
|
||||
config: &codex_config::McpServerConfig,
|
||||
) -> Result<(), String> {
|
||||
let codex_config::McpServerTransportConfig::Stdio { cwd, .. } = &config.transport else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(cwd) = cwd else {
|
||||
return Err(format!(
|
||||
"remote stdio MCP server `{server_name}` requires an absolute cwd"
|
||||
));
|
||||
};
|
||||
if cwd.is_absolute() {
|
||||
return Ok(());
|
||||
}
|
||||
Err(format!(
|
||||
"remote stdio MCP server `{server_name}` requires an absolute cwd, got `{}`",
|
||||
cwd.display()
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn emit_duration(metric: &str, duration: Duration, tags: &[(&str, &str)]) {
|
||||
if let Some(metrics) = codex_otel::global() {
|
||||
let _ = metrics.record_duration(metric, duration, tags);
|
||||
@@ -115,13 +120,15 @@ pub(crate) fn emit_duration(metric: &str, duration: Duration, tags: &[(&str, &st
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID;
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_config::McpServerTransportConfig;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn stdio_server(experimental_environment: Option<&str>) -> McpServerConfig {
|
||||
fn stdio_server(environment_id: &str) -> McpServerConfig {
|
||||
McpServerConfig {
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "echo".to_string(),
|
||||
@@ -130,7 +137,7 @@ mod tests {
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: experimental_environment.map(str::to_string),
|
||||
environment_id: environment_id.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -147,7 +154,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn http_server(experimental_environment: Option<&str>) -> McpServerConfig {
|
||||
fn http_server(environment_id: &str) -> McpServerConfig {
|
||||
McpServerConfig {
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "http://127.0.0.1:1".to_string(),
|
||||
@@ -155,111 +162,135 @@ mod tests {
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
experimental_environment: experimental_environment.map(str::to_string),
|
||||
..stdio_server(/*experimental_environment*/ None)
|
||||
environment_id: environment_id.to_string(),
|
||||
..stdio_server(environment_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_stdio_requires_local_stdio_availability() {
|
||||
let runtime_environment = McpRuntimeEnvironment::new(
|
||||
/*environment*/ None,
|
||||
/*local_environment*/ None,
|
||||
let runtime_context = McpRuntimeContext::new(
|
||||
Arc::new(EnvironmentManager::without_environments()),
|
||||
PathBuf::from("/tmp"),
|
||||
);
|
||||
|
||||
let error = match runtime_context
|
||||
.resolve_server_environment("stdio", &stdio_server(DEFAULT_MCP_SERVER_ENVIRONMENT_ID))
|
||||
{
|
||||
Ok(_) => panic!("local stdio MCP should require a local environment"),
|
||||
Err(error) => error,
|
||||
};
|
||||
assert_eq!(
|
||||
runtime_environment.startup_unavailable_reason(
|
||||
"stdio",
|
||||
&stdio_server(/*experimental_environment*/ None)
|
||||
),
|
||||
Some("local stdio MCP server `stdio` requires a local environment".to_string())
|
||||
error,
|
||||
"local stdio MCP server `stdio` requires a local environment"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_http_does_not_require_local_stdio_availability() {
|
||||
let runtime_environment = McpRuntimeEnvironment::new(
|
||||
/*environment*/ None,
|
||||
/*local_environment*/ None,
|
||||
let runtime_context = McpRuntimeContext::new(
|
||||
Arc::new(EnvironmentManager::without_environments()),
|
||||
PathBuf::from("/tmp"),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
runtime_environment.startup_unavailable_reason(
|
||||
"http",
|
||||
&http_server(/*experimental_environment*/ None)
|
||||
),
|
||||
None
|
||||
);
|
||||
let resolved_runtime = match runtime_context
|
||||
.resolve_server_environment("http", &http_server(DEFAULT_MCP_SERVER_ENVIRONMENT_ID))
|
||||
{
|
||||
Ok(resolved_runtime) => resolved_runtime,
|
||||
Err(error) => panic!("local HTTP MCP should resolve: {error}"),
|
||||
};
|
||||
assert!(resolved_runtime.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_stdio_requires_remote_environment() {
|
||||
let runtime_environment = McpRuntimeEnvironment::new(
|
||||
/*environment*/ None,
|
||||
/*local_environment*/ None,
|
||||
fn unknown_explicit_environment_is_rejected() {
|
||||
let runtime_context = McpRuntimeContext::new(
|
||||
Arc::new(EnvironmentManager::without_environments()),
|
||||
PathBuf::from("/tmp"),
|
||||
);
|
||||
|
||||
let error =
|
||||
match runtime_context.resolve_server_environment("stdio", &stdio_server("remote")) {
|
||||
Ok(_) => panic!("unknown MCP environment should fail"),
|
||||
Err(error) => error,
|
||||
};
|
||||
assert_eq!(
|
||||
runtime_environment.startup_unavailable_reason(
|
||||
"stdio",
|
||||
&stdio_server(/*experimental_environment*/ Some("remote")),
|
||||
),
|
||||
Some("remote MCP server `stdio` requires a remote environment".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_stdio_and_http_accept_remote_environment() {
|
||||
let environment = Arc::new(
|
||||
Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string()))
|
||||
.expect("remote environment"),
|
||||
);
|
||||
let runtime_environment = McpRuntimeEnvironment::new(
|
||||
Some(environment),
|
||||
/*local_environment*/ None,
|
||||
PathBuf::from("/tmp"),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
runtime_environment.startup_unavailable_reason(
|
||||
"stdio",
|
||||
&stdio_server(/*experimental_environment*/ Some("remote")),
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
runtime_environment.startup_unavailable_reason(
|
||||
"http",
|
||||
&http_server(/*experimental_environment*/ Some("remote")),
|
||||
),
|
||||
None
|
||||
error,
|
||||
"MCP server `stdio` references unknown environment id `remote`"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_stdio_accepts_remote_runtime_when_local_environment_exists() {
|
||||
let remote_environment = Arc::new(
|
||||
Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string()))
|
||||
.expect("remote environment"),
|
||||
);
|
||||
let local_environment = Arc::new(
|
||||
Environment::create_for_tests(/*exec_server_url*/ None).expect("local environment"),
|
||||
);
|
||||
let runtime_environment = McpRuntimeEnvironment::new(
|
||||
Some(remote_environment),
|
||||
Some(local_environment),
|
||||
async fn explicit_remote_stdio_and_http_accept_named_environment() {
|
||||
let runtime_context = McpRuntimeContext::new(
|
||||
Arc::new(
|
||||
EnvironmentManager::create_for_tests(
|
||||
Some("ws://127.0.0.1:8765".to_string()),
|
||||
/*local_runtime_paths*/ None,
|
||||
)
|
||||
.await,
|
||||
),
|
||||
PathBuf::from("/tmp"),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
runtime_environment.startup_unavailable_reason(
|
||||
"stdio",
|
||||
&stdio_server(/*experimental_environment*/ None),
|
||||
let mut remote_stdio = stdio_server("remote");
|
||||
let McpServerTransportConfig::Stdio { cwd, .. } = &mut remote_stdio.transport else {
|
||||
unreachable!("stdio helper should build stdio transport");
|
||||
};
|
||||
*cwd = Some(std::env::temp_dir());
|
||||
for resolved_runtime in [
|
||||
runtime_context.resolve_server_environment("stdio", &remote_stdio),
|
||||
runtime_context.resolve_server_environment("http", &http_server("remote")),
|
||||
] {
|
||||
let resolved_runtime = match resolved_runtime {
|
||||
Ok(resolved_runtime) => resolved_runtime,
|
||||
Err(error) => panic!("remote MCP should resolve: {error}"),
|
||||
};
|
||||
assert!(resolved_runtime.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_stdio_accepts_local_environment_when_available() {
|
||||
let runtime_context = McpRuntimeContext::new(
|
||||
Arc::new(EnvironmentManager::default_for_tests()),
|
||||
PathBuf::from("/tmp"),
|
||||
);
|
||||
|
||||
let resolved_runtime = match runtime_context
|
||||
.resolve_server_environment("stdio", &stdio_server(DEFAULT_MCP_SERVER_ENVIRONMENT_ID))
|
||||
{
|
||||
Ok(resolved_runtime) => resolved_runtime,
|
||||
Err(error) => panic!("local stdio MCP should resolve: {error}"),
|
||||
};
|
||||
assert!(resolved_runtime.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_stdio_requires_absolute_cwd() {
|
||||
let runtime_context = McpRuntimeContext::new(
|
||||
Arc::new(
|
||||
EnvironmentManager::create_for_tests(
|
||||
Some("ws://127.0.0.1:8765".to_string()),
|
||||
/*local_runtime_paths*/ None,
|
||||
)
|
||||
.await,
|
||||
),
|
||||
None
|
||||
PathBuf::from("/tmp"),
|
||||
);
|
||||
let mut remote_stdio = stdio_server("remote");
|
||||
let McpServerTransportConfig::Stdio { cwd, .. } = &mut remote_stdio.transport else {
|
||||
unreachable!("stdio helper should build stdio transport");
|
||||
};
|
||||
*cwd = Some(PathBuf::from("relative"));
|
||||
|
||||
let error = match runtime_context.resolve_server_environment("stdio", &remote_stdio) {
|
||||
Ok(_) => panic!("remote stdio MCP should require absolute cwd"),
|
||||
Err(error) => error,
|
||||
};
|
||||
assert_eq!(
|
||||
error,
|
||||
"remote stdio MCP server `stdio` requires an absolute cwd, got `relative`"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ pub use marketplace_edit::remove_user_marketplace_config;
|
||||
pub use mcp_edit::ConfigEditsBuilder;
|
||||
pub use mcp_edit::load_global_mcp_servers;
|
||||
pub use mcp_types::AppToolApproval;
|
||||
pub use mcp_types::DEFAULT_MCP_SERVER_ENVIRONMENT_ID;
|
||||
pub use mcp_types::McpServerConfig;
|
||||
pub use mcp_types::McpServerDisabledReason;
|
||||
pub use mcp_types::McpServerEnvVar;
|
||||
|
||||
@@ -235,7 +235,7 @@ pub async fn load_config_layers_state(
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"--profile-v2 `{active_user_profile}` cannot be used while {} contains legacy `[profiles.{active_user_profile}]` config; move those settings into {} or remove `[profiles.{active_user_profile}]`",
|
||||
"--profile `{active_user_profile}` cannot be used while {} contains legacy `[profiles.{active_user_profile}]` config; move those settings into {} or remove `[profiles.{active_user_profile}]`. See https://developers.openai.com/codex/config-advanced#profiles for more information.",
|
||||
base_user_file.as_path().display(),
|
||||
active_user_file.as_path().display()
|
||||
),
|
||||
|
||||
@@ -120,7 +120,7 @@ model = "gpt-work"
|
||||
);
|
||||
let message = err.to_string();
|
||||
assert!(
|
||||
message.contains("--profile-v2 `work` cannot be used"),
|
||||
message.contains("--profile `work` cannot be used"),
|
||||
"unexpected error message: {message}"
|
||||
);
|
||||
assert!(
|
||||
@@ -131,6 +131,10 @@ model = "gpt-work"
|
||||
message.contains("[profiles.work]"),
|
||||
"unexpected error message: {message}"
|
||||
);
|
||||
assert!(
|
||||
message.contains("https://developers.openai.com/codex/config-advanced#profiles"),
|
||||
"unexpected error message: {message}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -175,8 +175,8 @@ fn serialize_mcp_server(config: &McpServerConfig) -> TomlItem {
|
||||
if !config.enabled {
|
||||
entry["enabled"] = value(false);
|
||||
}
|
||||
if let Some(environment) = &config.experimental_environment {
|
||||
entry["experimental_environment"] = value(environment.clone());
|
||||
if !config.is_local_environment() {
|
||||
entry["environment_id"] = value(config.environment_id.clone());
|
||||
}
|
||||
if config.required {
|
||||
entry["required"] = value(true);
|
||||
|
||||
@@ -23,7 +23,7 @@ async fn replace_mcp_servers_serializes_per_tool_approval_overrides() -> anyhow:
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: crate::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: true,
|
||||
@@ -101,7 +101,7 @@ async fn replace_mcp_servers_serializes_oauth_client_id() -> anyhow::Result<()>
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: crate::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
|
||||
@@ -13,6 +13,9 @@ use serde::de::Error as SerdeError;
|
||||
|
||||
use crate::RequirementSource;
|
||||
|
||||
/// Effective MCP environment id when config omits `environment_id`.
|
||||
pub const DEFAULT_MCP_SERVER_ENVIRONMENT_ID: &str = "local";
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AppToolApproval {
|
||||
@@ -128,9 +131,8 @@ pub struct McpServerConfig {
|
||||
#[serde(flatten)]
|
||||
pub transport: McpServerTransportConfig,
|
||||
|
||||
/// Experimental environment selector for where Codex should start this MCP server.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub experimental_environment: Option<String>,
|
||||
/// Effective environment id for where Codex should start this MCP server.
|
||||
pub environment_id: String,
|
||||
|
||||
/// When `false`, Codex skips initializing this MCP server.
|
||||
#[serde(default = "default_enabled")]
|
||||
@@ -190,6 +192,10 @@ pub struct McpServerConfig {
|
||||
}
|
||||
|
||||
impl McpServerConfig {
|
||||
pub fn is_local_environment(&self) -> bool {
|
||||
self.environment_id == DEFAULT_MCP_SERVER_ENVIRONMENT_ID
|
||||
}
|
||||
|
||||
pub fn oauth_client_id(&self) -> Option<&str> {
|
||||
self.oauth
|
||||
.as_ref()
|
||||
@@ -231,7 +237,7 @@ pub struct RawMcpServerConfig {
|
||||
|
||||
// shared
|
||||
#[serde(default)]
|
||||
pub experimental_environment: Option<String>,
|
||||
pub environment_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub startup_timeout_sec: Option<f64>,
|
||||
#[serde(default)]
|
||||
@@ -279,7 +285,7 @@ impl TryFrom<RawMcpServerConfig> for McpServerConfig {
|
||||
url,
|
||||
bearer_token,
|
||||
bearer_token_env_var,
|
||||
experimental_environment,
|
||||
environment_id,
|
||||
startup_timeout_sec,
|
||||
startup_timeout_ms,
|
||||
tool_timeout_sec,
|
||||
@@ -350,9 +356,13 @@ impl TryFrom<RawMcpServerConfig> for McpServerConfig {
|
||||
return Err("invalid transport".to_string());
|
||||
};
|
||||
|
||||
let environment_id =
|
||||
environment_id.unwrap_or_else(|| DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string());
|
||||
validate_remote_stdio_cwd(&transport, &environment_id)?;
|
||||
|
||||
Ok(Self {
|
||||
transport,
|
||||
experimental_environment,
|
||||
environment_id,
|
||||
startup_timeout_sec,
|
||||
tool_timeout_sec,
|
||||
enabled: enabled.unwrap_or_else(default_enabled),
|
||||
@@ -385,6 +395,30 @@ const fn default_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn validate_remote_stdio_cwd(
|
||||
transport: &McpServerTransportConfig,
|
||||
environment_id: &str,
|
||||
) -> Result<(), String> {
|
||||
if environment_id == DEFAULT_MCP_SERVER_ENVIRONMENT_ID {
|
||||
return Ok(());
|
||||
}
|
||||
let McpServerTransportConfig::Stdio { cwd, .. } = transport else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(cwd) = cwd else {
|
||||
return Err(format!(
|
||||
"remote stdio MCP servers require an absolute cwd when environment_id is `{environment_id}`"
|
||||
));
|
||||
};
|
||||
if cwd.is_absolute() {
|
||||
return Ok(());
|
||||
}
|
||||
Err(format!(
|
||||
"remote stdio MCP servers require an absolute cwd when environment_id is `{environment_id}`, got `{}`",
|
||||
cwd.display()
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)]
|
||||
#[serde(untagged, deny_unknown_fields, rename_all = "snake_case")]
|
||||
pub enum McpServerTransportConfig {
|
||||
|
||||
@@ -51,6 +51,62 @@ fn deserialize_stdio_command_server_config_with_args() {
|
||||
assert!(cfg.enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_remote_stdio_server_requires_absolute_cwd() {
|
||||
let missing_cwd = toml::from_str::<McpServerConfig>(
|
||||
r#"
|
||||
command = "echo"
|
||||
environment_id = "remote"
|
||||
"#,
|
||||
)
|
||||
.expect_err("remote stdio MCP should require cwd");
|
||||
assert!(
|
||||
missing_cwd
|
||||
.to_string()
|
||||
.contains("remote stdio MCP servers require an absolute cwd"),
|
||||
"unexpected error: {missing_cwd}"
|
||||
);
|
||||
|
||||
let relative_cwd = toml::from_str::<McpServerConfig>(
|
||||
r#"
|
||||
command = "echo"
|
||||
environment_id = "remote"
|
||||
cwd = "relative"
|
||||
"#,
|
||||
)
|
||||
.expect_err("remote stdio MCP should require absolute cwd");
|
||||
assert!(
|
||||
relative_cwd.to_string().contains("got `relative`"),
|
||||
"unexpected error: {relative_cwd}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_remote_stdio_server_accepts_absolute_cwd() {
|
||||
let cwd = std::env::temp_dir();
|
||||
let cfg: McpServerConfig = match toml::from_str(&format!(
|
||||
r#"
|
||||
command = "echo"
|
||||
environment_id = "remote"
|
||||
cwd = {cwd:?}
|
||||
"#
|
||||
)) {
|
||||
Ok(cfg) => cfg,
|
||||
Err(error) => panic!("remote stdio MCP should accept absolute cwd: {error}"),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
cfg.transport,
|
||||
McpServerTransportConfig::Stdio {
|
||||
command: "echo".to_string(),
|
||||
args: vec![],
|
||||
env: None,
|
||||
env_vars: Vec::new(),
|
||||
cwd: Some(cwd),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_stdio_command_server_config_with_arg_with_args_and_env() {
|
||||
let cfg: McpServerConfig = toml::from_str(
|
||||
@@ -402,7 +458,7 @@ fn deserialize_ignores_unknown_server_fields() {
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: crate::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
|
||||
@@ -235,7 +235,7 @@ async fn load_plugins_loads_default_skills_and_mcp_servers() {
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: "local".to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -738,7 +738,7 @@ async fn load_plugins_uses_manifest_configured_component_paths() {
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: "local".to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -850,7 +850,7 @@ async fn load_plugins_ignores_manifest_component_paths_without_dot_slash() {
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: "local".to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -1012,7 +1012,7 @@ fn capability_index_filters_inactive_and_zero_capability_plugins() {
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: "local".to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
|
||||
@@ -2304,7 +2304,7 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"experimental_environment": {
|
||||
"environment_id": {
|
||||
"default": null,
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
+29
-12
@@ -260,6 +260,7 @@ struct WebsocketSession {
|
||||
connection: Option<ApiWebSocketConnection>,
|
||||
last_request: Option<ResponsesApiRequest>,
|
||||
last_response_rx: Option<oneshot::Receiver<LastResponse>>,
|
||||
last_response_from_untraced_warmup: bool,
|
||||
connection_reused: StdMutex<bool>,
|
||||
}
|
||||
|
||||
@@ -941,6 +942,7 @@ impl ModelClientSession {
|
||||
self.websocket_session.connection = None;
|
||||
self.websocket_session.last_request = None;
|
||||
self.websocket_session.last_response_rx = None;
|
||||
self.websocket_session.last_response_from_untraced_warmup = false;
|
||||
self.websocket_session
|
||||
.set_connection_reused(/*connection_reused*/ false);
|
||||
}
|
||||
@@ -1044,28 +1046,33 @@ impl ModelClientSession {
|
||||
&mut self,
|
||||
payload: ResponseCreateWsRequest,
|
||||
request: &ResponsesApiRequest,
|
||||
) -> ResponsesWsRequest {
|
||||
) -> (ResponsesWsRequest, bool) {
|
||||
let Some(last_response) = self.get_last_response() else {
|
||||
return ResponsesWsRequest::ResponseCreate(payload);
|
||||
return (ResponsesWsRequest::ResponseCreate(payload), false);
|
||||
};
|
||||
let previous_response_id_from_untraced_warmup =
|
||||
self.websocket_session.last_response_from_untraced_warmup;
|
||||
let Some(incremental_items) = self.get_incremental_items(
|
||||
request,
|
||||
Some(&last_response),
|
||||
/*allow_empty_delta*/ true,
|
||||
) else {
|
||||
return ResponsesWsRequest::ResponseCreate(payload);
|
||||
return (ResponsesWsRequest::ResponseCreate(payload), false);
|
||||
};
|
||||
|
||||
if last_response.response_id.is_empty() {
|
||||
trace!("incremental request failed, no previous response id");
|
||||
return ResponsesWsRequest::ResponseCreate(payload);
|
||||
return (ResponsesWsRequest::ResponseCreate(payload), false);
|
||||
}
|
||||
|
||||
ResponsesWsRequest::ResponseCreate(ResponseCreateWsRequest {
|
||||
previous_response_id: Some(last_response.response_id),
|
||||
input: incremental_items,
|
||||
..payload
|
||||
})
|
||||
(
|
||||
ResponsesWsRequest::ResponseCreate(ResponseCreateWsRequest {
|
||||
previous_response_id: Some(last_response.response_id),
|
||||
input: incremental_items,
|
||||
..payload
|
||||
}),
|
||||
previous_response_id_from_untraced_warmup,
|
||||
)
|
||||
}
|
||||
|
||||
/// Opportunistically preconnects a websocket for this turn-scoped client session.
|
||||
@@ -1144,6 +1151,7 @@ impl ModelClientSession {
|
||||
if needs_new {
|
||||
self.websocket_session.last_request = None;
|
||||
self.websocket_session.last_response_rx = None;
|
||||
self.websocket_session.last_response_from_untraced_warmup = false;
|
||||
let turn_state = options
|
||||
.turn_state
|
||||
.clone()
|
||||
@@ -1412,8 +1420,8 @@ impl ModelClientSession {
|
||||
Err(err) => return Err(map_api_error(err)),
|
||||
}
|
||||
|
||||
let mut ws_request = self.prepare_websocket_request(ws_payload, &request);
|
||||
self.websocket_session.last_request = Some(request);
|
||||
let (mut ws_request, previous_response_id_from_untraced_warmup) =
|
||||
self.prepare_websocket_request(ws_payload, &request);
|
||||
let inference_trace_attempt = if warmup {
|
||||
// Prewarm sends `generate=false`; it is connection setup, not a
|
||||
// model inference attempt that should appear in rollout traces.
|
||||
@@ -1422,7 +1430,16 @@ impl ModelClientSession {
|
||||
inference_trace.start_attempt()
|
||||
};
|
||||
stamp_ws_stream_request_start_ms(&mut ws_request);
|
||||
inference_trace_attempt.record_started(&ws_request);
|
||||
if previous_response_id_from_untraced_warmup {
|
||||
// The transport can reuse an untraced warmup response id and omit the
|
||||
// already-sent input, but rollout replay needs the logical model-visible
|
||||
// request rather than the compressed websocket delta.
|
||||
inference_trace_attempt.record_started(&request);
|
||||
} else {
|
||||
inference_trace_attempt.record_started(&ws_request);
|
||||
}
|
||||
self.websocket_session.last_request = Some(request);
|
||||
self.websocket_session.last_response_from_untraced_warmup = warmup;
|
||||
let websocket_connection =
|
||||
self.websocket_session.connection.as_ref().ok_or_else(|| {
|
||||
map_api_error(ApiError::Stream(
|
||||
|
||||
@@ -286,7 +286,7 @@ pub(crate) async fn process_compacted_history(
|
||||
/// - `user`-role warnings that parse as `TurnItem::UserMessage` and compaction-generated summary
|
||||
/// messages. Legacy warning fragments are filtered by `parse_turn_item` before they reach this
|
||||
/// check.
|
||||
fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
|
||||
pub(crate) fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
|
||||
match item {
|
||||
ResponseItem::Message { role, .. } if role == "developer" => false,
|
||||
ResponseItem::Message { role, .. } if role == "user" => {
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::compact::compaction_status_from_result;
|
||||
use crate::compact_remote::build_compact_request_log_data;
|
||||
use crate::compact_remote::log_remote_compact_failure;
|
||||
use crate::compact_remote::process_compacted_history;
|
||||
use crate::compact_remote::should_keep_compacted_history_item;
|
||||
use crate::compact_remote::trim_function_call_history_to_fit_context_window;
|
||||
use crate::hook_runtime::PostCompactHookOutcome;
|
||||
use crate::hook_runtime::PreCompactHookOutcome;
|
||||
@@ -27,17 +28,25 @@ use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::Result as CodexResult;
|
||||
use codex_protocol::items::ContextCompactionItem;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::CompactedItem;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::TruncationPolicy;
|
||||
use codex_protocol::protocol::TurnStartedEvent;
|
||||
use codex_rollout_trace::CompactionCheckpointTracePayload;
|
||||
use codex_rollout_trace::InferenceTraceContext;
|
||||
use codex_utils_output_truncation::approx_token_count;
|
||||
use codex_utils_output_truncation::truncate_text;
|
||||
use futures::StreamExt;
|
||||
use futures::TryFutureExt;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::info;
|
||||
|
||||
// Mirror the current /responses/compact retained-message default while the
|
||||
// server-side path remains the reference implementation.
|
||||
const RETAINED_MESSAGE_TOKEN_BUDGET: usize = 64_000;
|
||||
|
||||
pub(crate) async fn run_inline_remote_auto_compact_task(
|
||||
sess: Arc<Session>,
|
||||
turn_context: Arc<TurnContext>,
|
||||
@@ -343,11 +352,14 @@ fn build_v2_compacted_history(
|
||||
prompt_input: &[ResponseItem],
|
||||
compaction_output: ResponseItem,
|
||||
) -> Vec<ResponseItem> {
|
||||
let mut retained = prompt_input
|
||||
let retained = prompt_input
|
||||
.iter()
|
||||
.filter(|item| is_retained_for_remote_compaction_v2(item))
|
||||
.filter(|item| should_keep_compacted_history_item(item))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let mut retained =
|
||||
truncate_retained_messages_for_remote_compaction(retained, RETAINED_MESSAGE_TOKEN_BUDGET);
|
||||
retained.push(compaction_output);
|
||||
retained
|
||||
}
|
||||
@@ -360,6 +372,98 @@ fn is_retained_for_remote_compaction_v2(item: &ResponseItem) -> bool {
|
||||
matches!(role.as_str(), "user" | "developer" | "system")
|
||||
}
|
||||
|
||||
fn truncate_retained_messages_for_remote_compaction(
|
||||
items: Vec<ResponseItem>,
|
||||
max_tokens: usize,
|
||||
) -> Vec<ResponseItem> {
|
||||
let mut remaining = max_tokens;
|
||||
let mut truncated_reversed = Vec::with_capacity(items.len());
|
||||
for item in items.into_iter().rev() {
|
||||
if remaining == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let token_count = message_text_token_count(&item).max(1);
|
||||
if token_count <= remaining {
|
||||
truncated_reversed.push(item);
|
||||
remaining = remaining.saturating_sub(token_count);
|
||||
} else if let Some(truncated_item) =
|
||||
truncate_message_text_to_token_budget(item, /*max_tokens*/ remaining)
|
||||
{
|
||||
truncated_reversed.push(truncated_item);
|
||||
remaining = 0;
|
||||
}
|
||||
}
|
||||
truncated_reversed.reverse();
|
||||
truncated_reversed
|
||||
}
|
||||
|
||||
fn message_text_token_count(item: &ResponseItem) -> usize {
|
||||
let ResponseItem::Message { content, .. } = item else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
content
|
||||
.iter()
|
||||
.map(|item| match item {
|
||||
ContentItem::InputText { text } | ContentItem::OutputText { text } => {
|
||||
approx_token_count(text)
|
||||
}
|
||||
ContentItem::InputImage { .. } => 0,
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn truncate_message_text_to_token_budget(
|
||||
item: ResponseItem,
|
||||
max_tokens: usize,
|
||||
) -> Option<ResponseItem> {
|
||||
let ResponseItem::Message {
|
||||
id,
|
||||
role,
|
||||
content,
|
||||
phase,
|
||||
} = item
|
||||
else {
|
||||
return Some(item);
|
||||
};
|
||||
|
||||
let mut remaining = max_tokens;
|
||||
let mut truncated_content = Vec::with_capacity(content.len());
|
||||
for mut content_item in content {
|
||||
match &mut content_item {
|
||||
ContentItem::InputText { text } | ContentItem::OutputText { text } => {
|
||||
if remaining == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let token_count = approx_token_count(text);
|
||||
if token_count <= remaining {
|
||||
remaining = remaining.saturating_sub(token_count);
|
||||
} else {
|
||||
*text = truncate_text(text, TruncationPolicy::Tokens(remaining));
|
||||
remaining = 0;
|
||||
}
|
||||
if !text.is_empty() {
|
||||
truncated_content.push(content_item);
|
||||
}
|
||||
}
|
||||
ContentItem::InputImage { .. } => truncated_content.push(content_item),
|
||||
}
|
||||
}
|
||||
|
||||
if truncated_content.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(ResponseItem::Message {
|
||||
id,
|
||||
role,
|
||||
content: truncated_content,
|
||||
phase,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -395,7 +499,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_v2_compacted_history_matches_prod_retention_shape() {
|
||||
fn build_v2_compacted_history_filters_to_installed_retention_shape() {
|
||||
let input = vec![
|
||||
message("developer", "dev", /*phase*/ None),
|
||||
message("system", "sys", /*phase*/ None),
|
||||
@@ -421,15 +525,145 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
history,
|
||||
vec![message("user", "user", /*phase*/ None), output]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_v2_compacted_history_discards_messages_before_truncating() {
|
||||
let old = message("user", "old", /*phase*/ None);
|
||||
let new = message("user", "new", /*phase*/ None);
|
||||
let huge_developer_message = "d".repeat((RETAINED_MESSAGE_TOKEN_BUDGET + 1) * 4);
|
||||
let huge_contextual_message = format!(
|
||||
"<environment_context>\n{}\n</environment_context>",
|
||||
"c".repeat((RETAINED_MESSAGE_TOKEN_BUDGET + 1) * 4)
|
||||
);
|
||||
let input = vec![
|
||||
old.clone(),
|
||||
message("developer", &huge_developer_message, /*phase*/ None),
|
||||
message("user", &huge_contextual_message, /*phase*/ None),
|
||||
new.clone(),
|
||||
];
|
||||
let output = ResponseItem::Compaction {
|
||||
encrypted_content: "new".to_string(),
|
||||
};
|
||||
|
||||
let history = build_v2_compacted_history(&input, output.clone());
|
||||
|
||||
assert_eq!(history, vec![old, new, output]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retained_history_truncation_keeps_newest_messages_first() {
|
||||
let middle = message("user", "middle1234", /*phase*/ None);
|
||||
let new = message("user", "new", /*phase*/ None);
|
||||
let retained = vec![
|
||||
message("user", "old-old", /*phase*/ None),
|
||||
middle,
|
||||
new.clone(),
|
||||
];
|
||||
|
||||
let truncated =
|
||||
truncate_retained_messages_for_remote_compaction(retained, /*max_tokens*/ 3);
|
||||
|
||||
assert_eq!(
|
||||
truncated,
|
||||
vec![
|
||||
message("developer", "dev", /*phase*/ None),
|
||||
message("system", "sys", /*phase*/ None),
|
||||
message("user", "user", /*phase*/ None),
|
||||
output,
|
||||
message("user", "midd…1 tokens truncated…1234", /*phase*/ None),
|
||||
new,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retained_history_truncation_preserves_images_and_truncates_later_text_parts() {
|
||||
let item = ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![
|
||||
ContentItem::InputText {
|
||||
text: "abcdef".to_string(),
|
||||
},
|
||||
ContentItem::InputImage {
|
||||
image_url: "data:image/png;base64,abc".to_string(),
|
||||
detail: None,
|
||||
},
|
||||
ContentItem::OutputText {
|
||||
text: "uvwxyz".to_string(),
|
||||
},
|
||||
],
|
||||
phase: None,
|
||||
};
|
||||
|
||||
let truncated =
|
||||
truncate_retained_messages_for_remote_compaction(vec![item], /*max_tokens*/ 3);
|
||||
|
||||
assert_eq!(
|
||||
truncated,
|
||||
vec![ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![
|
||||
ContentItem::InputText {
|
||||
text: "abcdef".to_string(),
|
||||
},
|
||||
ContentItem::InputImage {
|
||||
image_url: "data:image/png;base64,abc".to_string(),
|
||||
detail: None,
|
||||
},
|
||||
ContentItem::OutputText {
|
||||
text: "uv…1 tokens truncated…yz".to_string(),
|
||||
},
|
||||
],
|
||||
phase: None,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retained_history_truncation_charges_image_only_messages() {
|
||||
let image_only_message = ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputImage {
|
||||
image_url: "data:image/png;base64,abc".to_string(),
|
||||
detail: None,
|
||||
}],
|
||||
phase: None,
|
||||
};
|
||||
let newest = message("user", "new", /*phase*/ None);
|
||||
let retained = vec![
|
||||
message("user", "old", /*phase*/ None),
|
||||
image_only_message.clone(),
|
||||
newest.clone(),
|
||||
];
|
||||
|
||||
let truncated =
|
||||
truncate_retained_messages_for_remote_compaction(retained, /*max_tokens*/ 2);
|
||||
|
||||
assert_eq!(truncated, vec![image_only_message, newest]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retained_history_truncation_drops_image_only_messages_after_budget_is_spent() {
|
||||
let image_only_message = ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputImage {
|
||||
image_url: "data:image/png;base64,abc".to_string(),
|
||||
detail: None,
|
||||
}],
|
||||
phase: None,
|
||||
};
|
||||
let newest = message("user", "new", /*phase*/ None);
|
||||
let retained = vec![image_only_message, newest.clone()];
|
||||
|
||||
let truncated =
|
||||
truncate_retained_messages_for_remote_compaction(retained, /*max_tokens*/ 1);
|
||||
|
||||
assert_eq!(truncated, vec![newest]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn collect_compaction_output_accepts_additional_output_items() {
|
||||
let compaction = ResponseItem::Compaction {
|
||||
|
||||
@@ -132,7 +132,7 @@ fn stdio_mcp(command: &str) -> McpServerConfig {
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -157,7 +157,7 @@ fn http_mcp(url: &str) -> McpServerConfig {
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -5144,9 +5144,9 @@ async fn replace_mcp_servers_round_trips_entries() -> anyhow::Result<()> {
|
||||
args: vec!["hello".to_string()],
|
||||
env: None,
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
cwd: Some(codex_home.path().to_path_buf()),
|
||||
},
|
||||
experimental_environment: Some("remote".to_string()),
|
||||
environment_id: "remote".to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -5184,13 +5184,13 @@ async fn replace_mcp_servers_round_trips_entries() -> anyhow::Result<()> {
|
||||
assert_eq!(args, &vec!["hello".to_string()]);
|
||||
assert!(env.is_none());
|
||||
assert!(env_vars.is_empty());
|
||||
assert!(cwd.is_none());
|
||||
assert_eq!(cwd, &Some(codex_home.path().to_path_buf()));
|
||||
}
|
||||
other => panic!("unexpected transport {other:?}"),
|
||||
}
|
||||
assert_eq!(docs.startup_timeout_sec, Some(Duration::from_secs(3)));
|
||||
assert_eq!(docs.tool_timeout_sec, Some(Duration::from_secs(5)));
|
||||
assert_eq!(docs.experimental_environment.as_deref(), Some("remote"));
|
||||
assert_eq!(docs.environment_id, "remote");
|
||||
assert!(docs.enabled);
|
||||
|
||||
let empty = BTreeMap::new();
|
||||
@@ -5493,7 +5493,7 @@ async fn replace_mcp_servers_serializes_env_sorted() -> anyhow::Result<()> {
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -5570,7 +5570,7 @@ async fn replace_mcp_servers_serializes_env_vars() -> anyhow::Result<()> {
|
||||
env_vars: vec!["ALPHA".into(), "BETA".into()],
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -5632,7 +5632,7 @@ async fn replace_mcp_servers_serializes_sourced_env_vars() -> anyhow::Result<()>
|
||||
],
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -5684,7 +5684,7 @@ async fn replace_mcp_servers_serializes_cwd() -> anyhow::Result<()> {
|
||||
env_vars: Vec::new(),
|
||||
cwd: Some(cwd_path.clone()),
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -5739,7 +5739,7 @@ async fn replace_mcp_servers_streamable_http_serializes_bearer_token() -> anyhow
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -5810,7 +5810,7 @@ async fn replace_mcp_servers_streamable_http_serializes_custom_headers() -> anyh
|
||||
"DOCS_AUTH".to_string(),
|
||||
)])),
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -5893,7 +5893,7 @@ async fn replace_mcp_servers_streamable_http_removes_optional_sections() -> anyh
|
||||
"DOCS_AUTH".to_string(),
|
||||
)])),
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -5929,7 +5929,7 @@ async fn replace_mcp_servers_streamable_http_removes_optional_sections() -> anyh
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -6000,7 +6000,7 @@ async fn replace_mcp_servers_streamable_http_isolates_headers_between_servers()
|
||||
"DOCS_AUTH".to_string(),
|
||||
)])),
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -6026,7 +6026,7 @@ async fn replace_mcp_servers_streamable_http_isolates_headers_between_servers()
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -6115,7 +6115,7 @@ async fn replace_mcp_servers_serializes_disabled_flag() -> anyhow::Result<()> {
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: false,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -6166,7 +6166,7 @@ async fn replace_mcp_servers_serializes_required_flag() -> anyhow::Result<()> {
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: true,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -6217,7 +6217,7 @@ async fn replace_mcp_servers_serializes_tool_filters() -> anyhow::Result<()> {
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -6272,7 +6272,7 @@ async fn replace_mcp_servers_streamable_http_serializes_oauth_resource() -> anyh
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
|
||||
@@ -300,8 +300,8 @@ mod document_helpers {
|
||||
if !config.enabled {
|
||||
entry["enabled"] = value(false);
|
||||
}
|
||||
if let Some(environment) = &config.experimental_environment {
|
||||
entry["experimental_environment"] = value(environment.clone());
|
||||
if !config.is_local_environment() {
|
||||
entry["environment_id"] = value(config.environment_id.clone());
|
||||
}
|
||||
if config.required {
|
||||
entry["required"] = value(true);
|
||||
|
||||
@@ -945,7 +945,7 @@ fn blocking_replace_mcp_servers_round_trips() {
|
||||
env_vars: vec!["FOO".into()],
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: true,
|
||||
@@ -975,7 +975,7 @@ fn blocking_replace_mcp_servers_round_trips() {
|
||||
),
|
||||
env_http_headers: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: false,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -1047,7 +1047,7 @@ fn blocking_replace_mcp_servers_serializes_tool_approval_overrides() {
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -1112,7 +1112,7 @@ foo = { command = "cmd" }
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -1167,7 +1167,7 @@ foo = { command = "cmd" } # keep me
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: false,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -1221,7 +1221,7 @@ foo = { command = "cmd", args = ["--flag"] } # keep me
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -1276,7 +1276,7 @@ foo = { command = "cmd" }
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: false,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
|
||||
@@ -35,7 +35,7 @@ use codex_login::CodexAuth;
|
||||
use codex_login::default_client::originator;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_mcp::McpConnectionManager;
|
||||
use codex_mcp::McpRuntimeEnvironment;
|
||||
use codex_mcp::McpRuntimeContext;
|
||||
use codex_mcp::ToolInfo;
|
||||
use codex_mcp::ToolPluginProvenance;
|
||||
use codex_mcp::codex_apps_tools_cache_key;
|
||||
@@ -203,7 +203,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status(
|
||||
list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
config,
|
||||
force_refetch,
|
||||
&environment_manager,
|
||||
Arc::new(environment_manager),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -211,7 +211,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status(
|
||||
pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
config: &Config,
|
||||
force_refetch: bool,
|
||||
environment_manager: &EnvironmentManager,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
) -> anyhow::Result<AccessibleConnectorsStatus> {
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false).await;
|
||||
@@ -272,11 +272,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
PermissionProfile::default(),
|
||||
// Connector discovery is threadless. Use an actually configured env if
|
||||
// one exists, but do not reintroduce the old hidden-local fallback.
|
||||
McpRuntimeEnvironment::new(
|
||||
environment_manager.default_or_local_environment(),
|
||||
environment_manager.try_local_environment(),
|
||||
config.cwd.to_path_buf(),
|
||||
),
|
||||
McpRuntimeContext::new(environment_manager, config.cwd.to_path_buf()),
|
||||
config.codex_home.to_path_buf(),
|
||||
codex_apps_tools_cache_key(auth.as_ref()),
|
||||
host_owned_codex_apps_enabled,
|
||||
|
||||
@@ -31,10 +31,7 @@ fn detects_subagent_notification_fragment_case_insensitively() {
|
||||
|
||||
#[test]
|
||||
fn detects_goal_context_fragment() {
|
||||
let text = GoalContext {
|
||||
prompt: "Continue working toward the active thread goal.".to_string(),
|
||||
}
|
||||
.render();
|
||||
let text = GoalContext::new("Continue working toward the active thread goal.").render();
|
||||
|
||||
assert!(is_contextual_user_fragment(&ContentItem::InputText {
|
||||
text
|
||||
@@ -43,9 +40,9 @@ fn detects_goal_context_fragment() {
|
||||
|
||||
#[test]
|
||||
fn contextual_user_fragment_is_dyn_compatible() {
|
||||
let fragment: Box<dyn ContextualUserFragment> = Box::new(GoalContext {
|
||||
prompt: "Continue working toward the active thread goal.".to_string(),
|
||||
});
|
||||
let fragment: Box<dyn ContextualUserFragment> = Box::new(GoalContext::new(
|
||||
"Continue working toward the active thread goal.",
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
fragment.render(),
|
||||
|
||||
@@ -1,10 +1,33 @@
|
||||
//! Hidden user-context fragment for runtime-owned goal steering prompts.
|
||||
|
||||
use super::ContextualUserFragment;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
|
||||
/// Hidden runtime-owned goal steering context injected into model input.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct GoalContext {
|
||||
pub(crate) prompt: String,
|
||||
pub struct GoalContext {
|
||||
prompt: String,
|
||||
}
|
||||
|
||||
impl GoalContext {
|
||||
/// Creates goal context around an already-rendered steering prompt.
|
||||
pub fn new(prompt: impl Into<String>) -> Self {
|
||||
Self {
|
||||
prompt: prompt.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the registered fragment into an active-turn injectable item.
|
||||
pub fn into_response_input_item(self) -> ResponseInputItem {
|
||||
ResponseInputItem::Message {
|
||||
role: <Self as ContextualUserFragment>::role().to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: self.render(),
|
||||
}],
|
||||
phase: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextualUserFragment for GoalContext {
|
||||
|
||||
@@ -40,7 +40,7 @@ pub(crate) use environment_context::EnvironmentContext;
|
||||
pub use fragment::ContextualUserFragment;
|
||||
pub(crate) use fragment::FragmentRegistration;
|
||||
pub(crate) use fragment::FragmentRegistrationProxy;
|
||||
pub(crate) use goal_context::GoalContext;
|
||||
pub use goal_context::GoalContext;
|
||||
pub(crate) use guardian_followup_review_reminder::GuardianFollowupReviewReminder;
|
||||
pub(crate) use hook_additional_context::HookAdditionalContext;
|
||||
pub(crate) use image_generation_instructions::ImageGenerationInstructions;
|
||||
|
||||
@@ -322,10 +322,7 @@ fn goal_context_does_not_parse_as_visible_turn_item() {
|
||||
id: Some("msg-1".to_string()),
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: GoalContext {
|
||||
prompt: "Continue working toward the active thread goal.".to_string(),
|
||||
}
|
||||
.render(),
|
||||
text: GoalContext::new("Continue working toward the active thread goal.").render(),
|
||||
}],
|
||||
phase: None,
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
//! events, and owns helper hooks used by goal lifecycle behavior.
|
||||
|
||||
use crate::StateDbHandle;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::GoalContext;
|
||||
use crate::session::TurnInput;
|
||||
use crate::session::session::Session;
|
||||
@@ -26,7 +25,6 @@ use codex_otel::GOAL_TOKEN_COUNT_METRIC;
|
||||
use codex_otel::GOAL_USAGE_LIMITED_METRIC;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ThreadGoal;
|
||||
@@ -1632,14 +1630,7 @@ fn budget_limit_steering_item(goal: &ThreadGoal) -> ResponseInputItem {
|
||||
}
|
||||
|
||||
fn goal_context_input_item(prompt: String) -> ResponseInputItem {
|
||||
let context = GoalContext { prompt };
|
||||
ResponseInputItem::Message {
|
||||
role: GoalContext::role().to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: context.render(),
|
||||
}],
|
||||
phase: None,
|
||||
}
|
||||
GoalContext::new(prompt).into_response_input_item()
|
||||
}
|
||||
|
||||
pub(crate) fn protocol_goal_from_state(goal: codex_state::ThreadGoal) -> ThreadGoal {
|
||||
|
||||
@@ -361,7 +361,7 @@ fn mcp_dependency_to_server_config(
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
@@ -391,7 +391,7 @@ fn mcp_dependency_to_server_config(
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
|
||||
@@ -1255,11 +1255,6 @@ fn codex_apps_auth_failure_metadata() -> McpToolApprovalMetadata {
|
||||
|
||||
async fn install_host_owned_codex_apps_manager(session: &Session, turn_context: &TurnContext) {
|
||||
let auth = session.services.auth_manager.auth().await;
|
||||
let environment = session
|
||||
.services
|
||||
.environment_manager
|
||||
.default_or_local_environment()
|
||||
.expect("test session should have an MCP runtime environment");
|
||||
let (manager, _cancel_token) = codex_mcp::McpConnectionManager::new(
|
||||
&HashMap::new(),
|
||||
turn_context.config.mcp_oauth_credentials_store_mode,
|
||||
@@ -1268,14 +1263,10 @@ async fn install_host_owned_codex_apps_manager(session: &Session, turn_context:
|
||||
turn_context.sub_id.clone(),
|
||||
session.get_tx_event(),
|
||||
turn_context.permission_profile(),
|
||||
codex_mcp::McpRuntimeEnvironment::new(
|
||||
Some(environment),
|
||||
session.services.environment_manager.try_local_environment(),
|
||||
{
|
||||
#[allow(deprecated)]
|
||||
turn_context.cwd.to_path_buf()
|
||||
},
|
||||
),
|
||||
codex_mcp::McpRuntimeContext::new(Arc::clone(&session.services.environment_manager), {
|
||||
#[allow(deprecated)]
|
||||
turn_context.cwd.to_path_buf()
|
||||
}),
|
||||
turn_context.config.codex_home.to_path_buf(),
|
||||
codex_mcp::codex_apps_tools_cache_key(auth.as_ref()),
|
||||
/*host_owned_codex_apps_enabled*/ true,
|
||||
|
||||
@@ -325,17 +325,13 @@ impl Session {
|
||||
host_owned_codex_apps_enabled(&mcp_config, auth.as_ref());
|
||||
let auth_statuses =
|
||||
compute_auth_statuses(mcp_servers.iter(), store_mode, auth.as_ref()).await;
|
||||
let mcp_runtime_environment = match turn_context.environments.primary() {
|
||||
Some(turn_environment) => McpRuntimeEnvironment::new(
|
||||
Some(Arc::clone(&turn_environment.environment)),
|
||||
self.services.environment_manager.try_local_environment(),
|
||||
let mcp_runtime_context = match turn_context.environments.primary() {
|
||||
Some(turn_environment) => McpRuntimeContext::new(
|
||||
Arc::clone(&self.services.environment_manager),
|
||||
turn_environment.cwd.to_path_buf(),
|
||||
),
|
||||
None => McpRuntimeEnvironment::new(
|
||||
self.services
|
||||
.environment_manager
|
||||
.default_or_local_environment(),
|
||||
self.services.environment_manager.try_local_environment(),
|
||||
None => McpRuntimeContext::new(
|
||||
Arc::clone(&self.services.environment_manager),
|
||||
#[allow(deprecated)]
|
||||
turn_context.cwd.to_path_buf(),
|
||||
),
|
||||
@@ -353,7 +349,7 @@ impl Session {
|
||||
turn_context.sub_id.clone(),
|
||||
self.get_tx_event(),
|
||||
turn_context.permission_profile(),
|
||||
mcp_runtime_environment,
|
||||
mcp_runtime_context,
|
||||
config.codex_home.to_path_buf(),
|
||||
codex_apps_tools_cache_key(auth.as_ref()),
|
||||
host_owned_codex_apps_enabled,
|
||||
|
||||
@@ -61,7 +61,7 @@ use codex_login::CodexAuth;
|
||||
use codex_login::auth_env_telemetry::collect_auth_env_telemetry;
|
||||
use codex_login::default_client::originator;
|
||||
use codex_mcp::McpConnectionManager;
|
||||
use codex_mcp::McpRuntimeEnvironment;
|
||||
use codex_mcp::McpRuntimeContext;
|
||||
use codex_mcp::codex_apps_tools_cache_key;
|
||||
use codex_models_manager::manager::RefreshStrategy;
|
||||
use codex_models_manager::manager::SharedModelsManager;
|
||||
|
||||
@@ -1135,15 +1135,13 @@ impl Session {
|
||||
})?
|
||||
.primary()
|
||||
.cloned();
|
||||
let mcp_runtime_environment = match turn_environment {
|
||||
Some(turn_environment) => McpRuntimeEnvironment::new(
|
||||
Some(Arc::clone(&turn_environment.environment)),
|
||||
sess.services.environment_manager.try_local_environment(),
|
||||
let mcp_runtime_context = match turn_environment {
|
||||
Some(turn_environment) => McpRuntimeContext::new(
|
||||
Arc::clone(&sess.services.environment_manager),
|
||||
turn_environment.cwd.to_path_buf(),
|
||||
),
|
||||
None => McpRuntimeEnvironment::new(
|
||||
sess.services.environment_manager.default_or_local_environment(),
|
||||
sess.services.environment_manager.try_local_environment(),
|
||||
None => McpRuntimeContext::new(
|
||||
Arc::clone(&sess.services.environment_manager),
|
||||
session_configuration.cwd.to_path_buf(),
|
||||
),
|
||||
};
|
||||
@@ -1155,7 +1153,7 @@ impl Session {
|
||||
INITIAL_SUBMIT_ID.to_owned(),
|
||||
tx_event.clone(),
|
||||
session_configuration.permission_profile(),
|
||||
mcp_runtime_environment,
|
||||
mcp_runtime_context,
|
||||
config.codex_home.to_path_buf(),
|
||||
codex_apps_tools_cache_key(auth),
|
||||
host_owned_codex_apps_enabled,
|
||||
|
||||
@@ -94,8 +94,8 @@ impl ToolExecutor<ToolInvocation> for CodeModeExecuteHandler {
|
||||
ToolName::plain(PUBLIC_TOOL_NAME)
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(self.spec.clone())
|
||||
fn spec(&self) -> ToolSpec {
|
||||
self.spec.clone()
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
|
||||
@@ -47,8 +47,8 @@ impl ToolExecutor<ToolInvocation> for CodeModeWaitHandler {
|
||||
ToolName::plain(WAIT_TOOL_NAME)
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_wait_tool())
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_wait_tool()
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
|
||||
@@ -19,8 +19,8 @@ impl ToolExecutor<ToolInvocation> for ReportAgentJobResultHandler {
|
||||
ToolName::plain("report_agent_job_result")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_report_agent_job_result_tool())
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_report_agent_job_result_tool()
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
|
||||
@@ -20,8 +20,8 @@ impl ToolExecutor<ToolInvocation> for SpawnAgentsOnCsvHandler {
|
||||
ToolName::plain("spawn_agents_on_csv")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_spawn_agents_on_csv_tool())
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_spawn_agents_on_csv_tool()
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
|
||||
@@ -303,8 +303,8 @@ impl ToolExecutor<ToolInvocation> for ApplyPatchHandler {
|
||||
ToolName::plain("apply_patch")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_apply_patch_freeform_tool(self.multi_environment))
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_apply_patch_freeform_tool(self.multi_environment)
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
|
||||
@@ -31,7 +31,7 @@ use tracing::warn;
|
||||
|
||||
pub struct DynamicToolHandler {
|
||||
tool_name: ToolName,
|
||||
spec: Option<ToolSpec>,
|
||||
spec: ToolSpec,
|
||||
exposure: ToolExposure,
|
||||
search_text: String,
|
||||
}
|
||||
@@ -50,7 +50,7 @@ impl DynamicToolHandler {
|
||||
};
|
||||
Some(Self {
|
||||
tool_name,
|
||||
spec: Some(spec),
|
||||
spec,
|
||||
exposure: if tool.defer_loading {
|
||||
ToolExposure::Deferred
|
||||
} else {
|
||||
@@ -67,7 +67,7 @@ impl ToolExecutor<ToolInvocation> for DynamicToolHandler {
|
||||
self.tool_name.clone()
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
fn spec(&self) -> ToolSpec {
|
||||
self.spec.clone()
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ impl CoreToolRuntime for DynamicToolHandler {
|
||||
fn search_info(&self) -> Option<ToolSearchInfo> {
|
||||
ToolSearchInfo::from_spec(
|
||||
self.search_text.clone(),
|
||||
self.spec()?,
|
||||
self.spec(),
|
||||
Some(ToolSearchSourceInfo {
|
||||
name: "Dynamic tools".to_string(),
|
||||
description: Some("Tools provided by the current Codex thread.".to_string()),
|
||||
|
||||
@@ -37,7 +37,7 @@ impl ToolExecutor<ToolInvocation> for ExtensionToolAdapter {
|
||||
self.0.tool_name()
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
fn spec(&self) -> ToolSpec {
|
||||
self.0.spec()
|
||||
}
|
||||
|
||||
@@ -130,25 +130,23 @@ mod tests {
|
||||
codex_tools::ToolName::plain("extension_echo")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<codex_tools::ToolSpec> {
|
||||
Some(codex_tools::ToolSpec::Function(
|
||||
codex_tools::ResponsesApiTool {
|
||||
name: "extension_echo".to_string(),
|
||||
description: "Echoes arguments.".to_string(),
|
||||
strict: true,
|
||||
parameters: codex_tools::parse_tool_input_schema(&json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": { "type": "string" },
|
||||
},
|
||||
"required": ["message"],
|
||||
"additionalProperties": false,
|
||||
}))
|
||||
.expect("extension schema should parse"),
|
||||
output_schema: None,
|
||||
defer_loading: None,
|
||||
},
|
||||
))
|
||||
fn spec(&self) -> codex_tools::ToolSpec {
|
||||
codex_tools::ToolSpec::Function(codex_tools::ResponsesApiTool {
|
||||
name: "extension_echo".to_string(),
|
||||
description: "Echoes arguments.".to_string(),
|
||||
strict: true,
|
||||
parameters: codex_tools::parse_tool_input_schema(&json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": { "type": "string" },
|
||||
},
|
||||
"required": ["message"],
|
||||
"additionalProperties": false,
|
||||
}))
|
||||
.expect("extension schema should parse"),
|
||||
output_schema: None,
|
||||
defer_loading: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
@@ -171,6 +169,17 @@ mod tests {
|
||||
codex_tools::ToolName::plain("extension_echo")
|
||||
}
|
||||
|
||||
fn spec(&self) -> codex_tools::ToolSpec {
|
||||
codex_tools::ToolSpec::Function(codex_tools::ResponsesApiTool {
|
||||
name: "extension_echo".to_string(),
|
||||
description: "Captures arguments.".to_string(),
|
||||
strict: false,
|
||||
parameters: codex_tools::JsonSchema::default(),
|
||||
output_schema: None,
|
||||
defer_loading: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
&self,
|
||||
call: codex_tools::ToolCall,
|
||||
|
||||
@@ -24,8 +24,8 @@ impl ToolExecutor<ToolInvocation> for CreateGoalHandler {
|
||||
ToolName::plain(CREATE_GOAL_TOOL_NAME)
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_create_goal_tool())
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_create_goal_tool()
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
|
||||
@@ -21,8 +21,8 @@ impl ToolExecutor<ToolInvocation> for GetGoalHandler {
|
||||
ToolName::plain(GET_GOAL_TOOL_NAME)
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_get_goal_tool())
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_get_goal_tool()
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
|
||||
@@ -26,8 +26,8 @@ impl ToolExecutor<ToolInvocation> for UpdateGoalHandler {
|
||||
ToolName::plain(UPDATE_GOAL_TOOL_NAME)
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_update_goal_tool())
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_update_goal_tool()
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
|
||||
@@ -60,8 +60,8 @@ impl ToolExecutor<ToolInvocation> for ListAvailablePluginsToInstallHandler {
|
||||
ToolName::plain(LIST_AVAILABLE_PLUGINS_TO_INSTALL_TOOL_NAME)
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_list_available_plugins_to_install_tool())
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_list_available_plugins_to_install_tool()
|
||||
}
|
||||
|
||||
fn supports_parallel_tool_calls(&self) -> bool {
|
||||
|
||||
@@ -14,7 +14,6 @@ use crate::tools::registry::CoreToolRuntime;
|
||||
use crate::tools::registry::PostToolUsePayload;
|
||||
use crate::tools::registry::PreToolUsePayload;
|
||||
use crate::tools::registry::ToolExecutor;
|
||||
use crate::tools::registry::ToolExposure;
|
||||
use crate::tools::registry::ToolTelemetryTags;
|
||||
use crate::tools::tool_search_entry::ToolSearchInfo;
|
||||
use codex_mcp::ToolInfo;
|
||||
@@ -29,19 +28,13 @@ use serde_json::Value;
|
||||
|
||||
pub struct McpHandler {
|
||||
tool_info: ToolInfo,
|
||||
exposure: ToolExposure,
|
||||
spec: ToolSpec,
|
||||
}
|
||||
|
||||
impl McpHandler {
|
||||
pub fn new(tool_info: ToolInfo) -> Self {
|
||||
Self::with_exposure(tool_info, ToolExposure::Direct)
|
||||
}
|
||||
|
||||
pub fn with_exposure(tool_info: ToolInfo, exposure: ToolExposure) -> Self {
|
||||
Self {
|
||||
tool_info,
|
||||
exposure,
|
||||
}
|
||||
pub fn new(tool_info: ToolInfo) -> Result<Self, serde_json::Error> {
|
||||
let spec = create_tool_spec(&tool_info)?;
|
||||
Ok(Self { tool_info, spec })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,36 +44,8 @@ impl ToolExecutor<ToolInvocation> for McpHandler {
|
||||
self.tool_info.canonical_tool_name()
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
let tool_name = self.tool_name();
|
||||
let namespace_name = tool_name.namespace.as_ref()?;
|
||||
let tool = mcp_tool_to_responses_api_tool(&tool_name, &self.tool_info.tool).ok()?;
|
||||
let description = self
|
||||
.tool_info
|
||||
.namespace_description
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|description| !description.is_empty())
|
||||
.map(str::to_string)
|
||||
.or_else(|| {
|
||||
self.tool_info
|
||||
.connector_name
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|connector_name| !connector_name.is_empty())
|
||||
.map(|connector_name| format!("Tools for working with {connector_name}."))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
Some(ToolSpec::Namespace(ResponsesApiNamespace {
|
||||
name: namespace_name.clone(),
|
||||
description,
|
||||
tools: vec![ResponsesApiNamespaceTool::Function(tool)],
|
||||
}))
|
||||
}
|
||||
|
||||
fn exposure(&self) -> ToolExposure {
|
||||
self.exposure
|
||||
fn spec(&self) -> ToolSpec {
|
||||
self.spec.clone()
|
||||
}
|
||||
|
||||
fn supports_parallel_tool_calls(&self) -> bool {
|
||||
@@ -152,7 +117,7 @@ impl CoreToolRuntime for McpHandler {
|
||||
|
||||
ToolSearchInfo::from_spec(
|
||||
build_mcp_search_text(&self.tool_info),
|
||||
self.spec()?,
|
||||
self.spec(),
|
||||
source_info,
|
||||
)
|
||||
}
|
||||
@@ -223,6 +188,32 @@ impl CoreToolRuntime for McpHandler {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_tool_spec(tool_info: &ToolInfo) -> Result<ToolSpec, serde_json::Error> {
|
||||
let tool_name = tool_info.canonical_tool_name();
|
||||
let tool = mcp_tool_to_responses_api_tool(&tool_name, &tool_info.tool)?;
|
||||
let description = tool_info
|
||||
.namespace_description
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|description| !description.is_empty())
|
||||
.map(str::to_string)
|
||||
.or_else(|| {
|
||||
tool_info
|
||||
.connector_name
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|connector_name| !connector_name.is_empty())
|
||||
.map(|connector_name| format!("Tools for working with {connector_name}."))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(ToolSpec::Namespace(ResponsesApiNamespace {
|
||||
name: tool_info.callable_namespace.clone(),
|
||||
description,
|
||||
tools: vec![ResponsesApiNamespaceTool::Function(tool)],
|
||||
}))
|
||||
}
|
||||
|
||||
fn mcp_hook_tool_input(raw_arguments: &str) -> Value {
|
||||
if raw_arguments.trim().is_empty() {
|
||||
return Value::Object(Map::new());
|
||||
@@ -306,7 +297,8 @@ mod tests {
|
||||
.to_string(),
|
||||
};
|
||||
let (session, turn) = make_session_and_context().await;
|
||||
let handler = McpHandler::new(tool_info("memory", "mcp__memory__", "create_entities"));
|
||||
let handler = McpHandler::new(tool_info("memory", "mcp__memory__", "create_entities"))
|
||||
.expect("MCP tool spec should build");
|
||||
assert_eq!(
|
||||
handler.pre_tool_use_payload(&ToolInvocation {
|
||||
session: session.into(),
|
||||
@@ -336,7 +328,8 @@ mod tests {
|
||||
arguments: json!({ "message": "hello" }).to_string(),
|
||||
};
|
||||
let (session, turn) = make_session_and_context().await;
|
||||
let handler = McpHandler::new(tool_info("foo", "mcp__foo__", "exec_command"));
|
||||
let handler = McpHandler::new(tool_info("foo", "mcp__foo__", "exec_command"))
|
||||
.expect("MCP tool spec should build");
|
||||
|
||||
assert_eq!(
|
||||
handler.pre_tool_use_payload(&ToolInvocation {
|
||||
@@ -362,7 +355,8 @@ mod tests {
|
||||
arguments: json!({ "message": "hello" }).to_string(),
|
||||
};
|
||||
let (session, turn) = make_session_and_context().await;
|
||||
let handler = McpHandler::new(tool_info("foo", "mcp__foo__", "exec_command"));
|
||||
let handler = McpHandler::new(tool_info("foo", "mcp__foo__", "exec_command"))
|
||||
.expect("MCP tool spec should build");
|
||||
|
||||
let invocation = handler
|
||||
.with_updated_hook_input(
|
||||
@@ -411,7 +405,8 @@ mod tests {
|
||||
truncation_policy: codex_utils_output_truncation::TruncationPolicy::Bytes(1024),
|
||||
};
|
||||
let (session, turn) = make_session_and_context().await;
|
||||
let handler = McpHandler::new(tool_info("filesystem", "mcp__filesystem__", "read_file"));
|
||||
let handler = McpHandler::new(tool_info("filesystem", "mcp__filesystem__", "read_file"))
|
||||
.expect("MCP tool spec should build");
|
||||
let invocation = ToolInvocation {
|
||||
session: session.into(),
|
||||
turn: turn.into(),
|
||||
|
||||
@@ -32,8 +32,8 @@ impl ToolExecutor<ToolInvocation> for ListMcpResourceTemplatesHandler {
|
||||
ToolName::plain("list_mcp_resource_templates")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_list_mcp_resource_templates_tool())
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_list_mcp_resource_templates_tool()
|
||||
}
|
||||
|
||||
fn supports_parallel_tool_calls(&self) -> bool {
|
||||
|
||||
@@ -32,8 +32,8 @@ impl ToolExecutor<ToolInvocation> for ListMcpResourcesHandler {
|
||||
ToolName::plain("list_mcp_resources")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_list_mcp_resources_tool())
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_list_mcp_resources_tool()
|
||||
}
|
||||
|
||||
fn supports_parallel_tool_calls(&self) -> bool {
|
||||
|
||||
@@ -32,8 +32,8 @@ impl ToolExecutor<ToolInvocation> for ReadMcpResourceHandler {
|
||||
ToolName::plain("read_mcp_resource")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_read_mcp_resource_tool())
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_read_mcp_resource_tool()
|
||||
}
|
||||
|
||||
fn supports_parallel_tool_calls(&self) -> bool {
|
||||
|
||||
@@ -6,7 +6,7 @@ use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn search_info_uses_mcp_tool_metadata_and_parameter_names() {
|
||||
let handler = McpHandler::new(tool_info());
|
||||
let handler = McpHandler::new(tool_info()).expect("MCP tool spec should build");
|
||||
let search_info = handler.search_info().expect("MCP search info");
|
||||
|
||||
assert_eq!(
|
||||
@@ -26,7 +26,7 @@ fn search_info_uses_mcp_tool_metadata_and_parameter_names() {
|
||||
fn search_info_uses_connector_name_for_output_namespace_description() {
|
||||
let mut tool_info = tool_info();
|
||||
tool_info.namespace_description = None;
|
||||
let handler = McpHandler::new(tool_info);
|
||||
let handler = McpHandler::new(tool_info).expect("MCP tool spec should build");
|
||||
let search_info = handler.search_info().expect("MCP search info");
|
||||
|
||||
let LoadableToolSpec::Namespace(namespace) = search_info.entry.output else {
|
||||
|
||||
@@ -11,8 +11,8 @@ impl ToolExecutor<ToolInvocation> for Handler {
|
||||
ToolName::namespaced(MULTI_AGENT_V1_NAMESPACE, "close_agent")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_close_agent_tool_v1())
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_close_agent_tool_v1()
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
@@ -110,7 +110,7 @@ impl CoreToolRuntime for Handler {
|
||||
fn search_info(&self) -> Option<ToolSearchInfo> {
|
||||
multi_agent_tool_search_info(
|
||||
"close_agent close shutdown stop agent subagent thread status target",
|
||||
self.spec()?,
|
||||
self.spec(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ impl ToolExecutor<ToolInvocation> for Handler {
|
||||
ToolName::namespaced(MULTI_AGENT_V1_NAMESPACE, "resume_agent")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_resume_agent_tool())
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_resume_agent_tool()
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
@@ -138,7 +138,7 @@ impl CoreToolRuntime for Handler {
|
||||
fn search_info(&self) -> Option<ToolSearchInfo> {
|
||||
multi_agent_tool_search_info(
|
||||
"resume_agent resume reopen closed agent subagent thread id target",
|
||||
self.spec()?,
|
||||
self.spec(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ impl ToolExecutor<ToolInvocation> for Handler {
|
||||
ToolName::namespaced(MULTI_AGENT_V1_NAMESPACE, "send_input")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_send_input_tool_v1())
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_send_input_tool_v1()
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
@@ -94,7 +94,7 @@ impl CoreToolRuntime for Handler {
|
||||
fn search_info(&self) -> Option<ToolSearchInfo> {
|
||||
multi_agent_tool_search_info(
|
||||
"send_input send message existing agent subagent follow up interrupt redirect queue target",
|
||||
self.spec()?,
|
||||
self.spec(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ impl ToolExecutor<ToolInvocation> for Handler {
|
||||
ToolName::namespaced(MULTI_AGENT_V1_NAMESPACE, "spawn_agent")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_spawn_agent_tool_v1(self.options.clone()))
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_spawn_agent_tool_v1(self.options.clone())
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
@@ -206,7 +206,7 @@ impl CoreToolRuntime for Handler {
|
||||
fn search_info(&self) -> Option<ToolSearchInfo> {
|
||||
multi_agent_tool_search_info(
|
||||
"spawn_agent spawn agent subagent sub-agent delegate delegation parallel work worker explorer no-apps fork model reasoning",
|
||||
self.spec()?,
|
||||
self.spec(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@ impl ToolExecutor<ToolInvocation> for Handler {
|
||||
ToolName::namespaced(MULTI_AGENT_V1_NAMESPACE, "wait_agent")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_wait_agent_tool_v1(self.options))
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_wait_agent_tool_v1(self.options)
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
@@ -206,7 +206,7 @@ impl CoreToolRuntime for Handler {
|
||||
fn search_info(&self) -> Option<ToolSearchInfo> {
|
||||
multi_agent_tool_search_info(
|
||||
"wait_agent wait agent subagent status final result complete timeout targets",
|
||||
self.spec()?,
|
||||
self.spec(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ impl ToolExecutor<ToolInvocation> for Handler {
|
||||
ToolName::plain("close_agent")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_close_agent_tool_v2())
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_close_agent_tool_v2()
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
|
||||
@@ -13,8 +13,8 @@ impl ToolExecutor<ToolInvocation> for Handler {
|
||||
ToolName::plain("followup_task")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_followup_task_tool())
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_followup_task_tool()
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
|
||||
@@ -11,8 +11,8 @@ impl ToolExecutor<ToolInvocation> for Handler {
|
||||
ToolName::plain("list_agents")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_list_agents_tool())
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_list_agents_tool()
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
|
||||
@@ -13,8 +13,8 @@ impl ToolExecutor<ToolInvocation> for Handler {
|
||||
ToolName::plain("send_message")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_send_message_tool())
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_send_message_tool()
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
|
||||
@@ -30,8 +30,8 @@ impl ToolExecutor<ToolInvocation> for Handler {
|
||||
ToolName::plain("spawn_agent")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_spawn_agent_tool_v2(self.options.clone()))
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_spawn_agent_tool_v2(self.options.clone())
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
|
||||
@@ -25,8 +25,8 @@ impl ToolExecutor<ToolInvocation> for Handler {
|
||||
ToolName::plain("wait_agent")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_wait_agent_tool_v2(self.options))
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_wait_agent_tool_v2(self.options)
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
|
||||
@@ -51,8 +51,8 @@ impl ToolExecutor<ToolInvocation> for PlanHandler {
|
||||
ToolName::plain("update_plan")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_update_plan_tool())
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_update_plan_tool()
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
|
||||
@@ -22,10 +22,8 @@ impl ToolExecutor<ToolInvocation> for RequestPermissionsHandler {
|
||||
ToolName::plain("request_permissions")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_request_permissions_tool(
|
||||
request_permissions_tool_description(),
|
||||
))
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_request_permissions_tool(request_permissions_tool_description())
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
|
||||
@@ -45,8 +45,8 @@ impl ToolExecutor<ToolInvocation> for RequestPluginInstallHandler {
|
||||
ToolName::plain(REQUEST_PLUGIN_INSTALL_TOOL_NAME)
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_request_plugin_install_tool())
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_request_plugin_install_tool()
|
||||
}
|
||||
|
||||
fn supports_parallel_tool_calls(&self) -> bool {
|
||||
|
||||
@@ -26,10 +26,8 @@ impl ToolExecutor<ToolInvocation> for RequestUserInputHandler {
|
||||
ToolName::plain(REQUEST_USER_INPUT_TOOL_NAME)
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_request_user_input_tool(
|
||||
request_user_input_tool_description(&self.available_modes),
|
||||
))
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_request_user_input_tool(request_user_input_tool_description(&self.available_modes))
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
|
||||
@@ -39,7 +39,7 @@ enum ShellCommandBackend {
|
||||
|
||||
pub struct ShellCommandHandler {
|
||||
backend: ShellCommandBackend,
|
||||
options: Option<ShellCommandHandlerOptions>,
|
||||
options: ShellCommandHandlerOptions,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -51,10 +51,11 @@ pub(crate) struct ShellCommandHandlerOptions {
|
||||
|
||||
impl ShellCommandHandler {
|
||||
pub(crate) fn new(options: ShellCommandHandlerOptions) -> Self {
|
||||
Self {
|
||||
options: Some(options),
|
||||
..Self::from(options.backend_config)
|
||||
}
|
||||
let backend = match options.backend_config {
|
||||
ShellCommandBackendConfig::Classic => ShellCommandBackend::Classic,
|
||||
ShellCommandBackendConfig::ZshFork => ShellCommandBackend::ZshFork,
|
||||
};
|
||||
Self { backend, options }
|
||||
}
|
||||
|
||||
fn shell_runtime_backend(&self) -> ShellRuntimeBackend {
|
||||
@@ -114,15 +115,12 @@ impl ShellCommandHandler {
|
||||
}
|
||||
|
||||
impl From<ShellCommandBackendConfig> for ShellCommandHandler {
|
||||
fn from(config: ShellCommandBackendConfig) -> Self {
|
||||
let backend = match config {
|
||||
ShellCommandBackendConfig::Classic => ShellCommandBackend::Classic,
|
||||
ShellCommandBackendConfig::ZshFork => ShellCommandBackend::ZshFork,
|
||||
};
|
||||
Self {
|
||||
backend,
|
||||
options: None,
|
||||
}
|
||||
fn from(backend_config: ShellCommandBackendConfig) -> Self {
|
||||
Self::new(ShellCommandHandlerOptions {
|
||||
backend_config,
|
||||
allow_login_shell: false,
|
||||
exec_permission_approvals_enabled: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,17 +130,15 @@ impl ToolExecutor<ToolInvocation> for ShellCommandHandler {
|
||||
ToolName::plain("shell_command")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
self.options.map(|options| {
|
||||
create_shell_command_tool(CommandToolOptions {
|
||||
allow_login_shell: options.allow_login_shell,
|
||||
exec_permission_approvals_enabled: options.exec_permission_approvals_enabled,
|
||||
})
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_shell_command_tool(CommandToolOptions {
|
||||
allow_login_shell: self.options.allow_login_shell,
|
||||
exec_permission_approvals_enabled: self.options.exec_permission_approvals_enabled,
|
||||
})
|
||||
}
|
||||
|
||||
fn supports_parallel_tool_calls(&self) -> bool {
|
||||
self.options.is_some()
|
||||
true
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
|
||||
@@ -63,8 +63,8 @@ impl ToolExecutor<ToolInvocation> for TestSyncHandler {
|
||||
ToolName::plain("test_sync_tool")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_test_sync_tool())
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_test_sync_tool()
|
||||
}
|
||||
|
||||
fn supports_parallel_tool_calls(&self) -> bool {
|
||||
|
||||
@@ -59,11 +59,8 @@ impl ToolExecutor<ToolInvocation> for ToolSearchHandler {
|
||||
ToolName::plain(TOOL_SEARCH_TOOL_NAME)
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_tool_search_tool(
|
||||
&self.search_source_infos,
|
||||
TOOL_SEARCH_DEFAULT_LIMIT,
|
||||
))
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_tool_search_tool(&self.search_source_infos, TOOL_SEARCH_DEFAULT_LIMIT)
|
||||
}
|
||||
|
||||
fn supports_parallel_tool_calls(&self) -> bool {
|
||||
@@ -174,6 +171,7 @@ mod tests {
|
||||
.iter()
|
||||
.map(|tool| {
|
||||
McpHandler::new(tool.clone())
|
||||
.expect("MCP tool should convert")
|
||||
.search_info()
|
||||
.expect("MCP handler should return search info")
|
||||
})
|
||||
|
||||
@@ -74,14 +74,14 @@ impl ToolExecutor<ToolInvocation> for ExecCommandHandler {
|
||||
ToolName::plain("exec_command")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_exec_command_tool_with_environment_id(
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_exec_command_tool_with_environment_id(
|
||||
CommandToolOptions {
|
||||
allow_login_shell: self.options.allow_login_shell,
|
||||
exec_permission_approvals_enabled: self.options.exec_permission_approvals_enabled,
|
||||
},
|
||||
self.options.include_environment_id,
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
fn supports_parallel_tool_calls(&self) -> bool {
|
||||
|
||||
@@ -36,8 +36,8 @@ impl ToolExecutor<ToolInvocation> for WriteStdinHandler {
|
||||
ToolName::plain("write_stdin")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_write_stdin_tool())
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_write_stdin_tool()
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
|
||||
@@ -70,8 +70,8 @@ impl ToolExecutor<ToolInvocation> for ViewImageHandler {
|
||||
ToolName::plain("view_image")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_view_image_tool(self.options))
|
||||
fn spec(&self) -> ToolSpec {
|
||||
create_view_image_tool(self.options)
|
||||
}
|
||||
|
||||
fn supports_parallel_tool_calls(&self) -> bool {
|
||||
|
||||
@@ -14,7 +14,6 @@ pub(crate) mod runtimes;
|
||||
pub(crate) mod sandboxing;
|
||||
pub(crate) mod spec_plan;
|
||||
pub(crate) mod tool_dispatch_trace;
|
||||
pub(crate) mod tool_family;
|
||||
pub(crate) mod tool_search_entry;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
@@ -250,6 +250,17 @@ mod tests {
|
||||
self.tool_name.clone()
|
||||
}
|
||||
|
||||
fn spec(&self) -> codex_tools::ToolSpec {
|
||||
codex_tools::ToolSpec::Function(codex_tools::ResponsesApiTool {
|
||||
name: self.tool_name.name.clone(),
|
||||
description: "Immediate test tool.".to_string(),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: codex_tools::JsonSchema::default(),
|
||||
output_schema: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
&self,
|
||||
_invocation: ToolInvocation,
|
||||
|
||||
@@ -184,7 +184,7 @@ impl ToolExecutor<ToolInvocation> for ExposureOverride {
|
||||
self.handler.tool_name()
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
fn spec(&self) -> ToolSpec {
|
||||
self.handler.spec()
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ impl ToolExecutor<ToolInvocation> for ExposureOverride {
|
||||
}
|
||||
|
||||
fn supports_parallel_tool_calls(&self) -> bool {
|
||||
self.handler.supports_parallel_tool_calls()
|
||||
self.exposure != ToolExposure::Hidden && self.handler.supports_parallel_tool_calls()
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
|
||||
@@ -11,6 +11,10 @@ impl ToolExecutor<ToolInvocation> for TestHandler {
|
||||
self.tool_name.clone()
|
||||
}
|
||||
|
||||
fn spec(&self) -> codex_tools::ToolSpec {
|
||||
test_spec(&self.tool_name)
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
&self,
|
||||
_invocation: ToolInvocation,
|
||||
@@ -40,6 +44,10 @@ impl ToolExecutor<ToolInvocation> for LifecycleTestHandler {
|
||||
self.tool_name.clone()
|
||||
}
|
||||
|
||||
fn spec(&self) -> codex_tools::ToolSpec {
|
||||
test_spec(&self.tool_name)
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
&self,
|
||||
_invocation: ToolInvocation,
|
||||
@@ -60,6 +68,17 @@ impl ToolExecutor<ToolInvocation> for LifecycleTestHandler {
|
||||
|
||||
impl CoreToolRuntime for LifecycleTestHandler {}
|
||||
|
||||
fn test_spec(tool_name: &codex_tools::ToolName) -> codex_tools::ToolSpec {
|
||||
codex_tools::ToolSpec::Function(codex_tools::ResponsesApiTool {
|
||||
name: tool_name.name.clone(),
|
||||
description: "Test tool.".to_string(),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: codex_tools::JsonSchema::default(),
|
||||
output_schema: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum RecordedToolLifecycle {
|
||||
Start {
|
||||
|
||||
@@ -49,8 +49,8 @@ impl ToolExecutor<ExtensionToolCall> for ExtensionEchoExecutor {
|
||||
ToolName::namespaced("extension/", "echo")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(ToolSpec::Namespace(ResponsesApiNamespace {
|
||||
fn spec(&self) -> ToolSpec {
|
||||
ToolSpec::Namespace(ResponsesApiNamespace {
|
||||
name: "extension/".to_string(),
|
||||
description: default_namespace_description("extension/"),
|
||||
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
|
||||
@@ -69,7 +69,7 @@ impl ToolExecutor<ExtensionToolCall> for ExtensionEchoExecutor {
|
||||
output_schema: None,
|
||||
defer_loading: None,
|
||||
})],
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
|
||||
@@ -6,6 +6,8 @@ use crate::tools::handlers::CodeModeExecuteHandler;
|
||||
use crate::tools::handlers::CodeModeWaitHandler;
|
||||
use crate::tools::handlers::CreateGoalHandler;
|
||||
use crate::tools::handlers::DynamicToolHandler;
|
||||
use crate::tools::handlers::ExecCommandHandler;
|
||||
use crate::tools::handlers::ExecCommandHandlerOptions;
|
||||
use crate::tools::handlers::GetGoalHandler;
|
||||
use crate::tools::handlers::ListAvailablePluginsToInstallHandler;
|
||||
use crate::tools::handlers::ListMcpResourceTemplatesHandler;
|
||||
@@ -16,10 +18,13 @@ use crate::tools::handlers::ReadMcpResourceHandler;
|
||||
use crate::tools::handlers::RequestPermissionsHandler;
|
||||
use crate::tools::handlers::RequestPluginInstallHandler;
|
||||
use crate::tools::handlers::RequestUserInputHandler;
|
||||
use crate::tools::handlers::ShellCommandHandler;
|
||||
use crate::tools::handlers::ShellCommandHandlerOptions;
|
||||
use crate::tools::handlers::TestSyncHandler;
|
||||
use crate::tools::handlers::ToolSearchHandler;
|
||||
use crate::tools::handlers::UpdateGoalHandler;
|
||||
use crate::tools::handlers::ViewImageHandler;
|
||||
use crate::tools::handlers::WriteStdinHandler;
|
||||
use crate::tools::handlers::agent_jobs::ReportAgentJobResultHandler;
|
||||
use crate::tools::handlers::agent_jobs::SpawnAgentsOnCsvHandler;
|
||||
use crate::tools::handlers::extension_tools::ExtensionToolAdapter;
|
||||
@@ -49,12 +54,11 @@ use crate::tools::registry::ToolRegistry;
|
||||
use crate::tools::registry::override_tool_exposure;
|
||||
use crate::tools::router::ToolRouter;
|
||||
use crate::tools::router::ToolRouterParams;
|
||||
use crate::tools::tool_family::shell::ShellToolsOptions;
|
||||
use crate::tools::tool_family::shell::build_shell_tools;
|
||||
use codex_features::Feature;
|
||||
use codex_login::AuthManager;
|
||||
use codex_mcp::ToolInfo;
|
||||
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use codex_protocol::openai_models::ConfigShellToolType;
|
||||
use codex_protocol::openai_models::InputModality;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
@@ -91,15 +95,34 @@ struct PlannedTools {
|
||||
}
|
||||
|
||||
impl PlannedTools {
|
||||
fn add_runtime<T>(&mut self, runtime: T)
|
||||
fn add<T>(&mut self, handler: T)
|
||||
where
|
||||
T: CoreToolRuntime + 'static,
|
||||
{
|
||||
self.runtimes.push(Arc::new(runtime));
|
||||
self.runtimes.push(Arc::new(handler));
|
||||
}
|
||||
|
||||
fn add_runtime_arc(&mut self, runtime: PlannedRuntime) {
|
||||
self.runtimes.push(runtime);
|
||||
fn add_arc(&mut self, handler: PlannedRuntime) {
|
||||
self.runtimes.push(handler);
|
||||
}
|
||||
|
||||
fn add_with_exposure<T>(&mut self, handler: T, exposure: ToolExposure)
|
||||
where
|
||||
T: CoreToolRuntime + 'static,
|
||||
{
|
||||
self.runtimes
|
||||
.push(override_tool_exposure(Arc::new(handler), exposure));
|
||||
}
|
||||
|
||||
fn add_dispatch_only<T>(&mut self, handler: T)
|
||||
where
|
||||
T: CoreToolRuntime + 'static,
|
||||
{
|
||||
self.add_with_exposure(handler, ToolExposure::Hidden);
|
||||
}
|
||||
|
||||
fn add_hosted_spec(&mut self, spec: ToolSpec) {
|
||||
self.hosted_specs.push(spec);
|
||||
}
|
||||
|
||||
fn runtimes(&self) -> &[PlannedRuntime] {
|
||||
@@ -173,10 +196,9 @@ fn build_model_visible_specs_and_registry(
|
||||
continue;
|
||||
}
|
||||
let exposure = runtime.exposure();
|
||||
if exposure.is_direct()
|
||||
&& !is_hidden_by_code_mode_only(turn_context, &tool_name, exposure)
|
||||
&& let Some(spec) = runtime.spec()
|
||||
if exposure.is_direct() && !is_hidden_by_code_mode_only(turn_context, &tool_name, exposure)
|
||||
{
|
||||
let spec = runtime.spec();
|
||||
specs.push(spec_for_model_request(turn_context, exposure, spec));
|
||||
}
|
||||
}
|
||||
@@ -375,9 +397,10 @@ fn build_code_mode_executors(
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(spec) = executor.spec() else {
|
||||
if exposure == ToolExposure::Hidden {
|
||||
continue;
|
||||
};
|
||||
}
|
||||
let spec = executor.spec();
|
||||
|
||||
if exposure != ToolExposure::Deferred {
|
||||
exec_prompt_tool_specs.push(spec.clone());
|
||||
@@ -481,30 +504,55 @@ fn add_tool_sources(context: &CoreToolPlanContext<'_>, planned_tools: &mut Plann
|
||||
add_mcp_runtime_tools(context, planned_tools);
|
||||
add_dynamic_tools(context, planned_tools);
|
||||
add_extension_tools(context, planned_tools);
|
||||
planned_tools
|
||||
.hosted_specs
|
||||
.extend(hosted_model_tool_specs(context.turn_context));
|
||||
for spec in hosted_model_tool_specs(context.turn_context) {
|
||||
planned_tools.add_hosted_spec(spec);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_shell_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {
|
||||
let turn_context = context.turn_context;
|
||||
let features = turn_context.features.get();
|
||||
planned_tools
|
||||
.runtimes
|
||||
.extend(build_shell_tools(ShellToolsOptions {
|
||||
shell_type: shell_type_for_model_and_features(&turn_context.model_info, features),
|
||||
shell_command_backend: shell_command_backend_for_features(features),
|
||||
environment_mode: turn_context.tool_environment_mode(),
|
||||
allow_login_shell: turn_context.config.permissions.allow_login_shell,
|
||||
exec_permission_approvals_enabled: features.enabled(Feature::ExecPermissionApprovals),
|
||||
}));
|
||||
let environment_mode = turn_context.tool_environment_mode();
|
||||
if !environment_mode.has_environment() {
|
||||
return;
|
||||
}
|
||||
|
||||
let allow_login_shell = turn_context.config.permissions.allow_login_shell;
|
||||
let exec_permission_approvals_enabled = features.enabled(Feature::ExecPermissionApprovals);
|
||||
let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple);
|
||||
let shell_command_options = ShellCommandHandlerOptions {
|
||||
backend_config: shell_command_backend_for_features(features),
|
||||
allow_login_shell,
|
||||
exec_permission_approvals_enabled,
|
||||
};
|
||||
|
||||
match shell_type_for_model_and_features(&turn_context.model_info, features) {
|
||||
ConfigShellToolType::UnifiedExec => {
|
||||
planned_tools.add(ExecCommandHandler::new(ExecCommandHandlerOptions {
|
||||
allow_login_shell,
|
||||
exec_permission_approvals_enabled,
|
||||
include_environment_id,
|
||||
}));
|
||||
planned_tools.add(WriteStdinHandler);
|
||||
|
||||
// Keep the legacy shell tool registered while unified exec is
|
||||
// model-visible.
|
||||
planned_tools.add_dispatch_only(ShellCommandHandler::new(shell_command_options));
|
||||
}
|
||||
ConfigShellToolType::Disabled => {}
|
||||
ConfigShellToolType::Default
|
||||
| ConfigShellToolType::Local
|
||||
| ConfigShellToolType::ShellCommand => {
|
||||
planned_tools.add(ShellCommandHandler::new(shell_command_options));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_mcp_resource_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {
|
||||
if context.mcp_tools.is_some() {
|
||||
planned_tools.add_runtime(ListMcpResourcesHandler);
|
||||
planned_tools.add_runtime(ListMcpResourceTemplatesHandler);
|
||||
planned_tools.add_runtime(ReadMcpResourceHandler);
|
||||
planned_tools.add(ListMcpResourcesHandler);
|
||||
planned_tools.add(ListMcpResourceTemplatesHandler);
|
||||
planned_tools.add(ReadMcpResourceHandler);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,35 +561,35 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut
|
||||
let features = turn_context.features.get();
|
||||
let environment_mode = turn_context.tool_environment_mode();
|
||||
|
||||
planned_tools.add_runtime(PlanHandler);
|
||||
planned_tools.add(PlanHandler);
|
||||
if goal_tools_enabled(turn_context) {
|
||||
planned_tools.add_runtime(GetGoalHandler);
|
||||
planned_tools.add_runtime(CreateGoalHandler);
|
||||
planned_tools.add_runtime(UpdateGoalHandler);
|
||||
planned_tools.add(GetGoalHandler);
|
||||
planned_tools.add(CreateGoalHandler);
|
||||
planned_tools.add(UpdateGoalHandler);
|
||||
}
|
||||
|
||||
planned_tools.add_runtime(RequestUserInputHandler {
|
||||
planned_tools.add(RequestUserInputHandler {
|
||||
available_modes: request_user_input_available_modes(features),
|
||||
});
|
||||
|
||||
if features.enabled(Feature::RequestPermissionsTool) {
|
||||
planned_tools.add_runtime(RequestPermissionsHandler);
|
||||
planned_tools.add(RequestPermissionsHandler);
|
||||
}
|
||||
|
||||
if tool_suggest_enabled(turn_context)
|
||||
&& let Some(discoverable_tools) =
|
||||
context.discoverable_tools.filter(|tools| !tools.is_empty())
|
||||
{
|
||||
planned_tools.add_runtime(ListAvailablePluginsToInstallHandler::new(
|
||||
planned_tools.add(ListAvailablePluginsToInstallHandler::new(
|
||||
collect_request_plugin_install_entries(discoverable_tools),
|
||||
));
|
||||
planned_tools.add_runtime(RequestPluginInstallHandler);
|
||||
planned_tools.add(RequestPluginInstallHandler);
|
||||
}
|
||||
|
||||
if environment_mode.has_environment() && turn_context.model_info.apply_patch_tool_type.is_some()
|
||||
{
|
||||
let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple);
|
||||
planned_tools.add_runtime(ApplyPatchHandler::new(include_environment_id));
|
||||
planned_tools.add(ApplyPatchHandler::new(include_environment_id));
|
||||
}
|
||||
|
||||
if turn_context
|
||||
@@ -550,12 +598,12 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut
|
||||
.iter()
|
||||
.any(|tool| tool == "test_sync_tool")
|
||||
{
|
||||
planned_tools.add_runtime(TestSyncHandler);
|
||||
planned_tools.add(TestSyncHandler);
|
||||
}
|
||||
|
||||
if environment_mode.has_environment() {
|
||||
let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple);
|
||||
planned_tools.add_runtime(ViewImageHandler::new(ViewImageToolOptions {
|
||||
planned_tools.add(ViewImageHandler::new(ViewImageToolOptions {
|
||||
can_request_original_image_detail: can_request_original_image_detail(
|
||||
&turn_context.model_info,
|
||||
),
|
||||
@@ -578,47 +626,47 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mu
|
||||
.flatten();
|
||||
let agent_type_description =
|
||||
agent_type_description(turn_context, context.default_agent_type_description);
|
||||
planned_tools.add_runtime_arc(multi_agent_v2_handler(
|
||||
SpawnAgentHandlerV2::new(SpawnAgentToolOptions {
|
||||
available_models: turn_context.available_models.clone(),
|
||||
agent_type_description,
|
||||
hide_agent_type_model_reasoning: turn_context
|
||||
.config
|
||||
.multi_agent_v2
|
||||
.hide_spawn_agent_metadata,
|
||||
include_usage_hint: turn_context.config.multi_agent_v2.usage_hint_enabled,
|
||||
usage_hint_text: turn_context.config.multi_agent_v2.usage_hint_text.clone(),
|
||||
max_concurrent_threads_per_session: max_concurrent_threads_per_session(
|
||||
turn_context,
|
||||
),
|
||||
}),
|
||||
planned_tools.add_arc(override_tool_exposure(
|
||||
multi_agent_v2_handler(
|
||||
SpawnAgentHandlerV2::new(SpawnAgentToolOptions {
|
||||
available_models: turn_context.available_models.clone(),
|
||||
agent_type_description,
|
||||
hide_agent_type_model_reasoning: turn_context
|
||||
.config
|
||||
.multi_agent_v2
|
||||
.hide_spawn_agent_metadata,
|
||||
include_usage_hint: turn_context.config.multi_agent_v2.usage_hint_enabled,
|
||||
usage_hint_text: turn_context.config.multi_agent_v2.usage_hint_text.clone(),
|
||||
max_concurrent_threads_per_session: max_concurrent_threads_per_session(
|
||||
turn_context,
|
||||
),
|
||||
}),
|
||||
tool_namespace,
|
||||
),
|
||||
exposure,
|
||||
tool_namespace,
|
||||
));
|
||||
planned_tools.add_runtime_arc(multi_agent_v2_handler(
|
||||
SendMessageHandlerV2,
|
||||
planned_tools.add_arc(override_tool_exposure(
|
||||
multi_agent_v2_handler(SendMessageHandlerV2, tool_namespace),
|
||||
exposure,
|
||||
tool_namespace,
|
||||
));
|
||||
planned_tools.add_runtime_arc(multi_agent_v2_handler(
|
||||
FollowupTaskHandlerV2,
|
||||
planned_tools.add_arc(override_tool_exposure(
|
||||
multi_agent_v2_handler(FollowupTaskHandlerV2, tool_namespace),
|
||||
exposure,
|
||||
tool_namespace,
|
||||
));
|
||||
planned_tools.add_runtime_arc(multi_agent_v2_handler(
|
||||
WaitAgentHandlerV2::new(context.wait_agent_timeouts),
|
||||
planned_tools.add_arc(override_tool_exposure(
|
||||
multi_agent_v2_handler(
|
||||
WaitAgentHandlerV2::new(context.wait_agent_timeouts),
|
||||
tool_namespace,
|
||||
),
|
||||
exposure,
|
||||
tool_namespace,
|
||||
));
|
||||
planned_tools.add_runtime_arc(multi_agent_v2_handler(
|
||||
CloseAgentHandlerV2,
|
||||
planned_tools.add_arc(override_tool_exposure(
|
||||
multi_agent_v2_handler(CloseAgentHandlerV2, tool_namespace),
|
||||
exposure,
|
||||
tool_namespace,
|
||||
));
|
||||
planned_tools.add_runtime_arc(multi_agent_v2_handler(
|
||||
ListAgentsHandlerV2,
|
||||
planned_tools.add_arc(override_tool_exposure(
|
||||
multi_agent_v2_handler(ListAgentsHandlerV2, tool_namespace),
|
||||
exposure,
|
||||
tool_namespace,
|
||||
));
|
||||
} else {
|
||||
let agent_type_description =
|
||||
@@ -629,7 +677,7 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mu
|
||||
} else {
|
||||
ToolExposure::Direct
|
||||
};
|
||||
planned_tools.add_runtime_arc(multi_agent_v1_handler(
|
||||
planned_tools.add_with_exposure(
|
||||
SpawnAgentHandler::new(SpawnAgentToolOptions {
|
||||
available_models: turn_context.available_models.clone(),
|
||||
agent_type_description,
|
||||
@@ -644,21 +692,19 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mu
|
||||
),
|
||||
}),
|
||||
exposure,
|
||||
));
|
||||
planned_tools.add_runtime_arc(multi_agent_v1_handler(SendInputHandler, exposure));
|
||||
planned_tools.add_runtime_arc(multi_agent_v1_handler(ResumeAgentHandler, exposure));
|
||||
planned_tools.add_runtime_arc(multi_agent_v1_handler(
|
||||
WaitAgentHandler::new(context.wait_agent_timeouts),
|
||||
exposure,
|
||||
));
|
||||
planned_tools.add_runtime_arc(multi_agent_v1_handler(CloseAgentHandler, exposure));
|
||||
);
|
||||
planned_tools.add_with_exposure(SendInputHandler, exposure);
|
||||
planned_tools.add_with_exposure(ResumeAgentHandler, exposure);
|
||||
planned_tools
|
||||
.add_with_exposure(WaitAgentHandler::new(context.wait_agent_timeouts), exposure);
|
||||
planned_tools.add_with_exposure(CloseAgentHandler, exposure);
|
||||
}
|
||||
}
|
||||
|
||||
if agent_jobs_tools_enabled(turn_context) {
|
||||
planned_tools.add_runtime(SpawnAgentsOnCsvHandler);
|
||||
planned_tools.add(SpawnAgentsOnCsvHandler);
|
||||
if agent_jobs_worker_tools_enabled(turn_context) {
|
||||
planned_tools.add_runtime(ReportAgentJobResultHandler);
|
||||
planned_tools.add(ReportAgentJobResultHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -666,16 +712,25 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mu
|
||||
fn add_mcp_runtime_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {
|
||||
if let Some(mcp_tools) = context.mcp_tools {
|
||||
for tool in mcp_tools {
|
||||
planned_tools.add_runtime(McpHandler::new(tool.clone()));
|
||||
match McpHandler::new(tool.clone()) {
|
||||
Ok(handler) => planned_tools.add(handler),
|
||||
Err(err) => warn!(
|
||||
"Skipping MCP tool `{}`: failed to build tool spec: {err}",
|
||||
tool.canonical_tool_name()
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(deferred_mcp_tools) = context.deferred_mcp_tools {
|
||||
for tool in deferred_mcp_tools {
|
||||
planned_tools.add_runtime(McpHandler::with_exposure(
|
||||
tool.clone(),
|
||||
ToolExposure::Deferred,
|
||||
));
|
||||
match McpHandler::new(tool.clone()) {
|
||||
Ok(handler) => planned_tools.add_with_exposure(handler, ToolExposure::Deferred),
|
||||
Err(err) => warn!(
|
||||
"Skipping deferred MCP tool `{}`: failed to build tool spec: {err}",
|
||||
tool.canonical_tool_name()
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -690,7 +745,7 @@ fn add_dynamic_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut Plan
|
||||
continue;
|
||||
};
|
||||
|
||||
planned_tools.add_runtime(handler);
|
||||
planned_tools.add(handler);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -723,7 +778,7 @@ fn append_tool_search_executor(
|
||||
return;
|
||||
}
|
||||
|
||||
planned_tools.add_runtime(ToolSearchHandler::new(search_infos));
|
||||
planned_tools.add(ToolSearchHandler::new(search_infos));
|
||||
}
|
||||
|
||||
fn prepend_code_mode_executors(
|
||||
@@ -778,30 +833,21 @@ fn append_extension_tool_executors(
|
||||
warn!("Skipping extension tool `{tool_name}`: tool already registered");
|
||||
continue;
|
||||
}
|
||||
planned_tools.add_runtime(ExtensionToolAdapter::new(executor));
|
||||
planned_tools.add(ExtensionToolAdapter::new(executor));
|
||||
}
|
||||
}
|
||||
|
||||
fn multi_agent_v1_handler(
|
||||
handler: impl CoreToolRuntime + 'static,
|
||||
exposure: ToolExposure,
|
||||
) -> Arc<dyn CoreToolRuntime> {
|
||||
override_tool_exposure(Arc::new(handler), exposure)
|
||||
}
|
||||
|
||||
fn multi_agent_v2_handler(
|
||||
handler: impl CoreToolRuntime + 'static,
|
||||
exposure: ToolExposure,
|
||||
namespace: Option<&str>,
|
||||
) -> Arc<dyn CoreToolRuntime> {
|
||||
let handler: Arc<dyn CoreToolRuntime> = match namespace {
|
||||
match namespace {
|
||||
Some(namespace) => Arc::new(MultiAgentV2NamespaceOverride {
|
||||
handler: Arc::new(handler),
|
||||
namespace: namespace.to_string(),
|
||||
}),
|
||||
None => Arc::new(handler),
|
||||
};
|
||||
override_tool_exposure(handler, exposure)
|
||||
}
|
||||
}
|
||||
|
||||
struct MultiAgentV2NamespaceOverride {
|
||||
@@ -815,14 +861,14 @@ impl ToolExecutor<ToolInvocation> for MultiAgentV2NamespaceOverride {
|
||||
ToolName::namespaced(self.namespace.clone(), self.handler.tool_name().name)
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
match self.handler.spec()? {
|
||||
ToolSpec::Function(tool) => Some(ToolSpec::Namespace(ResponsesApiNamespace {
|
||||
fn spec(&self) -> ToolSpec {
|
||||
match self.handler.spec() {
|
||||
ToolSpec::Function(tool) => ToolSpec::Namespace(ResponsesApiNamespace {
|
||||
name: self.namespace.clone(),
|
||||
description: MULTI_AGENT_V2_NAMESPACE_DESCRIPTION.to_string(),
|
||||
tools: vec![ResponsesApiNamespaceTool::Function(tool)],
|
||||
})),
|
||||
spec => Some(spec),
|
||||
}),
|
||||
spec => spec,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -288,6 +288,14 @@ fn mcp_tool(server: &str, namespace: &str, name: &str) -> ToolInfo {
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid_mcp_tool(server: &str, namespace: &str, name: &str) -> ToolInfo {
|
||||
let mut tool = mcp_tool(server, namespace, name);
|
||||
tool.tool.input_schema = Arc::new(rmcp::model::object(json!({
|
||||
"type": "null",
|
||||
})));
|
||||
tool
|
||||
}
|
||||
|
||||
fn dynamic_tool(namespace: Option<&str>, name: &str, defer_loading: bool) -> DynamicToolSpec {
|
||||
DynamicToolSpec {
|
||||
namespace: namespace.map(str::to_string),
|
||||
@@ -342,7 +350,7 @@ async fn shell_family_registers_visible_unified_exec_and_hidden_legacy_shell() {
|
||||
plan.assert_visible_contains(&["exec_command", "write_stdin"]);
|
||||
plan.assert_visible_lacks(&["shell_command"]);
|
||||
plan.assert_registered_contains(&["exec_command", "write_stdin", "shell_command"]);
|
||||
assert_eq!(plan.exposure("shell_command"), ToolExposure::Direct);
|
||||
assert_eq!(plan.exposure("shell_command"), ToolExposure::Hidden);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -507,6 +515,25 @@ async fn mcp_and_tool_search_follow_direct_and_deferred_tool_exposure() {
|
||||
enabled.assert_registered_contains(&["tool_search", "mcp__searchable__lookup"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalid_mcp_tools_are_not_registered() {
|
||||
let plan = probe_with(
|
||||
|_| {},
|
||||
ToolPlanInputs {
|
||||
mcp_tools: Some(vec![invalid_mcp_tool(
|
||||
"invalid",
|
||||
"mcp__invalid__",
|
||||
"lookup",
|
||||
)]),
|
||||
..ToolPlanInputs::default()
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
plan.assert_visible_lacks(&["mcp__invalid__"]);
|
||||
plan.assert_registered_lacks(&["mcp__invalid__lookup"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn request_plugin_install_requires_all_discovery_features_and_discoverable_tools() {
|
||||
let discoverable_tools = Some(vec![discoverable_plugin("github", "GitHub")]);
|
||||
|
||||
@@ -36,6 +36,17 @@ impl ToolExecutor<ToolInvocation> for TestHandler {
|
||||
self.tool_name.clone()
|
||||
}
|
||||
|
||||
fn spec(&self) -> codex_tools::ToolSpec {
|
||||
codex_tools::ToolSpec::Function(codex_tools::ResponsesApiTool {
|
||||
name: self.tool_name.name.clone(),
|
||||
description: "Test tool.".to_string(),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: codex_tools::JsonSchema::default(),
|
||||
output_schema: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
&self,
|
||||
_invocation: ToolInvocation,
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub(crate) mod shell;
|
||||
@@ -1,72 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_protocol::openai_models::ConfigShellToolType;
|
||||
use codex_tools::ShellCommandBackendConfig;
|
||||
use codex_tools::ToolEnvironmentMode;
|
||||
|
||||
use crate::tools::handlers::ExecCommandHandler;
|
||||
use crate::tools::handlers::ExecCommandHandlerOptions;
|
||||
use crate::tools::handlers::ShellCommandHandler;
|
||||
use crate::tools::handlers::ShellCommandHandlerOptions;
|
||||
use crate::tools::handlers::WriteStdinHandler;
|
||||
use crate::tools::registry::CoreToolRuntime;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct ShellToolsOptions {
|
||||
pub(crate) shell_type: ConfigShellToolType,
|
||||
pub(crate) shell_command_backend: ShellCommandBackendConfig,
|
||||
pub(crate) environment_mode: ToolEnvironmentMode,
|
||||
pub(crate) allow_login_shell: bool,
|
||||
pub(crate) exec_permission_approvals_enabled: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn build_shell_tools(options: ShellToolsOptions) -> Vec<Arc<dyn CoreToolRuntime>> {
|
||||
let mut runtimes = Vec::new();
|
||||
if !options.environment_mode.has_environment() {
|
||||
return runtimes;
|
||||
}
|
||||
|
||||
let include_environment_id = matches!(options.environment_mode, ToolEnvironmentMode::Multiple);
|
||||
match options.shell_type {
|
||||
ConfigShellToolType::UnifiedExec => {
|
||||
add_runtime(
|
||||
&mut runtimes,
|
||||
ExecCommandHandler::new(ExecCommandHandlerOptions {
|
||||
allow_login_shell: options.allow_login_shell,
|
||||
exec_permission_approvals_enabled: options.exec_permission_approvals_enabled,
|
||||
include_environment_id,
|
||||
}),
|
||||
);
|
||||
add_runtime(&mut runtimes, WriteStdinHandler);
|
||||
|
||||
// Keep the legacy shell tool registered as a hidden runtime while
|
||||
// unified exec is model-visible.
|
||||
add_runtime(
|
||||
&mut runtimes,
|
||||
ShellCommandHandler::from(options.shell_command_backend),
|
||||
);
|
||||
}
|
||||
ConfigShellToolType::Disabled => {}
|
||||
ConfigShellToolType::Default
|
||||
| ConfigShellToolType::Local
|
||||
| ConfigShellToolType::ShellCommand => {
|
||||
add_runtime(
|
||||
&mut runtimes,
|
||||
ShellCommandHandler::new(ShellCommandHandlerOptions {
|
||||
backend_config: options.shell_command_backend,
|
||||
allow_login_shell: options.allow_login_shell,
|
||||
exec_permission_approvals_enabled: options.exec_permission_approvals_enabled,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
runtimes
|
||||
}
|
||||
|
||||
fn add_runtime<T>(runtimes: &mut Vec<Arc<dyn CoreToolRuntime>>, runtime: T)
|
||||
where
|
||||
T: CoreToolRuntime + 'static,
|
||||
{
|
||||
runtimes.push(Arc::new(runtime));
|
||||
}
|
||||
@@ -196,9 +196,9 @@ async fn exec_cli_applies_model_instructions_file() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Verify that `codex exec --profile ...` preserves the active profile when it
|
||||
/// starts the in-process app-server thread, so profile-scoped
|
||||
/// `model_instructions_file` is applied to the outbound request.
|
||||
/// Verify that `codex exec --profile ...` preserves the active user config
|
||||
/// profile when it starts the in-process app-server thread, so the selected
|
||||
/// profile's `model_instructions_file` reaches the outbound request.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn exec_cli_profile_applies_model_instructions_file() {
|
||||
skip_if_no_network!();
|
||||
@@ -223,8 +223,8 @@ async fn exec_cli_profile_applies_model_instructions_file() {
|
||||
|
||||
let home = TempDir::new().unwrap();
|
||||
std::fs::write(
|
||||
home.path().join("config.toml"),
|
||||
format!("[profiles.default]\nmodel_instructions_file = \"{custom_path_str}\"\n",),
|
||||
home.path().join("default.config.toml"),
|
||||
format!("model_instructions_file = \"{custom_path_str}\"\n"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -30,6 +30,11 @@ use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::W3cTraceContext;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_rollout_trace::ConversationPart;
|
||||
use codex_rollout_trace::InferenceTraceContext;
|
||||
use codex_rollout_trace::RawTraceEventPayload;
|
||||
use codex_rollout_trace::TraceWriter;
|
||||
use codex_rollout_trace::replay_bundle;
|
||||
use core_test_support::load_default_config_for_test;
|
||||
use core_test_support::responses::WebSocketConnectionConfig;
|
||||
use core_test_support::responses::WebSocketTestServer;
|
||||
@@ -535,6 +540,112 @@ async fn responses_websocket_request_prewarm_reuses_connection() {
|
||||
server.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn responses_websocket_request_prewarm_traces_logical_request() {
|
||||
skip_if_no_network!();
|
||||
|
||||
let server = start_websocket_server(vec![vec![
|
||||
vec![ev_response_created("warm-1"), ev_completed("warm-1")],
|
||||
vec![ev_response_created("resp-1"), ev_completed("resp-1")],
|
||||
]])
|
||||
.await;
|
||||
|
||||
let harness = websocket_harness_with_options(&server, /*runtime_metrics_enabled*/ true).await;
|
||||
let mut client_session = harness.client.new_session();
|
||||
let prompt = prompt_with_input(vec![message_item("hello")]);
|
||||
|
||||
client_session
|
||||
.prewarm_websocket(
|
||||
&prompt,
|
||||
&harness.model_info,
|
||||
&harness.session_telemetry,
|
||||
harness.effort,
|
||||
harness.summary,
|
||||
/*service_tier*/ None,
|
||||
/*turn_metadata_header*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("websocket prewarm failed");
|
||||
|
||||
let trace_dir = TempDir::new().expect("trace dir");
|
||||
let writer = Arc::new(
|
||||
TraceWriter::create(
|
||||
trace_dir.path(),
|
||||
"trace-1".to_string(),
|
||||
harness.session_id.to_string(),
|
||||
harness.thread_id.to_string(),
|
||||
)
|
||||
.expect("trace writer"),
|
||||
);
|
||||
writer
|
||||
.append(RawTraceEventPayload::ThreadStarted {
|
||||
thread_id: harness.thread_id.to_string(),
|
||||
agent_path: "/root".to_string(),
|
||||
metadata_payload: None,
|
||||
})
|
||||
.expect("thread started");
|
||||
writer
|
||||
.append(RawTraceEventPayload::CodexTurnStarted {
|
||||
codex_turn_id: "turn-1".to_string(),
|
||||
thread_id: harness.thread_id.to_string(),
|
||||
})
|
||||
.expect("turn started");
|
||||
|
||||
let inference_trace = InferenceTraceContext::enabled(
|
||||
writer,
|
||||
harness.thread_id.to_string(),
|
||||
"turn-1".to_string(),
|
||||
harness.model_info.slug.clone(),
|
||||
"test-provider".to_string(),
|
||||
);
|
||||
|
||||
let mut stream = client_session
|
||||
.stream(
|
||||
&prompt,
|
||||
&harness.model_info,
|
||||
&harness.session_telemetry,
|
||||
harness.effort,
|
||||
harness.summary,
|
||||
/*service_tier*/ None,
|
||||
/*turn_metadata_header*/ None,
|
||||
&inference_trace,
|
||||
)
|
||||
.await
|
||||
.expect("websocket stream failed");
|
||||
|
||||
while let Some(event) = stream.next().await {
|
||||
if matches!(event, Ok(ResponseEvent::Completed { .. })) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let connection = server.single_connection();
|
||||
let follow_up = connection
|
||||
.get(1)
|
||||
.expect("missing follow-up request")
|
||||
.body_json();
|
||||
assert_eq!(follow_up["previous_response_id"].as_str(), Some("warm-1"));
|
||||
assert_eq!(follow_up["input"], serde_json::json!([]));
|
||||
|
||||
let rollout = replay_bundle(trace_dir.path()).expect("replay trace");
|
||||
let inference = rollout
|
||||
.inference_calls
|
||||
.values()
|
||||
.next()
|
||||
.expect("inference should be present");
|
||||
assert_eq!(inference.request_item_ids.len(), 1);
|
||||
assert_eq!(
|
||||
rollout.conversation_items[&inference.request_item_ids[0]]
|
||||
.body
|
||||
.parts,
|
||||
vec![ConversationPart::Text {
|
||||
text: "hello".to_string(),
|
||||
}],
|
||||
);
|
||||
|
||||
server.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn responses_websocket_reuses_connection_after_session_drop() {
|
||||
skip_if_no_network!();
|
||||
|
||||
@@ -242,7 +242,7 @@ async fn run_code_mode_turn_with_rmcp_config(
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: "local".to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
|
||||
@@ -182,7 +182,7 @@ fn insert_rmcp_test_server(config: &mut Config, command: String, approval_mode:
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
|
||||
@@ -139,11 +139,13 @@ enum McpCallEvent {
|
||||
|
||||
const REMOTE_MCP_ENVIRONMENT: &str = "remote";
|
||||
|
||||
fn remote_aware_experimental_environment() -> Option<String> {
|
||||
fn remote_aware_environment_id() -> String {
|
||||
// These tests run locally in normal CI and against the Docker-backed
|
||||
// executor in full-ci. Match that shared test environment instead of
|
||||
// parameterizing each stdio MCP test with its own local/remote cases.
|
||||
std::env::var_os(remote_env_env_var()).map(|_| REMOTE_MCP_ENVIRONMENT.to_string())
|
||||
std::env::var_os(remote_env_env_var())
|
||||
.map(|_| REMOTE_MCP_ENVIRONMENT.to_string())
|
||||
.unwrap_or_else(|| codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string())
|
||||
}
|
||||
|
||||
/// Returns the stdio MCP test server command path for the active test placement.
|
||||
@@ -285,13 +287,22 @@ async fn wait_for_mcp_server(fixture: &TestCodex, server_name: &str) -> anyhow::
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct TestMcpServerOptions {
|
||||
experimental_environment: Option<String>,
|
||||
environment_id: String,
|
||||
supports_parallel_tool_calls: bool,
|
||||
tool_timeout_sec: Option<Duration>,
|
||||
}
|
||||
|
||||
impl Default for TestMcpServerOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
supports_parallel_tool_calls: false,
|
||||
tool_timeout_sec: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stdio_transport(
|
||||
command: String,
|
||||
env: Option<HashMap<String, String>>,
|
||||
@@ -326,7 +337,7 @@ fn insert_mcp_server(
|
||||
server_name.to_string(),
|
||||
McpServerConfig {
|
||||
transport,
|
||||
experimental_environment: options.experimental_environment,
|
||||
environment_id: options.environment_id,
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: options.supports_parallel_tool_calls,
|
||||
@@ -480,7 +491,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> {
|
||||
Vec::new(),
|
||||
),
|
||||
TestMcpServerOptions {
|
||||
experimental_environment: remote_aware_experimental_environment(),
|
||||
environment_id: remote_aware_environment_id(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
@@ -603,7 +614,7 @@ async fn stdio_server_uses_configured_cwd_before_runtime_fallback() -> anyhow::R
|
||||
Some(configured_cwd),
|
||||
),
|
||||
TestMcpServerOptions {
|
||||
experimental_environment: remote_aware_experimental_environment(),
|
||||
environment_id: remote_aware_environment_id(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
@@ -623,59 +634,6 @@ async fn stdio_server_uses_configured_cwd_before_runtime_fallback() -> anyhow::R
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
#[serial(mcp_cwd)]
|
||||
async fn remote_stdio_server_uses_runtime_fallback_cwd_when_config_omits_cwd() -> anyhow::Result<()>
|
||||
{
|
||||
skip_if_no_network!(Ok(()));
|
||||
if std::env::var_os(remote_env_env_var()).is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let server_name = "rmcp_fallback_cwd";
|
||||
let expected_cwd = Arc::new(Mutex::new(None::<PathBuf>));
|
||||
let expected_cwd_for_config = Arc::clone(&expected_cwd);
|
||||
let rmcp_test_server_bin = remote_aware_stdio_server_bin()?;
|
||||
|
||||
let fixture = test_codex()
|
||||
.with_config(move |config| {
|
||||
*expected_cwd_for_config
|
||||
.lock()
|
||||
.expect("expected cwd lock should not be poisoned") =
|
||||
Some(config.cwd.to_path_buf());
|
||||
insert_mcp_server(
|
||||
config,
|
||||
server_name,
|
||||
stdio_transport(
|
||||
rmcp_test_server_bin,
|
||||
Some(HashMap::from([(
|
||||
"MCP_TEST_VALUE".to_string(),
|
||||
"fallback-cwd".to_string(),
|
||||
)])),
|
||||
Vec::new(),
|
||||
),
|
||||
TestMcpServerOptions {
|
||||
experimental_environment: remote_aware_experimental_environment(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
})
|
||||
.build_with_remote_env(&server)
|
||||
.await?;
|
||||
|
||||
let expected_cwd = expected_cwd
|
||||
.lock()
|
||||
.expect("expected cwd lock should not be poisoned")
|
||||
.clone()
|
||||
.expect("test config should record runtime fallback cwd");
|
||||
let structured = call_cwd_tool(&server, &fixture, server_name, "call-fallback-cwd").await?;
|
||||
|
||||
assert_cwd_tool_output(&structured, &expected_cwd);
|
||||
server.verify().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
#[serial(mcp_cwd)]
|
||||
@@ -775,7 +733,7 @@ async fn stdio_mcp_tool_call_includes_sandbox_state_meta() -> anyhow::Result<()>
|
||||
server_name,
|
||||
stdio_transport(rmcp_test_server_bin, /*env*/ None, Vec::new()),
|
||||
TestMcpServerOptions {
|
||||
experimental_environment: remote_aware_experimental_environment(),
|
||||
environment_id: remote_aware_environment_id(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
@@ -872,7 +830,7 @@ async fn stdio_mcp_parallel_tool_calls_default_false_runs_serially() -> anyhow::
|
||||
server_name,
|
||||
stdio_transport(rmcp_test_server_bin, /*env*/ None, Vec::new()),
|
||||
TestMcpServerOptions {
|
||||
experimental_environment: remote_aware_experimental_environment(),
|
||||
environment_id: remote_aware_environment_id(),
|
||||
tool_timeout_sec: Some(Duration::from_secs(2)),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -989,7 +947,7 @@ async fn stdio_mcp_parallel_tool_calls_opt_in_runs_concurrently() -> anyhow::Res
|
||||
server_name,
|
||||
stdio_transport(rmcp_test_server_bin, /*env*/ None, Vec::new()),
|
||||
TestMcpServerOptions {
|
||||
experimental_environment: remote_aware_experimental_environment(),
|
||||
environment_id: remote_aware_environment_id(),
|
||||
supports_parallel_tool_calls: true,
|
||||
tool_timeout_sec: Some(Duration::from_secs(2)),
|
||||
},
|
||||
@@ -1071,7 +1029,7 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> {
|
||||
Vec::new(),
|
||||
),
|
||||
TestMcpServerOptions {
|
||||
experimental_environment: remote_aware_experimental_environment(),
|
||||
environment_id: remote_aware_environment_id(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
@@ -1204,7 +1162,7 @@ async fn stdio_image_responses_preserve_original_detail_metadata() -> anyhow::Re
|
||||
server_name,
|
||||
stdio_transport(rmcp_test_server_bin, /*env*/ None, Vec::new()),
|
||||
TestMcpServerOptions {
|
||||
experimental_environment: remote_aware_experimental_environment(),
|
||||
environment_id: remote_aware_environment_id(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
@@ -1340,7 +1298,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re
|
||||
Vec::new(),
|
||||
),
|
||||
TestMcpServerOptions {
|
||||
experimental_environment: remote_aware_experimental_environment(),
|
||||
environment_id: remote_aware_environment_id(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
@@ -1442,7 +1400,7 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> {
|
||||
vec!["MCP_TEST_VALUE".into()],
|
||||
),
|
||||
TestMcpServerOptions {
|
||||
experimental_environment: remote_aware_experimental_environment(),
|
||||
environment_id: remote_aware_environment_id(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
@@ -1560,7 +1518,7 @@ async fn stdio_server_propagates_explicit_local_env_var_source() -> anyhow::Resu
|
||||
}],
|
||||
),
|
||||
TestMcpServerOptions {
|
||||
experimental_environment: remote_aware_experimental_environment(),
|
||||
environment_id: remote_aware_environment_id(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
@@ -1652,7 +1610,7 @@ async fn remote_stdio_env_var_source_does_not_copy_local_env() -> anyhow::Result
|
||||
}],
|
||||
),
|
||||
TestMcpServerOptions {
|
||||
experimental_environment: remote_aware_experimental_environment(),
|
||||
environment_id: remote_aware_environment_id(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
@@ -1835,7 +1793,7 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> {
|
||||
env_http_headers: None,
|
||||
},
|
||||
TestMcpServerOptions {
|
||||
experimental_environment: remote_aware_experimental_environment(),
|
||||
environment_id: remote_aware_environment_id(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
@@ -2021,7 +1979,7 @@ async fn streamable_http_with_oauth_round_trip_impl() -> anyhow::Result<()> {
|
||||
env_http_headers: None,
|
||||
},
|
||||
TestMcpServerOptions {
|
||||
experimental_environment: remote_aware_experimental_environment(),
|
||||
environment_id: remote_aware_environment_id(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1012,7 +1012,7 @@ async fn tool_search_indexes_only_enabled_non_app_mcp_tools() -> Result<()> {
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: "local".to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
disabled_reason: None,
|
||||
@@ -1141,7 +1141,7 @@ async fn tool_search_surfaced_mcp_tool_errors_are_returned_to_model() -> Result<
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: "local".to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
disabled_reason: None,
|
||||
@@ -1288,7 +1288,7 @@ async fn tool_search_uses_non_app_mcp_server_instructions_as_namespace_descripti
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
environment_id: "local".to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
disabled_reason: None,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user