mirror of
https://github.com/openai/codex.git
synced 2026-11-01 19:33:35 +00:00
Merge branch 'main' into pakrym/app-server-with-logs
# Conflicts: # codex-rs/app-server/src/lib.rs
This commit is contained in:
@@ -8,9 +8,25 @@ FROM ubuntu:24.04
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
curl git python3 ca-certificates && \
|
||||
curl git python3 ca-certificates xz-utils && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY codex-rs/node-version.txt /tmp/node-version.txt
|
||||
|
||||
RUN set -eux; \
|
||||
node_arch="$(dpkg --print-architecture)"; \
|
||||
case "${node_arch}" in \
|
||||
amd64) node_dist_arch="x64" ;; \
|
||||
arm64) node_dist_arch="arm64" ;; \
|
||||
*) echo "unsupported architecture: ${node_arch}"; exit 1 ;; \
|
||||
esac; \
|
||||
node_version="$(tr -d '[:space:]' </tmp/node-version.txt)"; \
|
||||
curl -fsSLO "https://nodejs.org/dist/v${node_version}/node-v${node_version}-linux-${node_dist_arch}.tar.xz"; \
|
||||
tar -xJf "node-v${node_version}-linux-${node_dist_arch}.tar.xz" -C /usr/local --strip-components=1; \
|
||||
rm "node-v${node_version}-linux-${node_dist_arch}.tar.xz" /tmp/node-version.txt; \
|
||||
node --version; \
|
||||
npm --version
|
||||
|
||||
# Install dotslash.
|
||||
RUN curl -LSfs "https://github.com/facebook/dotslash/releases/download/v0.5.8/dotslash-ubuntu-22.04.$(uname -m).tar.gz" | tar fxz - -C /usr/local/bin
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ osx.framework(name = "CFNetwork")
|
||||
osx.framework(name = "FontServices")
|
||||
osx.framework(name = "Foundation")
|
||||
osx.framework(name = "ImageIO")
|
||||
osx.framework(name = "IOKit")
|
||||
osx.framework(name = "Kernel")
|
||||
osx.framework(name = "OSLog")
|
||||
osx.framework(name = "Security")
|
||||
|
||||
Generated
+1
-4
@@ -612,10 +612,6 @@
|
||||
"arrayvec_0.7.6": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.4\"},{\"default_features\":false,\"name\":\"borsh\",\"optional\":true,\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"matches\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.4\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}",
|
||||
"ascii-canvas_3.0.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"diff\",\"req\":\"^0.1\"},{\"name\":\"term\",\"req\":\"^0.7\"}],\"features\":{}}",
|
||||
"ascii_1.1.0": "{\"dependencies\":[{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.25\"},{\"name\":\"serde_test\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}",
|
||||
"askama_0.15.4": "{\"dependencies\":[{\"default_features\":false,\"name\":\"askama_macros\",\"optional\":true,\"req\":\"=0.15.4\"},{\"kind\":\"dev\",\"name\":\"assert_matches\",\"req\":\"^1.5.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8\"},{\"name\":\"itoa\",\"req\":\"^1.0.11\"},{\"default_features\":false,\"name\":\"percent-encoding\",\"optional\":true,\"req\":\"^2.1.0\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"alloc\":[\"askama_macros?/alloc\",\"serde?/alloc\",\"serde_json?/alloc\",\"percent-encoding?/alloc\"],\"code-in-doc\":[\"askama_macros?/code-in-doc\"],\"config\":[\"askama_macros?/config\"],\"default\":[\"config\",\"derive\",\"std\",\"urlencode\"],\"derive\":[\"dep:askama_macros\",\"dep:askama_macros\"],\"full\":[\"default\",\"code-in-doc\",\"serde_json\"],\"nightly-spans\":[\"askama_macros/nightly-spans\"],\"serde_json\":[\"std\",\"askama_macros?/serde_json\",\"dep:serde\",\"dep:serde_json\"],\"std\":[\"alloc\",\"askama_macros?/std\",\"serde?/std\",\"serde_json?/std\",\"percent-encoding?/std\"],\"urlencode\":[\"askama_macros?/urlencode\",\"dep:percent-encoding\"]}}",
|
||||
"askama_derive_0.15.4": "{\"dependencies\":[{\"name\":\"basic-toml\",\"optional\":true,\"req\":\"^0.1.1\"},{\"kind\":\"dev\",\"name\":\"console\",\"req\":\"^0.16.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8\"},{\"name\":\"memchr\",\"req\":\"^2\"},{\"name\":\"parser\",\"package\":\"askama_parser\",\"req\":\"=0.15.4\"},{\"kind\":\"dev\",\"name\":\"prettyplease\",\"req\":\"^0.2.20\"},{\"default_features\":false,\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"pulldown-cmark\",\"optional\":true,\"req\":\"^0.13.0\"},{\"default_features\":false,\"name\":\"quote\",\"req\":\"^1\"},{\"name\":\"rustc-hash\",\"req\":\"^2.0.0\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serde_derive\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"similar\",\"req\":\"^2.6.0\"},{\"default_features\":false,\"features\":[\"clone-impls\",\"derive\",\"full\",\"parsing\",\"printing\"],\"name\":\"syn\",\"req\":\"^2.0.3\"}],\"features\":{\"alloc\":[],\"code-in-doc\":[\"dep:pulldown-cmark\"],\"config\":[\"external-sources\",\"dep:basic-toml\",\"dep:serde\",\"dep:serde_derive\",\"parser/config\"],\"default\":[\"alloc\",\"code-in-doc\",\"config\",\"external-sources\",\"proc-macro\",\"serde_json\",\"std\",\"urlencode\"],\"external-sources\":[],\"nightly-spans\":[],\"proc-macro\":[\"proc-macro2/proc-macro\"],\"serde_json\":[],\"std\":[\"alloc\"],\"urlencode\":[]}}",
|
||||
"askama_macros_0.15.4": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"external-sources\",\"proc-macro\"],\"name\":\"askama_derive\",\"package\":\"askama_derive\",\"req\":\"=0.15.4\"}],\"features\":{\"alloc\":[\"askama_derive/alloc\"],\"code-in-doc\":[\"askama_derive/code-in-doc\"],\"config\":[\"askama_derive/config\"],\"default\":[\"config\",\"derive\",\"std\",\"urlencode\"],\"derive\":[],\"full\":[\"default\",\"code-in-doc\",\"serde_json\"],\"nightly-spans\":[\"askama_derive/nightly-spans\"],\"serde_json\":[\"askama_derive/serde_json\"],\"std\":[\"askama_derive/std\"],\"urlencode\":[\"askama_derive/urlencode\"]}}",
|
||||
"askama_parser_0.15.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8\"},{\"name\":\"rustc-hash\",\"req\":\"^2.0.0\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serde_derive\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"unicode-ident\",\"req\":\"^1.0.12\"},{\"features\":[\"simd\"],\"name\":\"winnow\",\"req\":\"^0.7.0\"}],\"features\":{\"config\":[\"dep:serde\",\"dep:serde_derive\"]}}",
|
||||
"asn1-rs-derive_0.6.0": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0\"},{\"name\":\"synstructure\",\"req\":\"^0.13\"}],\"features\":{}}",
|
||||
"asn1-rs-impl_0.2.0": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"name\":\"syn\",\"req\":\"^2.0\"}],\"features\":{}}",
|
||||
"asn1-rs_0.7.1": "{\"dependencies\":[{\"name\":\"asn1-rs-derive\",\"req\":\"^0.6\"},{\"name\":\"asn1-rs-impl\",\"req\":\"^0.2\"},{\"name\":\"bitvec\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"colored\",\"optional\":true,\"req\":\"^3.0\"},{\"kind\":\"dev\",\"name\":\"colored\",\"req\":\"^3.0\"},{\"name\":\"cookie-factory\",\"optional\":true,\"req\":\"^0.3.0\"},{\"name\":\"displaydoc\",\"req\":\"^0.2.2\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"nom\",\"req\":\"^7.0\"},{\"name\":\"num-bigint\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"num-traits\",\"req\":\"^0.2.14\"},{\"kind\":\"dev\",\"name\":\"pem\",\"req\":\"^3.0\"},{\"name\":\"rusticata-macros\",\"req\":\"^4.0\"},{\"name\":\"thiserror\",\"req\":\"^2.0.0\"},{\"features\":[\"macros\",\"parsing\",\"formatting\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0\"}],\"features\":{\"bigint\":[\"num-bigint\"],\"bits\":[\"bitvec\"],\"datetime\":[\"time\"],\"debug\":[\"std\",\"colored\"],\"default\":[\"std\"],\"serialize\":[\"cookie-factory\"],\"std\":[],\"trace\":[\"debug\"]}}",
|
||||
@@ -1336,6 +1332,7 @@
|
||||
"tracing-error_0.2.1": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"req\":\"^0.1.35\"},{\"default_features\":false,\"features\":[\"registry\",\"fmt\"],\"name\":\"tracing-subscriber\",\"req\":\"^0.3.0\"}],\"features\":{\"default\":[\"traced-error\"],\"traced-error\":[]}}",
|
||||
"tracing-log_0.2.0": "{\"dependencies\":[{\"name\":\"ahash\",\"optional\":true,\"req\":\"^0.7.6\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.6\"},{\"name\":\"log\",\"req\":\"^0.4.17\"},{\"name\":\"lru\",\"optional\":true,\"req\":\"^0.7.7\"},{\"name\":\"once_cell\",\"req\":\"^1.13.0\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1.35\"},{\"name\":\"tracing-core\",\"req\":\"^0.1.28\"}],\"features\":{\"default\":[\"log-tracer\",\"std\"],\"interest-cache\":[\"lru\",\"ahash\"],\"log-tracer\":[],\"std\":[\"log/std\"]}}",
|
||||
"tracing-opentelemetry_0.32.1": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"},{\"name\":\"js-sys\",\"req\":\"^0.3.64\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(target_os = \\\"wasi\\\")))\"},{\"name\":\"lazy_static\",\"optional\":true,\"req\":\"^1.0.2\"},{\"default_features\":false,\"features\":[\"trace\"],\"name\":\"opentelemetry\",\"req\":\"^0.31.0\"},{\"features\":[\"trace\",\"metrics\"],\"kind\":\"dev\",\"name\":\"opentelemetry\",\"req\":\"^0.31.0\"},{\"features\":[\"metrics\",\"grpc-tonic\"],\"kind\":\"dev\",\"name\":\"opentelemetry-otlp\",\"req\":\"^0.31.0\"},{\"features\":[\"semconv_experimental\"],\"kind\":\"dev\",\"name\":\"opentelemetry-semantic-conventions\",\"req\":\"^0.31.0\"},{\"features\":[\"trace\",\"metrics\"],\"kind\":\"dev\",\"name\":\"opentelemetry-stdout\",\"req\":\"^0.31.0\"},{\"default_features\":false,\"features\":[\"trace\",\"experimental_metrics_custom_reader\",\"testing\"],\"kind\":\"dev\",\"name\":\"opentelemetry_sdk\",\"req\":\"^0.31.0\"},{\"features\":[\"flamegraph\",\"criterion\"],\"kind\":\"dev\",\"name\":\"pprof\",\"req\":\"^0.15.0\",\"target\":\"cfg(not(target_os = \\\"windows\\\"))\"},{\"name\":\"smallvec\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"req\":\"^0.1.35\"},{\"default_features\":false,\"features\":[\"std\",\"attributes\"],\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1.35\"},{\"name\":\"tracing-core\",\"req\":\"^0.1.28\"},{\"kind\":\"dev\",\"name\":\"tracing-error\",\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"tracing-log\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"features\":[\"registry\",\"std\"],\"name\":\"tracing-subscriber\",\"req\":\"^0.3.22\"},{\"default_features\":false,\"features\":[\"registry\",\"std\",\"fmt\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3.0\"},{\"name\":\"web-time\",\"req\":\"^1.0.0\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(target_os = \\\"wasi\\\")))\"}],\"features\":{\"default\":[\"tracing-log\",\"metrics\"],\"metrics\":[\"opentelemetry/metrics\",\"smallvec\"]}}",
|
||||
"tracing-serde_0.2.0": "{\"dependencies\":[{\"name\":\"serde\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"name\":\"tracing-core\",\"req\":\"^0.1.28\"},{\"default_features\":false,\"name\":\"valuable-serde\",\"optional\":true,\"req\":\"^0.1.0\",\"target\":\"cfg(tracing_unstable)\"},{\"default_features\":false,\"name\":\"valuable_crate\",\"optional\":true,\"package\":\"valuable\",\"req\":\"^0.1.0\",\"target\":\"cfg(tracing_unstable)\"}],\"features\":{\"valuable\":[\"valuable_crate\",\"valuable-serde\",\"tracing-core/valuable\"]}}",
|
||||
"tracing-subscriber_0.3.22": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"clock\",\"std\"],\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4.26\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.6\"},{\"kind\":\"dev\",\"name\":\"log\",\"req\":\"^0.4.17\"},{\"name\":\"matchers\",\"optional\":true,\"req\":\"^0.2.0\"},{\"name\":\"nu-ansi-term\",\"optional\":true,\"req\":\"^0.50.0\"},{\"name\":\"once_cell\",\"optional\":true,\"req\":\"^1.13.0\"},{\"name\":\"parking_lot\",\"optional\":true,\"req\":\"^0.12.1\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"regex-automata\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.140\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.82\"},{\"name\":\"sharded-slab\",\"optional\":true,\"req\":\"^0.1.4\"},{\"name\":\"smallvec\",\"optional\":true,\"req\":\"^1.9.0\"},{\"name\":\"thread_local\",\"optional\":true,\"req\":\"^1.1.4\"},{\"features\":[\"formatting\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.2\"},{\"features\":[\"formatting\",\"macros\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.2\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.43\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1.43\"},{\"default_features\":false,\"name\":\"tracing-core\",\"req\":\"^0.1.35\"},{\"default_features\":false,\"features\":[\"std-future\",\"std\"],\"kind\":\"dev\",\"name\":\"tracing-futures\",\"req\":\"^0.2.0\"},{\"default_features\":false,\"features\":[\"log-tracer\",\"std\"],\"name\":\"tracing-log\",\"optional\":true,\"req\":\"^0.2.0\"},{\"kind\":\"dev\",\"name\":\"tracing-log\",\"req\":\"^0.2.0\"},{\"name\":\"tracing-serde\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"valuable-serde\",\"optional\":true,\"req\":\"^0.1.0\",\"target\":\"cfg(tracing_unstable)\"},{\"default_features\":false,\"name\":\"valuable_crate\",\"optional\":true,\"package\":\"valuable\",\"req\":\"^0.1.0\",\"target\":\"cfg(tracing_unstable)\"}],\"features\":{\"alloc\":[],\"ansi\":[\"fmt\",\"nu-ansi-term\"],\"default\":[\"smallvec\",\"fmt\",\"ansi\",\"tracing-log\",\"std\"],\"env-filter\":[\"matchers\",\"once_cell\",\"tracing\",\"std\",\"thread_local\",\"dep:regex-automata\"],\"fmt\":[\"registry\",\"std\"],\"json\":[\"tracing-serde\",\"serde\",\"serde_json\"],\"local-time\":[\"time/local-offset\"],\"nu-ansi-term\":[\"dep:nu-ansi-term\"],\"regex\":[],\"registry\":[\"sharded-slab\",\"thread_local\",\"std\"],\"std\":[\"alloc\",\"tracing-core/std\"],\"valuable\":[\"tracing-core/valuable\",\"valuable_crate\",\"valuable-serde\",\"tracing-serde/valuable\"]}}",
|
||||
"tracing-test-macro_0.2.5": "{\"dependencies\":[{\"name\":\"quote\",\"req\":\"^1\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{\"no-env-filter\":[]}}",
|
||||
"tracing-test_0.2.5": "{\"dependencies\":[{\"features\":[\"rt-multi-thread\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1\"},{\"name\":\"tracing-core\",\"req\":\"^0.1\"},{\"features\":[\"env-filter\"],\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"name\":\"tracing-test-macro\",\"req\":\"^0.2.5\"}],\"features\":{\"no-env-filter\":[\"tracing-test-macro/no-env-filter\"]}}",
|
||||
|
||||
Generated
+27
-20
@@ -423,7 +423,7 @@ dependencies = [
|
||||
"objc2-foundation",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.60.2",
|
||||
"wl-clipboard-rs",
|
||||
"x11rb",
|
||||
]
|
||||
@@ -1327,6 +1327,7 @@ dependencies = [
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tokio-util",
|
||||
"toml 0.9.11+spec-1.1.0",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
@@ -1354,6 +1355,7 @@ dependencies = [
|
||||
"strum_macros 0.27.2",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
"ts-rs",
|
||||
"uuid",
|
||||
]
|
||||
@@ -1635,6 +1637,7 @@ dependencies = [
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"codex-rmcp-client",
|
||||
"codex-secrets",
|
||||
"codex-shell-command",
|
||||
"codex-state",
|
||||
"codex-utils-absolute-path",
|
||||
@@ -1642,7 +1645,6 @@ dependencies = [
|
||||
"codex-utils-home-dir",
|
||||
"codex-utils-pty",
|
||||
"codex-utils-readiness",
|
||||
"codex-utils-sanitizer",
|
||||
"codex-utils-string",
|
||||
"codex-windows-sandbox",
|
||||
"core-foundation 0.9.4",
|
||||
@@ -1985,7 +1987,6 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"clap",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-rustls-provider",
|
||||
@@ -2150,6 +2151,7 @@ dependencies = [
|
||||
"keyring",
|
||||
"pretty_assertions",
|
||||
"rand 0.9.2",
|
||||
"regex",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2438,19 +2440,11 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-utils-sanitizer"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-utils-sleep-inhibitor"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"core-foundation 0.9.4",
|
||||
"libc",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -3240,7 +3234,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users 0.5.2",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3485,7 +3479,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4878,7 +4872,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5586,7 +5580,7 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6152,7 +6146,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.45.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6713,7 +6707,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"socket2 0.6.2",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7539,7 +7533,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.11.0",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8916,7 +8910,7 @@ dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"rustix 1.1.3",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9530,6 +9524,16 @@ dependencies = [
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-serde"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.22"
|
||||
@@ -9540,12 +9544,15 @@ dependencies = [
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex-automata",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
"tracing-serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10276,7 +10283,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -53,7 +53,6 @@ members = [
|
||||
"utils/cli",
|
||||
"utils/elapsed",
|
||||
"utils/sandbox-summary",
|
||||
"utils/sanitizer",
|
||||
"utils/sleep-inhibitor",
|
||||
"utils/approval-presets",
|
||||
"utils/oss",
|
||||
@@ -131,7 +130,6 @@ codex-utils-pty = { path = "utils/pty" }
|
||||
codex-utils-readiness = { path = "utils/readiness" }
|
||||
codex-utils-rustls-provider = { path = "utils/rustls-provider" }
|
||||
codex-utils-sandbox-summary = { path = "utils/sandbox-summary" }
|
||||
codex-utils-sanitizer = { path = "utils/sanitizer" }
|
||||
codex-utils-sleep-inhibitor = { path = "utils/sleep-inhibitor" }
|
||||
codex-utils-string = { path = "utils/string" }
|
||||
codex-windows-sandbox = { path = "windows-sandbox-rs" }
|
||||
|
||||
@@ -25,6 +25,7 @@ strum_macros = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
ts-rs = { workspace = true }
|
||||
inventory = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
uuid = { workspace = true, features = ["serde", "v7"] }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -507,6 +507,15 @@
|
||||
"classification": {
|
||||
"type": "string"
|
||||
},
|
||||
"extraLogFiles": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"includeLogs": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -110,6 +110,30 @@
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"NetworkApprovalContext": {
|
||||
"properties": {
|
||||
"host": {
|
||||
"type": "string"
|
||||
},
|
||||
"protocol": {
|
||||
"$ref": "#/definitions/NetworkApprovalProtocol"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"host",
|
||||
"protocol"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkApprovalProtocol": {
|
||||
"enum": [
|
||||
"http",
|
||||
"https",
|
||||
"socks5Tcp",
|
||||
"socks5Udp"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
@@ -147,6 +171,17 @@
|
||||
"itemId": {
|
||||
"type": "string"
|
||||
},
|
||||
"networkApprovalContext": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/NetworkApprovalContext"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional context for managed-network approval prompts."
|
||||
},
|
||||
"proposedExecpolicyAmendment": {
|
||||
"description": "Optional proposed execpolicy amendment to allow similar commands without prompting.",
|
||||
"items": {
|
||||
|
||||
@@ -296,6 +296,75 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"CollabAgentRef": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"description": "Optional nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the receiver/new agent."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"thread_id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CollabAgentStatusEntry": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"description": "Optional nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentStatus"
|
||||
}
|
||||
],
|
||||
"description": "Last known status of the agent."
|
||||
},
|
||||
"thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the receiver/new agent."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"thread_id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ContentItem": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -2548,6 +2617,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"new_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the new agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"new_agent_role": {
|
||||
"description": "Optional role assigned to the new agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"new_thread_id": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -2653,6 +2736,20 @@
|
||||
"description": "Prompt sent from the sender to the receiver. Can be empty to prevent CoT leaking at the beginning.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -2703,6 +2800,13 @@
|
||||
"description": "ID of the waiting call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agents": {
|
||||
"description": "Optional nicknames/roles for receivers.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/CollabAgentRef"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"receiver_thread_ids": {
|
||||
"description": "Thread ID of the receivers.",
|
||||
"items": {
|
||||
@@ -2738,6 +2842,13 @@
|
||||
{
|
||||
"description": "Collab interaction: waiting end.",
|
||||
"properties": {
|
||||
"agent_statuses": {
|
||||
"description": "Optional receiver metadata paired with final statuses.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/CollabAgentStatusEntry"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"call_id": {
|
||||
"description": "ID of the waiting call.",
|
||||
"type": "string"
|
||||
@@ -2821,6 +2932,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -2870,6 +2995,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -2910,6 +3049,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -7528,6 +7681,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"new_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the new agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"new_agent_role": {
|
||||
"description": "Optional role assigned to the new agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"new_thread_id": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -7633,6 +7800,20 @@
|
||||
"description": "Prompt sent from the sender to the receiver. Can be empty to prevent CoT leaking at the beginning.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -7683,6 +7864,13 @@
|
||||
"description": "ID of the waiting call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agents": {
|
||||
"description": "Optional nicknames/roles for receivers.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/CollabAgentRef"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"receiver_thread_ids": {
|
||||
"description": "Thread ID of the receivers.",
|
||||
"items": {
|
||||
@@ -7718,6 +7906,13 @@
|
||||
{
|
||||
"description": "Collab interaction: waiting end.",
|
||||
"properties": {
|
||||
"agent_statuses": {
|
||||
"description": "Optional receiver metadata paired with final statuses.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/CollabAgentStatusEntry"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"call_id": {
|
||||
"description": "ID of the waiting call.",
|
||||
"type": "string"
|
||||
@@ -7801,6 +7996,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -7850,6 +8059,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -7890,6 +8113,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
|
||||
@@ -808,6 +808,36 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"CollabAgentRef": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"description": "Optional nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the receiver/new agent."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"thread_id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CollabAgentState": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -836,6 +866,45 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CollabAgentStatusEntry": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"description": "Optional nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentStatus"
|
||||
}
|
||||
],
|
||||
"description": "Last known status of the agent."
|
||||
},
|
||||
"thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the receiver/new agent."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"thread_id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CollabAgentTool": {
|
||||
"enum": [
|
||||
"spawnAgent",
|
||||
@@ -3364,6 +3433,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"new_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the new agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"new_agent_role": {
|
||||
"description": "Optional role assigned to the new agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"new_thread_id": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -3469,6 +3552,20 @@
|
||||
"description": "Prompt sent from the sender to the receiver. Can be empty to prevent CoT leaking at the beginning.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -3519,6 +3616,13 @@
|
||||
"description": "ID of the waiting call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agents": {
|
||||
"description": "Optional nicknames/roles for receivers.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/CollabAgentRef"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"receiver_thread_ids": {
|
||||
"description": "Thread ID of the receivers.",
|
||||
"items": {
|
||||
@@ -3554,6 +3658,13 @@
|
||||
{
|
||||
"description": "Collab interaction: waiting end.",
|
||||
"properties": {
|
||||
"agent_statuses": {
|
||||
"description": "Optional receiver metadata paired with final statuses.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/CollabAgentStatusEntry"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"call_id": {
|
||||
"description": "ID of the waiting call.",
|
||||
"type": "string"
|
||||
@@ -3637,6 +3748,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -3686,6 +3811,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -3726,6 +3865,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -6377,6 +6530,20 @@
|
||||
"properties": {
|
||||
"thread_spawn": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"depth": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
@@ -6524,6 +6691,20 @@
|
||||
},
|
||||
"Thread": {
|
||||
"properties": {
|
||||
"agentNickname": {
|
||||
"description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agentRole": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cliVersion": {
|
||||
"description": "Version of the CLI that created the thread.",
|
||||
"type": "string"
|
||||
|
||||
@@ -213,6 +213,17 @@
|
||||
"itemId": {
|
||||
"type": "string"
|
||||
},
|
||||
"networkApprovalContext": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/NetworkApprovalContext"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional context for managed-network approval prompts."
|
||||
},
|
||||
"proposedExecpolicyAmendment": {
|
||||
"description": "Optional proposed execpolicy amendment to allow similar commands without prompting.",
|
||||
"items": {
|
||||
@@ -419,6 +430,30 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkApprovalContext": {
|
||||
"properties": {
|
||||
"host": {
|
||||
"type": "string"
|
||||
},
|
||||
"protocol": {
|
||||
"$ref": "#/definitions/NetworkApprovalProtocol"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"host",
|
||||
"protocol"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkApprovalProtocol": {
|
||||
"enum": [
|
||||
"http",
|
||||
"https",
|
||||
"socks5Tcp",
|
||||
"socks5Udp"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ParsedCommand": {
|
||||
"oneOf": [
|
||||
{
|
||||
|
||||
@@ -2029,6 +2029,75 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"CollabAgentRef": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"description": "Optional nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the receiver/new agent."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"thread_id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CollabAgentStatusEntry": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"description": "Optional nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentStatus"
|
||||
}
|
||||
],
|
||||
"description": "Last known status of the agent."
|
||||
},
|
||||
"thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the receiver/new agent."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"thread_id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CommandExecutionApprovalDecision": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -2123,6 +2192,17 @@
|
||||
"itemId": {
|
||||
"type": "string"
|
||||
},
|
||||
"networkApprovalContext": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/NetworkApprovalContext"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional context for managed-network approval prompts."
|
||||
},
|
||||
"proposedExecpolicyAmendment": {
|
||||
"description": "Optional proposed execpolicy amendment to allow similar commands without prompting.",
|
||||
"items": {
|
||||
@@ -4592,6 +4672,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"new_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the new agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"new_agent_role": {
|
||||
"description": "Optional role assigned to the new agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"new_thread_id": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -4697,6 +4791,20 @@
|
||||
"description": "Prompt sent from the sender to the receiver. Can be empty to prevent CoT leaking at the beginning.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -4747,6 +4855,13 @@
|
||||
"description": "ID of the waiting call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agents": {
|
||||
"description": "Optional nicknames/roles for receivers.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/CollabAgentRef"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"receiver_thread_ids": {
|
||||
"description": "Thread ID of the receivers.",
|
||||
"items": {
|
||||
@@ -4782,6 +4897,13 @@
|
||||
{
|
||||
"description": "Collab interaction: waiting end.",
|
||||
"properties": {
|
||||
"agent_statuses": {
|
||||
"description": "Optional receiver metadata paired with final statuses.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/CollabAgentStatusEntry"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"call_id": {
|
||||
"description": "ID of the waiting call.",
|
||||
"type": "string"
|
||||
@@ -4865,6 +4987,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -4914,6 +5050,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -4954,6 +5104,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -9431,6 +9595,20 @@
|
||||
"properties": {
|
||||
"thread_spawn": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"depth": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
@@ -10487,30 +10665,51 @@
|
||||
},
|
||||
"AppConfig": {
|
||||
"properties": {
|
||||
"disabled_reason": {
|
||||
"default_tools_approval_mode": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/AppDisabledReason"
|
||||
"$ref": "#/definitions/v2/AppToolApproval"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_tools_enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"destructive_enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"enabled": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"open_world_enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/AppToolsConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AppDisabledReason": {
|
||||
"enum": [
|
||||
"unknown",
|
||||
"user"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AppInfo": {
|
||||
"description": "EXPERIMENTAL - app metadata returned by app-list APIs.",
|
||||
"properties": {
|
||||
@@ -10736,7 +10935,69 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AppToolApproval": {
|
||||
"enum": [
|
||||
"auto",
|
||||
"prompt",
|
||||
"approve"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AppToolConfig": {
|
||||
"properties": {
|
||||
"approval_mode": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/AppToolApproval"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AppToolsConfig": {
|
||||
"type": "object"
|
||||
},
|
||||
"AppsConfig": {
|
||||
"properties": {
|
||||
"_default": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/AppsDefaultConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AppsDefaultConfig": {
|
||||
"properties": {
|
||||
"destructive_enabled": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"enabled": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"open_world_enabled": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AppsListParams": {
|
||||
@@ -12288,6 +12549,15 @@
|
||||
"classification": {
|
||||
"type": "string"
|
||||
},
|
||||
"extraLogFiles": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"includeLogs": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -13288,6 +13558,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"dangerouslyAllowAllUnixSockets": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"dangerouslyAllowNonLoopbackAdmin": {
|
||||
"type": [
|
||||
"boolean",
|
||||
@@ -15092,6 +15368,20 @@
|
||||
"properties": {
|
||||
"thread_spawn": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"depth": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
@@ -15218,6 +15508,20 @@
|
||||
},
|
||||
"Thread": {
|
||||
"properties": {
|
||||
"agentNickname": {
|
||||
"description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agentRole": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cliVersion": {
|
||||
"description": "Version of the CLI that created the thread.",
|
||||
"type": "string"
|
||||
|
||||
@@ -296,6 +296,75 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"CollabAgentRef": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"description": "Optional nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the receiver/new agent."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"thread_id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CollabAgentStatusEntry": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"description": "Optional nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentStatus"
|
||||
}
|
||||
],
|
||||
"description": "Last known status of the agent."
|
||||
},
|
||||
"thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the receiver/new agent."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"thread_id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ContentItem": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -2548,6 +2617,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"new_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the new agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"new_agent_role": {
|
||||
"description": "Optional role assigned to the new agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"new_thread_id": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -2653,6 +2736,20 @@
|
||||
"description": "Prompt sent from the sender to the receiver. Can be empty to prevent CoT leaking at the beginning.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -2703,6 +2800,13 @@
|
||||
"description": "ID of the waiting call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agents": {
|
||||
"description": "Optional nicknames/roles for receivers.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/CollabAgentRef"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"receiver_thread_ids": {
|
||||
"description": "Thread ID of the receivers.",
|
||||
"items": {
|
||||
@@ -2738,6 +2842,13 @@
|
||||
{
|
||||
"description": "Collab interaction: waiting end.",
|
||||
"properties": {
|
||||
"agent_statuses": {
|
||||
"description": "Optional receiver metadata paired with final statuses.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/CollabAgentStatusEntry"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"call_id": {
|
||||
"description": "ID of the waiting call.",
|
||||
"type": "string"
|
||||
@@ -2821,6 +2932,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -2870,6 +2995,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -2910,6 +3049,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
|
||||
@@ -123,6 +123,20 @@
|
||||
"properties": {
|
||||
"thread_spawn": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"depth": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
|
||||
@@ -123,6 +123,20 @@
|
||||
"properties": {
|
||||
"thread_spawn": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"depth": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
|
||||
@@ -296,6 +296,75 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"CollabAgentRef": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"description": "Optional nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the receiver/new agent."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"thread_id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CollabAgentStatusEntry": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"description": "Optional nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentStatus"
|
||||
}
|
||||
],
|
||||
"description": "Last known status of the agent."
|
||||
},
|
||||
"thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the receiver/new agent."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"thread_id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ContentItem": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -2548,6 +2617,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"new_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the new agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"new_agent_role": {
|
||||
"description": "Optional role assigned to the new agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"new_thread_id": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -2653,6 +2736,20 @@
|
||||
"description": "Prompt sent from the sender to the receiver. Can be empty to prevent CoT leaking at the beginning.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -2703,6 +2800,13 @@
|
||||
"description": "ID of the waiting call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agents": {
|
||||
"description": "Optional nicknames/roles for receivers.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/CollabAgentRef"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"receiver_thread_ids": {
|
||||
"description": "Thread ID of the receivers.",
|
||||
"items": {
|
||||
@@ -2738,6 +2842,13 @@
|
||||
{
|
||||
"description": "Collab interaction: waiting end.",
|
||||
"properties": {
|
||||
"agent_statuses": {
|
||||
"description": "Optional receiver metadata paired with final statuses.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/CollabAgentStatusEntry"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"call_id": {
|
||||
"description": "ID of the waiting call.",
|
||||
"type": "string"
|
||||
@@ -2821,6 +2932,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -2870,6 +2995,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -2910,6 +3049,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
|
||||
@@ -296,6 +296,75 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"CollabAgentRef": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"description": "Optional nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the receiver/new agent."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"thread_id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CollabAgentStatusEntry": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"description": "Optional nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentStatus"
|
||||
}
|
||||
],
|
||||
"description": "Last known status of the agent."
|
||||
},
|
||||
"thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the receiver/new agent."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"thread_id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ContentItem": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -2548,6 +2617,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"new_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the new agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"new_agent_role": {
|
||||
"description": "Optional role assigned to the new agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"new_thread_id": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -2653,6 +2736,20 @@
|
||||
"description": "Prompt sent from the sender to the receiver. Can be empty to prevent CoT leaking at the beginning.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -2703,6 +2800,13 @@
|
||||
"description": "ID of the waiting call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agents": {
|
||||
"description": "Optional nicknames/roles for receivers.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/CollabAgentRef"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"receiver_thread_ids": {
|
||||
"description": "Thread ID of the receivers.",
|
||||
"items": {
|
||||
@@ -2738,6 +2842,13 @@
|
||||
{
|
||||
"description": "Collab interaction: waiting end.",
|
||||
"properties": {
|
||||
"agent_statuses": {
|
||||
"description": "Optional receiver metadata paired with final statuses.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/CollabAgentStatusEntry"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"call_id": {
|
||||
"description": "ID of the waiting call.",
|
||||
"type": "string"
|
||||
@@ -2821,6 +2932,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -2870,6 +2995,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -2910,6 +3049,20 @@
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_agent_nickname": {
|
||||
"description": "Optional nickname assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_agent_role": {
|
||||
"description": "Optional role assigned to the receiver agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
|
||||
@@ -19,10 +19,65 @@
|
||||
},
|
||||
"AppConfig": {
|
||||
"properties": {
|
||||
"disabled_reason": {
|
||||
"default_tools_approval_mode": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AppDisabledReason"
|
||||
"$ref": "#/definitions/AppToolApproval"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_tools_enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"destructive_enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"enabled": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"open_world_enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AppToolsConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AppToolApproval": {
|
||||
"enum": [
|
||||
"auto",
|
||||
"prompt",
|
||||
"approve"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AppToolConfig": {
|
||||
"properties": {
|
||||
"approval_mode": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AppToolApproval"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -30,20 +85,48 @@
|
||||
]
|
||||
},
|
||||
"enabled": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AppDisabledReason": {
|
||||
"enum": [
|
||||
"unknown",
|
||||
"user"
|
||||
],
|
||||
"type": "string"
|
||||
"AppToolsConfig": {
|
||||
"type": "object"
|
||||
},
|
||||
"AppsConfig": {
|
||||
"properties": {
|
||||
"_default": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AppsDefaultConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AppsDefaultConfig": {
|
||||
"properties": {
|
||||
"destructive_enabled": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"enabled": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"open_world_enabled": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AskForApproval": {
|
||||
|
||||
@@ -117,6 +117,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"dangerouslyAllowAllUnixSockets": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"dangerouslyAllowNonLoopbackAdmin": {
|
||||
"type": [
|
||||
"boolean",
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
"classification": {
|
||||
"type": "string"
|
||||
},
|
||||
"extraLogFiles": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"includeLogs": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -716,6 +716,20 @@
|
||||
"properties": {
|
||||
"thread_spawn": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"depth": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
@@ -777,6 +791,20 @@
|
||||
},
|
||||
"Thread": {
|
||||
"properties": {
|
||||
"agentNickname": {
|
||||
"description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agentRole": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cliVersion": {
|
||||
"description": "Version of the CLI that created the thread.",
|
||||
"type": "string"
|
||||
|
||||
@@ -489,6 +489,20 @@
|
||||
"properties": {
|
||||
"thread_spawn": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"depth": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
@@ -550,6 +564,20 @@
|
||||
},
|
||||
"Thread": {
|
||||
"properties": {
|
||||
"agentNickname": {
|
||||
"description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agentRole": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cliVersion": {
|
||||
"description": "Version of the CLI that created the thread.",
|
||||
"type": "string"
|
||||
|
||||
@@ -489,6 +489,20 @@
|
||||
"properties": {
|
||||
"thread_spawn": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"depth": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
@@ -550,6 +564,20 @@
|
||||
},
|
||||
"Thread": {
|
||||
"properties": {
|
||||
"agentNickname": {
|
||||
"description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agentRole": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cliVersion": {
|
||||
"description": "Version of the CLI that created the thread.",
|
||||
"type": "string"
|
||||
|
||||
@@ -716,6 +716,20 @@
|
||||
"properties": {
|
||||
"thread_spawn": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"depth": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
@@ -777,6 +791,20 @@
|
||||
},
|
||||
"Thread": {
|
||||
"properties": {
|
||||
"agentNickname": {
|
||||
"description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agentRole": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cliVersion": {
|
||||
"description": "Version of the CLI that created the thread.",
|
||||
"type": "string"
|
||||
|
||||
@@ -489,6 +489,20 @@
|
||||
"properties": {
|
||||
"thread_spawn": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"depth": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
@@ -550,6 +564,20 @@
|
||||
},
|
||||
"Thread": {
|
||||
"properties": {
|
||||
"agentNickname": {
|
||||
"description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agentRole": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cliVersion": {
|
||||
"description": "Version of the CLI that created the thread.",
|
||||
"type": "string"
|
||||
|
||||
@@ -716,6 +716,20 @@
|
||||
"properties": {
|
||||
"thread_spawn": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"depth": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
@@ -777,6 +791,20 @@
|
||||
},
|
||||
"Thread": {
|
||||
"properties": {
|
||||
"agentNickname": {
|
||||
"description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agentRole": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cliVersion": {
|
||||
"description": "Version of the CLI that created the thread.",
|
||||
"type": "string"
|
||||
|
||||
@@ -489,6 +489,20 @@
|
||||
"properties": {
|
||||
"thread_spawn": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"depth": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
@@ -550,6 +564,20 @@
|
||||
},
|
||||
"Thread": {
|
||||
"properties": {
|
||||
"agentNickname": {
|
||||
"description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agentRole": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cliVersion": {
|
||||
"description": "Version of the CLI that created the thread.",
|
||||
"type": "string"
|
||||
|
||||
@@ -489,6 +489,20 @@
|
||||
"properties": {
|
||||
"thread_spawn": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"depth": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
@@ -550,6 +564,20 @@
|
||||
},
|
||||
"Thread": {
|
||||
"properties": {
|
||||
"agentNickname": {
|
||||
"description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agentRole": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cliVersion": {
|
||||
"description": "Version of the CLI that created the thread.",
|
||||
"type": "string"
|
||||
|
||||
@@ -17,6 +17,14 @@ sender_thread_id: ThreadId,
|
||||
* Thread ID of the receiver.
|
||||
*/
|
||||
receiver_thread_id: ThreadId,
|
||||
/**
|
||||
* Optional nickname assigned to the receiver agent.
|
||||
*/
|
||||
receiver_agent_nickname?: string | null,
|
||||
/**
|
||||
* Optional role assigned to the receiver agent.
|
||||
*/
|
||||
receiver_agent_role?: string | null,
|
||||
/**
|
||||
* Prompt sent from the sender to the receiver. Can be empty to prevent CoT
|
||||
* leaking at the beginning.
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ThreadId } from "./ThreadId";
|
||||
|
||||
export type CollabAgentRef = {
|
||||
/**
|
||||
* Thread ID of the receiver/new agent.
|
||||
*/
|
||||
thread_id: ThreadId,
|
||||
/**
|
||||
* Optional nickname assigned to an AgentControl-spawned sub-agent.
|
||||
*/
|
||||
agent_nickname?: string | null,
|
||||
/**
|
||||
* Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.
|
||||
*/
|
||||
agent_role?: string | null, };
|
||||
@@ -17,6 +17,14 @@ sender_thread_id: ThreadId,
|
||||
* Thread ID of the newly spawned agent, if it was created.
|
||||
*/
|
||||
new_thread_id: ThreadId | null,
|
||||
/**
|
||||
* Optional nickname assigned to the new agent.
|
||||
*/
|
||||
new_agent_nickname?: string | null,
|
||||
/**
|
||||
* Optional role assigned to the new agent.
|
||||
*/
|
||||
new_agent_role?: string | null,
|
||||
/**
|
||||
* Initial prompt sent to the agent. Can be empty to prevent CoT leaking at the
|
||||
* beginning.
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AgentStatus } from "./AgentStatus";
|
||||
import type { ThreadId } from "./ThreadId";
|
||||
|
||||
export type CollabAgentStatusEntry = {
|
||||
/**
|
||||
* Thread ID of the receiver/new agent.
|
||||
*/
|
||||
thread_id: ThreadId,
|
||||
/**
|
||||
* Optional nickname assigned to an AgentControl-spawned sub-agent.
|
||||
*/
|
||||
agent_nickname?: string | null,
|
||||
/**
|
||||
* Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.
|
||||
*/
|
||||
agent_role?: string | null,
|
||||
/**
|
||||
* Last known status of the agent.
|
||||
*/
|
||||
status: AgentStatus, };
|
||||
@@ -17,6 +17,14 @@ sender_thread_id: ThreadId,
|
||||
* Thread ID of the receiver.
|
||||
*/
|
||||
receiver_thread_id: ThreadId,
|
||||
/**
|
||||
* Optional nickname assigned to the receiver agent.
|
||||
*/
|
||||
receiver_agent_nickname?: string | null,
|
||||
/**
|
||||
* Optional role assigned to the receiver agent.
|
||||
*/
|
||||
receiver_agent_role?: string | null,
|
||||
/**
|
||||
* Last known status of the receiver agent reported to the sender agent before
|
||||
* the close.
|
||||
|
||||
@@ -15,4 +15,12 @@ sender_thread_id: ThreadId,
|
||||
/**
|
||||
* Thread ID of the receiver.
|
||||
*/
|
||||
receiver_thread_id: ThreadId, };
|
||||
receiver_thread_id: ThreadId,
|
||||
/**
|
||||
* Optional nickname assigned to the receiver agent.
|
||||
*/
|
||||
receiver_agent_nickname?: string | null,
|
||||
/**
|
||||
* Optional role assigned to the receiver agent.
|
||||
*/
|
||||
receiver_agent_role?: string | null, };
|
||||
|
||||
@@ -17,6 +17,14 @@ sender_thread_id: ThreadId,
|
||||
* Thread ID of the receiver.
|
||||
*/
|
||||
receiver_thread_id: ThreadId,
|
||||
/**
|
||||
* Optional nickname assigned to the receiver agent.
|
||||
*/
|
||||
receiver_agent_nickname?: string | null,
|
||||
/**
|
||||
* Optional role assigned to the receiver agent.
|
||||
*/
|
||||
receiver_agent_role?: string | null,
|
||||
/**
|
||||
* Last known status of the receiver agent reported to the sender agent after
|
||||
* resume.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { CollabAgentRef } from "./CollabAgentRef";
|
||||
import type { ThreadId } from "./ThreadId";
|
||||
|
||||
export type CollabWaitingBeginEvent = {
|
||||
@@ -12,6 +13,10 @@ sender_thread_id: ThreadId,
|
||||
* Thread ID of the receivers.
|
||||
*/
|
||||
receiver_thread_ids: Array<ThreadId>,
|
||||
/**
|
||||
* Optional nicknames/roles for receivers.
|
||||
*/
|
||||
receiver_agents?: Array<CollabAgentRef>,
|
||||
/**
|
||||
* ID of the waiting call.
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AgentStatus } from "./AgentStatus";
|
||||
import type { CollabAgentStatusEntry } from "./CollabAgentStatusEntry";
|
||||
import type { ThreadId } from "./ThreadId";
|
||||
|
||||
export type CollabWaitingEndEvent = {
|
||||
@@ -13,6 +14,10 @@ sender_thread_id: ThreadId,
|
||||
* ID of the waiting call.
|
||||
*/
|
||||
call_id: string,
|
||||
/**
|
||||
* Optional receiver metadata paired with final statuses.
|
||||
*/
|
||||
agent_statuses?: Array<CollabAgentStatusEntry>,
|
||||
/**
|
||||
* Last known status of the receiver agents reported to the sender agent.
|
||||
*/
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ThreadId } from "./ThreadId";
|
||||
|
||||
export type SubAgentSource = "review" | "compact" | { "thread_spawn": { parent_thread_id: ThreadId, depth: number, } } | "memory_consolidation" | { "other": string };
|
||||
export type SubAgentSource = "review" | "compact" | { "thread_spawn": { parent_thread_id: ThreadId, depth: number, agent_nickname: string | null, agent_role: string | null, } } | "memory_consolidation" | { "other": string };
|
||||
|
||||
@@ -33,8 +33,10 @@ export type { ClientRequest } from "./ClientRequest";
|
||||
export type { CodexErrorInfo } from "./CodexErrorInfo";
|
||||
export type { CollabAgentInteractionBeginEvent } from "./CollabAgentInteractionBeginEvent";
|
||||
export type { CollabAgentInteractionEndEvent } from "./CollabAgentInteractionEndEvent";
|
||||
export type { CollabAgentRef } from "./CollabAgentRef";
|
||||
export type { CollabAgentSpawnBeginEvent } from "./CollabAgentSpawnBeginEvent";
|
||||
export type { CollabAgentSpawnEndEvent } from "./CollabAgentSpawnEndEvent";
|
||||
export type { CollabAgentStatusEntry } from "./CollabAgentStatusEntry";
|
||||
export type { CollabCloseBeginEvent } from "./CollabCloseBeginEvent";
|
||||
export type { CollabCloseEndEvent } from "./CollabCloseEndEvent";
|
||||
export type { CollabResumeBeginEvent } from "./CollabResumeBeginEvent";
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type AppDisabledReason = "unknown" | "user";
|
||||
export type AppToolApproval = "auto" | "prompt" | "approve";
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AppToolApproval } from "./AppToolApproval";
|
||||
|
||||
export type AppToolsConfig = { [key in string]?: { enabled: boolean | null, approval_mode: AppToolApproval | null, } };
|
||||
@@ -1,6 +1,8 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AppDisabledReason } from "./AppDisabledReason";
|
||||
import type { AppToolApproval } from "./AppToolApproval";
|
||||
import type { AppToolsConfig } from "./AppToolsConfig";
|
||||
import type { AppsDefaultConfig } from "./AppsDefaultConfig";
|
||||
|
||||
export type AppsConfig = { [key in string]?: { enabled: boolean, disabled_reason: AppDisabledReason | null, } };
|
||||
export type AppsConfig = { _default: AppsDefaultConfig | null, } & ({ [key in string]?: { enabled: boolean, destructive_enabled: boolean | null, open_world_enabled: boolean | null, default_tools_approval_mode: AppToolApproval | null, default_tools_enabled: boolean | null, tools: AppToolsConfig | null, } });
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type AppsDefaultConfig = { enabled: boolean, destructive_enabled: boolean, open_world_enabled: boolean, };
|
||||
+5
@@ -3,6 +3,7 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { CommandAction } from "./CommandAction";
|
||||
import type { ExecPolicyAmendment } from "./ExecPolicyAmendment";
|
||||
import type { NetworkApprovalContext } from "./NetworkApprovalContext";
|
||||
|
||||
export type CommandExecutionRequestApprovalParams = { threadId: string, turnId: string, itemId: string,
|
||||
/**
|
||||
@@ -19,6 +20,10 @@ approvalId?: string | null,
|
||||
* Optional explanatory reason (e.g. request for network access).
|
||||
*/
|
||||
reason?: string | null,
|
||||
/**
|
||||
* Optional context for managed-network approval prompts.
|
||||
*/
|
||||
networkApprovalContext?: NetworkApprovalContext | null,
|
||||
/**
|
||||
* The command to be executed.
|
||||
*/
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type FeedbackUploadParams = { classification: string, reason?: string | null, threadId?: string | null, includeLogs: boolean, };
|
||||
export type FeedbackUploadParams = { classification: string, reason?: string | null, threadId?: string | null, includeLogs: boolean, extraLogFiles?: Array<string> | null, };
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol";
|
||||
|
||||
export type NetworkApprovalContext = { host: string, protocol: NetworkApprovalProtocol, };
|
||||
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type NetworkApprovalProtocol = "http" | "https" | "socks5Tcp" | "socks5Udp";
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type NetworkRequirements = { enabled: boolean | null, httpPort: number | null, socksPort: number | null, allowUpstreamProxy: boolean | null, dangerouslyAllowNonLoopbackProxy: boolean | null, dangerouslyAllowNonLoopbackAdmin: boolean | null, allowedDomains: Array<string> | null, deniedDomains: Array<string> | null, allowUnixSockets: Array<string> | null, allowLocalBinding: boolean | null, };
|
||||
export type NetworkRequirements = { enabled: boolean | null, httpPort: number | null, socksPort: number | null, allowUpstreamProxy: boolean | null, dangerouslyAllowNonLoopbackProxy: boolean | null, dangerouslyAllowNonLoopbackAdmin: boolean | null, dangerouslyAllowAllUnixSockets: boolean | null, allowedDomains: Array<string> | null, deniedDomains: Array<string> | null, allowUnixSockets: Array<string> | null, allowLocalBinding: boolean | null, };
|
||||
|
||||
@@ -43,6 +43,14 @@ cliVersion: string,
|
||||
* Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.).
|
||||
*/
|
||||
source: SessionSource,
|
||||
/**
|
||||
* Optional random unique nickname assigned to an AgentControl-spawned sub-agent.
|
||||
*/
|
||||
agentNickname: string | null,
|
||||
/**
|
||||
* Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.
|
||||
*/
|
||||
agentRole: string | null,
|
||||
/**
|
||||
* Optional Git metadata captured when the thread was created.
|
||||
*/
|
||||
|
||||
@@ -7,13 +7,15 @@ export type { AccountUpdatedNotification } from "./AccountUpdatedNotification";
|
||||
export type { AgentMessageDeltaNotification } from "./AgentMessageDeltaNotification";
|
||||
export type { AnalyticsConfig } from "./AnalyticsConfig";
|
||||
export type { AppBranding } from "./AppBranding";
|
||||
export type { AppDisabledReason } from "./AppDisabledReason";
|
||||
export type { AppInfo } from "./AppInfo";
|
||||
export type { AppListUpdatedNotification } from "./AppListUpdatedNotification";
|
||||
export type { AppMetadata } from "./AppMetadata";
|
||||
export type { AppReview } from "./AppReview";
|
||||
export type { AppScreenshot } from "./AppScreenshot";
|
||||
export type { AppToolApproval } from "./AppToolApproval";
|
||||
export type { AppToolsConfig } from "./AppToolsConfig";
|
||||
export type { AppsConfig } from "./AppsConfig";
|
||||
export type { AppsDefaultConfig } from "./AppsDefaultConfig";
|
||||
export type { AppsListParams } from "./AppsListParams";
|
||||
export type { AppsListResponse } from "./AppsListResponse";
|
||||
export type { AskForApproval } from "./AskForApproval";
|
||||
@@ -100,6 +102,8 @@ export type { ModelListResponse } from "./ModelListResponse";
|
||||
export type { ModelRerouteReason } from "./ModelRerouteReason";
|
||||
export type { ModelReroutedNotification } from "./ModelReroutedNotification";
|
||||
export type { NetworkAccess } from "./NetworkAccess";
|
||||
export type { NetworkApprovalContext } from "./NetworkApprovalContext";
|
||||
export type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol";
|
||||
export type { NetworkRequirements } from "./NetworkRequirements";
|
||||
export type { OverriddenMetadata } from "./OverriddenMetadata";
|
||||
export type { PatchApplyStatus } from "./PatchApplyStatus";
|
||||
|
||||
@@ -32,10 +32,93 @@ use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::sync::LazyLock;
|
||||
use ts_rs::TS;
|
||||
|
||||
const HEADER: &str = "// GENERATED CODE! DO NOT MODIFY BY HAND!\n\n";
|
||||
const IGNORED_DEFINITIONS: &[&str] = &["Option<()>"];
|
||||
static VARIANT_TITLE_COLLISION_OVERRIDES: LazyLock<HashMap<&'static str, &'static str>> =
|
||||
LazyLock::new(|| {
|
||||
HashMap::from([
|
||||
(
|
||||
"base=McpStartupStatus|generated=StateMcpStartupStatus|literal:state=ready|only_property=state|required_only=state",
|
||||
"StateMcpStartupStatus2",
|
||||
),
|
||||
(
|
||||
"base=McpStartupStatus|generated=StateMcpStartupStatus|literal:state=cancelled|only_property=state|required_only=state",
|
||||
"StateMcpStartupStatus3",
|
||||
),
|
||||
])
|
||||
});
|
||||
static NUMBERED_DEFINITION_COLLISION_OVERRIDES: LazyLock<HashMap<&'static str, &'static str>> =
|
||||
LazyLock::new(|| {
|
||||
HashMap::from([
|
||||
(
|
||||
"schema=ClientRequest|container=definitions|generated=AskForApproval2|base=AskForApproval",
|
||||
"AskForApproval2",
|
||||
),
|
||||
(
|
||||
"schema=ClientRequest|container=definitions|generated=NetworkAccess2|base=NetworkAccess",
|
||||
"NetworkAccess2",
|
||||
),
|
||||
(
|
||||
"schema=ClientRequest|container=definitions|generated=ReadOnlyAccess2|base=ReadOnlyAccess",
|
||||
"ReadOnlyAccess2",
|
||||
),
|
||||
(
|
||||
"schema=ClientRequest|container=definitions|generated=SandboxMode2|base=SandboxMode",
|
||||
"SandboxMode2",
|
||||
),
|
||||
(
|
||||
"schema=ClientRequest|container=definitions|generated=SandboxPolicy2|base=SandboxPolicy",
|
||||
"SandboxPolicy2",
|
||||
),
|
||||
(
|
||||
"schema=ServerNotification|container=definitions|generated=ByteRange2|base=ByteRange",
|
||||
"ByteRange2",
|
||||
),
|
||||
(
|
||||
"schema=ServerNotification|container=definitions|generated=CodexErrorInfo2|base=CodexErrorInfo",
|
||||
"CodexErrorInfo2",
|
||||
),
|
||||
(
|
||||
"schema=ServerNotification|container=definitions|generated=CreditsSnapshot2|base=CreditsSnapshot",
|
||||
"CreditsSnapshot2",
|
||||
),
|
||||
(
|
||||
"schema=ServerNotification|container=definitions|generated=MessagePhase2|base=MessagePhase",
|
||||
"MessagePhase2",
|
||||
),
|
||||
(
|
||||
"schema=ServerNotification|container=definitions|generated=ModelRerouteReason2|base=ModelRerouteReason",
|
||||
"ModelRerouteReason2",
|
||||
),
|
||||
(
|
||||
"schema=ServerNotification|container=definitions|generated=PatchApplyStatus2|base=PatchApplyStatus",
|
||||
"PatchApplyStatus2",
|
||||
),
|
||||
(
|
||||
"schema=ServerNotification|container=definitions|generated=RateLimitSnapshot2|base=RateLimitSnapshot",
|
||||
"RateLimitSnapshot2",
|
||||
),
|
||||
(
|
||||
"schema=ServerNotification|container=definitions|generated=RateLimitWindow2|base=RateLimitWindow",
|
||||
"RateLimitWindow2",
|
||||
),
|
||||
(
|
||||
"schema=ServerNotification|container=definitions|generated=TextElement2|base=TextElement",
|
||||
"TextElement2",
|
||||
),
|
||||
(
|
||||
"schema=ServerNotification|container=definitions|generated=UserInput2|base=UserInput",
|
||||
"UserInput2",
|
||||
),
|
||||
(
|
||||
"schema=ServerNotification|container=definitions|generated=WebSearchAction2|base=WebSearchAction",
|
||||
"WebSearchAction2",
|
||||
),
|
||||
])
|
||||
});
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GeneratedSchema {
|
||||
@@ -950,6 +1033,7 @@ where
|
||||
let file_stem = name.trim();
|
||||
let schema = schema_for!(T);
|
||||
let mut schema_value = serde_json::to_value(schema)?;
|
||||
enforce_numbered_definition_collision_overrides(file_stem, &mut schema_value);
|
||||
annotate_schema(&mut schema_value, Some(file_stem));
|
||||
// If the name looks like a namespaced path (e.g., "v2::Type"), mirror
|
||||
// the TypeScript layout and write to out_dir/v2/Type.json. Otherwise
|
||||
@@ -980,6 +1064,101 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
fn enforce_numbered_definition_collision_overrides(schema_name: &str, schema: &mut Value) {
|
||||
for defs_key in ["definitions", "$defs"] {
|
||||
let renames = {
|
||||
let Some(defs) = schema.get(defs_key).and_then(Value::as_object) else {
|
||||
continue;
|
||||
};
|
||||
collect_numbered_definition_collision_renames(schema_name, defs_key, defs)
|
||||
};
|
||||
|
||||
if renames.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
let defs = schema
|
||||
.get_mut(defs_key)
|
||||
.and_then(Value::as_object_mut)
|
||||
.expect("definition map should still be present");
|
||||
for (old_name, new_name, collision_key) in &renames {
|
||||
if old_name == new_name {
|
||||
continue;
|
||||
}
|
||||
assert!(
|
||||
!defs.contains_key(new_name),
|
||||
"Numbered definition collision override produced duplicate definition {new_name} for {collision_key}",
|
||||
);
|
||||
let schema_value = defs.remove(old_name).unwrap_or_else(|| {
|
||||
panic!("Missing generated numbered definition {old_name} for {collision_key}")
|
||||
});
|
||||
defs.insert(new_name.clone(), schema_value);
|
||||
}
|
||||
|
||||
for (old_name, new_name, _) in renames {
|
||||
if old_name != new_name {
|
||||
rewrite_named_local_ref(schema, defs_key, &old_name, &new_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_numbered_definition_collision_renames(
|
||||
schema_name: &str,
|
||||
defs_key: &str,
|
||||
defs: &Map<String, Value>,
|
||||
) -> Vec<(String, String, String)> {
|
||||
let mut renames = Vec::new();
|
||||
|
||||
for generated_name in defs.keys() {
|
||||
let base_name = generated_name.trim_end_matches(|c: char| c.is_ascii_digit());
|
||||
if base_name == generated_name || !defs.contains_key(base_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let collision_key = format!(
|
||||
"schema={schema_name}|container={defs_key}|generated={generated_name}|base={base_name}"
|
||||
);
|
||||
let replacement = NUMBERED_DEFINITION_COLLISION_OVERRIDES
|
||||
.get(collision_key.as_str())
|
||||
.map(|value| (*value).to_string())
|
||||
.unwrap_or_else(|| {
|
||||
panic!("Missing numbered definition collision override for {collision_key}")
|
||||
});
|
||||
renames.push((generated_name.clone(), replacement, collision_key));
|
||||
}
|
||||
|
||||
renames
|
||||
}
|
||||
|
||||
fn rewrite_named_local_ref(value: &mut Value, defs_key: &str, old_name: &str, new_name: &str) {
|
||||
let direct = format!("#/{defs_key}/{old_name}");
|
||||
let prefixed = format!("{direct}/");
|
||||
let replacement = format!("#/{defs_key}/{new_name}");
|
||||
let replacement_prefixed = format!("{replacement}/");
|
||||
match value {
|
||||
Value::Object(obj) => {
|
||||
if let Some(Value::String(reference)) = obj.get_mut("$ref") {
|
||||
if reference == &direct {
|
||||
*reference = replacement;
|
||||
} else if let Some(rest) = reference.strip_prefix(&prefixed) {
|
||||
*reference = format!("{replacement_prefixed}{rest}");
|
||||
}
|
||||
}
|
||||
for child in obj.values_mut() {
|
||||
rewrite_named_local_ref(child, defs_key, old_name, new_name);
|
||||
}
|
||||
}
|
||||
Value::Array(items) => {
|
||||
for child in items {
|
||||
rewrite_named_local_ref(child, defs_key, old_name, new_name);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn write_json_schema<T>(out_dir: &Path, name: &str) -> Result<GeneratedSchema>
|
||||
where
|
||||
T: JsonSchema,
|
||||
@@ -1198,10 +1377,20 @@ fn annotate_variant_list(variants: &mut [Value], base: Option<&str>) {
|
||||
&& let Some(name) = variant_definition_name(base_name, variant)
|
||||
{
|
||||
let mut candidate = name.clone();
|
||||
let mut index = 2;
|
||||
while seen.contains(&candidate) {
|
||||
candidate = format!("{name}{index}");
|
||||
index += 1;
|
||||
if seen.contains(&candidate) {
|
||||
let collision_key = variant_title_collision_key(base_name, &name, variant);
|
||||
candidate = VARIANT_TITLE_COLLISION_OVERRIDES
|
||||
.get(collision_key.as_str())
|
||||
.map(|value| (*value).to_string())
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"Missing variant title collision override for {collision_key} (generated name: {name})"
|
||||
)
|
||||
});
|
||||
assert!(
|
||||
!seen.contains(&candidate),
|
||||
"Variant title collision override produced duplicate title {candidate} for {collision_key}",
|
||||
);
|
||||
}
|
||||
if let Some(obj) = variant.as_object_mut() {
|
||||
obj.insert("title".into(), Value::String(candidate.clone()));
|
||||
@@ -1221,6 +1410,48 @@ fn annotate_variant_list(variants: &mut [Value], base: Option<&str>) {
|
||||
}
|
||||
}
|
||||
|
||||
fn variant_title_collision_key(base: &str, generated_name: &str, variant: &Value) -> String {
|
||||
let mut parts = vec![
|
||||
format!("base={base}"),
|
||||
format!("generated={generated_name}"),
|
||||
];
|
||||
|
||||
if let Some(props) = variant.get("properties").and_then(Value::as_object) {
|
||||
for key in DISCRIMINATOR_KEYS {
|
||||
if let Some(value) = literal_from_property(props, key) {
|
||||
parts.push(format!("{key}={value}"));
|
||||
}
|
||||
}
|
||||
for (key, value) in props {
|
||||
if DISCRIMINATOR_KEYS.contains(&key.as_str()) {
|
||||
continue;
|
||||
}
|
||||
if let Some(literal) = string_literal(value) {
|
||||
parts.push(format!("literal:{key}={literal}"));
|
||||
}
|
||||
}
|
||||
|
||||
if props.len() == 1
|
||||
&& let Some(key) = props.keys().next()
|
||||
{
|
||||
parts.push(format!("only_property={key}"));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(required) = variant.get("required").and_then(Value::as_array)
|
||||
&& required.len() == 1
|
||||
&& let Some(key) = required[0].as_str()
|
||||
{
|
||||
parts.push(format!("required_only={key}"));
|
||||
}
|
||||
|
||||
if parts.len() == 2 {
|
||||
parts.push(format!("variant={variant}"));
|
||||
}
|
||||
|
||||
parts.join("|")
|
||||
}
|
||||
|
||||
const DISCRIMINATOR_KEYS: &[&str] = &["type", "method", "mode", "status", "role", "reason"];
|
||||
|
||||
fn set_discriminator_titles(props: &mut Map<String, Value>, owner: &str) {
|
||||
@@ -1475,7 +1706,18 @@ mod tests {
|
||||
.file_stem()
|
||||
.and_then(|stem| stem.to_str())
|
||||
.is_some_and(|stem| {
|
||||
stem.ends_with("Params") || stem == "InitializeCapabilities"
|
||||
stem.ends_with("Params")
|
||||
|| stem == "InitializeCapabilities"
|
||||
|| matches!(
|
||||
stem,
|
||||
"CollabAgentRef"
|
||||
| "CollabAgentStatusEntry"
|
||||
| "CollabAgentSpawnEndEvent"
|
||||
| "CollabAgentInteractionEndEvent"
|
||||
| "CollabCloseEndEvent"
|
||||
| "CollabResumeBeginEvent"
|
||||
| "CollabResumeEndEvent"
|
||||
)
|
||||
});
|
||||
|
||||
let contents = fs::read_to_string(&path)?;
|
||||
|
||||
@@ -24,9 +24,13 @@ use codex_protocol::protocol::CompactedItem;
|
||||
use codex_protocol::protocol::ContextCompactedEvent;
|
||||
use codex_protocol::protocol::ErrorEvent;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ExecCommandBeginEvent;
|
||||
use codex_protocol::protocol::ExecCommandEndEvent;
|
||||
use codex_protocol::protocol::ItemCompletedEvent;
|
||||
use codex_protocol::protocol::ItemStartedEvent;
|
||||
use codex_protocol::protocol::McpToolCallBeginEvent;
|
||||
use codex_protocol::protocol::McpToolCallEndEvent;
|
||||
use codex_protocol::protocol::PatchApplyBeginEvent;
|
||||
use codex_protocol::protocol::PatchApplyEndEvent;
|
||||
use codex_protocol::protocol::ReviewOutputEvent;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
@@ -36,8 +40,10 @@ use codex_protocol::protocol::TurnCompleteEvent;
|
||||
use codex_protocol::protocol::TurnStartedEvent;
|
||||
use codex_protocol::protocol::UserMessageEvent;
|
||||
use codex_protocol::protocol::ViewImageToolCallEvent;
|
||||
use codex_protocol::protocol::WebSearchBeginEvent;
|
||||
use codex_protocol::protocol::WebSearchEndEvent;
|
||||
use std::collections::HashMap;
|
||||
use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -57,14 +63,20 @@ pub fn build_turns_from_rollout_items(items: &[RolloutItem]) -> Vec<Turn> {
|
||||
builder.finish()
|
||||
}
|
||||
|
||||
struct ThreadHistoryBuilder {
|
||||
pub struct ThreadHistoryBuilder {
|
||||
turns: Vec<Turn>,
|
||||
current_turn: Option<PendingTurn>,
|
||||
next_item_index: i64,
|
||||
}
|
||||
|
||||
impl Default for ThreadHistoryBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ThreadHistoryBuilder {
|
||||
fn new() -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
turns: Vec::new(),
|
||||
current_turn: None,
|
||||
@@ -72,14 +84,32 @@ impl ThreadHistoryBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(mut self) -> Vec<Turn> {
|
||||
pub fn reset(&mut self) {
|
||||
*self = Self::new();
|
||||
}
|
||||
|
||||
pub fn finish(mut self) -> Vec<Turn> {
|
||||
self.finish_current_turn();
|
||||
self.turns
|
||||
}
|
||||
|
||||
pub fn active_turn_snapshot(&self) -> Option<Turn> {
|
||||
self.current_turn
|
||||
.as_ref()
|
||||
.map(Turn::from)
|
||||
.or_else(|| self.turns.last().cloned())
|
||||
}
|
||||
|
||||
pub fn has_active_turn(&self) -> bool {
|
||||
self.current_turn.is_some()
|
||||
}
|
||||
|
||||
/// Shared reducer for persisted rollout replay and in-memory current-turn
|
||||
/// tracking used by running thread resume/rejoin.
|
||||
///
|
||||
/// This function should handle all EventMsg variants that can be persisted in a rollout file.
|
||||
/// See `should_persist_event_msg` in `codex-rs/core/rollout/policy.rs`.
|
||||
fn handle_event(&mut self, event: &EventMsg) {
|
||||
pub fn handle_event(&mut self, event: &EventMsg) {
|
||||
match event {
|
||||
EventMsg::UserMessage(payload) => self.handle_user_message(payload),
|
||||
EventMsg::AgentMessage(payload) => {
|
||||
@@ -89,21 +119,35 @@ impl ThreadHistoryBuilder {
|
||||
EventMsg::AgentReasoningRawContent(payload) => {
|
||||
self.handle_agent_reasoning_raw_content(payload)
|
||||
}
|
||||
EventMsg::WebSearchBegin(payload) => self.handle_web_search_begin(payload),
|
||||
EventMsg::WebSearchEnd(payload) => self.handle_web_search_end(payload),
|
||||
EventMsg::ExecCommandBegin(payload) => self.handle_exec_command_begin(payload),
|
||||
EventMsg::ExecCommandEnd(payload) => self.handle_exec_command_end(payload),
|
||||
EventMsg::PatchApplyBegin(payload) => self.handle_patch_apply_begin(payload),
|
||||
EventMsg::PatchApplyEnd(payload) => self.handle_patch_apply_end(payload),
|
||||
EventMsg::McpToolCallBegin(payload) => self.handle_mcp_tool_call_begin(payload),
|
||||
EventMsg::McpToolCallEnd(payload) => self.handle_mcp_tool_call_end(payload),
|
||||
EventMsg::ViewImageToolCall(payload) => self.handle_view_image_tool_call(payload),
|
||||
EventMsg::CollabAgentSpawnBegin(payload) => {
|
||||
self.handle_collab_agent_spawn_begin(payload)
|
||||
}
|
||||
EventMsg::CollabAgentSpawnEnd(payload) => self.handle_collab_agent_spawn_end(payload),
|
||||
EventMsg::CollabAgentInteractionBegin(payload) => {
|
||||
self.handle_collab_agent_interaction_begin(payload)
|
||||
}
|
||||
EventMsg::CollabAgentInteractionEnd(payload) => {
|
||||
self.handle_collab_agent_interaction_end(payload)
|
||||
}
|
||||
EventMsg::CollabWaitingBegin(payload) => self.handle_collab_waiting_begin(payload),
|
||||
EventMsg::CollabWaitingEnd(payload) => self.handle_collab_waiting_end(payload),
|
||||
EventMsg::CollabCloseBegin(payload) => self.handle_collab_close_begin(payload),
|
||||
EventMsg::CollabCloseEnd(payload) => self.handle_collab_close_end(payload),
|
||||
EventMsg::CollabResumeBegin(payload) => self.handle_collab_resume_begin(payload),
|
||||
EventMsg::CollabResumeEnd(payload) => self.handle_collab_resume_end(payload),
|
||||
EventMsg::ContextCompacted(payload) => self.handle_context_compacted(payload),
|
||||
EventMsg::EnteredReviewMode(payload) => self.handle_entered_review_mode(payload),
|
||||
EventMsg::ExitedReviewMode(payload) => self.handle_exited_review_mode(payload),
|
||||
EventMsg::ItemStarted(payload) => self.handle_item_started(payload),
|
||||
EventMsg::ItemCompleted(payload) => self.handle_item_completed(payload),
|
||||
EventMsg::Error(payload) => self.handle_error(payload),
|
||||
EventMsg::TokenCount(_) => {}
|
||||
@@ -116,7 +160,7 @@ impl ThreadHistoryBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_rollout_item(&mut self, item: &RolloutItem) {
|
||||
pub fn handle_rollout_item(&mut self, item: &RolloutItem) {
|
||||
match item {
|
||||
RolloutItem::EventMsg(event) => self.handle_event(event),
|
||||
RolloutItem::Compacted(payload) => self.handle_compacted(payload),
|
||||
@@ -199,15 +243,51 @@ impl ThreadHistoryBuilder {
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_item_completed(&mut self, payload: &ItemCompletedEvent) {
|
||||
if let codex_protocol::items::TurnItem::Plan(plan) = &payload.item
|
||||
&& plan.text.is_empty()
|
||||
{
|
||||
return;
|
||||
fn handle_item_started(&mut self, payload: &ItemStartedEvent) {
|
||||
match &payload.item {
|
||||
codex_protocol::items::TurnItem::Plan(plan) => {
|
||||
if plan.text.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.upsert_item_in_turn_id(
|
||||
&payload.turn_id,
|
||||
ThreadItem::from(payload.item.clone()),
|
||||
);
|
||||
}
|
||||
codex_protocol::items::TurnItem::UserMessage(_)
|
||||
| codex_protocol::items::TurnItem::AgentMessage(_)
|
||||
| codex_protocol::items::TurnItem::Reasoning(_)
|
||||
| codex_protocol::items::TurnItem::WebSearch(_)
|
||||
| codex_protocol::items::TurnItem::ContextCompaction(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
let item = ThreadItem::from(payload.item.clone());
|
||||
self.ensure_turn().items.push(item);
|
||||
fn handle_item_completed(&mut self, payload: &ItemCompletedEvent) {
|
||||
match &payload.item {
|
||||
codex_protocol::items::TurnItem::Plan(plan) => {
|
||||
if plan.text.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.upsert_item_in_turn_id(
|
||||
&payload.turn_id,
|
||||
ThreadItem::from(payload.item.clone()),
|
||||
);
|
||||
}
|
||||
codex_protocol::items::TurnItem::UserMessage(_)
|
||||
| codex_protocol::items::TurnItem::AgentMessage(_)
|
||||
| codex_protocol::items::TurnItem::Reasoning(_)
|
||||
| codex_protocol::items::TurnItem::WebSearch(_)
|
||||
| codex_protocol::items::TurnItem::ContextCompaction(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_web_search_begin(&mut self, payload: &WebSearchBeginEvent) {
|
||||
let item = ThreadItem::WebSearch {
|
||||
id: payload.call_id.clone(),
|
||||
query: String::new(),
|
||||
action: None,
|
||||
};
|
||||
self.upsert_item_in_current_turn(item);
|
||||
}
|
||||
|
||||
fn handle_web_search_end(&mut self, payload: &WebSearchEndEvent) {
|
||||
@@ -216,7 +296,30 @@ impl ThreadHistoryBuilder {
|
||||
query: payload.query.clone(),
|
||||
action: Some(WebSearchAction::from(payload.action.clone())),
|
||||
};
|
||||
self.ensure_turn().items.push(item);
|
||||
self.upsert_item_in_current_turn(item);
|
||||
}
|
||||
|
||||
fn handle_exec_command_begin(&mut self, payload: &ExecCommandBeginEvent) {
|
||||
let command = shlex::try_join(payload.command.iter().map(String::as_str))
|
||||
.unwrap_or_else(|_| payload.command.join(" "));
|
||||
let command_actions = payload
|
||||
.parsed_cmd
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(CommandAction::from)
|
||||
.collect();
|
||||
let item = ThreadItem::CommandExecution {
|
||||
id: payload.call_id.clone(),
|
||||
command,
|
||||
cwd: payload.cwd.clone(),
|
||||
process_id: payload.process_id.clone(),
|
||||
status: CommandExecutionStatus::InProgress,
|
||||
command_actions,
|
||||
aggregated_output: None,
|
||||
exit_code: None,
|
||||
duration_ms: None,
|
||||
};
|
||||
self.upsert_item_in_turn_id(&payload.turn_id, item);
|
||||
}
|
||||
|
||||
fn handle_exec_command_end(&mut self, payload: &ExecCommandEndEvent) {
|
||||
@@ -246,33 +349,25 @@ impl ThreadHistoryBuilder {
|
||||
exit_code: Some(payload.exit_code),
|
||||
duration_ms: Some(duration_ms),
|
||||
};
|
||||
|
||||
// Command completions can arrive out of order. Unified exec may return
|
||||
// while a PTY is still running, then emit ExecCommandEnd later from a
|
||||
// background exit watcher when that process finally exits. By then, a
|
||||
// newer user turn may already have started. Route by event turn_id so
|
||||
// replay preserves the original turn association.
|
||||
if let Some(turn) = self.current_turn.as_mut()
|
||||
&& turn.id == payload.turn_id
|
||||
{
|
||||
turn.items.push(item);
|
||||
return;
|
||||
}
|
||||
self.upsert_item_in_turn_id(&payload.turn_id, item);
|
||||
}
|
||||
|
||||
// If the originating turn is already finalized, append there instead
|
||||
// of attaching to whichever turn is currently active during replay.
|
||||
if let Some(turn) = self
|
||||
.turns
|
||||
.iter_mut()
|
||||
.find(|turn| turn.id == payload.turn_id)
|
||||
{
|
||||
turn.items.push(item);
|
||||
return;
|
||||
fn handle_patch_apply_begin(&mut self, payload: &PatchApplyBeginEvent) {
|
||||
let item = ThreadItem::FileChange {
|
||||
id: payload.call_id.clone(),
|
||||
changes: convert_patch_changes(&payload.changes),
|
||||
status: PatchApplyStatus::InProgress,
|
||||
};
|
||||
if payload.turn_id.is_empty() {
|
||||
self.upsert_item_in_current_turn(item);
|
||||
} else {
|
||||
self.upsert_item_in_turn_id(&payload.turn_id, item);
|
||||
}
|
||||
|
||||
// Backward-compatibility fallback for partial/legacy streams where the
|
||||
// event turn_id does not match any known replay turn.
|
||||
self.ensure_turn().items.push(item);
|
||||
}
|
||||
|
||||
fn handle_patch_apply_end(&mut self, payload: &PatchApplyEndEvent) {
|
||||
@@ -282,7 +377,29 @@ impl ThreadHistoryBuilder {
|
||||
changes: convert_patch_changes(&payload.changes),
|
||||
status,
|
||||
};
|
||||
self.ensure_turn().items.push(item);
|
||||
if payload.turn_id.is_empty() {
|
||||
self.upsert_item_in_current_turn(item);
|
||||
} else {
|
||||
self.upsert_item_in_turn_id(&payload.turn_id, item);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_mcp_tool_call_begin(&mut self, payload: &McpToolCallBeginEvent) {
|
||||
let item = ThreadItem::McpToolCall {
|
||||
id: payload.call_id.clone(),
|
||||
server: payload.invocation.server.clone(),
|
||||
tool: payload.invocation.tool.clone(),
|
||||
status: McpToolCallStatus::InProgress,
|
||||
arguments: payload
|
||||
.invocation
|
||||
.arguments
|
||||
.clone()
|
||||
.unwrap_or(serde_json::Value::Null),
|
||||
result: None,
|
||||
error: None,
|
||||
duration_ms: None,
|
||||
};
|
||||
self.upsert_item_in_current_turn(item);
|
||||
}
|
||||
|
||||
fn handle_mcp_tool_call_end(&mut self, payload: &McpToolCallEndEvent) {
|
||||
@@ -321,7 +438,7 @@ impl ThreadHistoryBuilder {
|
||||
error,
|
||||
duration_ms,
|
||||
};
|
||||
self.ensure_turn().items.push(item);
|
||||
self.upsert_item_in_current_turn(item);
|
||||
}
|
||||
|
||||
fn handle_view_image_tool_call(&mut self, payload: &ViewImageToolCallEvent) {
|
||||
@@ -329,7 +446,23 @@ impl ThreadHistoryBuilder {
|
||||
id: payload.call_id.clone(),
|
||||
path: payload.path.to_string_lossy().into_owned(),
|
||||
};
|
||||
self.ensure_turn().items.push(item);
|
||||
self.upsert_item_in_current_turn(item);
|
||||
}
|
||||
|
||||
fn handle_collab_agent_spawn_begin(
|
||||
&mut self,
|
||||
payload: &codex_protocol::protocol::CollabAgentSpawnBeginEvent,
|
||||
) {
|
||||
let item = ThreadItem::CollabAgentToolCall {
|
||||
id: payload.call_id.clone(),
|
||||
tool: CollabAgentTool::SpawnAgent,
|
||||
status: CollabAgentToolCallStatus::InProgress,
|
||||
sender_thread_id: payload.sender_thread_id.to_string(),
|
||||
receiver_thread_ids: Vec::new(),
|
||||
prompt: Some(payload.prompt.clone()),
|
||||
agents_states: HashMap::new(),
|
||||
};
|
||||
self.upsert_item_in_current_turn(item);
|
||||
}
|
||||
|
||||
fn handle_collab_agent_spawn_end(
|
||||
@@ -353,17 +486,31 @@ impl ThreadHistoryBuilder {
|
||||
}
|
||||
None => (Vec::new(), HashMap::new()),
|
||||
};
|
||||
self.ensure_turn()
|
||||
.items
|
||||
.push(ThreadItem::CollabAgentToolCall {
|
||||
id: payload.call_id.clone(),
|
||||
tool: CollabAgentTool::SpawnAgent,
|
||||
status,
|
||||
sender_thread_id: payload.sender_thread_id.to_string(),
|
||||
receiver_thread_ids,
|
||||
prompt: Some(payload.prompt.clone()),
|
||||
agents_states,
|
||||
});
|
||||
self.upsert_item_in_current_turn(ThreadItem::CollabAgentToolCall {
|
||||
id: payload.call_id.clone(),
|
||||
tool: CollabAgentTool::SpawnAgent,
|
||||
status,
|
||||
sender_thread_id: payload.sender_thread_id.to_string(),
|
||||
receiver_thread_ids,
|
||||
prompt: Some(payload.prompt.clone()),
|
||||
agents_states,
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_collab_agent_interaction_begin(
|
||||
&mut self,
|
||||
payload: &codex_protocol::protocol::CollabAgentInteractionBeginEvent,
|
||||
) {
|
||||
let item = ThreadItem::CollabAgentToolCall {
|
||||
id: payload.call_id.clone(),
|
||||
tool: CollabAgentTool::SendInput,
|
||||
status: CollabAgentToolCallStatus::InProgress,
|
||||
sender_thread_id: payload.sender_thread_id.to_string(),
|
||||
receiver_thread_ids: vec![payload.receiver_thread_id.to_string()],
|
||||
prompt: Some(payload.prompt.clone()),
|
||||
agents_states: HashMap::new(),
|
||||
};
|
||||
self.upsert_item_in_current_turn(item);
|
||||
}
|
||||
|
||||
fn handle_collab_agent_interaction_end(
|
||||
@@ -376,17 +523,35 @@ impl ThreadHistoryBuilder {
|
||||
};
|
||||
let receiver_id = payload.receiver_thread_id.to_string();
|
||||
let received_status = CollabAgentState::from(payload.status.clone());
|
||||
self.ensure_turn()
|
||||
.items
|
||||
.push(ThreadItem::CollabAgentToolCall {
|
||||
id: payload.call_id.clone(),
|
||||
tool: CollabAgentTool::SendInput,
|
||||
status,
|
||||
sender_thread_id: payload.sender_thread_id.to_string(),
|
||||
receiver_thread_ids: vec![receiver_id.clone()],
|
||||
prompt: Some(payload.prompt.clone()),
|
||||
agents_states: [(receiver_id, received_status)].into_iter().collect(),
|
||||
});
|
||||
self.upsert_item_in_current_turn(ThreadItem::CollabAgentToolCall {
|
||||
id: payload.call_id.clone(),
|
||||
tool: CollabAgentTool::SendInput,
|
||||
status,
|
||||
sender_thread_id: payload.sender_thread_id.to_string(),
|
||||
receiver_thread_ids: vec![receiver_id.clone()],
|
||||
prompt: Some(payload.prompt.clone()),
|
||||
agents_states: [(receiver_id, received_status)].into_iter().collect(),
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_collab_waiting_begin(
|
||||
&mut self,
|
||||
payload: &codex_protocol::protocol::CollabWaitingBeginEvent,
|
||||
) {
|
||||
let item = ThreadItem::CollabAgentToolCall {
|
||||
id: payload.call_id.clone(),
|
||||
tool: CollabAgentTool::Wait,
|
||||
status: CollabAgentToolCallStatus::InProgress,
|
||||
sender_thread_id: payload.sender_thread_id.to_string(),
|
||||
receiver_thread_ids: payload
|
||||
.receiver_thread_ids
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect(),
|
||||
prompt: None,
|
||||
agents_states: HashMap::new(),
|
||||
};
|
||||
self.upsert_item_in_current_turn(item);
|
||||
}
|
||||
|
||||
fn handle_collab_waiting_end(
|
||||
@@ -410,17 +575,31 @@ impl ThreadHistoryBuilder {
|
||||
.iter()
|
||||
.map(|(id, status)| (id.to_string(), CollabAgentState::from(status.clone())))
|
||||
.collect();
|
||||
self.ensure_turn()
|
||||
.items
|
||||
.push(ThreadItem::CollabAgentToolCall {
|
||||
id: payload.call_id.clone(),
|
||||
tool: CollabAgentTool::Wait,
|
||||
status,
|
||||
sender_thread_id: payload.sender_thread_id.to_string(),
|
||||
receiver_thread_ids,
|
||||
prompt: None,
|
||||
agents_states,
|
||||
});
|
||||
self.upsert_item_in_current_turn(ThreadItem::CollabAgentToolCall {
|
||||
id: payload.call_id.clone(),
|
||||
tool: CollabAgentTool::Wait,
|
||||
status,
|
||||
sender_thread_id: payload.sender_thread_id.to_string(),
|
||||
receiver_thread_ids,
|
||||
prompt: None,
|
||||
agents_states,
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_collab_close_begin(
|
||||
&mut self,
|
||||
payload: &codex_protocol::protocol::CollabCloseBeginEvent,
|
||||
) {
|
||||
let item = ThreadItem::CollabAgentToolCall {
|
||||
id: payload.call_id.clone(),
|
||||
tool: CollabAgentTool::CloseAgent,
|
||||
status: CollabAgentToolCallStatus::InProgress,
|
||||
sender_thread_id: payload.sender_thread_id.to_string(),
|
||||
receiver_thread_ids: vec![payload.receiver_thread_id.to_string()],
|
||||
prompt: None,
|
||||
agents_states: HashMap::new(),
|
||||
};
|
||||
self.upsert_item_in_current_turn(item);
|
||||
}
|
||||
|
||||
fn handle_collab_close_end(&mut self, payload: &codex_protocol::protocol::CollabCloseEndEvent) {
|
||||
@@ -435,17 +614,31 @@ impl ThreadHistoryBuilder {
|
||||
)]
|
||||
.into_iter()
|
||||
.collect();
|
||||
self.ensure_turn()
|
||||
.items
|
||||
.push(ThreadItem::CollabAgentToolCall {
|
||||
id: payload.call_id.clone(),
|
||||
tool: CollabAgentTool::CloseAgent,
|
||||
status,
|
||||
sender_thread_id: payload.sender_thread_id.to_string(),
|
||||
receiver_thread_ids: vec![receiver_id],
|
||||
prompt: None,
|
||||
agents_states,
|
||||
});
|
||||
self.upsert_item_in_current_turn(ThreadItem::CollabAgentToolCall {
|
||||
id: payload.call_id.clone(),
|
||||
tool: CollabAgentTool::CloseAgent,
|
||||
status,
|
||||
sender_thread_id: payload.sender_thread_id.to_string(),
|
||||
receiver_thread_ids: vec![receiver_id],
|
||||
prompt: None,
|
||||
agents_states,
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_collab_resume_begin(
|
||||
&mut self,
|
||||
payload: &codex_protocol::protocol::CollabResumeBeginEvent,
|
||||
) {
|
||||
let item = ThreadItem::CollabAgentToolCall {
|
||||
id: payload.call_id.clone(),
|
||||
tool: CollabAgentTool::ResumeAgent,
|
||||
status: CollabAgentToolCallStatus::InProgress,
|
||||
sender_thread_id: payload.sender_thread_id.to_string(),
|
||||
receiver_thread_ids: vec![payload.receiver_thread_id.to_string()],
|
||||
prompt: None,
|
||||
agents_states: HashMap::new(),
|
||||
};
|
||||
self.upsert_item_in_current_turn(item);
|
||||
}
|
||||
|
||||
fn handle_collab_resume_end(
|
||||
@@ -463,17 +656,15 @@ impl ThreadHistoryBuilder {
|
||||
)]
|
||||
.into_iter()
|
||||
.collect();
|
||||
self.ensure_turn()
|
||||
.items
|
||||
.push(ThreadItem::CollabAgentToolCall {
|
||||
id: payload.call_id.clone(),
|
||||
tool: CollabAgentTool::ResumeAgent,
|
||||
status,
|
||||
sender_thread_id: payload.sender_thread_id.to_string(),
|
||||
receiver_thread_ids: vec![receiver_id],
|
||||
prompt: None,
|
||||
agents_states,
|
||||
});
|
||||
self.upsert_item_in_current_turn(ThreadItem::CollabAgentToolCall {
|
||||
id: payload.call_id.clone(),
|
||||
tool: CollabAgentTool::ResumeAgent,
|
||||
status,
|
||||
sender_thread_id: payload.sender_thread_id.to_string(),
|
||||
receiver_thread_ids: vec![receiver_id],
|
||||
prompt: None,
|
||||
agents_states,
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_context_compacted(&mut self, _payload: &ContextCompactedEvent) {
|
||||
@@ -548,6 +739,7 @@ impl ThreadHistoryBuilder {
|
||||
self.finish_current_turn();
|
||||
self.current_turn = Some(
|
||||
self.new_turn(Some(payload.turn_id.clone()))
|
||||
.with_status(TurnStatus::InProgress)
|
||||
.opened_explicitly(),
|
||||
);
|
||||
}
|
||||
@@ -642,6 +834,30 @@ impl ThreadHistoryBuilder {
|
||||
unreachable!("current turn must exist after initialization");
|
||||
}
|
||||
|
||||
fn upsert_item_in_turn_id(&mut self, turn_id: &str, item: ThreadItem) {
|
||||
if let Some(turn) = self.current_turn.as_mut()
|
||||
&& turn.id == turn_id
|
||||
{
|
||||
upsert_turn_item(&mut turn.items, item);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(turn) = self.turns.iter_mut().find(|turn| turn.id == turn_id) {
|
||||
upsert_turn_item(&mut turn.items, item);
|
||||
return;
|
||||
}
|
||||
|
||||
warn!(
|
||||
item_id = item.id(),
|
||||
"dropping turn-scoped item for unknown turn id `{turn_id}`"
|
||||
);
|
||||
}
|
||||
|
||||
fn upsert_item_in_current_turn(&mut self, item: ThreadItem) {
|
||||
let turn = self.ensure_turn();
|
||||
upsert_turn_item(&mut turn.items, item);
|
||||
}
|
||||
|
||||
fn next_item_id(&mut self) -> String {
|
||||
let id = format!("item-{}", self.next_item_index);
|
||||
self.next_item_index += 1;
|
||||
@@ -684,7 +900,7 @@ fn render_review_output_text(output: &ReviewOutputEvent) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_patch_changes(
|
||||
pub fn convert_patch_changes(
|
||||
changes: &HashMap<std::path::PathBuf, codex_protocol::protocol::FileChange>,
|
||||
) -> Vec<FileUpdateChange> {
|
||||
let mut converted: Vec<FileUpdateChange> = changes
|
||||
@@ -726,6 +942,17 @@ fn format_file_change_diff(change: &codex_protocol::protocol::FileChange) -> Str
|
||||
}
|
||||
}
|
||||
|
||||
fn upsert_turn_item(items: &mut Vec<ThreadItem>, item: ThreadItem) {
|
||||
if let Some(existing_item) = items
|
||||
.iter_mut()
|
||||
.find(|existing_item| existing_item.id() == item.id())
|
||||
{
|
||||
*existing_item = item;
|
||||
return;
|
||||
}
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
struct PendingTurn {
|
||||
id: String,
|
||||
items: Vec<ThreadItem>,
|
||||
@@ -744,6 +971,11 @@ impl PendingTurn {
|
||||
self.opened_explicitly = true;
|
||||
self
|
||||
}
|
||||
|
||||
fn with_status(mut self, status: TurnStatus) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PendingTurn> for Turn {
|
||||
@@ -757,10 +989,23 @@ impl From<PendingTurn> for Turn {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&PendingTurn> for Turn {
|
||||
fn from(value: &PendingTurn) -> Self {
|
||||
Self {
|
||||
id: value.id.clone(),
|
||||
items: value.items.clone(),
|
||||
error: value.error.clone(),
|
||||
status: value.status.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::items::TurnItem as CoreTurnItem;
|
||||
use codex_protocol::items::UserMessageItem as CoreUserMessageItem;
|
||||
use codex_protocol::models::MessagePhase as CoreMessagePhase;
|
||||
use codex_protocol::models::WebSearchAction as CoreWebSearchAction;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
@@ -771,6 +1016,7 @@ mod tests {
|
||||
use codex_protocol::protocol::CompactedItem;
|
||||
use codex_protocol::protocol::ExecCommandEndEvent;
|
||||
use codex_protocol::protocol::ExecCommandSource;
|
||||
use codex_protocol::protocol::ItemStartedEvent;
|
||||
use codex_protocol::protocol::McpInvocation;
|
||||
use codex_protocol::protocol::McpToolCallEndEvent;
|
||||
use codex_protocol::protocol::ThreadRolledBackEvent;
|
||||
@@ -816,11 +1062,11 @@ mod tests {
|
||||
}),
|
||||
];
|
||||
|
||||
let items = events
|
||||
.into_iter()
|
||||
.map(RolloutItem::EventMsg)
|
||||
.collect::<Vec<_>>();
|
||||
let turns = build_turns_from_rollout_items(&items);
|
||||
let mut builder = ThreadHistoryBuilder::new();
|
||||
for event in &events {
|
||||
builder.handle_event(event);
|
||||
}
|
||||
let turns = builder.finish();
|
||||
assert_eq!(turns.len(), 2);
|
||||
|
||||
let first = &turns[0];
|
||||
@@ -883,6 +1129,55 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_plan_item_lifecycle_events() {
|
||||
let turn_id = "turn-1";
|
||||
let thread_id = ThreadId::new();
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: turn_id.to_string(),
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
EventMsg::UserMessage(UserMessageEvent {
|
||||
message: "hello".into(),
|
||||
images: None,
|
||||
text_elements: Vec::new(),
|
||||
local_images: Vec::new(),
|
||||
}),
|
||||
EventMsg::ItemStarted(ItemStartedEvent {
|
||||
thread_id,
|
||||
turn_id: turn_id.to_string(),
|
||||
item: CoreTurnItem::UserMessage(CoreUserMessageItem {
|
||||
id: "user-item-id".to_string(),
|
||||
content: Vec::new(),
|
||||
}),
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: turn_id.to_string(),
|
||||
last_agent_message: None,
|
||||
}),
|
||||
];
|
||||
|
||||
let items = events
|
||||
.into_iter()
|
||||
.map(RolloutItem::EventMsg)
|
||||
.collect::<Vec<_>>();
|
||||
let turns = build_turns_from_rollout_items(&items);
|
||||
assert_eq!(turns.len(), 1);
|
||||
assert_eq!(turns[0].items.len(), 1);
|
||||
assert_eq!(
|
||||
turns[0].items[0],
|
||||
ThreadItem::UserMessage {
|
||||
id: "item-1".into(),
|
||||
content: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_agent_message_phase_in_history() {
|
||||
let events = vec![EventMsg::AgentMessage(AgentMessageEvent {
|
||||
@@ -1212,6 +1507,11 @@ mod tests {
|
||||
#[test]
|
||||
fn reconstructs_tool_items_from_persisted_completion_events() {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".into(),
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
EventMsg::UserMessage(UserMessageEvent {
|
||||
message: "run tools".into(),
|
||||
images: None,
|
||||
@@ -1311,6 +1611,11 @@ mod tests {
|
||||
#[test]
|
||||
fn reconstructs_declined_exec_and_patch_items() {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".into(),
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
EventMsg::UserMessage(UserMessageEvent {
|
||||
message: "run tools".into(),
|
||||
images: None,
|
||||
@@ -1471,6 +1776,82 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drops_late_turn_scoped_item_for_unknown_turn_id() {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
EventMsg::UserMessage(UserMessageEvent {
|
||||
message: "first".into(),
|
||||
images: None,
|
||||
text_elements: Vec::new(),
|
||||
local_images: Vec::new(),
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
last_agent_message: None,
|
||||
}),
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-b".into(),
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
EventMsg::UserMessage(UserMessageEvent {
|
||||
message: "second".into(),
|
||||
images: None,
|
||||
text_elements: Vec::new(),
|
||||
local_images: Vec::new(),
|
||||
}),
|
||||
EventMsg::ExecCommandEnd(ExecCommandEndEvent {
|
||||
call_id: "exec-unknown-turn".into(),
|
||||
process_id: Some("pid-42".into()),
|
||||
turn_id: "turn-missing".into(),
|
||||
command: vec!["echo".into(), "done".into()],
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
parsed_cmd: vec![ParsedCommand::Unknown {
|
||||
cmd: "echo done".into(),
|
||||
}],
|
||||
source: ExecCommandSource::Agent,
|
||||
interaction_input: None,
|
||||
stdout: "done\n".into(),
|
||||
stderr: String::new(),
|
||||
aggregated_output: "done\n".into(),
|
||||
exit_code: 0,
|
||||
duration: Duration::from_millis(5),
|
||||
formatted_output: "done\n".into(),
|
||||
status: CoreExecCommandStatus::Completed,
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-b".into(),
|
||||
last_agent_message: None,
|
||||
}),
|
||||
];
|
||||
|
||||
let mut builder = ThreadHistoryBuilder::new();
|
||||
for event in &events {
|
||||
builder.handle_event(event);
|
||||
}
|
||||
let turns = builder.finish();
|
||||
assert_eq!(turns.len(), 2);
|
||||
assert_eq!(turns[0].id, "turn-a");
|
||||
assert_eq!(turns[1].id, "turn-b");
|
||||
assert_eq!(turns[0].items.len(), 1);
|
||||
assert_eq!(turns[1].items.len(), 1);
|
||||
assert_eq!(
|
||||
turns[1].items[0],
|
||||
ThreadItem::UserMessage {
|
||||
id: "item-2".into(),
|
||||
content: vec![UserInput::Text {
|
||||
text: "second".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn late_turn_complete_does_not_close_active_turn() {
|
||||
let events = vec![
|
||||
@@ -1572,7 +1953,7 @@ mod tests {
|
||||
assert_eq!(turns.len(), 2);
|
||||
assert_eq!(turns[0].id, "turn-a");
|
||||
assert_eq!(turns[1].id, "turn-b");
|
||||
assert_eq!(turns[1].status, TurnStatus::Completed);
|
||||
assert_eq!(turns[1].status, TurnStatus::InProgress);
|
||||
assert_eq!(turns[1].items.len(), 2);
|
||||
}
|
||||
|
||||
@@ -1621,6 +2002,8 @@ mod tests {
|
||||
.expect("valid sender thread id"),
|
||||
receiver_thread_id: ThreadId::try_from("00000000-0000-0000-0000-000000000002")
|
||||
.expect("valid receiver thread id"),
|
||||
receiver_agent_nickname: None,
|
||||
receiver_agent_role: None,
|
||||
status: AgentStatus::Completed(None),
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -5,6 +5,8 @@ use crate::protocol::common::AuthMode;
|
||||
use codex_experimental_api_macros::ExperimentalApi;
|
||||
use codex_protocol::account::PlanType;
|
||||
use codex_protocol::approvals::ExecPolicyAmendment as CoreExecPolicyAmendment;
|
||||
use codex_protocol::approvals::NetworkApprovalContext as CoreNetworkApprovalContext;
|
||||
use codex_protocol::approvals::NetworkApprovalProtocol as CoreNetworkApprovalProtocol;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::CollaborationModeMask;
|
||||
use codex_protocol::config_types::ForcedLoginMethod;
|
||||
@@ -396,12 +398,41 @@ pub struct AnalyticsConfig {
|
||||
pub additional: HashMap<String, JsonValue>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum AppToolApproval {
|
||||
Auto,
|
||||
Prompt,
|
||||
Approve,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum AppDisabledReason {
|
||||
Unknown,
|
||||
User,
|
||||
pub struct AppsDefaultConfig {
|
||||
#[serde(default = "default_enabled")]
|
||||
pub enabled: bool,
|
||||
#[serde(default = "default_enabled")]
|
||||
pub destructive_enabled: bool,
|
||||
#[serde(default = "default_enabled")]
|
||||
pub open_world_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AppToolConfig {
|
||||
pub enabled: Option<bool>,
|
||||
pub approval_mode: Option<AppToolApproval>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AppToolsConfig {
|
||||
#[serde(default, flatten)]
|
||||
pub tools: HashMap<String, AppToolConfig>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
@@ -410,15 +441,20 @@ pub enum AppDisabledReason {
|
||||
pub struct AppConfig {
|
||||
#[serde(default = "default_enabled")]
|
||||
pub enabled: bool,
|
||||
pub disabled_reason: Option<AppDisabledReason>,
|
||||
pub destructive_enabled: Option<bool>,
|
||||
pub open_world_enabled: Option<bool>,
|
||||
pub default_tools_approval_mode: Option<AppToolApproval>,
|
||||
pub default_tools_enabled: Option<bool>,
|
||||
pub tools: Option<AppToolsConfig>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AppsConfig {
|
||||
#[serde(default, rename = "_default")]
|
||||
pub default: Option<AppsDefaultConfig>,
|
||||
#[serde(default, flatten)]
|
||||
#[schemars(with = "HashMap<String, AppConfig>")]
|
||||
pub apps: HashMap<String, AppConfig>,
|
||||
}
|
||||
|
||||
@@ -575,6 +611,7 @@ pub struct NetworkRequirements {
|
||||
pub allow_upstream_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_admin: Option<bool>,
|
||||
pub dangerously_allow_all_unix_sockets: Option<bool>,
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
pub denied_domains: Option<Vec<String>>,
|
||||
pub allow_unix_sockets: Option<Vec<String>>,
|
||||
@@ -650,6 +687,32 @@ pub enum CommandExecutionApprovalDecision {
|
||||
Cancel,
|
||||
}
|
||||
|
||||
v2_enum_from_core! {
|
||||
pub enum NetworkApprovalProtocol from CoreNetworkApprovalProtocol {
|
||||
Http,
|
||||
Https,
|
||||
Socks5Tcp,
|
||||
Socks5Udp,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct NetworkApprovalContext {
|
||||
pub host: String,
|
||||
pub protocol: NetworkApprovalProtocol,
|
||||
}
|
||||
|
||||
impl From<CoreNetworkApprovalContext> for NetworkApprovalContext {
|
||||
fn from(value: CoreNetworkApprovalContext) -> Self {
|
||||
Self {
|
||||
host: value.host,
|
||||
protocol: value.protocol.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -1443,6 +1506,8 @@ pub struct FeedbackUploadParams {
|
||||
#[ts(optional = nullable)]
|
||||
pub thread_id: Option<String>,
|
||||
pub include_logs: bool,
|
||||
#[ts(optional = nullable)]
|
||||
pub extra_log_files: Option<Vec<PathBuf>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
@@ -2206,6 +2271,10 @@ pub struct Thread {
|
||||
pub cli_version: String,
|
||||
/// Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.).
|
||||
pub source: SessionSource,
|
||||
/// Optional random unique nickname assigned to an AgentControl-spawned sub-agent.
|
||||
pub agent_nickname: Option<String>,
|
||||
/// Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.
|
||||
pub agent_role: Option<String>,
|
||||
/// Optional Git metadata captured when the thread was created.
|
||||
pub git_info: Option<GitInfo>,
|
||||
/// Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read`
|
||||
@@ -2727,6 +2796,26 @@ pub enum ThreadItem {
|
||||
ContextCompaction { id: String },
|
||||
}
|
||||
|
||||
impl ThreadItem {
|
||||
pub fn id(&self) -> &str {
|
||||
match self {
|
||||
ThreadItem::UserMessage { id, .. }
|
||||
| ThreadItem::AgentMessage { id, .. }
|
||||
| ThreadItem::Plan { id, .. }
|
||||
| ThreadItem::Reasoning { id, .. }
|
||||
| ThreadItem::CommandExecution { id, .. }
|
||||
| ThreadItem::FileChange { id, .. }
|
||||
| ThreadItem::McpToolCall { id, .. }
|
||||
| ThreadItem::CollabAgentToolCall { id, .. }
|
||||
| ThreadItem::WebSearch { id, .. }
|
||||
| ThreadItem::ImageView { id, .. }
|
||||
| ThreadItem::EnteredReviewMode { id, .. }
|
||||
| ThreadItem::ExitedReviewMode { id, .. }
|
||||
| ThreadItem::ContextCompaction { id, .. } => id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type", rename_all = "camelCase")]
|
||||
@@ -3307,6 +3396,10 @@ pub struct CommandExecutionRequestApprovalParams {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional = nullable)]
|
||||
pub reason: Option<String>,
|
||||
/// Optional context for managed-network approval prompts.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional = nullable)]
|
||||
pub network_approval_context: Option<NetworkApprovalContext>,
|
||||
/// The command to be executed.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional = nullable)]
|
||||
|
||||
@@ -1491,6 +1491,7 @@ impl CodexClient {
|
||||
item_id,
|
||||
approval_id,
|
||||
reason,
|
||||
network_approval_context,
|
||||
command,
|
||||
cwd,
|
||||
command_actions,
|
||||
@@ -1506,6 +1507,9 @@ impl CodexClient {
|
||||
if let Some(reason) = reason.as_deref() {
|
||||
println!("< reason: {reason}");
|
||||
}
|
||||
if let Some(network_approval_context) = network_approval_context.as_ref() {
|
||||
println!("< network approval context: {network_approval_context:?}");
|
||||
}
|
||||
if let Some(command) = command.as_deref() {
|
||||
println!("< command: {command}");
|
||||
}
|
||||
|
||||
@@ -48,9 +48,10 @@ tokio = { workspace = true, features = [
|
||||
"rt-multi-thread",
|
||||
"signal",
|
||||
] }
|
||||
tokio-util = { workspace = true }
|
||||
tokio-tungstenite = { workspace = true }
|
||||
tracing = { workspace = true, features = ["log"] }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt", "json"] }
|
||||
uuid = { workspace = true, features = ["serde", "v7"] }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -28,6 +28,11 @@ Supported transports:
|
||||
|
||||
Websocket transport is currently experimental and unsupported. Do not rely on it for production workloads.
|
||||
|
||||
Tracing/log output:
|
||||
|
||||
- `RUST_LOG` controls log filtering/verbosity.
|
||||
- Set `LOG_FORMAT=json` to emit app-server tracing logs to `stderr` as JSON (one event per line).
|
||||
|
||||
Backpressure behavior:
|
||||
|
||||
- The server uses bounded queues between transport ingress, request processing, and outbound writes.
|
||||
@@ -145,7 +150,7 @@ Example with notification opt-out:
|
||||
- `config/mcpServer/reload` — reload MCP server config from disk and queue a refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` without restarting the server.
|
||||
- `mcpServerStatus/list` — enumerate configured MCP servers with their tools, resources, resource templates, and auth status; supports cursor+limit pagination.
|
||||
- `windowsSandbox/setupStart` — start Windows sandbox setup for the selected mode (`elevated` or `unelevated`); returns `{ started: true }` immediately and later emits `windowsSandbox/setupCompleted`.
|
||||
- `feedback/upload` — submit a feedback report (classification + optional reason/logs and conversation_id); returns the tracking thread id.
|
||||
- `feedback/upload` — submit a feedback report (classification + optional reason/logs, conversation_id, and optional `extraLogFiles` attachments array); returns the tracking thread id.
|
||||
- `command/exec` — run a single command under the server sandbox without starting a thread/turn (handy for utilities and validation).
|
||||
- `config/read` — fetch the effective config on disk after resolving config layering.
|
||||
- `config/value/write` — write a single config key/value to the user's config.toml on disk.
|
||||
@@ -224,6 +229,7 @@ Experimental API: `thread/start`, `thread/resume`, and `thread/fork` accept `per
|
||||
- `sourceKinds` — restrict results to specific sources; omit or pass `[]` for interactive sessions only (`cli`, `vscode`).
|
||||
- `archived` — when `true`, list archived threads only. When `false` or `null`, list non-archived threads (default).
|
||||
- `cwd` — restrict results to threads whose session cwd exactly matches this path.
|
||||
- Responses include `agentNickname` and `agentRole` for AgentControl-spawned thread sub-agents when available.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -235,7 +241,7 @@ Example:
|
||||
} }
|
||||
{ "id": 20, "result": {
|
||||
"data": [
|
||||
{ "id": "thr_a", "preview": "Create a TUI", "modelProvider": "openai", "createdAt": 1730831111, "updatedAt": 1730831111, "status": { "type": "notLoaded" } },
|
||||
{ "id": "thr_a", "preview": "Create a TUI", "modelProvider": "openai", "createdAt": 1730831111, "updatedAt": 1730831111, "status": { "type": "notLoaded" }, "agentNickname": "Atlas", "agentRole": "explorer" },
|
||||
{ "id": "thr_b", "preview": "Fix tests", "modelProvider": "openai", "createdAt": 1730750000, "updatedAt": 1730750000, "status": { "type": "notLoaded" } }
|
||||
],
|
||||
"nextCursor": "opaque-token-or-null"
|
||||
@@ -271,7 +277,7 @@ When `nextCursor` is `null`, you’ve reached the final page.
|
||||
|
||||
### Example: Read a thread
|
||||
|
||||
Use `thread/read` to fetch a stored thread by id without resuming it. Pass `includeTurns` when you want the rollout history loaded into `thread.turns`.
|
||||
Use `thread/read` to fetch a stored thread by id without resuming it. Pass `includeTurns` when you want the rollout history loaded into `thread.turns`. The returned thread includes `agentNickname` and `agentRole` for AgentControl-spawned thread sub-agents when available.
|
||||
|
||||
```json
|
||||
{ "method": "thread/read", "id": 22, "params": { "threadId": "thr_123" } }
|
||||
@@ -661,7 +667,7 @@ Certain actions (shell commands or modifying files) may require explicit user ap
|
||||
Order of messages:
|
||||
|
||||
1. `item/started` — shows the pending `commandExecution` item with `command`, `cwd`, and other fields so you can render the proposed action.
|
||||
2. `item/commandExecution/requestApproval` (request) — carries the same `itemId`, `threadId`, `turnId`, optionally `approvalId` (for subcommand callbacks), `reason`, plus `command`, `cwd`, and `commandActions` for friendly display.
|
||||
2. `item/commandExecution/requestApproval` (request) — carries the same `itemId`, `threadId`, `turnId`, optionally `approvalId` (for subcommand callbacks), and `reason`. For normal command approvals, it also includes `command`, `cwd`, and `commandActions` for friendly display. For network-only approvals, those command fields may be omitted and `networkApprovalContext` is provided instead.
|
||||
3. Client response — `{ "decision": "accept", "acceptSettings": { "forSession": false } }` or `{ "decision": "decline" }`.
|
||||
4. `item/completed` — final `commandExecution` item with `status: "completed" | "failed" | "declined"` and execution output. Render this as the authoritative result.
|
||||
|
||||
|
||||
@@ -44,8 +44,8 @@ use codex_app_server_protocol::McpToolCallError;
|
||||
use codex_app_server_protocol::McpToolCallResult;
|
||||
use codex_app_server_protocol::McpToolCallStatus;
|
||||
use codex_app_server_protocol::ModelReroutedNotification;
|
||||
use codex_app_server_protocol::NetworkApprovalContext as V2NetworkApprovalContext;
|
||||
use codex_app_server_protocol::PatchApplyStatus;
|
||||
use codex_app_server_protocol::PatchChangeKind as V2PatchChangeKind;
|
||||
use codex_app_server_protocol::PlanDeltaNotification;
|
||||
use codex_app_server_protocol::RawResponseItemCompletedNotification;
|
||||
use codex_app_server_protocol::ReasoningSummaryPartAddedNotification;
|
||||
@@ -72,6 +72,7 @@ use codex_app_server_protocol::TurnPlanStep;
|
||||
use codex_app_server_protocol::TurnPlanUpdatedNotification;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_app_server_protocol::build_turns_from_rollout_items;
|
||||
use codex_app_server_protocol::convert_patch_changes;
|
||||
use codex_core::CodexThread;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::parse_command::shlex_join;
|
||||
@@ -81,7 +82,6 @@ use codex_core::protocol::Event;
|
||||
use codex_core::protocol::EventMsg;
|
||||
use codex_core::protocol::ExecApprovalRequestEvent;
|
||||
use codex_core::protocol::ExecCommandEndEvent;
|
||||
use codex_core::protocol::FileChange as CoreFileChange;
|
||||
use codex_core::protocol::McpToolCallBeginEvent;
|
||||
use codex_core::protocol::McpToolCallEndEvent;
|
||||
use codex_core::protocol::Op;
|
||||
@@ -107,6 +107,17 @@ use tracing::error;
|
||||
|
||||
type JsonValue = serde_json::Value;
|
||||
|
||||
enum CommandExecutionApprovalPresentation {
|
||||
Network(V2NetworkApprovalContext),
|
||||
Command(CommandExecutionCompletionItem),
|
||||
}
|
||||
|
||||
struct CommandExecutionCompletionItem {
|
||||
command: String,
|
||||
cwd: PathBuf,
|
||||
command_actions: Vec<V2ParsedCommand>,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn apply_bespoke_event_handling(
|
||||
event: Event,
|
||||
@@ -246,6 +257,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
command,
|
||||
cwd,
|
||||
reason,
|
||||
network_approval_context,
|
||||
proposed_execpolicy_amendment,
|
||||
parsed_cmd,
|
||||
..
|
||||
@@ -281,7 +293,32 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
.cloned()
|
||||
.map(V2ParsedCommand::from)
|
||||
.collect::<Vec<_>>();
|
||||
let command_string = shlex_join(&command);
|
||||
let presentation = if let Some(network_approval_context) =
|
||||
network_approval_context.map(V2NetworkApprovalContext::from)
|
||||
{
|
||||
CommandExecutionApprovalPresentation::Network(network_approval_context)
|
||||
} else {
|
||||
let command_string = shlex_join(&command);
|
||||
let completion_item = CommandExecutionCompletionItem {
|
||||
command: command_string,
|
||||
cwd: cwd.clone(),
|
||||
command_actions: command_actions.clone(),
|
||||
};
|
||||
CommandExecutionApprovalPresentation::Command(completion_item)
|
||||
};
|
||||
let (network_approval_context, command, cwd, command_actions, completion_item) =
|
||||
match presentation {
|
||||
CommandExecutionApprovalPresentation::Network(
|
||||
network_approval_context,
|
||||
) => (Some(network_approval_context), None, None, None, None),
|
||||
CommandExecutionApprovalPresentation::Command(completion_item) => (
|
||||
None,
|
||||
Some(completion_item.command.clone()),
|
||||
Some(completion_item.cwd.clone()),
|
||||
Some(completion_item.command_actions.clone()),
|
||||
Some(completion_item),
|
||||
),
|
||||
};
|
||||
let proposed_execpolicy_amendment_v2 =
|
||||
proposed_execpolicy_amendment.map(V2ExecPolicyAmendment::from);
|
||||
|
||||
@@ -291,9 +328,10 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
item_id: call_id.clone(),
|
||||
approval_id: approval_id.clone(),
|
||||
reason,
|
||||
command: Some(command_string.clone()),
|
||||
cwd: Some(cwd.clone()),
|
||||
command_actions: Some(command_actions.clone()),
|
||||
network_approval_context,
|
||||
command,
|
||||
cwd,
|
||||
command_actions,
|
||||
proposed_execpolicy_amendment: proposed_execpolicy_amendment_v2,
|
||||
};
|
||||
let rx = outgoing
|
||||
@@ -307,9 +345,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
conversation_id,
|
||||
approval_id,
|
||||
call_id,
|
||||
command_string,
|
||||
cwd,
|
||||
command_actions,
|
||||
completion_item,
|
||||
rx,
|
||||
conversation,
|
||||
outgoing,
|
||||
@@ -1706,46 +1742,6 @@ fn render_review_output_text(output: &ReviewOutputEvent) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_patch_changes(changes: &HashMap<PathBuf, CoreFileChange>) -> Vec<FileUpdateChange> {
|
||||
let mut converted: Vec<FileUpdateChange> = changes
|
||||
.iter()
|
||||
.map(|(path, change)| FileUpdateChange {
|
||||
path: path.to_string_lossy().into_owned(),
|
||||
kind: map_patch_change_kind(change),
|
||||
diff: format_file_change_diff(change),
|
||||
})
|
||||
.collect();
|
||||
converted.sort_by(|a, b| a.path.cmp(&b.path));
|
||||
converted
|
||||
}
|
||||
|
||||
fn map_patch_change_kind(change: &CoreFileChange) -> V2PatchChangeKind {
|
||||
match change {
|
||||
CoreFileChange::Add { .. } => V2PatchChangeKind::Add,
|
||||
CoreFileChange::Delete { .. } => V2PatchChangeKind::Delete,
|
||||
CoreFileChange::Update { move_path, .. } => V2PatchChangeKind::Update {
|
||||
move_path: move_path.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn format_file_change_diff(change: &CoreFileChange) -> String {
|
||||
match change {
|
||||
CoreFileChange::Add { content } => content.clone(),
|
||||
CoreFileChange::Delete { content } => content.clone(),
|
||||
CoreFileChange::Update {
|
||||
unified_diff,
|
||||
move_path,
|
||||
} => {
|
||||
if let Some(path) = move_path {
|
||||
format!("{unified_diff}\n\nMoved to: {}", path.display())
|
||||
} else {
|
||||
unified_diff.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn map_file_change_approval_decision(
|
||||
decision: FileChangeApprovalDecision,
|
||||
) -> (ReviewDecision, Option<PatchApplyStatus>) {
|
||||
@@ -1831,9 +1827,7 @@ async fn on_command_execution_request_approval_response(
|
||||
conversation_id: ThreadId,
|
||||
approval_id: Option<String>,
|
||||
item_id: String,
|
||||
command: String,
|
||||
cwd: PathBuf,
|
||||
command_actions: Vec<V2ParsedCommand>,
|
||||
completion_item: Option<CommandExecutionCompletionItem>,
|
||||
receiver: oneshot::Receiver<ClientRequestResult>,
|
||||
conversation: Arc<CodexThread>,
|
||||
outgoing: ThreadScopedOutgoingMessageSender,
|
||||
@@ -1905,15 +1899,16 @@ async fn on_command_execution_request_approval_response(
|
||||
|
||||
if let Some(status) = completion_status
|
||||
&& !suppress_subcommand_completion_item
|
||||
&& let Some(completion_item) = completion_item
|
||||
{
|
||||
complete_command_execution_item(
|
||||
conversation_id,
|
||||
event_turn_id.clone(),
|
||||
item_id.clone(),
|
||||
command.clone(),
|
||||
cwd.clone(),
|
||||
completion_item.command,
|
||||
completion_item.cwd,
|
||||
None,
|
||||
command_actions.clone(),
|
||||
completion_item.command_actions,
|
||||
status,
|
||||
&outgoing,
|
||||
)
|
||||
@@ -2100,6 +2095,8 @@ mod tests {
|
||||
call_id: "call-1".to_string(),
|
||||
sender_thread_id: ThreadId::new(),
|
||||
receiver_thread_id: ThreadId::new(),
|
||||
receiver_agent_nickname: None,
|
||||
receiver_agent_role: None,
|
||||
};
|
||||
|
||||
let item = collab_resume_begin_item(event.clone());
|
||||
@@ -2121,6 +2118,8 @@ mod tests {
|
||||
call_id: "call-2".to_string(),
|
||||
sender_thread_id: ThreadId::new(),
|
||||
receiver_thread_id: ThreadId::new(),
|
||||
receiver_agent_nickname: None,
|
||||
receiver_agent_role: None,
|
||||
status: codex_protocol::protocol::AgentStatus::NotFound,
|
||||
};
|
||||
|
||||
|
||||
@@ -336,6 +336,7 @@ pub(crate) struct CodexMessageProcessor {
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
config: Arc<Config>,
|
||||
single_client_mode: bool,
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
cloud_requirements: Arc<RwLock<CloudRequirementsLoader>>,
|
||||
active_login: Arc<Mutex<Option<ActiveLogin>>>,
|
||||
@@ -361,6 +362,7 @@ pub(crate) struct CodexMessageProcessorArgs {
|
||||
pub(crate) config: Arc<Config>,
|
||||
pub(crate) cli_overrides: Vec<(String, TomlValue)>,
|
||||
pub(crate) cloud_requirements: Arc<RwLock<CloudRequirementsLoader>>,
|
||||
pub(crate) single_client_mode: bool,
|
||||
pub(crate) feedback: CodexFeedback,
|
||||
}
|
||||
|
||||
@@ -397,6 +399,7 @@ impl CodexMessageProcessor {
|
||||
config,
|
||||
cli_overrides,
|
||||
cloud_requirements,
|
||||
single_client_mode,
|
||||
feedback,
|
||||
} = args;
|
||||
Self {
|
||||
@@ -405,6 +408,7 @@ impl CodexMessageProcessor {
|
||||
outgoing: outgoing.clone(),
|
||||
codex_linux_sandbox_exe,
|
||||
config,
|
||||
single_client_mode,
|
||||
cli_overrides,
|
||||
cloud_requirements,
|
||||
active_login: Arc::new(Mutex::new(None)),
|
||||
@@ -1759,7 +1763,6 @@ impl CodexMessageProcessor {
|
||||
network: started_network_proxy
|
||||
.as_ref()
|
||||
.map(codex_core::config::StartedNetworkProxy::proxy),
|
||||
network_attempt_id: None,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
windows_sandbox_level,
|
||||
justification: None,
|
||||
@@ -3042,21 +3045,14 @@ impl CodexMessageProcessor {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Err(err) = self
|
||||
.ensure_conversation_listener(
|
||||
existing_thread_id,
|
||||
request_id.connection_id,
|
||||
false,
|
||||
ApiVersion::V2,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(
|
||||
"failed to attach listener for thread {}: {}",
|
||||
existing_thread_id,
|
||||
err.message
|
||||
);
|
||||
}
|
||||
let thread_state = self.thread_state_manager.thread_state(existing_thread_id);
|
||||
self.ensure_listener_task_running(
|
||||
existing_thread_id,
|
||||
existing_thread.clone(),
|
||||
thread_state.clone(),
|
||||
ApiVersion::V2,
|
||||
)
|
||||
.await;
|
||||
|
||||
let config_snapshot = existing_thread.config_snapshot().await;
|
||||
let mismatch_details = collect_resume_override_mismatches(params, &config_snapshot);
|
||||
@@ -3068,41 +3064,39 @@ impl CodexMessageProcessor {
|
||||
);
|
||||
}
|
||||
|
||||
let Some(mut thread) = self
|
||||
.load_thread_from_rollout_or_send_internal(
|
||||
request_id.clone(),
|
||||
existing_thread_id,
|
||||
rollout_path.as_path(),
|
||||
config_snapshot.model_provider_id.as_str(),
|
||||
)
|
||||
.await
|
||||
else {
|
||||
let listener_command_tx = {
|
||||
let thread_state = thread_state.lock().await;
|
||||
thread_state.listener_command_tx()
|
||||
};
|
||||
let Some(listener_command_tx) = listener_command_tx else {
|
||||
let err = JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!(
|
||||
"failed to enqueue running thread resume for thread {existing_thread_id}: thread listener is not running"
|
||||
),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request_id, err).await;
|
||||
return true;
|
||||
};
|
||||
|
||||
let ThreadConfigSnapshot {
|
||||
model,
|
||||
model_provider_id,
|
||||
approval_policy,
|
||||
sandbox_policy,
|
||||
cwd,
|
||||
reasoning_effort,
|
||||
..
|
||||
} = config_snapshot;
|
||||
thread.status = self
|
||||
.thread_watch_manager
|
||||
.loaded_status_for_thread(&thread.id)
|
||||
.await;
|
||||
let response = ThreadResumeResponse {
|
||||
thread,
|
||||
model,
|
||||
model_provider: model_provider_id,
|
||||
cwd,
|
||||
approval_policy: approval_policy.into(),
|
||||
sandbox: sandbox_policy.into(),
|
||||
reasoning_effort,
|
||||
};
|
||||
self.outgoing.send_response(request_id, response).await;
|
||||
let command = crate::thread_state::ThreadListenerCommand::SendThreadResumeResponse(
|
||||
crate::thread_state::PendingThreadResumeRequest {
|
||||
request_id: request_id.clone(),
|
||||
rollout_path,
|
||||
config_snapshot,
|
||||
},
|
||||
);
|
||||
if listener_command_tx.send(command).is_err() {
|
||||
let err = JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!(
|
||||
"failed to enqueue running thread resume for thread {existing_thread_id}: thread listener command channel is closed"
|
||||
),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request_id, err).await;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
false
|
||||
@@ -5817,17 +5811,18 @@ impl CodexMessageProcessor {
|
||||
api_version: ApiVersion,
|
||||
) {
|
||||
let (cancel_tx, mut cancel_rx) = oneshot::channel();
|
||||
{
|
||||
let (mut listener_command_rx, listener_generation) = {
|
||||
let mut thread_state = thread_state.lock().await;
|
||||
if thread_state.listener_matches(&conversation) {
|
||||
return;
|
||||
}
|
||||
thread_state.set_listener(cancel_tx, &conversation);
|
||||
}
|
||||
thread_state.set_listener(cancel_tx, &conversation)
|
||||
};
|
||||
let outgoing_for_task = self.outgoing.clone();
|
||||
let thread_manager = self.thread_manager.clone();
|
||||
let thread_watch_manager = self.thread_watch_manager.clone();
|
||||
let fallback_model_provider = self.config.model_provider_id.clone();
|
||||
let single_client_mode = self.single_client_mode;
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::select! {
|
||||
@@ -5869,7 +5864,10 @@ impl CodexMessageProcessor {
|
||||
conversation_id.to_string().into(),
|
||||
);
|
||||
let (subscribed_connection_ids, raw_events_enabled) = {
|
||||
let thread_state = thread_state.lock().await;
|
||||
let mut thread_state = thread_state.lock().await;
|
||||
if !single_client_mode {
|
||||
thread_state.track_current_turn_event(&event.msg);
|
||||
}
|
||||
(
|
||||
thread_state.subscribed_connection_ids(),
|
||||
thread_state.experimental_raw_events,
|
||||
@@ -5908,8 +5906,32 @@ impl CodexMessageProcessor {
|
||||
)
|
||||
.await;
|
||||
}
|
||||
listener_command = listener_command_rx.recv() => {
|
||||
let Some(listener_command) = listener_command else {
|
||||
break;
|
||||
};
|
||||
match listener_command {
|
||||
crate::thread_state::ThreadListenerCommand::SendThreadResumeResponse(
|
||||
resume_request,
|
||||
) => {
|
||||
handle_pending_thread_resume_request(
|
||||
conversation_id,
|
||||
&thread_state,
|
||||
&thread_watch_manager,
|
||||
&outgoing_for_task,
|
||||
resume_request,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut thread_state = thread_state.lock().await;
|
||||
if thread_state.listener_generation == listener_generation {
|
||||
thread_state.clear_listener();
|
||||
}
|
||||
});
|
||||
}
|
||||
async fn git_diff_to_origin(&self, request_id: ConnectionRequestId, cwd: PathBuf) {
|
||||
@@ -6076,6 +6098,7 @@ impl CodexMessageProcessor {
|
||||
reason,
|
||||
thread_id,
|
||||
include_logs,
|
||||
extra_log_files,
|
||||
} = params;
|
||||
|
||||
let conversation_id = match thread_id.as_deref() {
|
||||
@@ -6105,15 +6128,19 @@ impl CodexMessageProcessor {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut attachment_paths = validated_rollout_path.into_iter().collect::<Vec<_>>();
|
||||
if let Some(extra_log_files) = extra_log_files {
|
||||
attachment_paths.extend(extra_log_files);
|
||||
}
|
||||
|
||||
let session_source = self.thread_manager.session_source();
|
||||
|
||||
let upload_result = tokio::task::spawn_blocking(move || {
|
||||
let rollout_path_ref = validated_rollout_path.as_deref();
|
||||
snapshot.upload_feedback(
|
||||
&classification,
|
||||
reason.as_deref(),
|
||||
include_logs,
|
||||
rollout_path_ref,
|
||||
&attachment_paths,
|
||||
Some(session_source),
|
||||
)
|
||||
})
|
||||
@@ -6206,6 +6233,106 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_pending_thread_resume_request(
|
||||
conversation_id: ThreadId,
|
||||
thread_state: &Arc<Mutex<ThreadState>>,
|
||||
thread_watch_manager: &ThreadWatchManager,
|
||||
outgoing: &Arc<OutgoingMessageSender>,
|
||||
pending: crate::thread_state::PendingThreadResumeRequest,
|
||||
) {
|
||||
let active_turn = {
|
||||
let state = thread_state.lock().await;
|
||||
state.active_turn_snapshot()
|
||||
};
|
||||
|
||||
let request_id = pending.request_id;
|
||||
let connection_id = request_id.connection_id;
|
||||
let mut thread = match load_thread_for_running_resume_response(
|
||||
conversation_id,
|
||||
pending.rollout_path.as_path(),
|
||||
pending.config_snapshot.model_provider_id.as_str(),
|
||||
active_turn.as_ref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(thread) => thread,
|
||||
Err(message) => {
|
||||
outgoing
|
||||
.send_error(
|
||||
request_id,
|
||||
JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message,
|
||||
data: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
thread.status = thread_watch_manager
|
||||
.loaded_status_for_thread(&thread.id)
|
||||
.await;
|
||||
|
||||
let ThreadConfigSnapshot {
|
||||
model,
|
||||
model_provider_id,
|
||||
approval_policy,
|
||||
sandbox_policy,
|
||||
cwd,
|
||||
reasoning_effort,
|
||||
..
|
||||
} = pending.config_snapshot;
|
||||
let response = ThreadResumeResponse {
|
||||
thread,
|
||||
model,
|
||||
model_provider: model_provider_id,
|
||||
cwd,
|
||||
approval_policy: approval_policy.into(),
|
||||
sandbox: sandbox_policy.into(),
|
||||
reasoning_effort,
|
||||
};
|
||||
outgoing.send_response(request_id, response).await;
|
||||
thread_state.lock().await.add_connection(connection_id);
|
||||
}
|
||||
|
||||
async fn load_thread_for_running_resume_response(
|
||||
conversation_id: ThreadId,
|
||||
rollout_path: &Path,
|
||||
fallback_provider: &str,
|
||||
active_turn: Option<&Turn>,
|
||||
) -> std::result::Result<Thread, String> {
|
||||
let mut thread = read_summary_from_rollout(rollout_path, fallback_provider)
|
||||
.await
|
||||
.map(summary_to_thread)
|
||||
.map_err(|err| {
|
||||
format!(
|
||||
"failed to load rollout `{}` for thread {conversation_id}: {err}",
|
||||
rollout_path.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut turns = read_rollout_items_from_rollout(rollout_path)
|
||||
.await
|
||||
.map(|items| build_turns_from_rollout_items(&items))
|
||||
.map_err(|err| {
|
||||
format!(
|
||||
"failed to load rollout `{}` for thread {conversation_id}: {err}",
|
||||
rollout_path.display()
|
||||
)
|
||||
})?;
|
||||
if let Some(active_turn) = active_turn {
|
||||
merge_turn_history_with_active_turn(&mut turns, active_turn.clone());
|
||||
}
|
||||
thread.turns = turns;
|
||||
Ok(thread)
|
||||
}
|
||||
|
||||
fn merge_turn_history_with_active_turn(turns: &mut Vec<Turn>, active_turn: Turn) {
|
||||
turns.retain(|turn| turn.id != active_turn.id);
|
||||
turns.push(active_turn);
|
||||
}
|
||||
|
||||
fn collect_resume_override_mismatches(
|
||||
request: &ThreadResumeParams,
|
||||
config_snapshot: &ThreadConfigSnapshot,
|
||||
@@ -6540,6 +6667,8 @@ async fn read_summary_from_state_db_context_by_thread_id(
|
||||
metadata.cwd,
|
||||
metadata.cli_version,
|
||||
metadata.source,
|
||||
metadata.agent_nickname,
|
||||
metadata.agent_role,
|
||||
metadata.git_sha,
|
||||
metadata.git_branch,
|
||||
metadata.git_origin_url,
|
||||
@@ -6560,9 +6689,12 @@ async fn summary_from_thread_list_item(
|
||||
.unwrap_or_else(|| fallback_provider.to_string());
|
||||
let cwd = it.cwd?;
|
||||
let cli_version = it.cli_version.unwrap_or_default();
|
||||
let source = it
|
||||
.source
|
||||
.unwrap_or(codex_protocol::protocol::SessionSource::Unknown);
|
||||
let source = with_thread_spawn_agent_metadata(
|
||||
it.source
|
||||
.unwrap_or(codex_protocol::protocol::SessionSource::Unknown),
|
||||
it.agent_nickname.clone(),
|
||||
it.agent_role.clone(),
|
||||
);
|
||||
return Some(ConversationSummary {
|
||||
conversation_id: thread_id,
|
||||
path: it.path,
|
||||
@@ -6617,13 +6749,17 @@ fn summary_from_state_db_metadata(
|
||||
cwd: PathBuf,
|
||||
cli_version: String,
|
||||
source: String,
|
||||
agent_nickname: Option<String>,
|
||||
agent_role: Option<String>,
|
||||
git_sha: Option<String>,
|
||||
git_branch: Option<String>,
|
||||
git_origin_url: Option<String>,
|
||||
) -> ConversationSummary {
|
||||
let preview = first_user_message.unwrap_or_default();
|
||||
let source = serde_json::from_value(serde_json::Value::String(source))
|
||||
let source = serde_json::from_str(&source)
|
||||
.or_else(|_| serde_json::from_value(serde_json::Value::String(source.clone())))
|
||||
.unwrap_or(codex_protocol::protocol::SessionSource::Unknown);
|
||||
let source = with_thread_spawn_agent_metadata(source, agent_nickname, agent_role);
|
||||
let git_info = if git_sha.is_none() && git_branch.is_none() && git_origin_url.is_none() {
|
||||
None
|
||||
} else {
|
||||
@@ -6671,6 +6807,12 @@ pub(crate) async fn read_summary_from_rollout(
|
||||
meta: session_meta,
|
||||
git,
|
||||
} = session_meta_line;
|
||||
let mut session_meta = session_meta;
|
||||
session_meta.source = with_thread_spawn_agent_metadata(
|
||||
session_meta.source.clone(),
|
||||
session_meta.agent_nickname.clone(),
|
||||
session_meta.agent_role.clone(),
|
||||
);
|
||||
|
||||
let created_at = if session_meta.timestamp.is_empty() {
|
||||
None
|
||||
@@ -6783,6 +6925,35 @@ fn map_git_info(git_info: &CoreGitInfo) -> ConversationGitInfo {
|
||||
}
|
||||
}
|
||||
|
||||
fn with_thread_spawn_agent_metadata(
|
||||
source: codex_protocol::protocol::SessionSource,
|
||||
agent_nickname: Option<String>,
|
||||
agent_role: Option<String>,
|
||||
) -> codex_protocol::protocol::SessionSource {
|
||||
if agent_nickname.is_none() && agent_role.is_none() {
|
||||
return source;
|
||||
}
|
||||
|
||||
match source {
|
||||
codex_protocol::protocol::SessionSource::SubAgent(
|
||||
codex_protocol::protocol::SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth,
|
||||
agent_nickname: existing_agent_nickname,
|
||||
agent_role: existing_agent_role,
|
||||
},
|
||||
) => codex_protocol::protocol::SessionSource::SubAgent(
|
||||
codex_protocol::protocol::SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth,
|
||||
agent_nickname: agent_nickname.or(existing_agent_nickname),
|
||||
agent_role: agent_role.or(existing_agent_role),
|
||||
},
|
||||
),
|
||||
_ => source,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_datetime(timestamp: Option<&str>) -> Option<DateTime<Utc>> {
|
||||
timestamp.and_then(|ts| {
|
||||
chrono::DateTime::parse_from_rfc3339(ts)
|
||||
@@ -6819,6 +6990,8 @@ fn build_thread_from_snapshot(
|
||||
path,
|
||||
cwd: config_snapshot.cwd.clone(),
|
||||
cli_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
agent_nickname: config_snapshot.session_source.get_nickname(),
|
||||
agent_role: config_snapshot.session_source.get_agent_role(),
|
||||
source: config_snapshot.session_source.clone().into(),
|
||||
git_info: None,
|
||||
turns: Vec::new(),
|
||||
@@ -6857,6 +7030,8 @@ pub(crate) fn summary_to_thread(summary: ConversationSummary) -> Thread {
|
||||
path: Some(path),
|
||||
cwd,
|
||||
cli_version,
|
||||
agent_nickname: source.get_nickname(),
|
||||
agent_role: source.get_agent_role(),
|
||||
source: source.into(),
|
||||
git_info,
|
||||
turns: Vec::new(),
|
||||
@@ -6868,6 +7043,7 @@ mod tests {
|
||||
use super::*;
|
||||
use anyhow::Result;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
@@ -7013,6 +7189,87 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_summary_from_rollout_preserves_agent_nickname() -> Result<()> {
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::RolloutLine;
|
||||
use codex_protocol::protocol::SessionMetaLine;
|
||||
use std::fs;
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let path = temp_dir.path().join("rollout.jsonl");
|
||||
|
||||
let conversation_id = ThreadId::from_string("bfd12a78-5900-467b-9bc5-d3d35df08191")?;
|
||||
let parent_thread_id = ThreadId::from_string("ad7f0408-99b8-4f6e-a46f-bd0eec433370")?;
|
||||
let timestamp = "2025-09-05T16:53:11.850Z".to_string();
|
||||
|
||||
let session_meta = SessionMeta {
|
||||
id: conversation_id,
|
||||
timestamp: timestamp.clone(),
|
||||
source: SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
}),
|
||||
agent_nickname: Some("atlas".to_string()),
|
||||
agent_role: Some("explorer".to_string()),
|
||||
model_provider: Some("test-provider".to_string()),
|
||||
..SessionMeta::default()
|
||||
};
|
||||
|
||||
let line = RolloutLine {
|
||||
timestamp,
|
||||
item: RolloutItem::SessionMeta(SessionMetaLine {
|
||||
meta: session_meta,
|
||||
git: None,
|
||||
}),
|
||||
};
|
||||
fs::write(&path, format!("{}\n", serde_json::to_string(&line)?))?;
|
||||
|
||||
let summary = read_summary_from_rollout(path.as_path(), "fallback").await?;
|
||||
let thread = summary_to_thread(summary);
|
||||
|
||||
assert_eq!(thread.agent_nickname, Some("atlas".to_string()));
|
||||
assert_eq!(thread.agent_role, Some("explorer".to_string()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_from_state_db_metadata_preserves_agent_nickname() -> Result<()> {
|
||||
let conversation_id = ThreadId::from_string("bfd12a78-5900-467b-9bc5-d3d35df08191")?;
|
||||
let source =
|
||||
serde_json::to_string(&SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: ThreadId::from_string("ad7f0408-99b8-4f6e-a46f-bd0eec433370")?,
|
||||
depth: 1,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
}))?;
|
||||
|
||||
let summary = summary_from_state_db_metadata(
|
||||
conversation_id,
|
||||
PathBuf::from("/tmp/rollout.jsonl"),
|
||||
Some("hi".to_string()),
|
||||
"2025-09-05T16:53:11Z".to_string(),
|
||||
"2025-09-05T16:53:12Z".to_string(),
|
||||
"test-provider".to_string(),
|
||||
PathBuf::from("/"),
|
||||
"0.0.0".to_string(),
|
||||
source,
|
||||
Some("atlas".to_string()),
|
||||
Some("explorer".to_string()),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let thread = summary_to_thread(summary);
|
||||
|
||||
assert_eq!(thread.agent_nickname, Some("atlas".to_string()));
|
||||
assert_eq!(thread.agent_role, Some("explorer".to_string()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn removing_one_listener_does_not_cancel_other_subscriptions_for_same_thread()
|
||||
-> Result<()> {
|
||||
|
||||
@@ -161,6 +161,7 @@ fn map_network_requirements_to_api(
|
||||
allow_upstream_proxy: network.allow_upstream_proxy,
|
||||
dangerously_allow_non_loopback_proxy: network.dangerously_allow_non_loopback_proxy,
|
||||
dangerously_allow_non_loopback_admin: network.dangerously_allow_non_loopback_admin,
|
||||
dangerously_allow_all_unix_sockets: network.dangerously_allow_all_unix_sockets,
|
||||
allowed_domains: network.allowed_domains,
|
||||
denied_domains: network.denied_domains,
|
||||
allow_unix_sockets: network.allow_unix_sockets,
|
||||
@@ -221,6 +222,7 @@ mod tests {
|
||||
allow_upstream_proxy: Some(false),
|
||||
dangerously_allow_non_loopback_proxy: Some(false),
|
||||
dangerously_allow_non_loopback_admin: Some(false),
|
||||
dangerously_allow_all_unix_sockets: Some(true),
|
||||
allowed_domains: Some(vec!["api.openai.com".to_string()]),
|
||||
denied_domains: Some(vec!["example.com".to_string()]),
|
||||
allow_unix_sockets: Some(vec!["/tmp/proxy.sock".to_string()]),
|
||||
@@ -258,6 +260,7 @@ mod tests {
|
||||
allow_upstream_proxy: Some(false),
|
||||
dangerously_allow_non_loopback_proxy: Some(false),
|
||||
dangerously_allow_non_loopback_admin: Some(false),
|
||||
dangerously_allow_all_unix_sockets: Some(true),
|
||||
allowed_domains: Some(vec!["api.openai.com".to_string()]),
|
||||
denied_domains: Some(vec!["example.com".to_string()]),
|
||||
allow_unix_sockets: Some(vec!["/tmp/proxy.sock".to_string()]),
|
||||
|
||||
@@ -133,6 +133,8 @@ mod tests {
|
||||
let spawn = CoreSessionSource::SubAgent(CoreSubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
});
|
||||
|
||||
assert!(source_kind_matches(
|
||||
|
||||
+116
-61
@@ -44,6 +44,7 @@ use codex_core::config_loader::TextRange as CoreTextRange;
|
||||
use codex_feedback::CodexFeedback;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use toml::Value as TomlValue;
|
||||
use tracing::error;
|
||||
use tracing::info;
|
||||
@@ -52,6 +53,7 @@ use tracing_subscriber::EnvFilter;
|
||||
use tracing_subscriber::Layer;
|
||||
use tracing_subscriber::fmt::writer::BoxMakeWriter;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::registry::Registry;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
|
||||
mod bespoke_event_handling;
|
||||
@@ -70,6 +72,16 @@ mod transport;
|
||||
|
||||
pub use crate::transport::AppServerTransport;
|
||||
|
||||
const LOG_FORMAT_ENV_VAR: &str = "LOG_FORMAT";
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum LogFormat {
|
||||
Default,
|
||||
Json,
|
||||
}
|
||||
|
||||
type StderrLogLayer = Box<dyn Layer<Registry> + Send + Sync + 'static>;
|
||||
|
||||
/// Control-plane messages from the processor/transport side to the outbound router task.
|
||||
///
|
||||
/// `run_main_with_transport` now uses two loops/tasks:
|
||||
@@ -84,6 +96,7 @@ enum OutboundControlEvent {
|
||||
Opened {
|
||||
connection_id: ConnectionId,
|
||||
writer: mpsc::Sender<crate::outgoing_message::OutgoingMessage>,
|
||||
disconnect_sender: Option<CancellationToken>,
|
||||
initialized: Arc<AtomicBool>,
|
||||
opted_out_notification_methods: Arc<RwLock<HashSet<String>>>,
|
||||
},
|
||||
@@ -201,6 +214,20 @@ fn project_config_warning(config: &Config) -> Option<ConfigWarningNotification>
|
||||
})
|
||||
}
|
||||
|
||||
impl LogFormat {
|
||||
fn from_env_value(value: Option<&str>) -> Self {
|
||||
match value.map(str::trim).map(str::to_ascii_lowercase) {
|
||||
Some(value) if value == "json" => Self::Json,
|
||||
_ => Self::Default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn log_format_from_env() -> LogFormat {
|
||||
let value = std::env::var(LOG_FORMAT_ENV_VAR).ok();
|
||||
LogFormat::from_env_value(value.as_deref())
|
||||
}
|
||||
|
||||
pub async fn run_main(
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
cli_config_overrides: CliConfigOverrides,
|
||||
@@ -241,7 +268,8 @@ pub async fn run_main_with_transport(
|
||||
Some(start_websocket_acceptor(bind_address, transport_event_tx.clone()).await?);
|
||||
}
|
||||
}
|
||||
let shutdown_when_no_connections = matches!(transport, AppServerTransport::Stdio);
|
||||
let single_client_mode = matches!(transport, AppServerTransport::Stdio);
|
||||
let shutdown_when_no_connections = single_client_mode;
|
||||
|
||||
// Parse CLI overrides once and derive the base Config eagerly so later
|
||||
// components do not need to work with raw TOML values.
|
||||
@@ -344,22 +372,26 @@ pub async fn run_main_with_transport(
|
||||
)
|
||||
})?;
|
||||
|
||||
// Install a simple subscriber so `tracing` output is visible. Users can
|
||||
// control the log level with `RUST_LOG`.
|
||||
let stderr_fmt = tracing_subscriber::fmt::layer()
|
||||
.with_writer(std::io::stderr)
|
||||
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::FULL)
|
||||
.with_filter(
|
||||
EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::TRACE.into())
|
||||
.from_env_lossy(),
|
||||
);
|
||||
// Install a simple subscriber so `tracing` output is visible. Users can
|
||||
// control the log level with `RUST_LOG` and switch to JSON logs with
|
||||
// `LOG_FORMAT=json`.
|
||||
let stderr_fmt: StderrLogLayer = match log_format_from_env() {
|
||||
LogFormat::Json => tracing_subscriber::fmt::layer()
|
||||
.json()
|
||||
.with_writer(std::io::stderr)
|
||||
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::FULL)
|
||||
.with_filter(EnvFilter::from_default_env())
|
||||
.boxed(),
|
||||
LogFormat::Default => tracing_subscriber::fmt::layer()
|
||||
.with_writer(std::io::stderr)
|
||||
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::FULL)
|
||||
.with_filter(EnvFilter::from_default_env())
|
||||
.boxed(),
|
||||
};
|
||||
|
||||
let feedback_layer = feedback.logger_layer();
|
||||
let feedback_metadata_layer = feedback.metadata_layer();
|
||||
|
||||
let otel_logger_layer = otel.as_ref().and_then(|o| o.logger_layer());
|
||||
|
||||
let otel_tracing_layer = otel.as_ref().and_then(|o| o.tracing_layer());
|
||||
|
||||
let mut trace_log_setup_warning = None;
|
||||
@@ -444,61 +476,43 @@ pub async fn run_main_with_transport(
|
||||
}
|
||||
}
|
||||
|
||||
let transport_event_tx_for_outbound = transport_event_tx.clone();
|
||||
let outbound_handle = tokio::spawn(async move {
|
||||
let mut outbound_connections = HashMap::<ConnectionId, OutboundConnectionState>::new();
|
||||
let mut pending_closed_connections = VecDeque::<ConnectionId>::new();
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
event = outbound_control_rx.recv() => {
|
||||
let Some(event) = event else {
|
||||
break;
|
||||
};
|
||||
match event {
|
||||
OutboundControlEvent::Opened {
|
||||
connection_id,
|
||||
writer,
|
||||
initialized,
|
||||
opted_out_notification_methods,
|
||||
} => {
|
||||
outbound_connections.insert(
|
||||
biased;
|
||||
event = outbound_control_rx.recv() => {
|
||||
let Some(event) = event else {
|
||||
break;
|
||||
};
|
||||
match event {
|
||||
OutboundControlEvent::Opened {
|
||||
connection_id,
|
||||
OutboundConnectionState::new(
|
||||
writer,
|
||||
initialized,
|
||||
opted_out_notification_methods,
|
||||
),
|
||||
);
|
||||
}
|
||||
OutboundControlEvent::Closed { connection_id } => {
|
||||
outbound_connections.remove(&connection_id);
|
||||
writer,
|
||||
disconnect_sender,
|
||||
initialized,
|
||||
opted_out_notification_methods,
|
||||
} => {
|
||||
outbound_connections.insert(
|
||||
connection_id,
|
||||
OutboundConnectionState::new(
|
||||
writer,
|
||||
initialized,
|
||||
opted_out_notification_methods,
|
||||
disconnect_sender,
|
||||
),
|
||||
);
|
||||
}
|
||||
OutboundControlEvent::Closed { connection_id } => {
|
||||
outbound_connections.remove(&connection_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
envelope = outgoing_rx.recv() => {
|
||||
envelope = outgoing_rx.recv() => {
|
||||
let Some(envelope) = envelope else {
|
||||
break;
|
||||
};
|
||||
let disconnected_connections =
|
||||
route_outgoing_envelope(&mut outbound_connections, envelope).await;
|
||||
pending_closed_connections.extend(disconnected_connections);
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(connection_id) = pending_closed_connections.front().copied() {
|
||||
match transport_event_tx_for_outbound
|
||||
.try_send(TransportEvent::ConnectionClosed { connection_id })
|
||||
{
|
||||
Ok(()) => {
|
||||
pending_closed_connections.pop_front();
|
||||
}
|
||||
Err(mpsc::error::TrySendError::Full(_)) => {
|
||||
break;
|
||||
}
|
||||
Err(mpsc::error::TrySendError::Closed(_)) => {
|
||||
return;
|
||||
}
|
||||
route_outgoing_envelope(&mut outbound_connections, envelope).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -514,6 +528,7 @@ pub async fn run_main_with_transport(
|
||||
outgoing: outgoing_message_sender,
|
||||
codex_linux_sandbox_exe,
|
||||
config: Arc::new(config),
|
||||
single_client_mode,
|
||||
cli_overrides,
|
||||
loader_overrides,
|
||||
cloud_requirements: cloud_requirements.clone(),
|
||||
@@ -531,7 +546,11 @@ pub async fn run_main_with_transport(
|
||||
break;
|
||||
};
|
||||
match event {
|
||||
TransportEvent::ConnectionOpened { connection_id, writer } => {
|
||||
TransportEvent::ConnectionOpened {
|
||||
connection_id,
|
||||
writer,
|
||||
disconnect_sender,
|
||||
} => {
|
||||
let outbound_initialized = Arc::new(AtomicBool::new(false));
|
||||
let outbound_opted_out_notification_methods =
|
||||
Arc::new(RwLock::new(HashSet::new()));
|
||||
@@ -539,6 +558,7 @@ pub async fn run_main_with_transport(
|
||||
.send(OutboundControlEvent::Opened {
|
||||
connection_id,
|
||||
writer,
|
||||
disconnect_sender,
|
||||
initialized: Arc::clone(&outbound_initialized),
|
||||
opted_out_notification_methods: Arc::clone(
|
||||
&outbound_opted_out_notification_methods,
|
||||
@@ -558,6 +578,9 @@ pub async fn run_main_with_transport(
|
||||
);
|
||||
}
|
||||
TransportEvent::ConnectionClosed { connection_id } => {
|
||||
if connections.remove(&connection_id).is_none() {
|
||||
continue;
|
||||
}
|
||||
if outbound_control_tx
|
||||
.send(OutboundControlEvent::Closed { connection_id })
|
||||
.await
|
||||
@@ -566,7 +589,6 @@ pub async fn run_main_with_transport(
|
||||
break;
|
||||
}
|
||||
processor.connection_closed(connection_id).await;
|
||||
connections.remove(&connection_id);
|
||||
if shutdown_when_no_connections && connections.is_empty() {
|
||||
break;
|
||||
}
|
||||
@@ -575,7 +597,7 @@ pub async fn run_main_with_transport(
|
||||
match message {
|
||||
JSONRPCMessage::Request(request) => {
|
||||
let Some(connection_state) = connections.get_mut(&connection_id) else {
|
||||
warn!("dropping request from unknown connection: {:?}", connection_id);
|
||||
warn!("dropping request from unknown connection: {connection_id:?}");
|
||||
continue;
|
||||
};
|
||||
let was_initialized = connection_state.session.initialized;
|
||||
@@ -605,12 +627,24 @@ pub async fn run_main_with_transport(
|
||||
}
|
||||
}
|
||||
JSONRPCMessage::Response(response) => {
|
||||
if !connections.contains_key(&connection_id) {
|
||||
warn!("dropping response from unknown connection: {connection_id:?}");
|
||||
continue;
|
||||
}
|
||||
processor.process_response(response).await;
|
||||
}
|
||||
JSONRPCMessage::Notification(notification) => {
|
||||
if !connections.contains_key(&connection_id) {
|
||||
warn!("dropping notification from unknown connection: {connection_id:?}");
|
||||
continue;
|
||||
}
|
||||
processor.process_notification(notification).await;
|
||||
}
|
||||
JSONRPCMessage::Error(err) => {
|
||||
if !connections.contains_key(&connection_id) {
|
||||
warn!("dropping error from unknown connection: {connection_id:?}");
|
||||
continue;
|
||||
}
|
||||
processor.process_error(err).await;
|
||||
}
|
||||
}
|
||||
@@ -668,3 +702,24 @@ pub async fn run_main_with_transport(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::LogFormat;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn log_format_from_env_value_matches_json_values_case_insensitively() {
|
||||
assert_eq!(LogFormat::from_env_value(Some("json")), LogFormat::Json);
|
||||
assert_eq!(LogFormat::from_env_value(Some("JSON")), LogFormat::Json);
|
||||
assert_eq!(LogFormat::from_env_value(Some(" Json ")), LogFormat::Json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn log_format_from_env_value_defaults_for_non_json_values() {
|
||||
assert_eq!(LogFormat::from_env_value(None), LogFormat::Default);
|
||||
assert_eq!(LogFormat::from_env_value(Some("")), LogFormat::Default);
|
||||
assert_eq!(LogFormat::from_env_value(Some("text")), LogFormat::Default);
|
||||
assert_eq!(LogFormat::from_env_value(Some("jsonl")), LogFormat::Default);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +139,7 @@ pub(crate) struct MessageProcessorArgs {
|
||||
pub(crate) outgoing: Arc<OutgoingMessageSender>,
|
||||
pub(crate) codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
pub(crate) config: Arc<Config>,
|
||||
pub(crate) single_client_mode: bool,
|
||||
pub(crate) cli_overrides: Vec<(String, TomlValue)>,
|
||||
pub(crate) loader_overrides: LoaderOverrides,
|
||||
pub(crate) cloud_requirements: CloudRequirementsLoader,
|
||||
@@ -154,6 +155,7 @@ impl MessageProcessor {
|
||||
outgoing,
|
||||
codex_linux_sandbox_exe,
|
||||
config,
|
||||
single_client_mode,
|
||||
cli_overrides,
|
||||
loader_overrides,
|
||||
cloud_requirements,
|
||||
@@ -184,6 +186,7 @@ impl MessageProcessor {
|
||||
config: Arc::clone(&config),
|
||||
cli_overrides: cli_overrides.clone(),
|
||||
cloud_requirements: cloud_requirements.clone(),
|
||||
single_client_mode,
|
||||
feedback,
|
||||
});
|
||||
let config_api = ConfigApi::new(
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
use crate::outgoing_message::ConnectionId;
|
||||
use crate::outgoing_message::ConnectionRequestId;
|
||||
use codex_app_server_protocol::ThreadHistoryBuilder;
|
||||
use codex_app_server_protocol::Turn;
|
||||
use codex_app_server_protocol::TurnError;
|
||||
use codex_core::CodexThread;
|
||||
use codex_core::ThreadConfigSnapshot;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Weak;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::oneshot;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -16,6 +22,16 @@ type PendingInterruptQueue = Vec<(
|
||||
crate::codex_message_processor::ApiVersion,
|
||||
)>;
|
||||
|
||||
pub(crate) struct PendingThreadResumeRequest {
|
||||
pub(crate) request_id: ConnectionRequestId,
|
||||
pub(crate) rollout_path: PathBuf,
|
||||
pub(crate) config_snapshot: ThreadConfigSnapshot,
|
||||
}
|
||||
|
||||
pub(crate) enum ThreadListenerCommand {
|
||||
SendThreadResumeResponse(PendingThreadResumeRequest),
|
||||
}
|
||||
|
||||
/// Per-conversation accumulation of the latest states e.g. error message while a turn runs.
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct TurnSummary {
|
||||
@@ -31,6 +47,9 @@ pub(crate) struct ThreadState {
|
||||
pub(crate) turn_summary: TurnSummary,
|
||||
pub(crate) cancel_tx: Option<oneshot::Sender<()>>,
|
||||
pub(crate) experimental_raw_events: bool,
|
||||
pub(crate) listener_generation: u64,
|
||||
listener_command_tx: Option<mpsc::UnboundedSender<ThreadListenerCommand>>,
|
||||
current_turn_history: ThreadHistoryBuilder,
|
||||
listener_thread: Option<Weak<CodexThread>>,
|
||||
subscribed_connections: HashSet<ConnectionId>,
|
||||
}
|
||||
@@ -47,17 +66,23 @@ impl ThreadState {
|
||||
&mut self,
|
||||
cancel_tx: oneshot::Sender<()>,
|
||||
conversation: &Arc<CodexThread>,
|
||||
) {
|
||||
) -> (mpsc::UnboundedReceiver<ThreadListenerCommand>, u64) {
|
||||
if let Some(previous) = self.cancel_tx.replace(cancel_tx) {
|
||||
let _ = previous.send(());
|
||||
}
|
||||
self.listener_generation = self.listener_generation.wrapping_add(1);
|
||||
let (listener_command_tx, listener_command_rx) = mpsc::unbounded_channel();
|
||||
self.listener_command_tx = Some(listener_command_tx);
|
||||
self.listener_thread = Some(Arc::downgrade(conversation));
|
||||
(listener_command_rx, self.listener_generation)
|
||||
}
|
||||
|
||||
pub(crate) fn clear_listener(&mut self) {
|
||||
if let Some(cancel_tx) = self.cancel_tx.take() {
|
||||
let _ = cancel_tx.send(());
|
||||
}
|
||||
self.listener_command_tx = None;
|
||||
self.current_turn_history.reset();
|
||||
self.listener_thread = None;
|
||||
}
|
||||
|
||||
@@ -76,6 +101,23 @@ impl ThreadState {
|
||||
pub(crate) fn set_experimental_raw_events(&mut self, enabled: bool) {
|
||||
self.experimental_raw_events = enabled;
|
||||
}
|
||||
|
||||
pub(crate) fn listener_command_tx(
|
||||
&self,
|
||||
) -> Option<mpsc::UnboundedSender<ThreadListenerCommand>> {
|
||||
self.listener_command_tx.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn active_turn_snapshot(&self) -> Option<Turn> {
|
||||
self.current_turn_history.active_turn_snapshot()
|
||||
}
|
||||
|
||||
pub(crate) fn track_current_turn_event(&mut self, event: &EventMsg) {
|
||||
self.current_turn_history.handle_event(event);
|
||||
if !self.current_turn_history.has_active_turn() {
|
||||
self.current_turn_history.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -200,12 +242,24 @@ impl ThreadStateManager {
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_connection(&mut self, connection_id: ConnectionId) {
|
||||
let Some(thread_ids) = self.thread_ids_by_connection.remove(&connection_id) else {
|
||||
return;
|
||||
};
|
||||
let thread_ids = self
|
||||
.thread_ids_by_connection
|
||||
.remove(&connection_id)
|
||||
.unwrap_or_default();
|
||||
self.subscription_state_by_id
|
||||
.retain(|_, state| state.connection_id != connection_id);
|
||||
|
||||
if thread_ids.is_empty() {
|
||||
for thread_state in self.thread_states.values() {
|
||||
let mut thread_state = thread_state.lock().await;
|
||||
thread_state.remove_connection(connection_id);
|
||||
if thread_state.subscribed_connection_ids().is_empty() {
|
||||
thread_state.clear_listener();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for thread_id in thread_ids {
|
||||
if let Some(thread_state) = self.thread_states.get(&thread_id) {
|
||||
let mut thread_state = thread_state.lock().await;
|
||||
|
||||
@@ -622,6 +622,8 @@ mod tests {
|
||||
path: None,
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
cli_version: "test".to_string(),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
source,
|
||||
git_info: None,
|
||||
turns: Vec::new(),
|
||||
|
||||
@@ -32,6 +32,7 @@ use tokio::sync::mpsc;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio_tungstenite::accept_async;
|
||||
use tokio_tungstenite::tungstenite::Message as WebSocketMessage;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::debug;
|
||||
use tracing::error;
|
||||
use tracing::info;
|
||||
@@ -135,6 +136,7 @@ pub(crate) enum TransportEvent {
|
||||
ConnectionOpened {
|
||||
connection_id: ConnectionId,
|
||||
writer: mpsc::Sender<OutgoingMessage>,
|
||||
disconnect_sender: Option<CancellationToken>,
|
||||
},
|
||||
ConnectionClosed {
|
||||
connection_id: ConnectionId,
|
||||
@@ -168,6 +170,7 @@ pub(crate) struct OutboundConnectionState {
|
||||
pub(crate) initialized: Arc<AtomicBool>,
|
||||
pub(crate) opted_out_notification_methods: Arc<RwLock<HashSet<String>>>,
|
||||
pub(crate) writer: mpsc::Sender<OutgoingMessage>,
|
||||
disconnect_sender: Option<CancellationToken>,
|
||||
}
|
||||
|
||||
impl OutboundConnectionState {
|
||||
@@ -175,11 +178,23 @@ impl OutboundConnectionState {
|
||||
writer: mpsc::Sender<OutgoingMessage>,
|
||||
initialized: Arc<AtomicBool>,
|
||||
opted_out_notification_methods: Arc<RwLock<HashSet<String>>>,
|
||||
disconnect_sender: Option<CancellationToken>,
|
||||
) -> Self {
|
||||
Self {
|
||||
initialized,
|
||||
opted_out_notification_methods,
|
||||
writer,
|
||||
disconnect_sender,
|
||||
}
|
||||
}
|
||||
|
||||
fn can_disconnect(&self) -> bool {
|
||||
self.disconnect_sender.is_some()
|
||||
}
|
||||
|
||||
fn request_disconnect(&self) {
|
||||
if let Some(disconnect_sender) = &self.disconnect_sender {
|
||||
disconnect_sender.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,6 +210,7 @@ pub(crate) async fn start_stdio_connection(
|
||||
.send(TransportEvent::ConnectionOpened {
|
||||
connection_id,
|
||||
writer: writer_tx,
|
||||
disconnect_sender: None,
|
||||
})
|
||||
.await
|
||||
.map_err(|_| std::io::Error::new(ErrorKind::BrokenPipe, "processor unavailable"))?;
|
||||
@@ -299,12 +315,14 @@ async fn run_websocket_connection(
|
||||
}
|
||||
};
|
||||
|
||||
let (writer_tx, mut writer_rx) = mpsc::channel::<OutgoingMessage>(CHANNEL_CAPACITY);
|
||||
let (writer_tx, writer_rx) = mpsc::channel::<OutgoingMessage>(CHANNEL_CAPACITY);
|
||||
let writer_tx_for_reader = writer_tx.clone();
|
||||
let disconnect_token = CancellationToken::new();
|
||||
if transport_event_tx
|
||||
.send(TransportEvent::ConnectionOpened {
|
||||
connection_id,
|
||||
writer: writer_tx,
|
||||
disconnect_sender: Some(disconnect_token.clone()),
|
||||
})
|
||||
.await
|
||||
.is_err()
|
||||
@@ -312,9 +330,62 @@ async fn run_websocket_connection(
|
||||
return;
|
||||
}
|
||||
|
||||
let (mut websocket_writer, mut websocket_reader) = websocket_stream.split();
|
||||
let (websocket_writer, websocket_reader) = websocket_stream.split();
|
||||
let (writer_control_tx, writer_control_rx) =
|
||||
mpsc::channel::<WebSocketMessage>(CHANNEL_CAPACITY);
|
||||
let mut outbound_task = tokio::spawn(run_websocket_outbound_loop(
|
||||
websocket_writer,
|
||||
writer_rx,
|
||||
writer_control_rx,
|
||||
disconnect_token.clone(),
|
||||
));
|
||||
let mut inbound_task = tokio::spawn(run_websocket_inbound_loop(
|
||||
websocket_reader,
|
||||
transport_event_tx.clone(),
|
||||
writer_tx_for_reader,
|
||||
writer_control_tx,
|
||||
connection_id,
|
||||
disconnect_token.clone(),
|
||||
));
|
||||
|
||||
tokio::select! {
|
||||
_ = &mut outbound_task => {
|
||||
disconnect_token.cancel();
|
||||
inbound_task.abort();
|
||||
}
|
||||
_ = &mut inbound_task => {
|
||||
disconnect_token.cancel();
|
||||
outbound_task.abort();
|
||||
}
|
||||
}
|
||||
|
||||
let _ = transport_event_tx
|
||||
.send(TransportEvent::ConnectionClosed { connection_id })
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn run_websocket_outbound_loop(
|
||||
mut websocket_writer: futures::stream::SplitSink<
|
||||
tokio_tungstenite::WebSocketStream<TcpStream>,
|
||||
WebSocketMessage,
|
||||
>,
|
||||
mut writer_rx: mpsc::Receiver<OutgoingMessage>,
|
||||
mut writer_control_rx: mpsc::Receiver<WebSocketMessage>,
|
||||
disconnect_token: CancellationToken,
|
||||
) {
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = disconnect_token.cancelled() => {
|
||||
break;
|
||||
}
|
||||
message = writer_control_rx.recv() => {
|
||||
let Some(message) = message else {
|
||||
break;
|
||||
};
|
||||
if websocket_writer.send(message).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
outgoing_message = writer_rx.recv() => {
|
||||
let Some(outgoing_message) = outgoing_message else {
|
||||
break;
|
||||
@@ -326,6 +397,25 @@ async fn run_websocket_connection(
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_websocket_inbound_loop(
|
||||
mut websocket_reader: futures::stream::SplitStream<
|
||||
tokio_tungstenite::WebSocketStream<TcpStream>,
|
||||
>,
|
||||
transport_event_tx: mpsc::Sender<TransportEvent>,
|
||||
writer_tx_for_reader: mpsc::Sender<OutgoingMessage>,
|
||||
writer_control_tx: mpsc::Sender<WebSocketMessage>,
|
||||
connection_id: ConnectionId,
|
||||
disconnect_token: CancellationToken,
|
||||
) {
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = disconnect_token.cancelled() => {
|
||||
break;
|
||||
}
|
||||
incoming_message = websocket_reader.next() => {
|
||||
match incoming_message {
|
||||
Some(Ok(WebSocketMessage::Text(text))) => {
|
||||
@@ -341,8 +431,13 @@ async fn run_websocket_connection(
|
||||
}
|
||||
}
|
||||
Some(Ok(WebSocketMessage::Ping(payload))) => {
|
||||
if websocket_writer.send(WebSocketMessage::Pong(payload)).await.is_err() {
|
||||
break;
|
||||
match writer_control_tx.try_send(WebSocketMessage::Pong(payload)) {
|
||||
Ok(()) => {}
|
||||
Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => break,
|
||||
Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
|
||||
warn!("websocket control queue full while replying to ping; closing connection");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Ok(WebSocketMessage::Pong(_))) => {}
|
||||
@@ -359,10 +454,6 @@ async fn run_websocket_connection(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = transport_event_tx
|
||||
.send(TransportEvent::ConnectionClosed { connection_id })
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn forward_incoming_message(
|
||||
@@ -461,30 +552,61 @@ fn should_skip_notification_for_connection(
|
||||
}
|
||||
}
|
||||
|
||||
fn disconnect_connection(
|
||||
connections: &mut HashMap<ConnectionId, OutboundConnectionState>,
|
||||
connection_id: ConnectionId,
|
||||
) -> bool {
|
||||
if let Some(connection_state) = connections.remove(&connection_id) {
|
||||
connection_state.request_disconnect();
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
async fn send_message_to_connection(
|
||||
connections: &mut HashMap<ConnectionId, OutboundConnectionState>,
|
||||
connection_id: ConnectionId,
|
||||
message: OutgoingMessage,
|
||||
) -> bool {
|
||||
let Some(connection_state) = connections.get(&connection_id) else {
|
||||
warn!("dropping message for disconnected connection: {connection_id:?}");
|
||||
return false;
|
||||
};
|
||||
if should_skip_notification_for_connection(connection_state, &message) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let writer = connection_state.writer.clone();
|
||||
if connection_state.can_disconnect() {
|
||||
match writer.try_send(message) {
|
||||
Ok(()) => false,
|
||||
Err(mpsc::error::TrySendError::Full(_)) => {
|
||||
warn!(
|
||||
"disconnecting slow connection after outbound queue filled: {connection_id:?}"
|
||||
);
|
||||
disconnect_connection(connections, connection_id)
|
||||
}
|
||||
Err(mpsc::error::TrySendError::Closed(_)) => {
|
||||
disconnect_connection(connections, connection_id)
|
||||
}
|
||||
}
|
||||
} else if writer.send(message).await.is_err() {
|
||||
disconnect_connection(connections, connection_id)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn route_outgoing_envelope(
|
||||
connections: &mut HashMap<ConnectionId, OutboundConnectionState>,
|
||||
envelope: OutgoingEnvelope,
|
||||
) -> Vec<ConnectionId> {
|
||||
let mut disconnected = Vec::new();
|
||||
) {
|
||||
match envelope {
|
||||
OutgoingEnvelope::ToConnection {
|
||||
connection_id,
|
||||
message,
|
||||
} => {
|
||||
let Some(connection_state) = connections.get(&connection_id) else {
|
||||
warn!(
|
||||
"dropping message for disconnected connection: {:?}",
|
||||
connection_id
|
||||
);
|
||||
return disconnected;
|
||||
};
|
||||
if should_skip_notification_for_connection(connection_state, &message) {
|
||||
return disconnected;
|
||||
}
|
||||
if connection_state.writer.send(message).await.is_err() {
|
||||
connections.remove(&connection_id);
|
||||
disconnected.push(connection_id);
|
||||
}
|
||||
let _ = send_message_to_connection(connections, connection_id, message).await;
|
||||
}
|
||||
OutgoingEnvelope::Broadcast { message } => {
|
||||
let target_connections: Vec<ConnectionId> = connections
|
||||
@@ -501,17 +623,11 @@ pub(crate) async fn route_outgoing_envelope(
|
||||
.collect();
|
||||
|
||||
for connection_id in target_connections {
|
||||
let Some(connection_state) = connections.get(&connection_id) else {
|
||||
continue;
|
||||
};
|
||||
if connection_state.writer.send(message.clone()).await.is_err() {
|
||||
connections.remove(&connection_id);
|
||||
disconnected.push(connection_id);
|
||||
}
|
||||
let _ =
|
||||
send_message_to_connection(connections, connection_id, message.clone()).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
disconnected
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -520,6 +636,8 @@ mod tests {
|
||||
use crate::error_code::OVERLOADED_ERROR_CODE;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use tokio::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
|
||||
#[test]
|
||||
fn app_server_transport_parses_stdio_listen_url() {
|
||||
@@ -754,10 +872,15 @@ mod tests {
|
||||
let mut connections = HashMap::new();
|
||||
connections.insert(
|
||||
connection_id,
|
||||
OutboundConnectionState::new(writer_tx, initialized, opted_out_notification_methods),
|
||||
OutboundConnectionState::new(
|
||||
writer_tx,
|
||||
initialized,
|
||||
opted_out_notification_methods,
|
||||
None,
|
||||
),
|
||||
);
|
||||
|
||||
let disconnected = route_outgoing_envelope(
|
||||
route_outgoing_envelope(
|
||||
&mut connections,
|
||||
OutgoingEnvelope::ToConnection {
|
||||
connection_id,
|
||||
@@ -771,10 +894,161 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(disconnected, Vec::<ConnectionId>::new());
|
||||
assert!(
|
||||
writer_rx.try_recv().is_err(),
|
||||
"opted-out notification should be dropped"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn broadcast_does_not_block_on_slow_connection() {
|
||||
let fast_connection_id = ConnectionId(1);
|
||||
let slow_connection_id = ConnectionId(2);
|
||||
|
||||
let (fast_writer_tx, mut fast_writer_rx) = mpsc::channel(1);
|
||||
let (slow_writer_tx, mut slow_writer_rx) = mpsc::channel(1);
|
||||
let fast_disconnect_token = CancellationToken::new();
|
||||
let slow_disconnect_token = CancellationToken::new();
|
||||
|
||||
let mut connections = HashMap::new();
|
||||
connections.insert(
|
||||
fast_connection_id,
|
||||
OutboundConnectionState::new(
|
||||
fast_writer_tx,
|
||||
Arc::new(AtomicBool::new(true)),
|
||||
Arc::new(RwLock::new(HashSet::new())),
|
||||
Some(fast_disconnect_token.clone()),
|
||||
),
|
||||
);
|
||||
connections.insert(
|
||||
slow_connection_id,
|
||||
OutboundConnectionState::new(
|
||||
slow_writer_tx.clone(),
|
||||
Arc::new(AtomicBool::new(true)),
|
||||
Arc::new(RwLock::new(HashSet::new())),
|
||||
Some(slow_disconnect_token.clone()),
|
||||
),
|
||||
);
|
||||
|
||||
let queued_message =
|
||||
OutgoingMessage::Notification(crate::outgoing_message::OutgoingNotification {
|
||||
method: "codex/event/already-buffered".to_string(),
|
||||
params: None,
|
||||
});
|
||||
slow_writer_tx
|
||||
.try_send(queued_message)
|
||||
.expect("channel should have room");
|
||||
|
||||
let broadcast_message =
|
||||
OutgoingMessage::Notification(crate::outgoing_message::OutgoingNotification {
|
||||
method: "codex/event/test".to_string(),
|
||||
params: None,
|
||||
});
|
||||
timeout(
|
||||
Duration::from_millis(100),
|
||||
route_outgoing_envelope(
|
||||
&mut connections,
|
||||
OutgoingEnvelope::Broadcast {
|
||||
message: broadcast_message,
|
||||
},
|
||||
),
|
||||
)
|
||||
.await
|
||||
.expect("broadcast should not block on a full writer");
|
||||
assert!(!connections.contains_key(&slow_connection_id));
|
||||
assert!(slow_disconnect_token.is_cancelled());
|
||||
assert!(!fast_disconnect_token.is_cancelled());
|
||||
let fast_message = fast_writer_rx
|
||||
.try_recv()
|
||||
.expect("fast connection should receive broadcast");
|
||||
assert!(matches!(
|
||||
fast_message,
|
||||
OutgoingMessage::Notification(crate::outgoing_message::OutgoingNotification {
|
||||
method,
|
||||
params: None,
|
||||
}) if method == "codex/event/test"
|
||||
));
|
||||
|
||||
let slow_message = slow_writer_rx
|
||||
.try_recv()
|
||||
.expect("slow connection should retain its original buffered message");
|
||||
assert!(matches!(
|
||||
slow_message,
|
||||
OutgoingMessage::Notification(crate::outgoing_message::OutgoingNotification {
|
||||
method,
|
||||
params: None,
|
||||
}) if method == "codex/event/already-buffered"
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn to_connection_stdio_waits_instead_of_disconnecting_when_writer_queue_is_full() {
|
||||
let connection_id = ConnectionId(3);
|
||||
let (writer_tx, mut writer_rx) = mpsc::channel(1);
|
||||
writer_tx
|
||||
.send(OutgoingMessage::Notification(
|
||||
crate::outgoing_message::OutgoingNotification {
|
||||
method: "queued".to_string(),
|
||||
params: None,
|
||||
},
|
||||
))
|
||||
.await
|
||||
.expect("channel should accept the first queued message");
|
||||
|
||||
let mut connections = HashMap::new();
|
||||
connections.insert(
|
||||
connection_id,
|
||||
OutboundConnectionState::new(
|
||||
writer_tx,
|
||||
Arc::new(AtomicBool::new(true)),
|
||||
Arc::new(RwLock::new(HashSet::new())),
|
||||
None,
|
||||
),
|
||||
);
|
||||
|
||||
let route_task = tokio::spawn(async move {
|
||||
route_outgoing_envelope(
|
||||
&mut connections,
|
||||
OutgoingEnvelope::ToConnection {
|
||||
connection_id,
|
||||
message: OutgoingMessage::Notification(
|
||||
crate::outgoing_message::OutgoingNotification {
|
||||
method: "second".to_string(),
|
||||
params: None,
|
||||
},
|
||||
),
|
||||
},
|
||||
)
|
||||
.await
|
||||
});
|
||||
|
||||
let first = timeout(Duration::from_millis(100), writer_rx.recv())
|
||||
.await
|
||||
.expect("first queued message should be readable")
|
||||
.expect("first queued message should exist");
|
||||
let second = timeout(Duration::from_millis(100), writer_rx.recv())
|
||||
.await
|
||||
.expect("second message should eventually be delivered")
|
||||
.expect("second message should exist");
|
||||
|
||||
timeout(Duration::from_millis(100), route_task)
|
||||
.await
|
||||
.expect("routing should finish after writer drains")
|
||||
.expect("routing task should succeed");
|
||||
|
||||
assert!(matches!(
|
||||
first,
|
||||
OutgoingMessage::Notification(crate::outgoing_message::OutgoingNotification {
|
||||
method,
|
||||
params: None,
|
||||
}) if method == "queued"
|
||||
));
|
||||
assert!(matches!(
|
||||
second,
|
||||
OutgoingMessage::Notification(crate::outgoing_message::OutgoingNotification {
|
||||
method,
|
||||
params: None,
|
||||
}) if method == "second"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ use codex_app_server_protocol::ThreadResumeParams;
|
||||
use codex_app_server_protocol::ThreadRollbackParams;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadUnarchiveParams;
|
||||
use codex_app_server_protocol::TurnCompletedNotification;
|
||||
use codex_app_server_protocol::TurnInterruptParams;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnSteerParams;
|
||||
@@ -572,6 +573,63 @@ impl McpProcess {
|
||||
self.send_request("turn/interrupt", params).await
|
||||
}
|
||||
|
||||
/// Deterministically clean up an intentionally in-flight turn.
|
||||
///
|
||||
/// Some tests assert behavior while a turn is still running. Returning from those tests
|
||||
/// without an explicit interrupt + `codex/event/turn_aborted` wait can leave in-flight work
|
||||
/// racing teardown and intermittently show up as `LEAK` in nextest.
|
||||
///
|
||||
/// In rare races, the turn can also fail or complete on its own after we send
|
||||
/// `turn/interrupt` but before the server emits the interrupt response. The helper treats a
|
||||
/// buffered matching `turn/completed` notification as sufficient terminal cleanup in that
|
||||
/// case so teardown does not flap on timing.
|
||||
pub async fn interrupt_turn_and_wait_for_aborted(
|
||||
&mut self,
|
||||
thread_id: String,
|
||||
turn_id: String,
|
||||
read_timeout: std::time::Duration,
|
||||
) -> anyhow::Result<()> {
|
||||
let interrupt_request_id = self
|
||||
.send_turn_interrupt_request(TurnInterruptParams {
|
||||
thread_id: thread_id.clone(),
|
||||
turn_id: turn_id.clone(),
|
||||
})
|
||||
.await?;
|
||||
match tokio::time::timeout(
|
||||
read_timeout,
|
||||
self.read_stream_until_response_message(RequestId::Integer(interrupt_request_id)),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
result.with_context(|| "failed while waiting for turn interrupt response")?;
|
||||
}
|
||||
Err(err) => {
|
||||
if self.pending_turn_completed_notification(&thread_id, &turn_id) {
|
||||
return Ok(());
|
||||
}
|
||||
return Err(err).with_context(|| "timed out waiting for turn interrupt response");
|
||||
}
|
||||
}
|
||||
match tokio::time::timeout(
|
||||
read_timeout,
|
||||
self.read_stream_until_notification_message("codex/event/turn_aborted"),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
result.with_context(|| "failed while waiting for turn aborted notification")?;
|
||||
}
|
||||
Err(err) => {
|
||||
if self.pending_turn_completed_notification(&thread_id, &turn_id) {
|
||||
return Ok(());
|
||||
}
|
||||
return Err(err).with_context(|| "timed out waiting for turn aborted notification");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a `turn/steer` JSON-RPC request (v2).
|
||||
pub async fn send_turn_steer_request(
|
||||
&mut self,
|
||||
@@ -940,6 +998,25 @@ impl McpProcess {
|
||||
None
|
||||
}
|
||||
|
||||
fn pending_turn_completed_notification(&self, thread_id: &str, turn_id: &str) -> bool {
|
||||
self.pending_messages.iter().any(|message| {
|
||||
let JSONRPCMessage::Notification(notification) = message else {
|
||||
return false;
|
||||
};
|
||||
if notification.method != "turn/completed" {
|
||||
return false;
|
||||
}
|
||||
let Some(params) = notification.params.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
let Ok(payload) = serde_json::from_value::<TurnCompletedNotification>(params.clone())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
payload.thread_id == thread_id && payload.turn.id == turn_id
|
||||
})
|
||||
}
|
||||
|
||||
fn message_request_id(message: &JSONRPCMessage) -> Option<&RequestId> {
|
||||
match message {
|
||||
JSONRPCMessage::Request(request) => Some(&request.id),
|
||||
|
||||
@@ -79,6 +79,8 @@ pub fn create_fake_rollout_with_source(
|
||||
originator: "codex".to_string(),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
source,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: model_provider.map(str::to_string),
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
@@ -158,6 +160,8 @@ pub fn create_fake_rollout_with_text_elements(
|
||||
originator: "codex".to_string(),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
source: SessionSource::Cli,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: model_provider.map(str::to_string),
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
|
||||
@@ -15,6 +15,7 @@ use axum::Router;
|
||||
use axum::extract::State;
|
||||
use axum::http::HeaderMap;
|
||||
use axum::http::StatusCode;
|
||||
use axum::http::Uri;
|
||||
use axum::http::header::AUTHORIZATION;
|
||||
use axum::routing::get;
|
||||
use codex_app_server_protocol::AppBranding;
|
||||
@@ -491,39 +492,60 @@ async fn list_apps_returns_connectors_with_accessible_flags() -> Result<()> {
|
||||
})
|
||||
.await?;
|
||||
|
||||
let expected_directory_first = vec![
|
||||
AppInfo {
|
||||
id: "alpha".to_string(),
|
||||
name: "Alpha".to_string(),
|
||||
description: Some("Alpha connector".to_string()),
|
||||
logo_url: Some("https://example.com/alpha.png".to_string()),
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
},
|
||||
AppInfo {
|
||||
id: "beta".to_string(),
|
||||
name: "beta".to_string(),
|
||||
description: None,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: Some("https://chatgpt.com/apps/beta/beta".to_string()),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
},
|
||||
];
|
||||
let expected_accessible_first = vec![AppInfo {
|
||||
id: "beta".to_string(),
|
||||
name: "Beta App".to_string(),
|
||||
description: None,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: Some("https://chatgpt.com/apps/beta-app/beta".to_string()),
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
}];
|
||||
|
||||
let first_update = read_app_list_updated_notification(&mut mcp).await?;
|
||||
assert_eq!(
|
||||
first_update.data,
|
||||
vec![
|
||||
AppInfo {
|
||||
id: "alpha".to_string(),
|
||||
name: "Alpha".to_string(),
|
||||
description: Some("Alpha connector".to_string()),
|
||||
logo_url: Some("https://example.com/alpha.png".to_string()),
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
},
|
||||
AppInfo {
|
||||
id: "beta".to_string(),
|
||||
name: "beta".to_string(),
|
||||
description: None,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: Some("https://chatgpt.com/apps/beta/beta".to_string()),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
},
|
||||
]
|
||||
// app/list emits an update after whichever async load finishes first. Even with
|
||||
// a tools delay in this test, the accessible-tools path can return first if the
|
||||
// process-global Codex Apps tools cache is warm from another test.
|
||||
assert!(
|
||||
first_update.data == expected_directory_first
|
||||
|| first_update.data == expected_accessible_first,
|
||||
"unexpected first app/list update: {:#?}",
|
||||
first_update.data
|
||||
);
|
||||
|
||||
let expected = vec![
|
||||
@@ -1194,6 +1216,7 @@ async fn start_apps_server_with_delays_and_control(
|
||||
async fn list_directory_connectors(
|
||||
State(state): State<Arc<AppsServerState>>,
|
||||
headers: HeaderMap,
|
||||
uri: Uri,
|
||||
) -> Result<impl axum::response::IntoResponse, StatusCode> {
|
||||
if state.directory_delay > Duration::ZERO {
|
||||
tokio::time::sleep(state.directory_delay).await;
|
||||
@@ -1207,16 +1230,21 @@ async fn list_directory_connectors(
|
||||
.get("chatgpt-account-id")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.is_some_and(|value| value == state.expected_account_id);
|
||||
let external_logos_ok = uri
|
||||
.query()
|
||||
.is_some_and(|query| query.split('&').any(|pair| pair == "external_logos=true"));
|
||||
|
||||
if bearer_ok && account_ok {
|
||||
if !bearer_ok || !account_ok {
|
||||
Err(StatusCode::UNAUTHORIZED)
|
||||
} else if !external_logos_ok {
|
||||
Err(StatusCode::BAD_REQUEST)
|
||||
} else {
|
||||
let response = state
|
||||
.response
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone();
|
||||
Ok(Json(response))
|
||||
} else {
|
||||
Err(StatusCode::UNAUTHORIZED)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use app_test_support::test_path_buf_with_windows;
|
||||
use app_test_support::test_tmp_path_buf;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::AppConfig;
|
||||
use codex_app_server_protocol::AppDisabledReason;
|
||||
use codex_app_server_protocol::AppToolApproval;
|
||||
use codex_app_server_protocol::AppsConfig;
|
||||
use codex_app_server_protocol::AskForApproval;
|
||||
use codex_app_server_protocol::ConfigBatchWriteParams;
|
||||
@@ -156,7 +156,8 @@ async fn config_read_includes_apps() -> Result<()> {
|
||||
r#"
|
||||
[apps.app1]
|
||||
enabled = false
|
||||
disabled_reason = "user"
|
||||
destructive_enabled = false
|
||||
default_tools_approval_mode = "prompt"
|
||||
"#,
|
||||
)?;
|
||||
let codex_home_path = codex_home.path().canonicalize()?;
|
||||
@@ -185,11 +186,16 @@ disabled_reason = "user"
|
||||
assert_eq!(
|
||||
config.apps,
|
||||
Some(AppsConfig {
|
||||
default: None,
|
||||
apps: std::collections::HashMap::from([(
|
||||
"app1".to_string(),
|
||||
AppConfig {
|
||||
enabled: false,
|
||||
disabled_reason: Some(AppDisabledReason::User),
|
||||
destructive_enabled: Some(false),
|
||||
open_world_enabled: None,
|
||||
default_tools_approval_mode: Some(AppToolApproval::Prompt),
|
||||
default_tools_enabled: None,
|
||||
tools: None,
|
||||
},
|
||||
)]),
|
||||
})
|
||||
@@ -202,7 +208,16 @@ disabled_reason = "user"
|
||||
);
|
||||
assert_eq!(
|
||||
origins
|
||||
.get("apps.app1.disabled_reason")
|
||||
.get("apps.app1.destructive_enabled")
|
||||
.expect("origin")
|
||||
.name,
|
||||
ConfigLayerSource::User {
|
||||
file: user_file.clone(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
origins
|
||||
.get("apps.app1.default_tools_approval_mode")
|
||||
.expect("origin")
|
||||
.name,
|
||||
ConfigLayerSource::User {
|
||||
|
||||
@@ -582,6 +582,8 @@ async fn thread_list_filters_by_source_kind_subagent_thread_spawn() -> Result<()
|
||||
CoreSessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
}),
|
||||
)?;
|
||||
|
||||
@@ -643,6 +645,8 @@ async fn thread_list_filters_by_subagent_variant() -> Result<()> {
|
||||
CoreSessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
}),
|
||||
)?;
|
||||
let other_id = create_fake_rollout_with_source(
|
||||
|
||||
@@ -15,8 +15,6 @@ use codex_app_server_protocol::ThreadResumeResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::ThreadStatus;
|
||||
use codex_app_server_protocol::TurnInterruptParams;
|
||||
use codex_app_server_protocol::TurnInterruptResponse;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
@@ -317,9 +315,14 @@ async fn thread_resume_rejects_history_when_thread_is_running() -> Result<()> {
|
||||
responses::ev_assistant_message("msg-1", "Done"),
|
||||
responses::ev_completed("resp-1"),
|
||||
]);
|
||||
let second_body = responses::sse(vec![responses::ev_response_created("resp-2")]);
|
||||
let second_response = responses::sse_response(responses::sse(vec![
|
||||
responses::ev_response_created("resp-2"),
|
||||
responses::ev_assistant_message("msg-2", "Done"),
|
||||
responses::ev_completed("resp-2"),
|
||||
]))
|
||||
.set_delay(std::time::Duration::from_millis(500));
|
||||
let _first_response_mock = responses::mount_sse_once(&server, first_body).await;
|
||||
let _second_response_mock = responses::mount_sse_once(&server, second_body).await;
|
||||
let _second_response_mock = responses::mount_response_once(&server, second_response).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
|
||||
@@ -413,28 +416,9 @@ async fn thread_resume_rejects_history_when_thread_is_running() -> Result<()> {
|
||||
resume_err.error.message
|
||||
);
|
||||
|
||||
// This test intentionally keeps a turn running to exercise the resume error path.
|
||||
// Keep this explicit interrupt + turn_aborted wait so teardown does not leave
|
||||
// in-flight work behind (which can show up as LEAK in nextest).
|
||||
let interrupt_id = primary
|
||||
.send_turn_interrupt_request(TurnInterruptParams {
|
||||
thread_id,
|
||||
turn_id: running_turn.id,
|
||||
})
|
||||
primary
|
||||
.interrupt_turn_and_wait_for_aborted(thread_id, running_turn.id, DEFAULT_READ_TIMEOUT)
|
||||
.await?;
|
||||
let interrupt_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
primary.read_stream_until_response_message(RequestId::Integer(interrupt_id)),
|
||||
)
|
||||
.await??;
|
||||
let _turn_interrupt_response: TurnInterruptResponse =
|
||||
to_response::<TurnInterruptResponse>(interrupt_resp)?;
|
||||
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
primary.read_stream_until_notification_message("codex/event/turn_aborted"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -447,9 +431,14 @@ async fn thread_resume_rejects_mismatched_path_when_thread_is_running() -> Resul
|
||||
responses::ev_assistant_message("msg-1", "Done"),
|
||||
responses::ev_completed("resp-1"),
|
||||
]);
|
||||
let second_body = responses::sse(vec![responses::ev_response_created("resp-2")]);
|
||||
let second_response = responses::sse_response(responses::sse(vec![
|
||||
responses::ev_response_created("resp-2"),
|
||||
responses::ev_assistant_message("msg-2", "Done"),
|
||||
responses::ev_completed("resp-2"),
|
||||
]))
|
||||
.set_delay(std::time::Duration::from_millis(500));
|
||||
let _first_response_mock = responses::mount_sse_once(&server, first_body).await;
|
||||
let _second_response_mock = responses::mount_sse_once(&server, second_body).await;
|
||||
let _second_response_mock = responses::mount_response_once(&server, second_response).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
|
||||
@@ -533,28 +522,9 @@ async fn thread_resume_rejects_mismatched_path_when_thread_is_running() -> Resul
|
||||
resume_err.error.message
|
||||
);
|
||||
|
||||
// This test intentionally keeps a turn running to exercise the resume error path.
|
||||
// Keep this explicit interrupt + turn_aborted wait so teardown does not leave
|
||||
// in-flight work behind (which can show up as LEAK in nextest).
|
||||
let interrupt_id = primary
|
||||
.send_turn_interrupt_request(TurnInterruptParams {
|
||||
thread_id,
|
||||
turn_id: running_turn.id,
|
||||
})
|
||||
primary
|
||||
.interrupt_turn_and_wait_for_aborted(thread_id, running_turn.id, DEFAULT_READ_TIMEOUT)
|
||||
.await?;
|
||||
let interrupt_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
primary.read_stream_until_response_message(RequestId::Integer(interrupt_id)),
|
||||
)
|
||||
.await??;
|
||||
let _turn_interrupt_response: TurnInterruptResponse =
|
||||
to_response::<TurnInterruptResponse>(interrupt_resp)?;
|
||||
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
primary.read_stream_until_notification_message("codex/event/turn_aborted"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -747,7 +747,12 @@ async fn turn_start_accepts_local_image_input() -> Result<()> {
|
||||
let TurnStartResponse { turn } = to_response::<TurnStartResponse>(turn_resp)?;
|
||||
assert!(!turn.id.is_empty());
|
||||
|
||||
// This test only validates that turn/start responds and returns a turn.
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnCompletedNotification;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use codex_core::features::FEATURES;
|
||||
@@ -100,7 +101,7 @@ async fn turn_start_shell_zsh_fork_executes_command_v2() -> Result<()> {
|
||||
|
||||
let turn_id = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id,
|
||||
thread_id: thread.id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "run echo hi".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -114,11 +115,12 @@ async fn turn_start_shell_zsh_fork_executes_command_v2() -> Result<()> {
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
timeout(
|
||||
let turn_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_id)),
|
||||
)
|
||||
.await??;
|
||||
let TurnStartResponse { turn } = to_response::<TurnStartResponse>(turn_resp)?;
|
||||
|
||||
let started_command_execution = timeout(DEFAULT_READ_TIMEOUT, async {
|
||||
loop {
|
||||
@@ -149,6 +151,9 @@ async fn turn_start_shell_zsh_fork_executes_command_v2() -> Result<()> {
|
||||
assert!(command.contains(" -lc 'echo hi'"));
|
||||
assert_eq!(cwd, workspace);
|
||||
|
||||
mcp.interrupt_turn_and_wait_for_aborted(thread.id, turn.id, DEFAULT_READ_TIMEOUT)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -504,11 +509,12 @@ async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2()
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
timeout(
|
||||
let turn_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_id)),
|
||||
)
|
||||
.await??;
|
||||
let TurnStartResponse { turn } = to_response::<TurnStartResponse>(turn_resp)?;
|
||||
|
||||
let mut approval_ids = Vec::new();
|
||||
for decision in [
|
||||
@@ -577,6 +583,9 @@ async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2()
|
||||
assert_eq!(approval_ids.len(), 2);
|
||||
assert_ne!(approval_ids[0], approval_ids[1]);
|
||||
|
||||
mcp.interrupt_turn_and_wait_for_aborted(thread.id, turn.id, DEFAULT_READ_TIMEOUT)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ async fn turn_steer_returns_active_turn_id() -> Result<()> {
|
||||
|
||||
let steer_req = mcp
|
||||
.send_turn_steer_request(TurnSteerParams {
|
||||
thread_id: thread.id,
|
||||
thread_id: thread.id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "steer".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -152,6 +152,9 @@ async fn turn_steer_returns_active_turn_id() -> Result<()> {
|
||||
let steer: TurnSteerResponse = to_response::<TurnSteerResponse>(steer_resp)?;
|
||||
assert_eq!(steer.turn_id, turn.id);
|
||||
|
||||
mcp.interrupt_turn_and_wait_for_aborted(thread.id, steer.turn_id, DEFAULT_READ_TIMEOUT)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@ use std::sync::LazyLock;
|
||||
use std::sync::Mutex as StdMutex;
|
||||
|
||||
use codex_core::config::Config;
|
||||
use codex_core::default_client::is_first_party_chat_originator;
|
||||
use codex_core::default_client::originator;
|
||||
use codex_core::features::Feature;
|
||||
use codex_core::token_data::TokenData;
|
||||
use serde::Deserialize;
|
||||
@@ -22,8 +20,10 @@ use codex_core::connectors::AppMetadata;
|
||||
use codex_core::connectors::CONNECTORS_CACHE_TTL;
|
||||
pub use codex_core::connectors::connector_display_label;
|
||||
use codex_core::connectors::connector_install_url;
|
||||
use codex_core::connectors::filter_disallowed_connectors;
|
||||
pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools;
|
||||
pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_options;
|
||||
pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_options_and_status;
|
||||
pub use codex_core::connectors::list_cached_accessible_connectors_from_mcp_tools;
|
||||
use codex_core::connectors::merge_connectors;
|
||||
pub use codex_core::connectors::with_app_enabled_state;
|
||||
@@ -106,7 +106,7 @@ pub async fn list_cached_all_connectors(config: &Config) -> Option<Vec<AppInfo>>
|
||||
}
|
||||
let token_data = get_chatgpt_token_data()?;
|
||||
let cache_key = all_connectors_cache_key(config, &token_data);
|
||||
read_cached_all_connectors(&cache_key)
|
||||
read_cached_all_connectors(&cache_key).map(filter_disallowed_connectors)
|
||||
}
|
||||
|
||||
pub async fn list_all_connectors_with_options(
|
||||
@@ -123,7 +123,7 @@ pub async fn list_all_connectors_with_options(
|
||||
get_chatgpt_token_data().ok_or_else(|| anyhow::anyhow!("ChatGPT token not available"))?;
|
||||
let cache_key = all_connectors_cache_key(config, &token_data);
|
||||
if !force_refetch && let Some(cached_connectors) = read_cached_all_connectors(&cache_key) {
|
||||
return Ok(cached_connectors);
|
||||
return Ok(filter_disallowed_connectors(cached_connectors));
|
||||
}
|
||||
|
||||
let mut apps = list_directory_connectors(config).await?;
|
||||
@@ -149,6 +149,7 @@ pub async fn list_all_connectors_with_options(
|
||||
.cmp(&right.name)
|
||||
.then_with(|| left.id.cmp(&right.id))
|
||||
});
|
||||
let connectors = filter_disallowed_connectors(connectors);
|
||||
write_cached_all_connectors(cache_key, &connectors);
|
||||
Ok(connectors)
|
||||
}
|
||||
@@ -219,9 +220,11 @@ async fn list_directory_connectors(config: &Config) -> anyhow::Result<Vec<Direct
|
||||
let path = match next_token.as_deref() {
|
||||
Some(token) => {
|
||||
let encoded_token = urlencoding::encode(token);
|
||||
format!("/connectors/directory/list?tier=categorized&token={encoded_token}")
|
||||
format!(
|
||||
"/connectors/directory/list?tier=categorized&token={encoded_token}&external_logos=true"
|
||||
)
|
||||
}
|
||||
None => "/connectors/directory/list?tier=categorized".to_string(),
|
||||
None => "/connectors/directory/list?tier=categorized&external_logos=true".to_string(),
|
||||
};
|
||||
let response: DirectoryListResponse =
|
||||
chatgpt_get_request_with_timeout(config, path, Some(DIRECTORY_CONNECTORS_TIMEOUT))
|
||||
@@ -246,7 +249,7 @@ async fn list_directory_connectors(config: &Config) -> anyhow::Result<Vec<Direct
|
||||
async fn list_workspace_connectors(config: &Config) -> anyhow::Result<Vec<DirectoryApp>> {
|
||||
let response: anyhow::Result<DirectoryListResponse> = chatgpt_get_request_with_timeout(
|
||||
config,
|
||||
"/connectors/directory/list_workspace".to_string(),
|
||||
"/connectors/directory/list_workspace?external_logos=true".to_string(),
|
||||
Some(DIRECTORY_CONNECTORS_TIMEOUT),
|
||||
)
|
||||
.await;
|
||||
@@ -453,45 +456,6 @@ fn normalize_connector_value(value: Option<&str>) -> Option<String> {
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
const DISALLOWED_CONNECTOR_IDS: &[&str] = &[
|
||||
"asdk_app_6938a94a61d881918ef32cb999ff937c",
|
||||
"connector_2b0a9009c9c64bf9933a3dae3f2b1254",
|
||||
"connector_68de829bf7648191acd70a907364c67c",
|
||||
"connector_68e004f14af881919eb50893d3d9f523",
|
||||
"connector_69272cb413a081919685ec3c88d1744e",
|
||||
"connector_0f9c9d4592e54d0a9a12b3f44a1e2010",
|
||||
];
|
||||
const FIRST_PARTY_CHAT_DISALLOWED_CONNECTOR_IDS: &[&str] =
|
||||
&["connector_0f9c9d4592e54d0a9a12b3f44a1e2010"];
|
||||
const DISALLOWED_CONNECTOR_PREFIX: &str = "connector_openai_";
|
||||
|
||||
fn filter_disallowed_connectors(connectors: Vec<AppInfo>) -> Vec<AppInfo> {
|
||||
filter_disallowed_connectors_for_originator(connectors, originator().value.as_str())
|
||||
}
|
||||
|
||||
fn filter_disallowed_connectors_for_originator(
|
||||
connectors: Vec<AppInfo>,
|
||||
originator_value: &str,
|
||||
) -> Vec<AppInfo> {
|
||||
let disallowed_connector_ids = if is_first_party_chat_originator(originator_value) {
|
||||
FIRST_PARTY_CHAT_DISALLOWED_CONNECTOR_IDS
|
||||
} else {
|
||||
DISALLOWED_CONNECTOR_IDS
|
||||
};
|
||||
|
||||
connectors
|
||||
.into_iter()
|
||||
.filter(|connector| is_connector_allowed(connector, disallowed_connector_ids))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn is_connector_allowed(connector: &AppInfo, disallowed_connector_ids: &[&str]) -> bool {
|
||||
let connector_id = connector.id.as_str();
|
||||
!connector_id.starts_with(DISALLOWED_CONNECTOR_PREFIX)
|
||||
&& !disallowed_connector_ids.contains(&connector_id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -554,22 +518,6 @@ mod tests {
|
||||
assert_eq!(filtered, vec![app("delta")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_party_chat_originator_filters_target_and_openai_prefixed_connectors() {
|
||||
let filtered = filter_disallowed_connectors_for_originator(
|
||||
vec![
|
||||
app("connector_openai_foo"),
|
||||
app("asdk_app_6938a94a61d881918ef32cb999ff937c"),
|
||||
app("connector_0f9c9d4592e54d0a9a12b3f44a1e2010"),
|
||||
],
|
||||
"codex_atlas",
|
||||
);
|
||||
assert_eq!(
|
||||
filtered,
|
||||
vec![app("asdk_app_6938a94a61d881918ef32cb999ff937c"),]
|
||||
);
|
||||
}
|
||||
|
||||
fn merged_app(id: &str, is_accessible: bool) -> AppInfo {
|
||||
AppInfo {
|
||||
id: id.to_string(),
|
||||
|
||||
@@ -175,6 +175,19 @@ pub struct ResponsesWebsocketConnection {
|
||||
telemetry: Option<Arc<dyn WebsocketTelemetry>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ResponsesWebsocketConnection {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ResponsesWebsocketConnection")
|
||||
.field("stream", &"<ws-stream>")
|
||||
.field("idle_timeout", &self.idle_timeout)
|
||||
.field("server_reasoning_included", &self.server_reasoning_included)
|
||||
.field("models_etag", &self.models_etag)
|
||||
.field("server_model", &self.server_model)
|
||||
.field("telemetry", &self.telemetry.as_ref().map(|_| "<telemetry>"))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponsesWebsocketConnection {
|
||||
fn new(
|
||||
stream: WsStream,
|
||||
|
||||
@@ -135,6 +135,7 @@ pub struct NetworkRequirementsToml {
|
||||
pub allow_upstream_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_admin: Option<bool>,
|
||||
pub dangerously_allow_all_unix_sockets: Option<bool>,
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
pub denied_domains: Option<Vec<String>>,
|
||||
pub allow_unix_sockets: Option<Vec<String>>,
|
||||
@@ -150,6 +151,7 @@ pub struct NetworkConstraints {
|
||||
pub allow_upstream_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_admin: Option<bool>,
|
||||
pub dangerously_allow_all_unix_sockets: Option<bool>,
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
pub denied_domains: Option<Vec<String>>,
|
||||
pub allow_unix_sockets: Option<Vec<String>>,
|
||||
@@ -165,6 +167,7 @@ impl From<NetworkRequirementsToml> for NetworkConstraints {
|
||||
allow_upstream_proxy,
|
||||
dangerously_allow_non_loopback_proxy,
|
||||
dangerously_allow_non_loopback_admin,
|
||||
dangerously_allow_all_unix_sockets,
|
||||
allowed_domains,
|
||||
denied_domains,
|
||||
allow_unix_sockets,
|
||||
@@ -177,6 +180,7 @@ impl From<NetworkRequirementsToml> for NetworkConstraints {
|
||||
allow_upstream_proxy,
|
||||
dangerously_allow_non_loopback_proxy,
|
||||
dangerously_allow_non_loopback_admin,
|
||||
dangerously_allow_all_unix_sockets,
|
||||
allowed_domains,
|
||||
denied_domains,
|
||||
allow_unix_sockets,
|
||||
@@ -1040,6 +1044,7 @@ mod tests {
|
||||
[experimental_network]
|
||||
enabled = true
|
||||
allow_upstream_proxy = false
|
||||
dangerously_allow_all_unix_sockets = true
|
||||
allowed_domains = ["api.example.com", "*.openai.com"]
|
||||
denied_domains = ["blocked.example.com"]
|
||||
allow_unix_sockets = ["/tmp/example.sock"]
|
||||
@@ -1058,6 +1063,10 @@ mod tests {
|
||||
assert_eq!(sourced_network.source, source);
|
||||
assert_eq!(sourced_network.value.enabled, Some(true));
|
||||
assert_eq!(sourced_network.value.allow_upstream_proxy, Some(false));
|
||||
assert_eq!(
|
||||
sourced_network.value.dangerously_allow_all_unix_sockets,
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
sourced_network.value.allowed_domains.as_ref(),
|
||||
Some(&vec![
|
||||
|
||||
@@ -48,7 +48,7 @@ codex-utils-absolute-path = { workspace = true }
|
||||
codex-utils-home-dir = { workspace = true }
|
||||
codex-utils-pty = { workspace = true }
|
||||
codex-utils-readiness = { workspace = true }
|
||||
codex-utils-sanitizer = { workspace = true }
|
||||
codex-secrets = { workspace = true }
|
||||
codex-utils-string = { workspace = true }
|
||||
codex-windows-sandbox = { package = "codex-windows-sandbox", path = "../windows-sandbox-rs" }
|
||||
dirs = { workspace = true }
|
||||
|
||||
@@ -85,34 +85,111 @@
|
||||
"additionalProperties": false,
|
||||
"description": "Config values for a single app/connector.",
|
||||
"properties": {
|
||||
"disabled_reason": {
|
||||
"default_tools_approval_mode": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AppDisabledReason"
|
||||
"$ref": "#/definitions/AppToolApproval"
|
||||
}
|
||||
],
|
||||
"description": "Reason this app was disabled."
|
||||
"description": "Approval mode for tools in this app unless a tool override exists."
|
||||
},
|
||||
"default_tools_enabled": {
|
||||
"description": "Whether tools are enabled by default for this app.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"destructive_enabled": {
|
||||
"description": "Whether tools with `destructive_hint = true` are allowed for this app.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"enabled": {
|
||||
"default": true,
|
||||
"description": "When `false`, Codex does not surface this app.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"open_world_enabled": {
|
||||
"description": "Whether tools with `open_world_hint = true` are allowed for this app.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"tools": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AppToolsConfig"
|
||||
}
|
||||
],
|
||||
"description": "Per-tool settings for this app."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AppDisabledReason": {
|
||||
"AppToolApproval": {
|
||||
"enum": [
|
||||
"unknown",
|
||||
"user"
|
||||
"auto",
|
||||
"prompt",
|
||||
"approve"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AppToolConfig": {
|
||||
"additionalProperties": false,
|
||||
"description": "Per-tool settings for a single app tool.",
|
||||
"properties": {
|
||||
"approval_mode": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AppToolApproval"
|
||||
}
|
||||
],
|
||||
"description": "Approval mode for this tool."
|
||||
},
|
||||
"enabled": {
|
||||
"description": "Whether this tool is enabled. `Some(true)` explicitly allows this tool.",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AppToolsConfig": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/AppToolConfig"
|
||||
},
|
||||
"description": "Tool settings for a single app.",
|
||||
"type": "object"
|
||||
},
|
||||
"AppsConfigToml": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/AppConfig"
|
||||
},
|
||||
"description": "App/connector settings loaded from `config.toml`.",
|
||||
"properties": {
|
||||
"_default": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AppsDefaultConfig"
|
||||
}
|
||||
],
|
||||
"description": "Default settings for all apps."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AppsDefaultConfig": {
|
||||
"additionalProperties": false,
|
||||
"description": "Default settings that apply to all apps.",
|
||||
"properties": {
|
||||
"destructive_enabled": {
|
||||
"description": "Whether tools with `destructive_hint = true` are allowed by default.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"enabled": {
|
||||
"default": true,
|
||||
"description": "When `false`, apps are disabled unless overridden by per-app settings.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"open_world_enabled": {
|
||||
"description": "Whether tools with `open_world_hint = true` are allowed by default.",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AskForApproval": {
|
||||
@@ -656,6 +733,9 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"dangerously_allow_all_unix_sockets": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"dangerously_allow_non_loopback_admin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -1389,6 +1469,10 @@
|
||||
],
|
||||
"description": "Agent-related settings (thread limits, etc.)."
|
||||
},
|
||||
"allow_login_shell": {
|
||||
"description": "Whether the model may request a login shell for shell-based tools. Default to `true`\n\nIf `true`, the model may request a login shell (`login = true`), and omitting `login` defaults to using a login shell. If `false`, the model can never use a login shell: `login = true` requests are rejected, and omitting `login` defaults to a non-login shell.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"analytics": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -1723,7 +1807,7 @@
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Optional path to a JSON file containing a complete model catalog. When set, this replaces the bundled catalog for this process."
|
||||
"description": "Optional path to a JSON model catalog (applied on startup only). Per-thread `config` overrides are accepted but do not reapply this (no-ops)."
|
||||
},
|
||||
"model_context_window": {
|
||||
"description": "Size of the context window for the model, in tokens.",
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
Ash
|
||||
Elm
|
||||
Yew
|
||||
Fir
|
||||
Oak
|
||||
Pine
|
||||
Spruce
|
||||
Cedar
|
||||
Birch
|
||||
Maple
|
||||
Beech
|
||||
Alder
|
||||
Willow
|
||||
Poplar
|
||||
Aspen
|
||||
Larch
|
||||
Juniper
|
||||
Cypress
|
||||
Hemlock
|
||||
Sequoia
|
||||
Baobab
|
||||
Acacia
|
||||
Banyan
|
||||
Teak
|
||||
Olive
|
||||
Cherry
|
||||
Apple
|
||||
Pear
|
||||
Plum
|
||||
Peach
|
||||
Mango
|
||||
Guava
|
||||
Fig
|
||||
Date
|
||||
Palm
|
||||
Holly
|
||||
Ivy
|
||||
Fern
|
||||
Moss
|
||||
Reed
|
||||
Cane
|
||||
Rush
|
||||
Sage
|
||||
Mint
|
||||
Thyme
|
||||
Basil
|
||||
Dill
|
||||
Fennel
|
||||
Parsley
|
||||
Laurel
|
||||
Myrtle
|
||||
Lotus
|
||||
Tulip
|
||||
Rose
|
||||
Daisy
|
||||
Lily
|
||||
Iris
|
||||
Poppy
|
||||
Aster
|
||||
Peony
|
||||
Camellia
|
||||
Azalea
|
||||
Begonia
|
||||
Petunia
|
||||
Verbena
|
||||
Marigold
|
||||
Daffodil
|
||||
Orchid
|
||||
Violet
|
||||
Clover
|
||||
Bamboo
|
||||
Eucalypt
|
||||
Oleander
|
||||
Yucca
|
||||
Agave
|
||||
Aloe
|
||||
Cactus
|
||||
Sedum
|
||||
Boxwood
|
||||
Hawthorn
|
||||
Redwood
|
||||
Ironwood
|
||||
Sandal
|
||||
Kapok
|
||||
Mahonia
|
||||
Spirea
|
||||
Wisteria
|
||||
@@ -1,2 +0,0 @@
|
||||
model = "gpt-5.1-codex-mini"
|
||||
model_reasoning_effort = "medium"
|
||||
|
||||
@@ -3,7 +3,9 @@ use crate::agent::guards::Guards;
|
||||
use crate::agent::status::is_final;
|
||||
use crate::error::CodexErr;
|
||||
use crate::error::Result as CodexResult;
|
||||
use crate::find_thread_path_by_id_str;
|
||||
use crate::session_prefix::format_subagent_notification_message;
|
||||
use crate::state_db;
|
||||
use crate::thread_manager::ThreadManagerState;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::Op;
|
||||
@@ -11,11 +13,20 @@ use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Weak;
|
||||
use tokio::sync::watch;
|
||||
|
||||
const AGENT_NAMES: &str = include_str!("agent_names.txt");
|
||||
|
||||
fn agent_nickname_list() -> Vec<&'static str> {
|
||||
AGENT_NAMES
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|name| !name.is_empty())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Control-plane handle for multi-agent operations.
|
||||
/// `AgentControl` is held by each session (via `SessionServices`). It provides capability to
|
||||
/// spawn new agents and the inter-agent communication layer.
|
||||
@@ -48,7 +59,24 @@ impl AgentControl {
|
||||
session_source: Option<SessionSource>,
|
||||
) -> CodexResult<ThreadId> {
|
||||
let state = self.upgrade()?;
|
||||
let reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?;
|
||||
let mut reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?;
|
||||
let session_source = match session_source {
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth,
|
||||
agent_role,
|
||||
..
|
||||
})) => {
|
||||
let agent_nickname = reservation.reserve_agent_nickname(&agent_nickname_list())?;
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth,
|
||||
agent_nickname: Some(agent_nickname),
|
||||
agent_role,
|
||||
}))
|
||||
}
|
||||
other => other,
|
||||
};
|
||||
let notification_source = session_source.clone();
|
||||
|
||||
// The same `AgentControl` is sent to spawn the thread.
|
||||
@@ -77,12 +105,51 @@ impl AgentControl {
|
||||
pub(crate) async fn resume_agent_from_rollout(
|
||||
&self,
|
||||
config: crate::config::Config,
|
||||
rollout_path: PathBuf,
|
||||
thread_id: ThreadId,
|
||||
session_source: SessionSource,
|
||||
) -> CodexResult<ThreadId> {
|
||||
let state = self.upgrade()?;
|
||||
let reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?;
|
||||
let mut reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?;
|
||||
let session_source = match session_source {
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth,
|
||||
..
|
||||
}) => {
|
||||
// Collab resume callers rebuild a placeholder ThreadSpawn source. Rehydrate the
|
||||
// stored nickname/role from sqlite when available; otherwise leave both unset.
|
||||
let (resumed_agent_nickname, resumed_agent_role) =
|
||||
if let Some(state_db_ctx) = state_db::get_state_db(&config, None).await {
|
||||
match state_db_ctx.get_thread(thread_id).await {
|
||||
Ok(Some(metadata)) => (metadata.agent_nickname, metadata.agent_role),
|
||||
Ok(None) | Err(_) => (None, None),
|
||||
}
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
let reserved_agent_nickname = resumed_agent_nickname
|
||||
.as_deref()
|
||||
.map(|agent_nickname| {
|
||||
reservation.reserve_agent_nickname_with_preference(
|
||||
&agent_nickname_list(),
|
||||
Some(agent_nickname),
|
||||
)
|
||||
})
|
||||
.transpose()?;
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth,
|
||||
agent_nickname: reserved_agent_nickname,
|
||||
agent_role: resumed_agent_role,
|
||||
})
|
||||
}
|
||||
other => other,
|
||||
};
|
||||
let notification_source = session_source.clone();
|
||||
let rollout_path =
|
||||
find_thread_path_by_id_str(config.codex_home.as_path(), &thread_id.to_string())
|
||||
.await?
|
||||
.ok_or_else(|| CodexErr::ThreadNotFound(thread_id))?;
|
||||
|
||||
let resumed_thread = state
|
||||
.resume_thread_from_rollout_with_source(
|
||||
@@ -151,6 +218,23 @@ impl AgentControl {
|
||||
thread.agent_status().await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_agent_nickname_and_role(
|
||||
&self,
|
||||
agent_id: ThreadId,
|
||||
) -> Option<(Option<String>, Option<String>)> {
|
||||
let Ok(state) = self.upgrade() else {
|
||||
return None;
|
||||
};
|
||||
let Ok(thread) = state.get_thread(agent_id).await else {
|
||||
return None;
|
||||
};
|
||||
let session_source = thread.config_snapshot().await.session_source;
|
||||
Some((
|
||||
session_source.get_nickname(),
|
||||
session_source.get_agent_role(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Subscribe to status updates for `agent_id`, yielding the latest value and changes.
|
||||
pub(crate) async fn subscribe_status(
|
||||
&self,
|
||||
@@ -234,6 +318,8 @@ mod tests {
|
||||
use crate::agent::agent_status_from_event;
|
||||
use crate::config::Config;
|
||||
use crate::config::ConfigBuilder;
|
||||
use crate::config_loader::LoaderOverrides;
|
||||
use crate::features::Feature;
|
||||
use crate::session_prefix::SUBAGENT_NOTIFICATION_OPEN_TAG;
|
||||
use assert_matches::assert_matches;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
@@ -261,6 +347,12 @@ mod tests {
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(home.path().to_path_buf())
|
||||
.cli_overrides(cli_overrides)
|
||||
.loader_overrides(LoaderOverrides {
|
||||
#[cfg(target_os = "macos")]
|
||||
managed_preferences_base64: Some(String::new()),
|
||||
macos_managed_config_requirements_base64: Some(String::new()),
|
||||
..LoaderOverrides::default()
|
||||
})
|
||||
.build()
|
||||
.await
|
||||
.expect("load default test config");
|
||||
@@ -441,11 +533,7 @@ mod tests {
|
||||
let control = AgentControl::default();
|
||||
let (_home, config) = test_config().await;
|
||||
let err = control
|
||||
.resume_agent_from_rollout(
|
||||
config,
|
||||
PathBuf::from("/tmp/missing-rollout.jsonl"),
|
||||
SessionSource::Exec,
|
||||
)
|
||||
.resume_agent_from_rollout(config, ThreadId::new(), SessionSource::Exec)
|
||||
.await
|
||||
.expect_err("resume_agent should fail without a manager");
|
||||
assert_eq!(
|
||||
@@ -717,12 +805,6 @@ mod tests {
|
||||
.spawn_agent(config.clone(), text_input("hello"), None)
|
||||
.await
|
||||
.expect("spawn_agent should succeed");
|
||||
let rollout_path = manager
|
||||
.get_thread(resumable_id)
|
||||
.await
|
||||
.expect("thread should exist")
|
||||
.rollout_path()
|
||||
.expect("rollout path should exist");
|
||||
let _ = control
|
||||
.shutdown_agent(resumable_id)
|
||||
.await
|
||||
@@ -734,7 +816,7 @@ mod tests {
|
||||
.expect("spawn_agent should succeed for active slot");
|
||||
|
||||
let err = control
|
||||
.resume_agent_from_rollout(config, rollout_path, SessionSource::Exec)
|
||||
.resume_agent_from_rollout(config, resumable_id, SessionSource::Exec)
|
||||
.await
|
||||
.expect_err("resume should respect max threads");
|
||||
let CodexErr::AgentLimitReached {
|
||||
@@ -766,9 +848,8 @@ mod tests {
|
||||
);
|
||||
let control = manager.agent_control();
|
||||
|
||||
let missing_rollout = config.codex_home.join("sessions/missing-rollout.jsonl");
|
||||
let _ = control
|
||||
.resume_agent_from_rollout(config.clone(), missing_rollout, SessionSource::Exec)
|
||||
.resume_agent_from_rollout(config.clone(), ThreadId::new(), SessionSource::Exec)
|
||||
.await
|
||||
.expect_err("resume should fail for missing rollout path");
|
||||
|
||||
@@ -795,6 +876,8 @@ mod tests {
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_nickname: None,
|
||||
agent_role: Some("explorer".to_string()),
|
||||
})),
|
||||
)
|
||||
.await
|
||||
@@ -812,4 +895,176 @@ mod tests {
|
||||
|
||||
assert_eq!(wait_for_subagent_notification(&parent_thread).await, true);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn spawn_thread_subagent_gets_random_nickname_in_session_source() {
|
||||
let harness = AgentControlHarness::new().await;
|
||||
let (parent_thread_id, _parent_thread) = harness.start_thread().await;
|
||||
|
||||
let child_thread_id = harness
|
||||
.control
|
||||
.spawn_agent(
|
||||
harness.config.clone(),
|
||||
text_input("hello child"),
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_nickname: None,
|
||||
agent_role: Some("explorer".to_string()),
|
||||
})),
|
||||
)
|
||||
.await
|
||||
.expect("child spawn should succeed");
|
||||
|
||||
let child_thread = harness
|
||||
.manager
|
||||
.get_thread(child_thread_id)
|
||||
.await
|
||||
.expect("child thread should be registered");
|
||||
let snapshot = child_thread.config_snapshot().await;
|
||||
|
||||
let SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: seen_parent_thread_id,
|
||||
depth,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
}) = snapshot.session_source
|
||||
else {
|
||||
panic!("expected thread-spawn sub-agent source");
|
||||
};
|
||||
assert_eq!(seen_parent_thread_id, parent_thread_id);
|
||||
assert_eq!(depth, 1);
|
||||
assert!(agent_nickname.is_some());
|
||||
assert_eq!(agent_role, Some("explorer".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resume_thread_subagent_restores_stored_nickname_and_role() {
|
||||
let (home, mut config) = test_config().await;
|
||||
config.features.enable(Feature::Sqlite);
|
||||
let manager = ThreadManager::with_models_provider_and_home_for_tests(
|
||||
CodexAuth::from_api_key("dummy"),
|
||||
config.model_provider.clone(),
|
||||
config.codex_home.clone(),
|
||||
);
|
||||
let control = manager.agent_control();
|
||||
let harness = AgentControlHarness {
|
||||
_home: home,
|
||||
config,
|
||||
manager,
|
||||
control,
|
||||
};
|
||||
let (parent_thread_id, _parent_thread) = harness.start_thread().await;
|
||||
|
||||
let child_thread_id = harness
|
||||
.control
|
||||
.spawn_agent(
|
||||
harness.config.clone(),
|
||||
text_input("hello child"),
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_nickname: None,
|
||||
agent_role: Some("explorer".to_string()),
|
||||
})),
|
||||
)
|
||||
.await
|
||||
.expect("child spawn should succeed");
|
||||
|
||||
let child_thread = harness
|
||||
.manager
|
||||
.get_thread(child_thread_id)
|
||||
.await
|
||||
.expect("child thread should exist");
|
||||
let mut status_rx = harness
|
||||
.control
|
||||
.subscribe_status(child_thread_id)
|
||||
.await
|
||||
.expect("status subscription should succeed");
|
||||
if matches!(status_rx.borrow().clone(), AgentStatus::PendingInit) {
|
||||
timeout(Duration::from_secs(5), async {
|
||||
loop {
|
||||
status_rx
|
||||
.changed()
|
||||
.await
|
||||
.expect("child status should advance past pending init");
|
||||
if !matches!(status_rx.borrow().clone(), AgentStatus::PendingInit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("child should initialize before shutdown");
|
||||
}
|
||||
let original_snapshot = child_thread.config_snapshot().await;
|
||||
let original_nickname = original_snapshot
|
||||
.session_source
|
||||
.get_nickname()
|
||||
.expect("spawned sub-agent should have a nickname");
|
||||
let state_db = child_thread
|
||||
.state_db()
|
||||
.expect("sqlite state db should be available for nickname resume test");
|
||||
timeout(Duration::from_secs(5), async {
|
||||
loop {
|
||||
if let Ok(Some(metadata)) = state_db.get_thread(child_thread_id).await
|
||||
&& metadata.agent_nickname.is_some()
|
||||
&& metadata.agent_role.as_deref() == Some("explorer")
|
||||
{
|
||||
break;
|
||||
}
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("child thread metadata should be persisted to sqlite before shutdown");
|
||||
|
||||
let _ = harness
|
||||
.control
|
||||
.shutdown_agent(child_thread_id)
|
||||
.await
|
||||
.expect("child shutdown should submit");
|
||||
|
||||
let resumed_thread_id = harness
|
||||
.control
|
||||
.resume_agent_from_rollout(
|
||||
harness.config.clone(),
|
||||
child_thread_id,
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("resume should succeed");
|
||||
assert_eq!(resumed_thread_id, child_thread_id);
|
||||
|
||||
let resumed_snapshot = harness
|
||||
.manager
|
||||
.get_thread(resumed_thread_id)
|
||||
.await
|
||||
.expect("resumed child thread should exist")
|
||||
.config_snapshot()
|
||||
.await;
|
||||
let SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: resumed_parent_thread_id,
|
||||
depth: resumed_depth,
|
||||
agent_nickname: resumed_nickname,
|
||||
agent_role: resumed_role,
|
||||
}) = resumed_snapshot.session_source
|
||||
else {
|
||||
panic!("expected thread-spawn sub-agent source");
|
||||
};
|
||||
assert_eq!(resumed_parent_thread_id, parent_thread_id);
|
||||
assert_eq!(resumed_depth, 1);
|
||||
assert_eq!(resumed_nickname, Some(original_nickname));
|
||||
assert_eq!(resumed_role, Some("explorer".to_string()));
|
||||
|
||||
let _ = harness
|
||||
.control
|
||||
.shutdown_agent(resumed_thread_id)
|
||||
.await
|
||||
.expect("resumed child shutdown should submit");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ use crate::error::Result;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use rand::prelude::IndexedRandom;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
@@ -17,10 +19,18 @@ use std::sync::atomic::Ordering;
|
||||
/// is).
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Guards {
|
||||
threads_set: Mutex<HashSet<ThreadId>>,
|
||||
active_agents: Mutex<ActiveAgents>,
|
||||
total_count: AtomicUsize,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ActiveAgents {
|
||||
threads_set: HashSet<ThreadId>,
|
||||
thread_agent_nicknames: HashMap<ThreadId, String>,
|
||||
used_agent_nicknames: HashSet<String>,
|
||||
nickname_reset_count: usize,
|
||||
}
|
||||
|
||||
fn session_depth(session_source: &SessionSource) -> i32 {
|
||||
match session_source {
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) => *depth,
|
||||
@@ -52,28 +62,69 @@ impl Guards {
|
||||
Ok(SpawnReservation {
|
||||
state: Arc::clone(self),
|
||||
active: true,
|
||||
reserved_agent_nickname: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn release_spawned_thread(&self, thread_id: ThreadId) {
|
||||
let removed = {
|
||||
let mut threads = self
|
||||
.threads_set
|
||||
let mut active_agents = self
|
||||
.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
threads.remove(&thread_id)
|
||||
let removed = active_agents.threads_set.remove(&thread_id);
|
||||
active_agents.thread_agent_nicknames.remove(&thread_id);
|
||||
removed
|
||||
};
|
||||
if removed {
|
||||
self.total_count.fetch_sub(1, Ordering::AcqRel);
|
||||
}
|
||||
}
|
||||
|
||||
fn register_spawned_thread(&self, thread_id: ThreadId) {
|
||||
let mut threads = self
|
||||
.threads_set
|
||||
fn register_spawned_thread(&self, thread_id: ThreadId, agent_nickname: Option<String>) {
|
||||
let mut active_agents = self
|
||||
.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
threads.insert(thread_id);
|
||||
active_agents.threads_set.insert(thread_id);
|
||||
if let Some(agent_nickname) = agent_nickname {
|
||||
active_agents
|
||||
.used_agent_nicknames
|
||||
.insert(agent_nickname.clone());
|
||||
active_agents
|
||||
.thread_agent_nicknames
|
||||
.insert(thread_id, agent_nickname);
|
||||
}
|
||||
}
|
||||
|
||||
fn reserve_agent_nickname(&self, names: &[&str], preferred: Option<&str>) -> Option<String> {
|
||||
let mut active_agents = self
|
||||
.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let agent_nickname = if let Some(preferred) = preferred {
|
||||
preferred.to_string()
|
||||
} else {
|
||||
if names.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let available_names: Vec<&str> = names
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|name| !active_agents.used_agent_nicknames.contains(*name))
|
||||
.collect();
|
||||
if let Some(name) = available_names.choose(&mut rand::rng()) {
|
||||
(*name).to_string()
|
||||
} else {
|
||||
active_agents.used_agent_nicknames.clear();
|
||||
active_agents.nickname_reset_count += 1;
|
||||
names.choose(&mut rand::rng())?.to_string()
|
||||
}
|
||||
};
|
||||
active_agents
|
||||
.used_agent_nicknames
|
||||
.insert(agent_nickname.clone());
|
||||
Some(agent_nickname)
|
||||
}
|
||||
|
||||
fn try_increment_spawned(&self, max_threads: usize) -> bool {
|
||||
@@ -98,11 +149,41 @@ impl Guards {
|
||||
pub(crate) struct SpawnReservation {
|
||||
state: Arc<Guards>,
|
||||
active: bool,
|
||||
reserved_agent_nickname: Option<String>,
|
||||
}
|
||||
|
||||
impl SpawnReservation {
|
||||
pub(crate) fn commit(mut self, thread_id: ThreadId) {
|
||||
self.state.register_spawned_thread(thread_id);
|
||||
pub(crate) fn reserve_agent_nickname(&mut self, names: &[&str]) -> Result<String> {
|
||||
self.reserve_agent_nickname_with_preference(names, None)
|
||||
}
|
||||
|
||||
pub(crate) fn reserve_agent_nickname_with_preference(
|
||||
&mut self,
|
||||
names: &[&str],
|
||||
preferred: Option<&str>,
|
||||
) -> Result<String> {
|
||||
let agent_nickname = self
|
||||
.state
|
||||
.reserve_agent_nickname(names, preferred)
|
||||
.ok_or_else(|| {
|
||||
CodexErr::UnsupportedOperation("no available agent nicknames".to_string())
|
||||
})?;
|
||||
self.reserved_agent_nickname = Some(agent_nickname.clone());
|
||||
Ok(agent_nickname)
|
||||
}
|
||||
|
||||
pub(crate) fn commit(self, thread_id: ThreadId) {
|
||||
self.commit_with_agent_nickname(thread_id, None);
|
||||
}
|
||||
|
||||
pub(crate) fn commit_with_agent_nickname(
|
||||
mut self,
|
||||
thread_id: ThreadId,
|
||||
agent_nickname: Option<String>,
|
||||
) {
|
||||
let agent_nickname = self.reserved_agent_nickname.take().or(agent_nickname);
|
||||
self.state
|
||||
.register_spawned_thread(thread_id, agent_nickname);
|
||||
self.active = false;
|
||||
}
|
||||
}
|
||||
@@ -130,6 +211,8 @@ mod tests {
|
||||
let session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: ThreadId::new(),
|
||||
depth: 1,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
});
|
||||
let child_depth = next_thread_spawn_depth(&session_source);
|
||||
assert_eq!(child_depth, 2);
|
||||
@@ -232,4 +315,83 @@ mod tests {
|
||||
.expect("slot released after second thread removal");
|
||||
drop(reservation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_spawn_keeps_nickname_marked_used() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
let mut reservation = guards.reserve_spawn_slot(None).expect("reserve slot");
|
||||
let agent_nickname = reservation
|
||||
.reserve_agent_nickname(&["alpha"])
|
||||
.expect("reserve agent name");
|
||||
assert_eq!(agent_nickname, "alpha");
|
||||
drop(reservation);
|
||||
|
||||
let mut reservation = guards.reserve_spawn_slot(None).expect("reserve slot");
|
||||
let agent_nickname = reservation
|
||||
.reserve_agent_nickname(&["alpha", "beta"])
|
||||
.expect("unused name should still be preferred");
|
||||
assert_eq!(agent_nickname, "beta");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_nickname_resets_used_pool_when_exhausted() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
let mut first = guards.reserve_spawn_slot(None).expect("reserve first slot");
|
||||
let first_name = first
|
||||
.reserve_agent_nickname(&["alpha"])
|
||||
.expect("reserve first agent name");
|
||||
let first_id = ThreadId::new();
|
||||
first.commit(first_id);
|
||||
assert_eq!(first_name, "alpha");
|
||||
|
||||
let mut second = guards
|
||||
.reserve_spawn_slot(None)
|
||||
.expect("reserve second slot");
|
||||
let second_name = second
|
||||
.reserve_agent_nickname(&["alpha"])
|
||||
.expect("name should be reused after pool reset");
|
||||
assert_eq!(second_name, "alpha");
|
||||
let active_agents = guards
|
||||
.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
assert_eq!(active_agents.nickname_reset_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn released_nickname_stays_used_until_pool_reset() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
|
||||
let mut first = guards.reserve_spawn_slot(None).expect("reserve first slot");
|
||||
let first_name = first
|
||||
.reserve_agent_nickname(&["alpha"])
|
||||
.expect("reserve first agent name");
|
||||
let first_id = ThreadId::new();
|
||||
first.commit(first_id);
|
||||
assert_eq!(first_name, "alpha");
|
||||
|
||||
guards.release_spawned_thread(first_id);
|
||||
|
||||
let mut second = guards
|
||||
.reserve_spawn_slot(None)
|
||||
.expect("reserve second slot");
|
||||
let second_name = second
|
||||
.reserve_agent_nickname(&["alpha", "beta"])
|
||||
.expect("released name should still be marked used");
|
||||
assert_eq!(second_name, "beta");
|
||||
let second_id = ThreadId::new();
|
||||
second.commit(second_id);
|
||||
guards.release_spawned_thread(second_id);
|
||||
|
||||
let mut third = guards.reserve_spawn_slot(None).expect("reserve third slot");
|
||||
let third_name = third
|
||||
.reserve_agent_nickname(&["alpha", "beta"])
|
||||
.expect("pool reset should permit a duplicate");
|
||||
assert!(third_name == "alpha" || third_name == "beta");
|
||||
let active_agents = guards
|
||||
.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
assert_eq!(active_agents.nickname_reset_count, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,11 +162,10 @@ mod built_in {
|
||||
(
|
||||
"explorer".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: Some(r#"Use `explorer` for all codebase questions.
|
||||
description: Some(r#"Use `explorer` for specific codebase questions.
|
||||
Explorers are fast and authoritative.
|
||||
Always prefer them over manual search or file reading.
|
||||
They must be used to ask specific, well-scoped questions on the codebase.
|
||||
Rules:
|
||||
- Ask explorers first and precisely.
|
||||
- Do not re-read or re-search code they cover.
|
||||
- Trust explorer results without verification.
|
||||
- Run explorers in parallel when useful.
|
||||
@@ -268,6 +267,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "No role requiring it for now"]
|
||||
async fn apply_explorer_role_sets_model_and_adds_session_flags_layer() {
|
||||
let (_home, mut config) = test_config_with_cli_overrides(Vec::new()).await;
|
||||
let before_layers = session_flags_layer_count(&config);
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex as StdMutex;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -123,6 +124,7 @@ struct ModelClientState {
|
||||
include_timing_metrics: bool,
|
||||
beta_features_header: Option<String>,
|
||||
disable_websockets: AtomicBool,
|
||||
cached_websocket_connection: StdMutex<Option<ApiWebSocketConnection>>,
|
||||
}
|
||||
|
||||
/// Resolved API client setup for a single request attempt.
|
||||
@@ -228,6 +230,7 @@ impl ModelClient {
|
||||
include_timing_metrics,
|
||||
beta_features_header,
|
||||
disable_websockets: AtomicBool::new(false),
|
||||
cached_websocket_connection: StdMutex::new(None),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -239,13 +242,29 @@ impl ModelClient {
|
||||
pub fn new_session(&self) -> ModelClientSession {
|
||||
ModelClientSession {
|
||||
client: self.clone(),
|
||||
connection: None,
|
||||
connection: self.take_cached_websocket_connection(),
|
||||
websocket_last_request: None,
|
||||
websocket_last_response_rx: None,
|
||||
turn_state: Arc::new(OnceLock::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn take_cached_websocket_connection(&self) -> Option<ApiWebSocketConnection> {
|
||||
self.state
|
||||
.cached_websocket_connection
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.take()
|
||||
}
|
||||
|
||||
fn store_cached_websocket_connection(&self, connection: ApiWebSocketConnection) {
|
||||
*self
|
||||
.state
|
||||
.cached_websocket_connection
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(connection);
|
||||
}
|
||||
|
||||
/// Compacts the current conversation history using the Compact endpoint.
|
||||
///
|
||||
/// This is a unary call (no streaming) that returns a new list of
|
||||
@@ -452,6 +471,14 @@ impl ModelClient {
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ModelClientSession {
|
||||
fn drop(&mut self) {
|
||||
if let Some(connection) = self.connection.take() {
|
||||
self.client.store_cached_websocket_connection(connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModelClientSession {
|
||||
fn activate_http_fallback(&self, websocket_enabled: bool) -> bool {
|
||||
websocket_enabled
|
||||
|
||||
+17
-12
@@ -149,6 +149,7 @@ use crate::mcp::effective_mcp_servers;
|
||||
use crate::mcp::maybe_prompt_and_install_mcp_dependencies;
|
||||
use crate::mcp::with_codex_apps_mcp;
|
||||
use crate::mcp_connection_manager::McpConnectionManager;
|
||||
use crate::mcp_connection_manager::codex_apps_tools_cache_key;
|
||||
use crate::mcp_connection_manager::filter_codex_apps_mcp_tools_only;
|
||||
use crate::mcp_connection_manager::filter_mcp_tools_by_name;
|
||||
use crate::mcp_connection_manager::filter_non_codex_apps_mcp_tools_only;
|
||||
@@ -608,6 +609,7 @@ impl TurnContext {
|
||||
features: &features,
|
||||
web_search_mode: self.tools_config.web_search_mode,
|
||||
})
|
||||
.with_allow_login_shell(self.tools_config.allow_login_shell)
|
||||
.with_agent_roles(config.agent_roles.clone());
|
||||
|
||||
Self {
|
||||
@@ -940,6 +942,7 @@ impl Session {
|
||||
features: &per_turn_config.features,
|
||||
web_search_mode: Some(per_turn_config.web_search_mode.value()),
|
||||
})
|
||||
.with_allow_login_shell(per_turn_config.permissions.allow_login_shell)
|
||||
.with_agent_roles(per_turn_config.agent_roles.clone());
|
||||
|
||||
let cwd = session_configuration.cwd.clone();
|
||||
@@ -1420,6 +1423,8 @@ impl Session {
|
||||
&session_configuration.approval_policy,
|
||||
tx_event.clone(),
|
||||
sandbox_state,
|
||||
config.codex_home.clone(),
|
||||
codex_apps_tools_cache_key(auth),
|
||||
)
|
||||
.await;
|
||||
{
|
||||
@@ -3068,6 +3073,8 @@ impl Session {
|
||||
&turn_context.config.permissions.approval_policy,
|
||||
self.get_tx_event(),
|
||||
sandbox_state,
|
||||
config.codex_home.clone(),
|
||||
codex_apps_tools_cache_key(auth.as_ref()),
|
||||
)
|
||||
.await;
|
||||
{
|
||||
@@ -4117,6 +4124,7 @@ async fn spawn_review_thread(
|
||||
features: &review_features,
|
||||
web_search_mode: Some(review_web_search_mode),
|
||||
})
|
||||
.with_allow_login_shell(config.permissions.allow_login_shell)
|
||||
.with_agent_roles(config.agent_roles.clone());
|
||||
|
||||
let review_prompt = resolved.prompt.clone();
|
||||
@@ -4826,6 +4834,7 @@ fn connector_inserted_in_messages(
|
||||
fn filter_codex_apps_mcp_tools(
|
||||
mcp_tools: &HashMap<String, crate::mcp_connection_manager::ToolInfo>,
|
||||
connectors: &[connectors::AppInfo],
|
||||
config: &Config,
|
||||
) -> HashMap<String, crate::mcp_connection_manager::ToolInfo> {
|
||||
let allowed: HashSet<&str> = connectors
|
||||
.iter()
|
||||
@@ -4841,7 +4850,7 @@ fn filter_codex_apps_mcp_tools(
|
||||
let Some(connector_id) = codex_apps_connector_id(tool) else {
|
||||
return false;
|
||||
};
|
||||
allowed.contains(connector_id)
|
||||
allowed.contains(connector_id) && connectors::codex_app_tool_is_enabled(config, tool)
|
||||
})
|
||||
.map(|(name, tool)| (name.clone(), tool.clone()))
|
||||
.collect()
|
||||
@@ -5015,9 +5024,9 @@ async fn built_tools(
|
||||
None
|
||||
};
|
||||
|
||||
let app_tools = connectors
|
||||
.as_ref()
|
||||
.map(|connectors| filter_codex_apps_mcp_tools(&mcp_tools, connectors));
|
||||
let app_tools = connectors.as_ref().map(|connectors| {
|
||||
filter_codex_apps_mcp_tools(&mcp_tools, connectors, &turn_context.config)
|
||||
});
|
||||
|
||||
if let Some(connectors) = connectors.as_ref() {
|
||||
let skill_name_counts_lower = skills_outcome.map_or_else(HashMap::new, |outcome| {
|
||||
@@ -5042,7 +5051,8 @@ async fn built_tools(
|
||||
explicitly_enabled.as_ref(),
|
||||
));
|
||||
|
||||
mcp_tools = selected_mcp_tools;
|
||||
mcp_tools =
|
||||
connectors::filter_codex_apps_tools_by_policy(selected_mcp_tools, &turn_context.config);
|
||||
}
|
||||
|
||||
Ok(Arc::new(ToolRouter::from_config(
|
||||
@@ -5993,7 +6003,7 @@ mod tests {
|
||||
let config_toml_path = codex_home.join(CONFIG_TOML_FILE);
|
||||
std::fs::write(
|
||||
&config_toml_path,
|
||||
"[apps.calendar]\nenabled = false\ndisabled_reason = \"user\"\n",
|
||||
"[apps.calendar]\nenabled = false\ndestructive_enabled = false\n",
|
||||
)
|
||||
.expect("write user config");
|
||||
|
||||
@@ -6015,10 +6025,7 @@ mod tests {
|
||||
.expect("calendar app config exists");
|
||||
|
||||
assert!(!app.enabled);
|
||||
assert_eq!(
|
||||
app.disabled_reason,
|
||||
Some(crate::config::types::AppDisabledReason::User)
|
||||
);
|
||||
assert_eq!(app.destructive_enabled, Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -8342,7 +8349,6 @@ mod tests {
|
||||
expiration: timeout_ms.into(),
|
||||
env: HashMap::new(),
|
||||
network: None,
|
||||
network_attempt_id: None,
|
||||
sandbox_permissions,
|
||||
windows_sandbox_level: turn_context.windows_sandbox_level,
|
||||
justification: Some("test".to_string()),
|
||||
@@ -8356,7 +8362,6 @@ mod tests {
|
||||
expiration: timeout_ms.into(),
|
||||
env: HashMap::new(),
|
||||
network: None,
|
||||
network_attempt_id: None,
|
||||
windows_sandbox_level: turn_context.windows_sandbox_level,
|
||||
justification: params.justification.clone(),
|
||||
arg0: None,
|
||||
|
||||
@@ -139,6 +139,15 @@ pub struct Permissions {
|
||||
pub sandbox_policy: Constrained<SandboxPolicy>,
|
||||
/// Effective network configuration applied to all spawned processes.
|
||||
pub network: Option<NetworkProxySpec>,
|
||||
/// Whether the model may request a login shell for shell-based tools.
|
||||
/// Default to `true`
|
||||
///
|
||||
/// If `true`, the model may request a login shell (`login = true`), and
|
||||
/// omitting `login` defaults to using a login shell.
|
||||
/// If `false`, the model can never use a login shell: `login = true`
|
||||
/// requests are rejected, and omitting `login` defaults to a non-login
|
||||
/// shell.
|
||||
pub allow_login_shell: bool,
|
||||
/// Policy used to build process environments for shell/unified exec.
|
||||
pub shell_environment_policy: ShellEnvironmentPolicy,
|
||||
/// Effective Windows sandbox mode derived from `[windows].sandbox` or
|
||||
@@ -952,6 +961,16 @@ pub struct ConfigToml {
|
||||
#[serde(default)]
|
||||
pub shell_environment_policy: ShellEnvironmentPolicyToml,
|
||||
|
||||
/// Whether the model may request a login shell for shell-based tools.
|
||||
/// Default to `true`
|
||||
///
|
||||
/// If `true`, the model may request a login shell (`login = true`), and
|
||||
/// omitting `login` defaults to using a login shell.
|
||||
/// If `false`, the model can never use a login shell: `login = true`
|
||||
/// requests are rejected, and omitting `login` defaults to a non-login
|
||||
/// shell.
|
||||
pub allow_login_shell: Option<bool>,
|
||||
|
||||
/// Sandbox mode to use.
|
||||
pub sandbox_mode: Option<SandboxMode>,
|
||||
|
||||
@@ -1090,8 +1109,8 @@ pub struct ConfigToml {
|
||||
/// Override to force-enable reasoning summaries for the configured model.
|
||||
pub model_supports_reasoning_summaries: Option<bool>,
|
||||
|
||||
/// Optional path to a JSON file containing a complete model catalog.
|
||||
/// When set, this replaces the bundled catalog for this process.
|
||||
/// Optional path to a JSON model catalog (applied on startup only).
|
||||
/// Per-thread `config` overrides are accepted but do not reapply this (no-ops).
|
||||
pub model_catalog_json: Option<AbsolutePathBuf>,
|
||||
|
||||
/// Optionally specify a personality for the model
|
||||
@@ -1710,6 +1729,7 @@ impl Config {
|
||||
.clone();
|
||||
|
||||
let shell_environment_policy = cfg.shell_environment_policy.into();
|
||||
let allow_login_shell = cfg.allow_login_shell.unwrap_or(true);
|
||||
|
||||
let history = cfg.history.unwrap_or_default();
|
||||
|
||||
@@ -1950,6 +1970,7 @@ impl Config {
|
||||
approval_policy: constrained_approval_policy.value,
|
||||
sandbox_policy: constrained_sandbox_policy.value,
|
||||
network,
|
||||
allow_login_shell,
|
||||
shell_environment_policy,
|
||||
windows_sandbox_mode,
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
@@ -2401,6 +2422,7 @@ allowed_domains = ["openai.com"]
|
||||
allow_upstream_proxy: Some(false),
|
||||
dangerously_allow_non_loopback_proxy: None,
|
||||
dangerously_allow_non_loopback_admin: None,
|
||||
dangerously_allow_all_unix_sockets: None,
|
||||
mode: None,
|
||||
allowed_domains: Some(vec!["openai.com".to_string()]),
|
||||
denied_domains: None,
|
||||
@@ -4518,6 +4540,7 @@ model_verbosity = "high"
|
||||
approval_policy: Constrained::allow_any(AskForApproval::Never),
|
||||
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
@@ -4636,6 +4659,7 @@ model_verbosity = "high"
|
||||
approval_policy: Constrained::allow_any(AskForApproval::UnlessTrusted),
|
||||
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
@@ -4752,6 +4776,7 @@ model_verbosity = "high"
|
||||
approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
|
||||
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
@@ -4854,6 +4879,7 @@ model_verbosity = "high"
|
||||
approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
|
||||
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
@@ -5428,6 +5454,27 @@ mcp_oauth_callback_port = 5678
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_loads_allow_login_shell_from_toml() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cfg: ConfigToml = toml::from_str(
|
||||
r#"
|
||||
model = "gpt-5.1"
|
||||
allow_login_shell = false
|
||||
"#,
|
||||
)
|
||||
.expect("TOML deserialization should succeed for allow_login_shell");
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
cfg,
|
||||
ConfigOverrides::default(),
|
||||
codex_home.path().to_path_buf(),
|
||||
)?;
|
||||
|
||||
assert!(!config.permissions.allow_login_shell);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_loads_mcp_oauth_callback_url_from_toml() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -178,6 +178,13 @@ impl NetworkProxySpec {
|
||||
constraints.dangerously_allow_non_loopback_admin =
|
||||
Some(dangerously_allow_non_loopback_admin);
|
||||
}
|
||||
if let Some(dangerously_allow_all_unix_sockets) =
|
||||
requirements.dangerously_allow_all_unix_sockets
|
||||
{
|
||||
config.network.dangerously_allow_all_unix_sockets = dangerously_allow_all_unix_sockets;
|
||||
constraints.dangerously_allow_all_unix_sockets =
|
||||
Some(dangerously_allow_all_unix_sockets);
|
||||
}
|
||||
if let Some(allowed_domains) = requirements.allowed_domains.clone() {
|
||||
config.network.allowed_domains = allowed_domains.clone();
|
||||
constraints.allowed_domains = Some(allowed_domains);
|
||||
|
||||
@@ -24,6 +24,7 @@ pub struct NetworkToml {
|
||||
pub allow_upstream_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_admin: Option<bool>,
|
||||
pub dangerously_allow_all_unix_sockets: Option<bool>,
|
||||
#[schemars(with = "Option<NetworkModeSchema>")]
|
||||
pub mode: Option<NetworkMode>,
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
@@ -74,6 +75,9 @@ impl NetworkToml {
|
||||
config.network.dangerously_allow_non_loopback_admin =
|
||||
dangerously_allow_non_loopback_admin;
|
||||
}
|
||||
if let Some(dangerously_allow_all_unix_sockets) = self.dangerously_allow_all_unix_sockets {
|
||||
config.network.dangerously_allow_all_unix_sockets = dangerously_allow_all_unix_sockets;
|
||||
}
|
||||
if let Some(mode) = self.mode {
|
||||
config.network.mode = mode;
|
||||
}
|
||||
|
||||
@@ -701,7 +701,7 @@ mod tests {
|
||||
use super::*;
|
||||
use anyhow::Result;
|
||||
use codex_app_server_protocol::AppConfig;
|
||||
use codex_app_server_protocol::AppDisabledReason;
|
||||
use codex_app_server_protocol::AppToolApproval;
|
||||
use codex_app_server_protocol::AppsConfig;
|
||||
use codex_app_server_protocol::AskForApproval;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
@@ -827,13 +827,13 @@ personality = true
|
||||
service
|
||||
.write_value(ConfigValueWriteParams {
|
||||
file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()),
|
||||
key_path: "apps.app1.disabled_reason".to_string(),
|
||||
value: serde_json::json!("user"),
|
||||
key_path: "apps.app1.default_tools_approval_mode".to_string(),
|
||||
value: serde_json::json!("prompt"),
|
||||
merge_strategy: MergeStrategy::Replace,
|
||||
expected_version: None,
|
||||
})
|
||||
.await
|
||||
.expect("write apps.app1.disabled_reason succeeds");
|
||||
.expect("write apps.app1.default_tools_approval_mode succeeds");
|
||||
|
||||
let read = service
|
||||
.read(ConfigReadParams {
|
||||
@@ -846,11 +846,16 @@ personality = true
|
||||
assert_eq!(
|
||||
read.config.apps,
|
||||
Some(AppsConfig {
|
||||
default: None,
|
||||
apps: std::collections::HashMap::from([(
|
||||
"app1".to_string(),
|
||||
AppConfig {
|
||||
enabled: false,
|
||||
disabled_reason: Some(AppDisabledReason::User),
|
||||
destructive_enabled: None,
|
||||
open_world_enabled: None,
|
||||
default_tools_approval_mode: Some(AppToolApproval::Prompt),
|
||||
default_tools_enabled: None,
|
||||
tools: None,
|
||||
},
|
||||
)]),
|
||||
})
|
||||
|
||||
@@ -425,20 +425,58 @@ impl From<MemoriesToml> for MemoriesConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AppDisabledReason {
|
||||
Unknown,
|
||||
User,
|
||||
pub enum AppToolApproval {
|
||||
#[default]
|
||||
Auto,
|
||||
Prompt,
|
||||
Approve,
|
||||
}
|
||||
|
||||
impl fmt::Display for AppDisabledReason {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
AppDisabledReason::Unknown => write!(f, "unknown"),
|
||||
AppDisabledReason::User => write!(f, "user"),
|
||||
}
|
||||
}
|
||||
/// Default settings that apply to all apps.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct AppsDefaultConfig {
|
||||
/// When `false`, apps are disabled unless overridden by per-app settings.
|
||||
#[serde(default = "default_enabled")]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Whether tools with `destructive_hint = true` are allowed by default.
|
||||
#[serde(
|
||||
default = "default_enabled",
|
||||
skip_serializing_if = "std::clone::Clone::clone"
|
||||
)]
|
||||
pub destructive_enabled: bool,
|
||||
|
||||
/// Whether tools with `open_world_hint = true` are allowed by default.
|
||||
#[serde(
|
||||
default = "default_enabled",
|
||||
skip_serializing_if = "std::clone::Clone::clone"
|
||||
)]
|
||||
pub open_world_enabled: bool,
|
||||
}
|
||||
|
||||
/// Per-tool settings for a single app tool.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct AppToolConfig {
|
||||
/// Whether this tool is enabled. `Some(true)` explicitly allows this tool.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
|
||||
/// Approval mode for this tool.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub approval_mode: Option<AppToolApproval>,
|
||||
}
|
||||
|
||||
/// Tool settings for a single app.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct AppToolsConfig {
|
||||
/// Per-tool overrides keyed by tool name (for example `repos/list`).
|
||||
#[serde(default, flatten)]
|
||||
pub tools: HashMap<String, AppToolConfig>,
|
||||
}
|
||||
|
||||
/// Config values for a single app/connector.
|
||||
@@ -449,15 +487,35 @@ pub struct AppConfig {
|
||||
#[serde(default = "default_enabled")]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Reason this app was disabled.
|
||||
/// Whether tools with `destructive_hint = true` are allowed for this app.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub disabled_reason: Option<AppDisabledReason>,
|
||||
pub destructive_enabled: Option<bool>,
|
||||
|
||||
/// Whether tools with `open_world_hint = true` are allowed for this app.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub open_world_enabled: Option<bool>,
|
||||
|
||||
/// Approval mode for tools in this app unless a tool override exists.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub default_tools_approval_mode: Option<AppToolApproval>,
|
||||
|
||||
/// Whether tools are enabled by default for this app.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub default_tools_enabled: Option<bool>,
|
||||
|
||||
/// Per-tool settings for this app.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub tools: Option<AppToolsConfig>,
|
||||
}
|
||||
|
||||
/// App/connector settings loaded from `config.toml`.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct AppsConfigToml {
|
||||
/// Default settings for all apps.
|
||||
#[serde(default, rename = "_default", skip_serializing_if = "Option::is_none")]
|
||||
pub default: Option<AppsDefaultConfig>,
|
||||
|
||||
/// Per-app settings keyed by app ID (for example `[apps.google_drive]`).
|
||||
#[serde(default, flatten)]
|
||||
pub apps: HashMap<String, AppConfig>,
|
||||
|
||||
+661
-19
@@ -11,6 +11,7 @@ pub use codex_app_server_protocol::AppBranding;
|
||||
pub use codex_app_server_protocol::AppInfo;
|
||||
pub use codex_app_server_protocol::AppMetadata;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use rmcp::model::ToolAnnotations;
|
||||
use serde::Deserialize;
|
||||
use tracing::warn;
|
||||
|
||||
@@ -18,16 +19,35 @@ use crate::AuthManager;
|
||||
use crate::CodexAuth;
|
||||
use crate::SandboxState;
|
||||
use crate::config::Config;
|
||||
use crate::config::types::AppToolApproval;
|
||||
use crate::config::types::AppsConfigToml;
|
||||
use crate::default_client::is_first_party_chat_originator;
|
||||
use crate::default_client::originator;
|
||||
use crate::features::Feature;
|
||||
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use crate::mcp::auth::compute_auth_statuses;
|
||||
use crate::mcp::with_codex_apps_mcp;
|
||||
use crate::mcp_connection_manager::DEFAULT_STARTUP_TIMEOUT;
|
||||
use crate::mcp_connection_manager::McpConnectionManager;
|
||||
use crate::mcp_connection_manager::codex_apps_tools_cache_key;
|
||||
use crate::token_data::TokenData;
|
||||
|
||||
pub const CONNECTORS_CACHE_TTL: Duration = Duration::from_secs(3600);
|
||||
const CONNECTORS_READY_TIMEOUT_ON_EMPTY_TOOLS: Duration = Duration::from_secs(30);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct AppToolPolicy {
|
||||
pub enabled: bool,
|
||||
pub approval: AppToolApproval,
|
||||
}
|
||||
|
||||
impl Default for AppToolPolicy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
approval: AppToolApproval::Auto,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
struct AccessibleConnectorsCacheKey {
|
||||
@@ -47,10 +67,20 @@ struct CachedAccessibleConnectors {
|
||||
static ACCESSIBLE_CONNECTORS_CACHE: LazyLock<StdMutex<Option<CachedAccessibleConnectors>>> =
|
||||
LazyLock::new(|| StdMutex::new(None));
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AccessibleConnectorsStatus {
|
||||
pub connectors: Vec<AppInfo>,
|
||||
pub codex_apps_ready: bool,
|
||||
}
|
||||
|
||||
pub async fn list_accessible_connectors_from_mcp_tools(
|
||||
config: &Config,
|
||||
) -> anyhow::Result<Vec<AppInfo>> {
|
||||
list_accessible_connectors_from_mcp_tools_with_options(config, false).await
|
||||
Ok(
|
||||
list_accessible_connectors_from_mcp_tools_with_options_and_status(config, false)
|
||||
.await?
|
||||
.connectors,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn list_cached_accessible_connectors_from_mcp_tools(
|
||||
@@ -63,15 +93,29 @@ pub async fn list_cached_accessible_connectors_from_mcp_tools(
|
||||
let auth_manager = auth_manager_from_config(config);
|
||||
let auth = auth_manager.auth().await;
|
||||
let cache_key = accessible_connectors_cache_key(config, auth.as_ref());
|
||||
read_cached_accessible_connectors(&cache_key)
|
||||
read_cached_accessible_connectors(&cache_key).map(filter_disallowed_connectors)
|
||||
}
|
||||
|
||||
pub async fn list_accessible_connectors_from_mcp_tools_with_options(
|
||||
config: &Config,
|
||||
force_refetch: bool,
|
||||
) -> anyhow::Result<Vec<AppInfo>> {
|
||||
Ok(
|
||||
list_accessible_connectors_from_mcp_tools_with_options_and_status(config, force_refetch)
|
||||
.await?
|
||||
.connectors,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status(
|
||||
config: &Config,
|
||||
force_refetch: bool,
|
||||
) -> anyhow::Result<AccessibleConnectorsStatus> {
|
||||
if !config.features.enabled(Feature::Apps) {
|
||||
return Ok(Vec::new());
|
||||
return Ok(AccessibleConnectorsStatus {
|
||||
connectors: Vec::new(),
|
||||
codex_apps_ready: true,
|
||||
});
|
||||
}
|
||||
|
||||
let auth_manager = auth_manager_from_config(config);
|
||||
@@ -79,12 +123,19 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options(
|
||||
let cache_key = accessible_connectors_cache_key(config, auth.as_ref());
|
||||
if !force_refetch && let Some(cached_connectors) = read_cached_accessible_connectors(&cache_key)
|
||||
{
|
||||
return Ok(cached_connectors);
|
||||
let cached_connectors = filter_disallowed_connectors(cached_connectors);
|
||||
return Ok(AccessibleConnectorsStatus {
|
||||
connectors: cached_connectors,
|
||||
codex_apps_ready: true,
|
||||
});
|
||||
}
|
||||
|
||||
let mcp_servers = with_codex_apps_mcp(HashMap::new(), true, auth.as_ref(), config);
|
||||
if mcp_servers.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
return Ok(AccessibleConnectorsStatus {
|
||||
connectors: Vec::new(),
|
||||
codex_apps_ready: true,
|
||||
});
|
||||
}
|
||||
|
||||
let auth_status_entries =
|
||||
@@ -107,6 +158,8 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options(
|
||||
&config.permissions.approval_policy,
|
||||
tx_event,
|
||||
sandbox_state,
|
||||
config.codex_home.clone(),
|
||||
codex_apps_tools_cache_key(auth.as_ref()),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -120,23 +173,45 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options(
|
||||
);
|
||||
}
|
||||
|
||||
let mut tools = mcp_connection_manager.list_all_tools().await;
|
||||
let mut should_reload_tools = false;
|
||||
let codex_apps_ready = if let Some(cfg) = mcp_servers.get(CODEX_APPS_MCP_SERVER_NAME) {
|
||||
let timeout = cfg.startup_timeout_sec.unwrap_or(DEFAULT_STARTUP_TIMEOUT);
|
||||
mcp_connection_manager
|
||||
.wait_for_server_ready(CODEX_APPS_MCP_SERVER_NAME, timeout)
|
||||
.await
|
||||
let immediate_ready = mcp_connection_manager
|
||||
.wait_for_server_ready(CODEX_APPS_MCP_SERVER_NAME, Duration::ZERO)
|
||||
.await;
|
||||
if immediate_ready {
|
||||
true
|
||||
} else if tools.is_empty() {
|
||||
let timeout = cfg
|
||||
.startup_timeout_sec
|
||||
.unwrap_or(CONNECTORS_READY_TIMEOUT_ON_EMPTY_TOOLS);
|
||||
let ready = mcp_connection_manager
|
||||
.wait_for_server_ready(CODEX_APPS_MCP_SERVER_NAME, timeout)
|
||||
.await;
|
||||
should_reload_tools = ready;
|
||||
ready
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if should_reload_tools {
|
||||
tools = mcp_connection_manager.list_all_tools().await;
|
||||
}
|
||||
if codex_apps_ready {
|
||||
cancel_token.cancel();
|
||||
}
|
||||
|
||||
let tools = mcp_connection_manager.list_all_tools().await;
|
||||
cancel_token.cancel();
|
||||
|
||||
let accessible_connectors = accessible_connectors_from_mcp_tools(&tools);
|
||||
let accessible_connectors =
|
||||
filter_disallowed_connectors(accessible_connectors_from_mcp_tools(&tools));
|
||||
if codex_apps_ready || !accessible_connectors.is_empty() {
|
||||
write_cached_accessible_connectors(cache_key, &accessible_connectors);
|
||||
}
|
||||
Ok(accessible_connectors)
|
||||
Ok(AccessibleConnectorsStatus {
|
||||
connectors: accessible_connectors,
|
||||
codex_apps_ready,
|
||||
})
|
||||
}
|
||||
|
||||
fn accessible_connectors_cache_key(
|
||||
@@ -279,21 +354,185 @@ pub fn merge_connectors(
|
||||
}
|
||||
|
||||
pub fn with_app_enabled_state(mut connectors: Vec<AppInfo>, config: &Config) -> Vec<AppInfo> {
|
||||
let apps = read_apps_config(config).map(|apps_config| apps_config.apps);
|
||||
for connector in &mut connectors {
|
||||
if let Some(app) = apps.as_ref().and_then(|apps| apps.get(&connector.id)) {
|
||||
connector.is_enabled = app.enabled;
|
||||
let apps_config = read_apps_config(config);
|
||||
if let Some(apps_config) = apps_config.as_ref() {
|
||||
for connector in &mut connectors {
|
||||
connector.is_enabled = app_is_enabled(apps_config, Some(connector.id.as_str()));
|
||||
}
|
||||
}
|
||||
connectors
|
||||
}
|
||||
|
||||
pub(crate) fn app_tool_policy(
|
||||
config: &Config,
|
||||
connector_id: Option<&str>,
|
||||
tool_name: &str,
|
||||
tool_title: Option<&str>,
|
||||
annotations: Option<&ToolAnnotations>,
|
||||
) -> AppToolPolicy {
|
||||
let apps_config = read_apps_config(config);
|
||||
app_tool_policy_from_apps_config(
|
||||
apps_config.as_ref(),
|
||||
connector_id,
|
||||
tool_name,
|
||||
tool_title,
|
||||
annotations,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn codex_app_tool_is_enabled(
|
||||
config: &Config,
|
||||
tool_info: &crate::mcp_connection_manager::ToolInfo,
|
||||
) -> bool {
|
||||
if tool_info.server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
return true;
|
||||
}
|
||||
|
||||
app_tool_policy(
|
||||
config,
|
||||
tool_info.connector_id.as_deref(),
|
||||
&tool_info.tool_name,
|
||||
tool_info.tool.title.as_deref(),
|
||||
tool_info.tool.annotations.as_ref(),
|
||||
)
|
||||
.enabled
|
||||
}
|
||||
|
||||
pub(crate) fn filter_codex_apps_tools_by_policy(
|
||||
mut mcp_tools: HashMap<String, crate::mcp_connection_manager::ToolInfo>,
|
||||
config: &Config,
|
||||
) -> HashMap<String, crate::mcp_connection_manager::ToolInfo> {
|
||||
mcp_tools.retain(|_, tool_info| codex_app_tool_is_enabled(config, tool_info));
|
||||
mcp_tools
|
||||
}
|
||||
|
||||
const DISALLOWED_CONNECTOR_IDS: &[&str] = &[
|
||||
"asdk_app_6938a94a61d881918ef32cb999ff937c",
|
||||
"connector_2b0a9009c9c64bf9933a3dae3f2b1254",
|
||||
"connector_68de829bf7648191acd70a907364c67c",
|
||||
"connector_68e004f14af881919eb50893d3d9f523",
|
||||
"connector_69272cb413a081919685ec3c88d1744e",
|
||||
];
|
||||
const FIRST_PARTY_CHAT_DISALLOWED_CONNECTOR_IDS: &[&str] =
|
||||
&["connector_0f9c9d4592e54d0a9a12b3f44a1e2010"];
|
||||
const DISALLOWED_CONNECTOR_PREFIX: &str = "connector_openai_";
|
||||
|
||||
pub fn filter_disallowed_connectors(connectors: Vec<AppInfo>) -> Vec<AppInfo> {
|
||||
filter_disallowed_connectors_for_originator(connectors, originator().value.as_str())
|
||||
}
|
||||
|
||||
pub(crate) fn is_connector_id_allowed(connector_id: &str) -> bool {
|
||||
is_connector_id_allowed_for_originator(connector_id, originator().value.as_str())
|
||||
}
|
||||
|
||||
fn filter_disallowed_connectors_for_originator(
|
||||
connectors: Vec<AppInfo>,
|
||||
originator_value: &str,
|
||||
) -> Vec<AppInfo> {
|
||||
connectors
|
||||
.into_iter()
|
||||
.filter(|connector| {
|
||||
is_connector_id_allowed_for_originator(connector.id.as_str(), originator_value)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn is_connector_id_allowed_for_originator(connector_id: &str, originator_value: &str) -> bool {
|
||||
let disallowed_connector_ids = if is_first_party_chat_originator(originator_value) {
|
||||
FIRST_PARTY_CHAT_DISALLOWED_CONNECTOR_IDS
|
||||
} else {
|
||||
DISALLOWED_CONNECTOR_IDS
|
||||
};
|
||||
|
||||
!connector_id.starts_with(DISALLOWED_CONNECTOR_PREFIX)
|
||||
&& !disallowed_connector_ids.contains(&connector_id)
|
||||
}
|
||||
|
||||
fn read_apps_config(config: &Config) -> Option<AppsConfigToml> {
|
||||
let effective_config = config.config_layer_stack.effective_config();
|
||||
let apps_config = effective_config.as_table()?.get("apps")?.clone();
|
||||
AppsConfigToml::deserialize(apps_config).ok()
|
||||
}
|
||||
|
||||
fn app_is_enabled(apps_config: &AppsConfigToml, connector_id: Option<&str>) -> bool {
|
||||
let default_enabled = apps_config
|
||||
.default
|
||||
.as_ref()
|
||||
.map(|defaults| defaults.enabled)
|
||||
.unwrap_or(true);
|
||||
|
||||
connector_id
|
||||
.and_then(|connector_id| apps_config.apps.get(connector_id))
|
||||
.map(|app| app.enabled)
|
||||
.unwrap_or(default_enabled)
|
||||
}
|
||||
|
||||
fn app_tool_policy_from_apps_config(
|
||||
apps_config: Option<&AppsConfigToml>,
|
||||
connector_id: Option<&str>,
|
||||
tool_name: &str,
|
||||
tool_title: Option<&str>,
|
||||
annotations: Option<&ToolAnnotations>,
|
||||
) -> AppToolPolicy {
|
||||
let Some(apps_config) = apps_config else {
|
||||
return AppToolPolicy::default();
|
||||
};
|
||||
|
||||
let app = connector_id.and_then(|connector_id| apps_config.apps.get(connector_id));
|
||||
let tools = app.and_then(|app| app.tools.as_ref());
|
||||
let tool_config = tools.and_then(|tools| {
|
||||
tools
|
||||
.tools
|
||||
.get(tool_name)
|
||||
.or_else(|| tool_title.and_then(|title| tools.tools.get(title)))
|
||||
});
|
||||
let approval = tool_config
|
||||
.and_then(|tool| tool.approval_mode)
|
||||
.or_else(|| app.and_then(|app| app.default_tools_approval_mode))
|
||||
.unwrap_or(AppToolApproval::Auto);
|
||||
|
||||
if !app_is_enabled(apps_config, connector_id) {
|
||||
return AppToolPolicy {
|
||||
enabled: false,
|
||||
approval,
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(enabled) = tool_config.and_then(|tool| tool.enabled) {
|
||||
return AppToolPolicy { enabled, approval };
|
||||
}
|
||||
|
||||
if let Some(enabled) = app.and_then(|app| app.default_tools_enabled) {
|
||||
return AppToolPolicy { enabled, approval };
|
||||
}
|
||||
|
||||
let app_defaults = apps_config.default.as_ref();
|
||||
let destructive_enabled = app
|
||||
.and_then(|app| app.destructive_enabled)
|
||||
.unwrap_or_else(|| {
|
||||
app_defaults
|
||||
.map(|defaults| defaults.destructive_enabled)
|
||||
.unwrap_or(true)
|
||||
});
|
||||
let open_world_enabled = app
|
||||
.and_then(|app| app.open_world_enabled)
|
||||
.unwrap_or_else(|| {
|
||||
app_defaults
|
||||
.map(|defaults| defaults.open_world_enabled)
|
||||
.unwrap_or(true)
|
||||
});
|
||||
let destructive_hint = annotations
|
||||
.and_then(|annotations| annotations.destructive_hint)
|
||||
.unwrap_or(false);
|
||||
let open_world_hint = annotations
|
||||
.and_then(|annotations| annotations.open_world_hint)
|
||||
.unwrap_or(false);
|
||||
let enabled =
|
||||
(destructive_enabled || !destructive_hint) && (open_world_enabled || !open_world_hint);
|
||||
|
||||
AppToolPolicy { enabled, approval }
|
||||
}
|
||||
|
||||
fn collect_accessible_connectors<I>(tools: I) -> Vec<AppInfo>
|
||||
where
|
||||
I: IntoIterator<Item = (String, Option<String>)>,
|
||||
@@ -368,3 +607,406 @@ pub fn connector_name_slug(name: &str) -> String {
|
||||
fn format_connector_label(name: &str, _id: &str) -> String {
|
||||
name.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::types::AppConfig;
|
||||
use crate::config::types::AppToolConfig;
|
||||
use crate::config::types::AppToolsConfig;
|
||||
use crate::config::types::AppsDefaultConfig;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn annotations(
|
||||
destructive_hint: Option<bool>,
|
||||
open_world_hint: Option<bool>,
|
||||
) -> ToolAnnotations {
|
||||
ToolAnnotations {
|
||||
destructive_hint,
|
||||
idempotent_hint: None,
|
||||
open_world_hint,
|
||||
read_only_hint: None,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn app(id: &str) -> AppInfo {
|
||||
AppInfo {
|
||||
id: id.to_string(),
|
||||
name: id.to_string(),
|
||||
description: None,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
install_url: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_tool_policy_uses_global_defaults_for_destructive_hints() {
|
||||
let apps_config = AppsConfigToml {
|
||||
default: Some(AppsDefaultConfig {
|
||||
enabled: true,
|
||||
destructive_enabled: false,
|
||||
open_world_enabled: true,
|
||||
}),
|
||||
apps: HashMap::new(),
|
||||
};
|
||||
|
||||
let policy = app_tool_policy_from_apps_config(
|
||||
Some(&apps_config),
|
||||
Some("calendar"),
|
||||
"events/create",
|
||||
None,
|
||||
Some(&annotations(Some(true), None)),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
policy,
|
||||
AppToolPolicy {
|
||||
enabled: false,
|
||||
approval: AppToolApproval::Auto,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_is_enabled_uses_default_for_unconfigured_apps() {
|
||||
let apps_config = AppsConfigToml {
|
||||
default: Some(AppsDefaultConfig {
|
||||
enabled: false,
|
||||
destructive_enabled: true,
|
||||
open_world_enabled: true,
|
||||
}),
|
||||
apps: HashMap::new(),
|
||||
};
|
||||
|
||||
assert!(!app_is_enabled(&apps_config, Some("calendar")));
|
||||
assert!(!app_is_enabled(&apps_config, None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_is_enabled_prefers_per_app_override_over_default() {
|
||||
let apps_config = AppsConfigToml {
|
||||
default: Some(AppsDefaultConfig {
|
||||
enabled: false,
|
||||
destructive_enabled: true,
|
||||
open_world_enabled: true,
|
||||
}),
|
||||
apps: HashMap::from([(
|
||||
"calendar".to_string(),
|
||||
AppConfig {
|
||||
enabled: true,
|
||||
destructive_enabled: None,
|
||||
open_world_enabled: None,
|
||||
default_tools_approval_mode: None,
|
||||
default_tools_enabled: None,
|
||||
tools: None,
|
||||
},
|
||||
)]),
|
||||
};
|
||||
|
||||
assert!(app_is_enabled(&apps_config, Some("calendar")));
|
||||
assert!(!app_is_enabled(&apps_config, Some("drive")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_tool_policy_honors_default_app_enabled_false() {
|
||||
let apps_config = AppsConfigToml {
|
||||
default: Some(AppsDefaultConfig {
|
||||
enabled: false,
|
||||
destructive_enabled: true,
|
||||
open_world_enabled: true,
|
||||
}),
|
||||
apps: HashMap::new(),
|
||||
};
|
||||
|
||||
let policy = app_tool_policy_from_apps_config(
|
||||
Some(&apps_config),
|
||||
Some("calendar"),
|
||||
"events/list",
|
||||
None,
|
||||
Some(&annotations(None, None)),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
policy,
|
||||
AppToolPolicy {
|
||||
enabled: false,
|
||||
approval: AppToolApproval::Auto,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_tool_policy_allows_per_app_enable_when_default_is_disabled() {
|
||||
let apps_config = AppsConfigToml {
|
||||
default: Some(AppsDefaultConfig {
|
||||
enabled: false,
|
||||
destructive_enabled: true,
|
||||
open_world_enabled: true,
|
||||
}),
|
||||
apps: HashMap::from([(
|
||||
"calendar".to_string(),
|
||||
AppConfig {
|
||||
enabled: true,
|
||||
destructive_enabled: None,
|
||||
open_world_enabled: None,
|
||||
default_tools_approval_mode: None,
|
||||
default_tools_enabled: None,
|
||||
tools: None,
|
||||
},
|
||||
)]),
|
||||
};
|
||||
|
||||
let policy = app_tool_policy_from_apps_config(
|
||||
Some(&apps_config),
|
||||
Some("calendar"),
|
||||
"events/list",
|
||||
None,
|
||||
Some(&annotations(None, None)),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
policy,
|
||||
AppToolPolicy {
|
||||
enabled: true,
|
||||
approval: AppToolApproval::Auto,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_tool_policy_per_tool_enabled_true_overrides_app_level_disable_flags() {
|
||||
let apps_config = AppsConfigToml {
|
||||
default: None,
|
||||
apps: HashMap::from([(
|
||||
"calendar".to_string(),
|
||||
AppConfig {
|
||||
enabled: true,
|
||||
destructive_enabled: Some(false),
|
||||
open_world_enabled: Some(false),
|
||||
default_tools_approval_mode: None,
|
||||
default_tools_enabled: None,
|
||||
tools: Some(AppToolsConfig {
|
||||
tools: HashMap::from([(
|
||||
"events/create".to_string(),
|
||||
AppToolConfig {
|
||||
enabled: Some(true),
|
||||
approval_mode: None,
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
},
|
||||
)]),
|
||||
};
|
||||
|
||||
let policy = app_tool_policy_from_apps_config(
|
||||
Some(&apps_config),
|
||||
Some("calendar"),
|
||||
"events/create",
|
||||
None,
|
||||
Some(&annotations(Some(true), Some(true))),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
policy,
|
||||
AppToolPolicy {
|
||||
enabled: true,
|
||||
approval: AppToolApproval::Auto,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_tool_policy_default_tools_enabled_true_overrides_app_level_tool_hints() {
|
||||
let apps_config = AppsConfigToml {
|
||||
default: None,
|
||||
apps: HashMap::from([(
|
||||
"calendar".to_string(),
|
||||
AppConfig {
|
||||
enabled: true,
|
||||
destructive_enabled: Some(false),
|
||||
open_world_enabled: Some(false),
|
||||
default_tools_approval_mode: None,
|
||||
default_tools_enabled: Some(true),
|
||||
tools: None,
|
||||
},
|
||||
)]),
|
||||
};
|
||||
|
||||
let policy = app_tool_policy_from_apps_config(
|
||||
Some(&apps_config),
|
||||
Some("calendar"),
|
||||
"events/create",
|
||||
None,
|
||||
Some(&annotations(Some(true), Some(true))),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
policy,
|
||||
AppToolPolicy {
|
||||
enabled: true,
|
||||
approval: AppToolApproval::Auto,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_tool_policy_default_tools_enabled_false_overrides_app_level_tool_hints() {
|
||||
let apps_config = AppsConfigToml {
|
||||
default: None,
|
||||
apps: HashMap::from([(
|
||||
"calendar".to_string(),
|
||||
AppConfig {
|
||||
enabled: true,
|
||||
destructive_enabled: Some(true),
|
||||
open_world_enabled: Some(true),
|
||||
default_tools_approval_mode: Some(AppToolApproval::Approve),
|
||||
default_tools_enabled: Some(false),
|
||||
tools: None,
|
||||
},
|
||||
)]),
|
||||
};
|
||||
|
||||
let policy = app_tool_policy_from_apps_config(
|
||||
Some(&apps_config),
|
||||
Some("calendar"),
|
||||
"events/list",
|
||||
None,
|
||||
Some(&annotations(None, None)),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
policy,
|
||||
AppToolPolicy {
|
||||
enabled: false,
|
||||
approval: AppToolApproval::Approve,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_tool_policy_uses_default_tools_approval_mode() {
|
||||
let apps_config = AppsConfigToml {
|
||||
default: None,
|
||||
apps: HashMap::from([(
|
||||
"calendar".to_string(),
|
||||
AppConfig {
|
||||
enabled: true,
|
||||
destructive_enabled: None,
|
||||
open_world_enabled: None,
|
||||
default_tools_approval_mode: Some(AppToolApproval::Prompt),
|
||||
default_tools_enabled: None,
|
||||
tools: Some(AppToolsConfig {
|
||||
tools: HashMap::new(),
|
||||
}),
|
||||
},
|
||||
)]),
|
||||
};
|
||||
|
||||
let policy = app_tool_policy_from_apps_config(
|
||||
Some(&apps_config),
|
||||
Some("calendar"),
|
||||
"events/list",
|
||||
None,
|
||||
Some(&annotations(None, None)),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
policy,
|
||||
AppToolPolicy {
|
||||
enabled: true,
|
||||
approval: AppToolApproval::Prompt,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_tool_policy_matches_prefix_stripped_tool_name_for_tool_config() {
|
||||
let apps_config = AppsConfigToml {
|
||||
default: None,
|
||||
apps: HashMap::from([(
|
||||
"calendar".to_string(),
|
||||
AppConfig {
|
||||
enabled: true,
|
||||
destructive_enabled: Some(false),
|
||||
open_world_enabled: Some(false),
|
||||
default_tools_approval_mode: Some(AppToolApproval::Auto),
|
||||
default_tools_enabled: Some(false),
|
||||
tools: Some(AppToolsConfig {
|
||||
tools: HashMap::from([(
|
||||
"events/create".to_string(),
|
||||
AppToolConfig {
|
||||
enabled: Some(true),
|
||||
approval_mode: Some(AppToolApproval::Approve),
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
},
|
||||
)]),
|
||||
};
|
||||
|
||||
let policy = app_tool_policy_from_apps_config(
|
||||
Some(&apps_config),
|
||||
Some("calendar"),
|
||||
"calendar_events/create",
|
||||
Some("events/create"),
|
||||
Some(&annotations(Some(true), Some(true))),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
policy,
|
||||
AppToolPolicy {
|
||||
enabled: true,
|
||||
approval: AppToolApproval::Approve,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_disallowed_connectors_allows_non_disallowed_connectors() {
|
||||
let filtered = filter_disallowed_connectors(vec![app("asdk_app_hidden"), app("alpha")]);
|
||||
assert_eq!(filtered, vec![app("asdk_app_hidden"), app("alpha")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_disallowed_connectors_filters_openai_prefix() {
|
||||
let filtered = filter_disallowed_connectors(vec![
|
||||
app("connector_openai_foo"),
|
||||
app("connector_openai_bar"),
|
||||
app("gamma"),
|
||||
]);
|
||||
assert_eq!(filtered, vec![app("gamma")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_disallowed_connectors_filters_disallowed_connector_ids() {
|
||||
let filtered = filter_disallowed_connectors(vec![
|
||||
app("asdk_app_6938a94a61d881918ef32cb999ff937c"),
|
||||
app("delta"),
|
||||
]);
|
||||
assert_eq!(filtered, vec![app("delta")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_party_chat_originator_filters_target_and_openai_prefixed_connectors() {
|
||||
let filtered = filter_disallowed_connectors_for_originator(
|
||||
vec![
|
||||
app("connector_openai_foo"),
|
||||
app("asdk_app_6938a94a61d881918ef32cb999ff937c"),
|
||||
app("connector_0f9c9d4592e54d0a9a12b3f44a1e2010"),
|
||||
],
|
||||
"codex_atlas",
|
||||
);
|
||||
assert_eq!(
|
||||
filtered,
|
||||
vec![app("asdk_app_6938a94a61d881918ef32cb999ff937c")]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ use tokio::io::AsyncReadExt;
|
||||
use tokio::io::BufReader;
|
||||
use tokio::process::Child;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::error::CodexErr;
|
||||
use crate::error::Result;
|
||||
@@ -67,7 +66,6 @@ pub struct ExecParams {
|
||||
pub expiration: ExecExpiration,
|
||||
pub env: HashMap<String, String>,
|
||||
pub network: Option<NetworkProxy>,
|
||||
pub network_attempt_id: Option<Uuid>,
|
||||
pub sandbox_permissions: SandboxPermissions,
|
||||
pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel,
|
||||
pub justification: Option<String>,
|
||||
@@ -186,15 +184,13 @@ pub async fn process_exec_tool_call(
|
||||
mut env,
|
||||
expiration,
|
||||
network,
|
||||
network_attempt_id,
|
||||
sandbox_permissions,
|
||||
windows_sandbox_level,
|
||||
justification,
|
||||
arg0: _,
|
||||
} = params;
|
||||
let network_attempt_id = network_attempt_id.map(|attempt_id| attempt_id.to_string());
|
||||
if let Some(network) = network.as_ref() {
|
||||
network.apply_to_env_for_attempt(&mut env, network_attempt_id.as_deref());
|
||||
network.apply_to_env(&mut env);
|
||||
}
|
||||
let (program, args) = command.split_first().ok_or_else(|| {
|
||||
CodexErr::Io(io::Error::new(
|
||||
@@ -242,7 +238,6 @@ pub(crate) async fn execute_exec_env(
|
||||
cwd,
|
||||
env,
|
||||
network,
|
||||
network_attempt_id,
|
||||
expiration,
|
||||
sandbox,
|
||||
windows_sandbox_level,
|
||||
@@ -251,18 +246,12 @@ pub(crate) async fn execute_exec_env(
|
||||
arg0,
|
||||
} = env;
|
||||
|
||||
let network_attempt_id = match network_attempt_id.as_deref() {
|
||||
Some(attempt_id) => Uuid::parse_str(attempt_id).ok(),
|
||||
None => network.as_ref().map(|_| Uuid::new_v4()),
|
||||
};
|
||||
|
||||
let params = ExecParams {
|
||||
command,
|
||||
cwd,
|
||||
expiration,
|
||||
env,
|
||||
network: network.clone(),
|
||||
network_attempt_id,
|
||||
sandbox_permissions,
|
||||
windows_sandbox_level,
|
||||
justification,
|
||||
@@ -356,14 +345,12 @@ async fn exec_windows_sandbox(
|
||||
cwd,
|
||||
mut env,
|
||||
network,
|
||||
network_attempt_id,
|
||||
expiration,
|
||||
windows_sandbox_level,
|
||||
..
|
||||
} = params;
|
||||
let network_attempt_id = network_attempt_id.map(|attempt_id| attempt_id.to_string());
|
||||
if let Some(network) = network.as_ref() {
|
||||
network.apply_to_env_for_attempt(&mut env, network_attempt_id.as_deref());
|
||||
network.apply_to_env(&mut env);
|
||||
}
|
||||
|
||||
// TODO(iceweasel-oai): run_windows_sandbox_capture should support all
|
||||
@@ -717,16 +704,13 @@ async fn exec(
|
||||
cwd,
|
||||
mut env,
|
||||
network,
|
||||
network_attempt_id,
|
||||
arg0,
|
||||
expiration,
|
||||
windows_sandbox_level: _,
|
||||
..
|
||||
} = params;
|
||||
let network_attempt_id = network_attempt_id.map(|attempt_id| attempt_id.to_string());
|
||||
|
||||
if let Some(network) = network.as_ref() {
|
||||
network.apply_to_env_for_attempt(&mut env, network_attempt_id.as_deref());
|
||||
network.apply_to_env(&mut env);
|
||||
}
|
||||
|
||||
let (program, args) = command.split_first().ok_or_else(|| {
|
||||
@@ -1137,7 +1121,6 @@ mod tests {
|
||||
expiration: 500.into(),
|
||||
env,
|
||||
network: None,
|
||||
network_attempt_id: None,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled,
|
||||
justification: None,
|
||||
@@ -1191,7 +1174,6 @@ mod tests {
|
||||
expiration: ExecExpiration::Cancellation(cancel_token),
|
||||
env,
|
||||
network: None,
|
||||
network_attempt_id: None,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled,
|
||||
justification: None,
|
||||
|
||||
@@ -24,13 +24,14 @@ use crate::features::Feature;
|
||||
use crate::mcp::auth::compute_auth_statuses;
|
||||
use crate::mcp_connection_manager::McpConnectionManager;
|
||||
use crate::mcp_connection_manager::SandboxState;
|
||||
use crate::mcp_connection_manager::codex_apps_tools_cache_key;
|
||||
|
||||
const MCP_TOOL_NAME_PREFIX: &str = "mcp";
|
||||
const MCP_TOOL_NAME_DELIMITER: &str = "__";
|
||||
pub(crate) const CODEX_APPS_MCP_SERVER_NAME: &str = "codex_apps";
|
||||
const CODEX_CONNECTORS_TOKEN_ENV_VAR: &str = "CODEX_CONNECTORS_TOKEN";
|
||||
const OPENAI_CONNECTORS_MCP_BASE_URL: &str = "https://api.openai.com";
|
||||
const OPENAI_CONNECTORS_MCP_PATH: &str = "/v1/connectors/mcp/";
|
||||
const OPENAI_CONNECTORS_MCP_PATH: &str = "/v1/connectors/gateways/flat/mcp";
|
||||
|
||||
// Legacy vs new MCP gateway
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -208,6 +209,8 @@ pub async fn collect_mcp_snapshot(config: &Config) -> McpListToolsResponseEvent
|
||||
&config.permissions.approval_policy,
|
||||
tx_event,
|
||||
sandbox_state,
|
||||
config.codex_home.clone(),
|
||||
codex_apps_tools_cache_key(auth.as_ref()),
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,8 @@ use crate::analytics_client::AppInvocation;
|
||||
use crate::analytics_client::build_track_events_context;
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::config::types::AppToolApproval;
|
||||
use crate::connectors;
|
||||
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use crate::protocol::EventMsg;
|
||||
use crate::protocol::McpInvocation;
|
||||
@@ -64,9 +66,51 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
arguments: arguments_value.clone(),
|
||||
};
|
||||
|
||||
if let Some(decision) =
|
||||
maybe_request_mcp_tool_approval(sess.as_ref(), turn_context, &call_id, &server, &tool_name)
|
||||
.await
|
||||
let metadata = lookup_mcp_tool_metadata(sess.as_ref(), &server, &tool_name).await;
|
||||
let app_tool_policy = if server == CODEX_APPS_MCP_SERVER_NAME {
|
||||
connectors::app_tool_policy(
|
||||
&turn_context.config,
|
||||
metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.connector_id.as_deref()),
|
||||
&tool_name,
|
||||
metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.tool_title.as_deref()),
|
||||
metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.annotations.as_ref()),
|
||||
)
|
||||
} else {
|
||||
connectors::AppToolPolicy::default()
|
||||
};
|
||||
|
||||
if server == CODEX_APPS_MCP_SERVER_NAME && !app_tool_policy.enabled {
|
||||
let result = notify_mcp_tool_call_skip(
|
||||
sess.as_ref(),
|
||||
turn_context,
|
||||
&call_id,
|
||||
invocation,
|
||||
"MCP tool call blocked by app configuration".to_string(),
|
||||
)
|
||||
.await;
|
||||
let status = if result.is_ok() { "ok" } else { "error" };
|
||||
turn_context
|
||||
.otel_manager
|
||||
.counter("codex.mcp.call", 1, &[("status", status)]);
|
||||
return ResponseInputItem::McpToolCallOutput { call_id, result };
|
||||
}
|
||||
|
||||
if let Some(decision) = maybe_request_mcp_tool_approval(
|
||||
sess.as_ref(),
|
||||
turn_context,
|
||||
&call_id,
|
||||
&server,
|
||||
&tool_name,
|
||||
metadata.as_ref(),
|
||||
app_tool_policy.approval,
|
||||
)
|
||||
.await
|
||||
{
|
||||
let result = match decision {
|
||||
McpToolApprovalDecision::Accept | McpToolApprovalDecision::AcceptAndRemember => {
|
||||
@@ -267,7 +311,7 @@ enum McpToolApprovalDecision {
|
||||
}
|
||||
|
||||
struct McpToolApprovalMetadata {
|
||||
annotations: ToolAnnotations,
|
||||
annotations: Option<ToolAnnotations>,
|
||||
connector_id: Option<String>,
|
||||
connector_name: Option<String>,
|
||||
tool_title: Option<String>,
|
||||
@@ -292,21 +336,39 @@ async fn maybe_request_mcp_tool_approval(
|
||||
call_id: &str,
|
||||
server: &str,
|
||||
tool_name: &str,
|
||||
metadata: Option<&McpToolApprovalMetadata>,
|
||||
approval_mode: AppToolApproval,
|
||||
) -> Option<McpToolApprovalDecision> {
|
||||
if is_full_access_mode(turn_context) {
|
||||
if approval_mode == AppToolApproval::Approve {
|
||||
return None;
|
||||
}
|
||||
let annotations = metadata.and_then(|metadata| metadata.annotations.as_ref());
|
||||
if approval_mode == AppToolApproval::Auto {
|
||||
if is_full_access_mode(turn_context) {
|
||||
return None;
|
||||
}
|
||||
if !annotations.is_some_and(requires_mcp_tool_approval) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let metadata = lookup_mcp_tool_metadata(sess, server, tool_name).await?;
|
||||
if !requires_mcp_tool_approval(&metadata.annotations) {
|
||||
return None;
|
||||
}
|
||||
let approval_key = McpToolApprovalKey {
|
||||
server: server.to_string(),
|
||||
connector_id: metadata.connector_id.clone(),
|
||||
tool_name: tool_name.to_string(),
|
||||
let approval_key = if approval_mode == AppToolApproval::Auto {
|
||||
let connector_id = metadata.and_then(|metadata| metadata.connector_id.clone());
|
||||
if server == CODEX_APPS_MCP_SERVER_NAME && connector_id.is_none() {
|
||||
None
|
||||
} else {
|
||||
Some(McpToolApprovalKey {
|
||||
server: server.to_string(),
|
||||
connector_id,
|
||||
tool_name: tool_name.to_string(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if mcp_tool_approval_is_remembered(sess, &approval_key).await {
|
||||
if let Some(key) = approval_key.as_ref()
|
||||
&& mcp_tool_approval_is_remembered(sess, key).await
|
||||
{
|
||||
return Some(McpToolApprovalDecision::Accept);
|
||||
}
|
||||
|
||||
@@ -315,10 +377,10 @@ async fn maybe_request_mcp_tool_approval(
|
||||
question_id.clone(),
|
||||
server,
|
||||
tool_name,
|
||||
metadata.tool_title.as_deref(),
|
||||
metadata.connector_name.as_deref(),
|
||||
&metadata.annotations,
|
||||
true,
|
||||
metadata.and_then(|metadata| metadata.tool_title.as_deref()),
|
||||
metadata.and_then(|metadata| metadata.connector_name.as_deref()),
|
||||
annotations,
|
||||
approval_key.is_some(),
|
||||
);
|
||||
let args = RequestUserInputArgs {
|
||||
questions: vec![question],
|
||||
@@ -326,9 +388,14 @@ async fn maybe_request_mcp_tool_approval(
|
||||
let response = sess
|
||||
.request_user_input(turn_context, call_id.to_string(), args)
|
||||
.await;
|
||||
let decision = parse_mcp_tool_approval_response(response, &question_id);
|
||||
if matches!(decision, McpToolApprovalDecision::AcceptAndRemember) {
|
||||
remember_mcp_tool_approval(sess, approval_key).await;
|
||||
let decision = normalize_approval_decision_for_mode(
|
||||
parse_mcp_tool_approval_response(response, &question_id),
|
||||
approval_mode,
|
||||
);
|
||||
if matches!(decision, McpToolApprovalDecision::AcceptAndRemember)
|
||||
&& let Some(key) = approval_key
|
||||
{
|
||||
remember_mcp_tool_approval(sess, key).await;
|
||||
}
|
||||
Some(decision)
|
||||
}
|
||||
@@ -356,15 +423,12 @@ async fn lookup_mcp_tool_metadata(
|
||||
|
||||
tools.into_values().find_map(|tool_info| {
|
||||
if tool_info.server_name == server && tool_info.tool_name == tool_name {
|
||||
tool_info
|
||||
.tool
|
||||
.annotations
|
||||
.map(|annotations| McpToolApprovalMetadata {
|
||||
annotations,
|
||||
connector_id: tool_info.connector_id,
|
||||
connector_name: tool_info.connector_name,
|
||||
tool_title: tool_info.tool.title,
|
||||
})
|
||||
Some(McpToolApprovalMetadata {
|
||||
annotations: tool_info.tool.annotations,
|
||||
connector_id: tool_info.connector_id,
|
||||
connector_name: tool_info.connector_name,
|
||||
tool_title: tool_info.tool.title,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -402,11 +466,12 @@ fn build_mcp_tool_approval_question(
|
||||
tool_name: &str,
|
||||
tool_title: Option<&str>,
|
||||
connector_name: Option<&str>,
|
||||
annotations: &ToolAnnotations,
|
||||
annotations: Option<&ToolAnnotations>,
|
||||
allow_remember_option: bool,
|
||||
) -> RequestUserInputQuestion {
|
||||
let destructive = annotations.destructive_hint == Some(true);
|
||||
let open_world = annotations.open_world_hint == Some(true);
|
||||
let destructive =
|
||||
annotations.and_then(|annotations| annotations.destructive_hint) == Some(true);
|
||||
let open_world = annotations.and_then(|annotations| annotations.open_world_hint) == Some(true);
|
||||
let reason = match (destructive, open_world) {
|
||||
(true, true) => "may modify data and access external systems",
|
||||
(true, false) => "may modify or delete data",
|
||||
@@ -493,6 +558,19 @@ fn parse_mcp_tool_approval_response(
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_approval_decision_for_mode(
|
||||
decision: McpToolApprovalDecision,
|
||||
approval_mode: AppToolApproval,
|
||||
) -> McpToolApprovalDecision {
|
||||
if approval_mode == AppToolApproval::Prompt
|
||||
&& decision == McpToolApprovalDecision::AcceptAndRemember
|
||||
{
|
||||
McpToolApprovalDecision::Accept
|
||||
} else {
|
||||
decision
|
||||
}
|
||||
}
|
||||
|
||||
async fn mcp_tool_approval_is_remembered(sess: &Session, key: &McpToolApprovalKey) -> bool {
|
||||
let store = sess.services.tool_approvals.lock().await;
|
||||
matches!(store.get(key), Some(ReviewDecision::ApprovedForSession))
|
||||
@@ -504,8 +582,11 @@ async fn remember_mcp_tool_approval(sess: &Session, key: McpToolApprovalKey) {
|
||||
}
|
||||
|
||||
fn requires_mcp_tool_approval(annotations: &ToolAnnotations) -> bool {
|
||||
annotations.read_only_hint == Some(false)
|
||||
&& (annotations.destructive_hint == Some(true) || annotations.open_world_hint == Some(true))
|
||||
if annotations.destructive_hint == Some(true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
annotations.read_only_hint == Some(false) && annotations.open_world_hint == Some(true)
|
||||
}
|
||||
|
||||
async fn notify_mcp_tool_call_skip(
|
||||
@@ -563,9 +644,20 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn approval_not_required_when_read_only_true() {
|
||||
fn approval_required_when_destructive_even_if_read_only_true() {
|
||||
let annotations = annotations(Some(true), Some(true), Some(true));
|
||||
assert_eq!(requires_mcp_tool_approval(&annotations), false);
|
||||
assert_eq!(requires_mcp_tool_approval(&annotations), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_mode_does_not_allow_session_remember() {
|
||||
assert_eq!(
|
||||
normalize_approval_decision_for_mode(
|
||||
McpToolApprovalDecision::AcceptAndRemember,
|
||||
AppToolApproval::Prompt,
|
||||
),
|
||||
McpToolApprovalDecision::Accept
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -576,7 +668,7 @@ mod tests {
|
||||
"run_action",
|
||||
Some("Run Action"),
|
||||
None,
|
||||
&annotations(Some(false), Some(true), None),
|
||||
Some(&annotations(Some(false), Some(true), None)),
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -603,7 +695,7 @@ mod tests {
|
||||
"run_action",
|
||||
Some("Run Action"),
|
||||
None,
|
||||
&annotations(Some(false), Some(true), None),
|
||||
Some(&annotations(Some(false), Some(true), None)),
|
||||
true,
|
||||
);
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ mod storage;
|
||||
mod tests;
|
||||
pub(crate) mod usage;
|
||||
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
|
||||
/// Starts the memory startup pipeline for eligible root sessions.
|
||||
/// This is the single entrypoint that `codex` uses to trigger memory startup.
|
||||
///
|
||||
@@ -28,6 +30,8 @@ mod artifacts {
|
||||
mod phase_one {
|
||||
/// Default model used for phase 1.
|
||||
pub(super) const MODEL: &str = "gpt-5.1-codex-mini";
|
||||
/// Default reasoning effort used for phase 1.
|
||||
pub(super) const REASONING_EFFORT: super::ReasoningEffort = super::ReasoningEffort::Low;
|
||||
/// Prompt used for phase 1.
|
||||
pub(super) const PROMPT: &str = include_str!("../../templates/memories/stage_one_system.md");
|
||||
/// Concurrency cap for startup memory extraction and consolidation scheduling.
|
||||
@@ -56,6 +60,8 @@ mod phase_one {
|
||||
mod phase_two {
|
||||
/// Default model used for phase 2.
|
||||
pub(super) const MODEL: &str = "gpt-5.3-codex";
|
||||
/// Default reasoning effort used for phase 2.
|
||||
pub(super) const REASONING_EFFORT: super::ReasoningEffort = super::ReasoningEffort::Medium;
|
||||
/// Lease duration (seconds) for phase-2 consolidation job ownership.
|
||||
pub(super) const JOB_LEASE_SECONDS: i64 = 3_600;
|
||||
/// Backoff delay (seconds) before retrying a failed phase-2 consolidation
|
||||
|
||||
@@ -20,7 +20,7 @@ use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
use codex_utils_sanitizer::redact_secrets;
|
||||
use codex_secrets::redact_secrets;
|
||||
use futures::StreamExt;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
@@ -142,7 +142,7 @@ impl RequestContext {
|
||||
model_info,
|
||||
turn_metadata_header,
|
||||
otel_manager: turn_context.otel_manager.clone(),
|
||||
reasoning_effort: turn_context.reasoning_effort,
|
||||
reasoning_effort: Some(phase_one::REASONING_EFFORT),
|
||||
reasoning_summary: turn_context.reasoning_summary,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::agent::AgentStatus;
|
||||
use crate::agent::status::is_final as is_final_agent_status;
|
||||
use crate::codex::Session;
|
||||
use crate::config::Config;
|
||||
use crate::features::Feature;
|
||||
use crate::memories::memory_root;
|
||||
use crate::memories::metrics;
|
||||
use crate::memories::phase_two;
|
||||
@@ -227,6 +228,8 @@ mod agent {
|
||||
agent_config.cwd = root;
|
||||
// Approval policy
|
||||
agent_config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never);
|
||||
// Consolidation runs as an internal sub-agent and must not recursively delegate.
|
||||
agent_config.features.disable(Feature::Collab);
|
||||
|
||||
// Sandbox policy
|
||||
let mut writable_roots = Vec::new();
|
||||
@@ -258,6 +261,7 @@ mod agent {
|
||||
.clone()
|
||||
.unwrap_or(phase_two::MODEL.to_string()),
|
||||
);
|
||||
agent_config.model_reasoning_effort = Some(phase_two::REASONING_EFFORT);
|
||||
|
||||
Some(agent_config)
|
||||
}
|
||||
|
||||
@@ -82,11 +82,11 @@ async fn rebuild_raw_memories_file(
|
||||
)
|
||||
.map_err(raw_memories_format_error)?;
|
||||
writeln!(body, "cwd: {}", memory.cwd.display()).map_err(raw_memories_format_error)?;
|
||||
writeln!(body).map_err(raw_memories_format_error)?;
|
||||
let rollout_summary_file = format!("{}.md", rollout_summary_file_stem(memory));
|
||||
let raw_memory =
|
||||
replace_rollout_summary_file_in_raw_memory(&memory.raw_memory, &rollout_summary_file);
|
||||
body.push_str(raw_memory.trim());
|
||||
writeln!(body, "rollout_summary_file: {rollout_summary_file}")
|
||||
.map_err(raw_memories_format_error)?;
|
||||
writeln!(body).map_err(raw_memories_format_error)?;
|
||||
body.push_str(memory.raw_memory.trim());
|
||||
body.push_str("\n\n");
|
||||
}
|
||||
|
||||
@@ -161,26 +161,6 @@ fn rollout_summary_format_error(err: std::fmt::Error) -> std::io::Error {
|
||||
std::io::Error::other(format!("format rollout summary: {err}"))
|
||||
}
|
||||
|
||||
fn replace_rollout_summary_file_in_raw_memory(
|
||||
raw_memory: &str,
|
||||
rollout_summary_file: &str,
|
||||
) -> String {
|
||||
const ROLLOUT_SUMMARY_PREFIX: &str = "rollout_summary_file: ";
|
||||
|
||||
let replacement = format!("rollout_summary_file: {rollout_summary_file}");
|
||||
raw_memory
|
||||
.split('\n')
|
||||
.map(|line| {
|
||||
if line.starts_with(ROLLOUT_SUMMARY_PREFIX) {
|
||||
replacement.as_str()
|
||||
} else {
|
||||
line
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
pub(crate) fn rollout_summary_file_stem(memory: &Stage1Output) -> String {
|
||||
rollout_summary_file_stem_from_parts(
|
||||
memory.thread_id,
|
||||
@@ -270,7 +250,6 @@ pub(super) fn rollout_summary_file_stem_from_parts(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::replace_rollout_summary_file_in_raw_memory;
|
||||
use super::rollout_summary_file_stem;
|
||||
use super::rollout_summary_file_stem_from_parts;
|
||||
use chrono::TimeZone;
|
||||
@@ -339,71 +318,4 @@ mod tests {
|
||||
|
||||
assert_eq!(rollout_summary_file_stem(&memory), FIXED_PREFIX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_rollout_summary_file_in_raw_memory_replaces_existing_value() {
|
||||
let raw_memory = "\
|
||||
---
|
||||
rollout_summary_file: wrong.md
|
||||
description: demo
|
||||
keywords: one, two
|
||||
---
|
||||
- body line";
|
||||
let normalized = replace_rollout_summary_file_in_raw_memory(
|
||||
raw_memory,
|
||||
"2025-01-01T00-00-00-abcd-demo.md",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
normalized,
|
||||
"\
|
||||
---
|
||||
rollout_summary_file: 2025-01-01T00-00-00-abcd-demo.md
|
||||
description: demo
|
||||
keywords: one, two
|
||||
---
|
||||
- body line"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_rollout_summary_file_in_raw_memory_replaces_placeholder() {
|
||||
let raw_memory = "\
|
||||
---
|
||||
rollout_summary_file: <system_populated_file.md>
|
||||
description: demo
|
||||
keywords: one, two
|
||||
---
|
||||
- body line";
|
||||
let normalized = replace_rollout_summary_file_in_raw_memory(
|
||||
raw_memory,
|
||||
"2025-01-01T00-00-00-abcd-demo.md",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
normalized,
|
||||
"\
|
||||
---
|
||||
rollout_summary_file: 2025-01-01T00-00-00-abcd-demo.md
|
||||
description: demo
|
||||
keywords: one, two
|
||||
---
|
||||
- body line"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_rollout_summary_file_in_raw_memory_leaves_text_without_field_unchanged() {
|
||||
let raw_memory = "\
|
||||
---
|
||||
description: demo
|
||||
keywords: one, two
|
||||
---
|
||||
- body line";
|
||||
let normalized = replace_rollout_summary_file_in_raw_memory(
|
||||
raw_memory,
|
||||
"2025-01-01T00-00-00-abcd-demo.md",
|
||||
);
|
||||
assert_eq!(normalized, raw_memory);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user