Merge remote-tracking branch 'origin/main' into jif/diff-based-phase-2

# Conflicts:
#	codex-rs/Cargo.lock
This commit is contained in:
jif-oai
2026-04-21 15:16:06 +01:00
455 changed files with 30996 additions and 16498 deletions
+38 -14
View File
@@ -1473,6 +1473,7 @@ dependencies = [
"codex-git-utils",
"codex-login",
"codex-mcp",
"codex-model-provider-info",
"codex-models-manager",
"codex-otel",
"codex-protocol",
@@ -1527,6 +1528,7 @@ dependencies = [
"codex-app-server",
"codex-app-server-protocol",
"codex-arg0",
"codex-config",
"codex-core",
"codex-exec-server",
"codex-feedback",
@@ -1875,6 +1877,7 @@ name = "codex-config"
version = "0.0.0"
dependencies = [
"anyhow",
"async-trait",
"codex-app-server-protocol",
"codex-execpolicy",
"codex-features",
@@ -1883,7 +1886,10 @@ dependencies = [
"codex-protocol",
"codex-utils-absolute-path",
"codex-utils-path",
"dns-lookup",
"futures",
"gethostname",
"libc",
"multimap",
"pretty_assertions",
"schemars 0.8.22",
@@ -1898,6 +1904,7 @@ dependencies = [
"toml_edit 0.24.0+spec-1.1.0",
"tracing",
"wildmatch",
"winapi-util",
]
[[package]]
@@ -1943,7 +1950,6 @@ dependencies = [
"codex-feedback",
"codex-git-utils",
"codex-hooks",
"codex-instructions",
"codex-login",
"codex-mcp",
"codex-model-provider",
@@ -2077,7 +2083,6 @@ dependencies = [
"codex-app-server-protocol",
"codex-config",
"codex-exec-server",
"codex-instructions",
"codex-login",
"codex-otel",
"codex-protocol",
@@ -2322,15 +2327,6 @@ dependencies = [
"tempfile",
]
[[package]]
name = "codex-instructions"
version = "0.0.0"
dependencies = [
"codex-protocol",
"pretty_assertions",
"serde",
]
[[package]]
name = "codex-keyring-store"
version = "0.0.0"
@@ -2395,6 +2391,7 @@ dependencies = [
"codex-terminal-detection",
"codex-utils-template",
"core_test_support",
"crypto_box",
"ed25519-dalek",
"keyring",
"once_cell",
@@ -2900,10 +2897,11 @@ name = "codex-stdio-to-uds"
version = "0.0.0"
dependencies = [
"anyhow",
"codex-uds",
"codex-utils-cargo-bin",
"pretty_assertions",
"tempfile",
"uds_windows",
"tokio",
]
[[package]]
@@ -3008,6 +3006,7 @@ dependencies = [
"codex-utils-elapsed",
"codex-utils-fuzzy-match",
"codex-utils-oss",
"codex-utils-path",
"codex-utils-plugins",
"codex-utils-pty",
"codex-utils-sandbox-summary",
@@ -3066,6 +3065,18 @@ dependencies = [
"winsplit",
]
[[package]]
name = "codex-uds"
version = "0.0.0"
dependencies = [
"async-io",
"pretty_assertions",
"tempfile",
"tokio",
"tokio-util",
"uds_windows",
]
[[package]]
name = "codex-utils-absolute-path"
version = "0.0.0"
@@ -4330,6 +4341,18 @@ dependencies = [
"syn 2.0.114",
]
[[package]]
name = "dns-lookup"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e39034cee21a2f5bbb66ba0e3689819c4bb5d00382a282006e802a7ffa6c41d"
dependencies = [
"cfg-if",
"libc",
"socket2 0.6.2",
"windows-sys 0.60.2",
]
[[package]]
name = "dotenvy"
version = "0.15.7"
@@ -9237,9 +9260,9 @@ dependencies = [
[[package]]
name = "quinn-proto"
version = "0.11.13"
version = "0.11.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
dependencies = [
"bytes",
"getrandom 0.3.4",
@@ -11987,6 +12010,7 @@ checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
dependencies = [
"bytes",
"futures-core",
"futures-io",
"futures-sink",
"futures-util",
"pin-project-lite",
+7 -2
View File
@@ -31,7 +31,6 @@ members = [
"core-plugins",
"core-skills",
"hooks",
"instructions",
"secrets",
"exec",
"exec-server",
@@ -90,6 +89,7 @@ members = [
"terminal-detection",
"test-binary-support",
"thread-store",
"uds",
"codex-experimental-api-macros",
"plugin",
"model-provider",
@@ -142,7 +142,6 @@ codex-install-context = { path = "install-context" }
codex-file-search = { path = "file-search" }
codex-git-utils = { path = "git-utils" }
codex-hooks = { path = "hooks" }
codex-instructions = { path = "instructions" }
codex-keyring-store = { path = "keyring-store" }
codex-linux-sandbox = { path = "linux-sandbox" }
codex-lmstudio = { path = "lmstudio" }
@@ -175,6 +174,7 @@ codex-test-binary-support = { path = "test-binary-support" }
codex-thread-store = { path = "thread-store" }
codex-tools = { path = "tools" }
codex-tui = { path = "tui" }
codex-uds = { path = "uds" }
codex-utils-absolute-path = { path = "utils/absolute-path" }
codex-utils-approval-presets = { path = "utils/approval-presets" }
codex-utils-cache = { path = "utils/cache" }
@@ -212,6 +212,7 @@ arc-swap = "1.9.0"
assert_cmd = "2"
assert_matches = "1.5.0"
async-channel = "2.3.1"
async-io = "2.6.0"
async-stream = "0.3.6"
async-trait = "0.1.89"
axum = { version = "0.8", default-features = false }
@@ -233,6 +234,7 @@ deno_core_icudata = "0.77.0"
derive_more = "2"
diffy = "0.4.2"
dirs = "6"
dns-lookup = "3.0.1"
dotenvy = "0.15.7"
dunce = "1.0.4"
ed25519-dalek = { version = "2.2.0", features = ["pkcs8"] }
@@ -371,6 +373,7 @@ webbrowser = "1.0"
which = "8"
whoami = "1.6.1"
wildmatch = "2.6.1"
winapi-util = "0.1.11"
zip = "2.4.2"
zstd = "0.13"
@@ -381,6 +384,8 @@ zeroize = "1.8.2"
rust = {}
[workspace.lints.clippy]
await_holding_invalid_type = "deny"
await_holding_lock = "deny"
expect_used = "deny"
identity_op = "deny"
manual_clamp = "deny"
@@ -9,6 +9,12 @@ use crate::events::CodexPluginEventRequest;
use crate::events::CodexPluginUsedEventRequest;
use crate::events::CodexRuntimeMetadata;
use crate::events::CodexTurnEventRequest;
use crate::events::GuardianApprovalRequestSource;
use crate::events::GuardianReviewDecision;
use crate::events::GuardianReviewEventParams;
use crate::events::GuardianReviewFailureReason;
use crate::events::GuardianReviewTerminalStatus;
use crate::events::GuardianReviewedAction;
use crate::events::ThreadInitializedEvent;
use crate::events::ThreadInitializedEventParams;
use crate::events::TrackEventRequest;
@@ -82,6 +88,7 @@ use codex_plugin::AppConnectorId;
use codex_plugin::PluginCapabilitySummary;
use codex_plugin::PluginId;
use codex_plugin::PluginTelemetryMetadata;
use codex_protocol::approvals::NetworkApprovalProtocol;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::ModeKind;
use codex_protocol::protocol::AskForApproval;
@@ -1050,6 +1057,135 @@ async fn compaction_event_ingests_custom_fact() {
assert_eq!(payload[0]["event_params"]["status"], "failed");
}
#[tokio::test]
async fn guardian_review_event_ingests_custom_fact_with_optional_target_item() {
let mut reducer = AnalyticsReducer::default();
let mut events = Vec::new();
reducer
.ingest(
AnalyticsFact::Initialize {
connection_id: 7,
params: InitializeParams {
client_info: ClientInfo {
name: "codex-tui".to_string(),
title: None,
version: "1.0.0".to_string(),
},
capabilities: Some(InitializeCapabilities {
experimental_api: false,
opt_out_notification_methods: None,
}),
},
product_client_id: DEFAULT_ORIGINATOR.to_string(),
runtime: sample_runtime_metadata(),
rpc_transport: AppServerRpcTransport::Websocket,
},
&mut events,
)
.await;
reducer
.ingest(
AnalyticsFact::Response {
connection_id: 7,
response: Box::new(sample_thread_start_response(
"thread-guardian",
/*ephemeral*/ false,
"gpt-5",
)),
},
&mut events,
)
.await;
events.clear();
reducer
.ingest(
AnalyticsFact::Custom(CustomAnalyticsFact::GuardianReview(Box::new(
GuardianReviewEventParams {
thread_id: "thread-guardian".to_string(),
turn_id: "turn-guardian".to_string(),
review_id: "review-guardian".to_string(),
target_item_id: None,
approval_request_source: GuardianApprovalRequestSource::DelegatedSubagent,
reviewed_action: GuardianReviewedAction::NetworkAccess {
protocol: NetworkApprovalProtocol::Https,
port: 443,
},
reviewed_action_truncated: false,
decision: GuardianReviewDecision::Denied,
terminal_status: GuardianReviewTerminalStatus::TimedOut,
failure_reason: Some(GuardianReviewFailureReason::Timeout),
risk_level: None,
user_authorization: None,
outcome: None,
guardian_thread_id: None,
guardian_session_kind: None,
guardian_model: None,
guardian_reasoning_effort: None,
had_prior_review_context: None,
review_timeout_ms: 90_000,
tool_call_count: None,
time_to_first_token_ms: None,
completion_latency_ms: Some(90_000),
started_at: 100,
completed_at: Some(190),
input_tokens: None,
cached_input_tokens: None,
output_tokens: None,
reasoning_output_tokens: None,
total_tokens: None,
},
))),
&mut events,
)
.await;
let payload = serde_json::to_value(&events).expect("serialize events");
assert_eq!(payload.as_array().expect("events array").len(), 1);
assert_eq!(payload[0]["event_type"], "codex_guardian_review");
assert_eq!(payload[0]["event_params"]["thread_id"], "thread-guardian");
assert_eq!(payload[0]["event_params"]["turn_id"], "turn-guardian");
assert_eq!(payload[0]["event_params"]["review_id"], "review-guardian");
assert_eq!(payload[0]["event_params"]["target_item_id"], json!(null));
assert_eq!(
payload[0]["event_params"]["approval_request_source"],
"delegated_subagent"
);
assert_eq!(
payload[0]["event_params"]["app_server_client"]["product_client_id"],
DEFAULT_ORIGINATOR
);
assert_eq!(
payload[0]["event_params"]["runtime"]["codex_rs_version"],
"0.1.0"
);
assert_eq!(
payload[0]["event_params"]["reviewed_action"]["type"],
"network_access"
);
assert_eq!(
payload[0]["event_params"]["reviewed_action"]["protocol"],
"https"
);
assert_eq!(payload[0]["event_params"]["reviewed_action"]["port"], 443);
assert!(payload[0]["event_params"].get("retry_reason").is_none());
assert!(payload[0]["event_params"].get("rationale").is_none());
assert!(
payload[0]["event_params"]["reviewed_action"]
.get("target")
.is_none()
);
assert!(
payload[0]["event_params"]["reviewed_action"]
.get("host")
.is_none()
);
assert_eq!(payload[0]["event_params"]["terminal_status"], "timed_out");
assert_eq!(payload[0]["event_params"]["failure_reason"], "timeout");
assert_eq!(payload[0]["event_params"]["review_timeout_ms"], 90_000);
}
#[test]
fn subagent_thread_started_review_serializes_expected_shape() {
let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request(
+13 -9
View File
@@ -297,7 +297,7 @@ impl AnalyticsEventsClient {
}
async fn send_track_events(
auth_manager: &AuthManager,
auth_manager: &Arc<AuthManager>,
base_url: &str,
events: Vec<TrackEventRequest>,
) {
@@ -310,9 +310,11 @@ async fn send_track_events(
if !auth.is_chatgpt_auth() {
return;
}
let access_token = match auth.get_token() {
Ok(token) => token,
Err(_) => return,
let Some(authorization_header_value) = auth_manager
.chatgpt_authorization_header_for_auth(&auth)
.await
else {
return;
};
let Some(account_id) = auth.get_account_id() else {
return;
@@ -322,15 +324,17 @@ async fn send_track_events(
let url = format!("{base_url}/codex/analytics-events/events");
let payload = TrackEventsRequest { events };
let response = create_client()
let mut request = create_client()
.post(&url)
.timeout(ANALYTICS_EVENTS_TIMEOUT)
.bearer_auth(&access_token)
.header("authorization", authorization_header_value)
.header("chatgpt-account-id", &account_id)
.header("Content-Type", "application/json")
.json(&payload)
.send()
.await;
.json(&payload);
if auth.is_fedramp_account() {
request = request.header("X-OpenAI-Fedramp", "true");
}
let response = request.send().await;
match response {
Ok(response) if response.status().is_success() => {}
+22 -61
View File
@@ -1,5 +1,11 @@
use crate::facts::AppInvocation;
use crate::facts::CodexCompactionEvent;
use crate::facts::CompactionImplementation;
use crate::facts::CompactionPhase;
use crate::facts::CompactionReason;
use crate::facts::CompactionStatus;
use crate::facts::CompactionStrategy;
use crate::facts::CompactionTrigger;
use crate::facts::HookRunFact;
use crate::facts::InvocationType;
use crate::facts::PluginState;
@@ -16,6 +22,10 @@ use codex_plugin::PluginTelemetryMetadata;
use codex_protocol::approvals::NetworkApprovalProtocol;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::SandboxPermissions;
use codex_protocol::protocol::GuardianAssessmentOutcome;
use codex_protocol::protocol::GuardianCommandSource;
use codex_protocol::protocol::GuardianRiskLevel;
use codex_protocol::protocol::GuardianUserAuthorization;
use codex_protocol::protocol::HookEventName;
use codex_protocol::protocol::HookRunStatus;
use codex_protocol::protocol::HookSource;
@@ -151,31 +161,6 @@ pub enum GuardianReviewSessionKind {
EphemeralForked,
}
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum GuardianReviewRiskLevel {
Low,
Medium,
High,
Critical,
}
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum GuardianReviewUserAuthorization {
Unknown,
Low,
Medium,
High,
}
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum GuardianReviewOutcome {
Allow,
Deny,
}
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum GuardianApprovalRequestSource {
@@ -190,36 +175,21 @@ pub enum GuardianApprovalRequestSource {
#[serde(tag = "type", rename_all = "snake_case")]
pub enum GuardianReviewedAction {
Shell {
command: Vec<String>,
command_display: String,
cwd: String,
sandbox_permissions: SandboxPermissions,
additional_permissions: Option<PermissionProfile>,
justification: Option<String>,
},
UnifiedExec {
command: Vec<String>,
command_display: String,
cwd: String,
sandbox_permissions: SandboxPermissions,
additional_permissions: Option<PermissionProfile>,
justification: Option<String>,
tty: bool,
},
Execve {
source: GuardianCommandSource,
program: String,
argv: Vec<String>,
cwd: String,
additional_permissions: Option<PermissionProfile>,
},
ApplyPatch {
cwd: String,
files: Vec<String>,
},
ApplyPatch {},
NetworkAccess {
target: String,
host: String,
protocol: NetworkApprovalProtocol,
port: u16,
},
@@ -232,37 +202,28 @@ pub enum GuardianReviewedAction {
},
}
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum GuardianCommandSource {
Shell,
UnifiedExec,
}
#[derive(Clone, Serialize)]
pub struct GuardianReviewEventParams {
pub thread_id: String,
pub turn_id: String,
pub review_id: String,
pub target_item_id: String,
pub retry_reason: Option<String>,
pub target_item_id: Option<String>,
pub approval_request_source: GuardianApprovalRequestSource,
pub reviewed_action: GuardianReviewedAction,
pub reviewed_action_truncated: bool,
pub decision: GuardianReviewDecision,
pub terminal_status: GuardianReviewTerminalStatus,
pub failure_reason: Option<GuardianReviewFailureReason>,
pub risk_level: Option<GuardianReviewRiskLevel>,
pub user_authorization: Option<GuardianReviewUserAuthorization>,
pub outcome: Option<GuardianReviewOutcome>,
pub rationale: Option<String>,
pub risk_level: Option<GuardianRiskLevel>,
pub user_authorization: Option<GuardianUserAuthorization>,
pub outcome: Option<GuardianAssessmentOutcome>,
pub guardian_thread_id: Option<String>,
pub guardian_session_kind: Option<GuardianReviewSessionKind>,
pub guardian_model: Option<String>,
pub guardian_reasoning_effort: Option<String>,
pub had_prior_review_context: Option<bool>,
pub review_timeout_ms: u64,
pub tool_call_count: u64,
pub tool_call_count: Option<u64>,
pub time_to_first_token_ms: Option<u64>,
pub completion_latency_ms: Option<u64>,
pub started_at: u64,
@@ -330,12 +291,12 @@ pub(crate) struct CodexCompactionEventParams {
pub(crate) thread_source: Option<&'static str>,
pub(crate) subagent_source: Option<String>,
pub(crate) parent_thread_id: Option<String>,
pub(crate) trigger: crate::facts::CompactionTrigger,
pub(crate) reason: crate::facts::CompactionReason,
pub(crate) implementation: crate::facts::CompactionImplementation,
pub(crate) phase: crate::facts::CompactionPhase,
pub(crate) strategy: crate::facts::CompactionStrategy,
pub(crate) status: crate::facts::CompactionStatus,
pub(crate) trigger: CompactionTrigger,
pub(crate) reason: CompactionReason,
pub(crate) implementation: CompactionImplementation,
pub(crate) phase: CompactionPhase,
pub(crate) strategy: CompactionStrategy,
pub(crate) status: CompactionStatus,
pub(crate) error: Option<String>,
pub(crate) active_context_tokens_before: i64,
pub(crate) active_context_tokens_after: i64,
-4
View File
@@ -9,15 +9,11 @@ use std::time::UNIX_EPOCH;
pub use client::AnalyticsEventsClient;
pub use events::AppServerRpcTransport;
pub use events::GuardianApprovalRequestSource;
pub use events::GuardianCommandSource;
pub use events::GuardianReviewDecision;
pub use events::GuardianReviewEventParams;
pub use events::GuardianReviewFailureReason;
pub use events::GuardianReviewOutcome;
pub use events::GuardianReviewRiskLevel;
pub use events::GuardianReviewSessionKind;
pub use events::GuardianReviewTerminalStatus;
pub use events::GuardianReviewUserAuthorization;
pub use events::GuardianReviewedAction;
pub use facts::AnalyticsJsonRpcError;
pub use facts::AppInvocation;
+1
View File
@@ -15,6 +15,7 @@ workspace = true
codex-app-server = { workspace = true }
codex-app-server-protocol = { workspace = true }
codex-arg0 = { workspace = true }
codex-config = { workspace = true }
codex-core = { workspace = true }
codex-exec-server = { workspace = true }
codex-feedback = { workspace = true }
+2 -5
View File
@@ -41,6 +41,7 @@ use codex_app_server_protocol::Result as JsonRpcResult;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerRequest;
use codex_arg0::Arg0DispatchPaths;
use codex_config::NoopThreadConfigLoader;
use codex_core::config::Config;
use codex_core::config_loader::CloudRequirementsLoader;
use codex_core::config_loader::LoaderOverrides;
@@ -73,7 +74,6 @@ pub mod legacy_core {
pub use codex_core::grant_read_root_non_elevated;
pub use codex_core::lookup_message_history_entry;
pub use codex_core::message_history_metadata;
pub use codex_core::path_utils;
pub use codex_core::web_search_detail;
pub mod config {
@@ -84,10 +84,6 @@ pub mod legacy_core {
}
}
pub mod config_loader {
pub use codex_core::config_loader::*;
}
pub mod connectors {
pub use codex_core::connectors::*;
}
@@ -390,6 +386,7 @@ impl InProcessClientStartArgs {
cli_overrides: self.cli_overrides,
loader_overrides: self.loader_overrides,
cloud_requirements: self.cloud_requirements,
thread_config_loader: Arc::new(NoopThreadConfigLoader),
feedback: self.feedback,
log_db: self.log_db,
environment_manager: self.environment_manager,
+10 -2
View File
@@ -532,6 +532,12 @@
"inputSchema": true,
"name": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
}
},
"required": [
@@ -1304,7 +1310,10 @@
"type": "string"
},
"threadId": {
"type": "string"
"type": [
"string",
"null"
]
},
"uri": {
"type": "string"
@@ -1312,7 +1321,6 @@
},
"required": [
"server",
"threadId",
"uri"
],
"type": "object"
@@ -7,6 +7,23 @@
},
"AdditionalFileSystemPermissions": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/FileSystemSandboxEntry"
},
"type": [
"array",
"null"
]
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"read": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
@@ -253,6 +270,217 @@
}
]
},
"FileSystemAccessMode": {
"enum": [
"read",
"write",
"none"
],
"type": "string"
},
"FileSystemPath": {
"oneOf": [
{
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"path"
],
"title": "PathFileSystemPathType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "PathFileSystemPath",
"type": "object"
},
{
"properties": {
"pattern": {
"type": "string"
},
"type": {
"enum": [
"glob_pattern"
],
"title": "GlobPatternFileSystemPathType",
"type": "string"
}
},
"required": [
"pattern",
"type"
],
"title": "GlobPatternFileSystemPath",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"special"
],
"title": "SpecialFileSystemPathType",
"type": "string"
},
"value": {
"$ref": "#/definitions/FileSystemSpecialPath"
}
},
"required": [
"type",
"value"
],
"title": "SpecialFileSystemPath",
"type": "object"
}
]
},
"FileSystemSandboxEntry": {
"properties": {
"access": {
"$ref": "#/definitions/FileSystemAccessMode"
},
"path": {
"$ref": "#/definitions/FileSystemPath"
}
},
"required": [
"access",
"path"
],
"type": "object"
},
"FileSystemSpecialPath": {
"oneOf": [
{
"properties": {
"kind": {
"enum": [
"root"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "RootFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"minimal"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "MinimalFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"current_working_directory"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "CurrentWorkingDirectoryFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"project_roots"
],
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind"
],
"title": "KindFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"tmpdir"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "TmpdirFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"slash_tmp"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "SlashTmpFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"unknown"
],
"type": "string"
},
"path": {
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind",
"path"
],
"type": "object"
}
]
},
"NetworkApprovalContext": {
"properties": {
"host": {
@@ -5,6 +5,12 @@
"callId": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
},
"threadId": {
"type": "string"
},
@@ -24,4 +30,4 @@
],
"title": "DynamicToolCallParams",
"type": "object"
}
}
@@ -7,6 +7,23 @@
},
"AdditionalFileSystemPermissions": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/FileSystemSandboxEntry"
},
"type": [
"array",
"null"
]
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"read": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
@@ -39,6 +56,217 @@
},
"type": "object"
},
"FileSystemAccessMode": {
"enum": [
"read",
"write",
"none"
],
"type": "string"
},
"FileSystemPath": {
"oneOf": [
{
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"path"
],
"title": "PathFileSystemPathType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "PathFileSystemPath",
"type": "object"
},
{
"properties": {
"pattern": {
"type": "string"
},
"type": {
"enum": [
"glob_pattern"
],
"title": "GlobPatternFileSystemPathType",
"type": "string"
}
},
"required": [
"pattern",
"type"
],
"title": "GlobPatternFileSystemPath",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"special"
],
"title": "SpecialFileSystemPathType",
"type": "string"
},
"value": {
"$ref": "#/definitions/FileSystemSpecialPath"
}
},
"required": [
"type",
"value"
],
"title": "SpecialFileSystemPath",
"type": "object"
}
]
},
"FileSystemSandboxEntry": {
"properties": {
"access": {
"$ref": "#/definitions/FileSystemAccessMode"
},
"path": {
"$ref": "#/definitions/FileSystemPath"
}
},
"required": [
"access",
"path"
],
"type": "object"
},
"FileSystemSpecialPath": {
"oneOf": [
{
"properties": {
"kind": {
"enum": [
"root"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "RootFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"minimal"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "MinimalFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"current_working_directory"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "CurrentWorkingDirectoryFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"project_roots"
],
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind"
],
"title": "KindFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"tmpdir"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "TmpdirFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"slash_tmp"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "SlashTmpFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"unknown"
],
"type": "string"
},
"path": {
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind",
"path"
],
"type": "object"
}
]
},
"RequestPermissionProfile": {
"additionalProperties": false,
"properties": {
@@ -7,6 +7,23 @@
},
"AdditionalFileSystemPermissions": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/FileSystemSandboxEntry"
},
"type": [
"array",
"null"
]
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"read": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
@@ -39,6 +56,217 @@
},
"type": "object"
},
"FileSystemAccessMode": {
"enum": [
"read",
"write",
"none"
],
"type": "string"
},
"FileSystemPath": {
"oneOf": [
{
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"path"
],
"title": "PathFileSystemPathType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "PathFileSystemPath",
"type": "object"
},
{
"properties": {
"pattern": {
"type": "string"
},
"type": {
"enum": [
"glob_pattern"
],
"title": "GlobPatternFileSystemPathType",
"type": "string"
}
},
"required": [
"pattern",
"type"
],
"title": "GlobPatternFileSystemPath",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"special"
],
"title": "SpecialFileSystemPathType",
"type": "string"
},
"value": {
"$ref": "#/definitions/FileSystemSpecialPath"
}
},
"required": [
"type",
"value"
],
"title": "SpecialFileSystemPath",
"type": "object"
}
]
},
"FileSystemSandboxEntry": {
"properties": {
"access": {
"$ref": "#/definitions/FileSystemAccessMode"
},
"path": {
"$ref": "#/definitions/FileSystemPath"
}
},
"required": [
"access",
"path"
],
"type": "object"
},
"FileSystemSpecialPath": {
"oneOf": [
{
"properties": {
"kind": {
"enum": [
"root"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "RootFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"minimal"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "MinimalFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"current_working_directory"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "CurrentWorkingDirectoryFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"project_roots"
],
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind"
],
"title": "KindFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"tmpdir"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "TmpdirFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"slash_tmp"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "SlashTmpFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"unknown"
],
"type": "string"
},
"path": {
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind",
"path"
],
"type": "object"
}
]
},
"GrantedPermissionProfile": {
"properties": {
"fileSystem": {
@@ -64,6 +64,57 @@
},
"type": "object"
},
"AdditionalFileSystemPermissions": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/FileSystemSandboxEntry"
},
"type": [
"array",
"null"
]
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"read": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
},
"write": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
}
},
"type": "object"
},
"AdditionalNetworkPermissions": {
"properties": {
"enabled": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"AgentMessageDeltaNotification": {
"properties": {
"delta": {
@@ -993,6 +1044,243 @@
],
"type": "object"
},
"FileChangePatchUpdatedNotification": {
"properties": {
"changes": {
"items": {
"$ref": "#/definitions/FileUpdateChange"
},
"type": "array"
},
"itemId": {
"type": "string"
},
"threadId": {
"type": "string"
},
"turnId": {
"type": "string"
}
},
"required": [
"changes",
"itemId",
"threadId",
"turnId"
],
"type": "object"
},
"FileSystemAccessMode": {
"enum": [
"read",
"write",
"none"
],
"type": "string"
},
"FileSystemPath": {
"oneOf": [
{
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"path"
],
"title": "PathFileSystemPathType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "PathFileSystemPath",
"type": "object"
},
{
"properties": {
"pattern": {
"type": "string"
},
"type": {
"enum": [
"glob_pattern"
],
"title": "GlobPatternFileSystemPathType",
"type": "string"
}
},
"required": [
"pattern",
"type"
],
"title": "GlobPatternFileSystemPath",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"special"
],
"title": "SpecialFileSystemPathType",
"type": "string"
},
"value": {
"$ref": "#/definitions/FileSystemSpecialPath"
}
},
"required": [
"type",
"value"
],
"title": "SpecialFileSystemPath",
"type": "object"
}
]
},
"FileSystemSandboxEntry": {
"properties": {
"access": {
"$ref": "#/definitions/FileSystemAccessMode"
},
"path": {
"$ref": "#/definitions/FileSystemPath"
}
},
"required": [
"access",
"path"
],
"type": "object"
},
"FileSystemSpecialPath": {
"oneOf": [
{
"properties": {
"kind": {
"enum": [
"root"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "RootFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"minimal"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "MinimalFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"current_working_directory"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "CurrentWorkingDirectoryFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"project_roots"
],
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind"
],
"title": "KindFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"tmpdir"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "TmpdirFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"slash_tmp"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "SlashTmpFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"unknown"
],
"type": "string"
},
"path": {
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind",
"path"
],
"type": "object"
}
]
},
"FileUpdateChange": {
"properties": {
"diff": {
@@ -1342,6 +1630,32 @@
],
"title": "McpToolCallGuardianApprovalReviewAction",
"type": "object"
},
{
"properties": {
"permissions": {
"$ref": "#/definitions/RequestPermissionProfile"
},
"reason": {
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"requestPermissions"
],
"title": "RequestPermissionsGuardianApprovalReviewActionType",
"type": "string"
}
},
"required": [
"permissions",
"type"
],
"title": "RequestPermissionsGuardianApprovalReviewAction",
"type": "object"
}
]
},
@@ -2259,6 +2573,32 @@
}
]
},
"RequestPermissionProfile": {
"additionalProperties": false,
"properties": {
"fileSystem": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalFileSystemPermissions"
},
{
"type": "null"
}
]
},
"network": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalNetworkPermissions"
},
{
"type": "null"
}
]
}
},
"type": "object"
},
"ServerRequestResolvedNotification": {
"properties": {
"requestId": {
@@ -3003,6 +3343,12 @@
"id": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
@@ -4668,6 +5014,26 @@
"title": "Item/fileChange/outputDeltaNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"item/fileChange/patchUpdated"
],
"title": "Item/fileChange/patchUpdatedNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/FileChangePatchUpdatedNotification"
}
},
"required": [
"method",
"params"
],
"title": "Item/fileChange/patchUpdatedNotification",
"type": "object"
},
{
"properties": {
"method": {
@@ -7,6 +7,23 @@
},
"AdditionalFileSystemPermissions": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/FileSystemSandboxEntry"
},
"type": [
"array",
"null"
]
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"read": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
@@ -417,6 +434,12 @@
"callId": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
},
"threadId": {
"type": "string"
},
@@ -586,6 +609,217 @@
],
"type": "object"
},
"FileSystemAccessMode": {
"enum": [
"read",
"write",
"none"
],
"type": "string"
},
"FileSystemPath": {
"oneOf": [
{
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"path"
],
"title": "PathFileSystemPathType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "PathFileSystemPath",
"type": "object"
},
{
"properties": {
"pattern": {
"type": "string"
},
"type": {
"enum": [
"glob_pattern"
],
"title": "GlobPatternFileSystemPathType",
"type": "string"
}
},
"required": [
"pattern",
"type"
],
"title": "GlobPatternFileSystemPath",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"special"
],
"title": "SpecialFileSystemPathType",
"type": "string"
},
"value": {
"$ref": "#/definitions/FileSystemSpecialPath"
}
},
"required": [
"type",
"value"
],
"title": "SpecialFileSystemPath",
"type": "object"
}
]
},
"FileSystemSandboxEntry": {
"properties": {
"access": {
"$ref": "#/definitions/FileSystemAccessMode"
},
"path": {
"$ref": "#/definitions/FileSystemPath"
}
},
"required": [
"access",
"path"
],
"type": "object"
},
"FileSystemSpecialPath": {
"oneOf": [
{
"properties": {
"kind": {
"enum": [
"root"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "RootFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"minimal"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "MinimalFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"current_working_directory"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "CurrentWorkingDirectoryFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"project_roots"
],
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind"
],
"title": "KindFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"tmpdir"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "TmpdirFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"slash_tmp"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "SlashTmpFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"unknown"
],
"type": "string"
},
"path": {
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind",
"path"
],
"type": "object"
}
]
},
"McpElicitationArrayType": {
"enum": [
"array"
@@ -5,46 +5,12 @@
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"AdditionalFileSystemPermissions": {
"properties": {
"read": {
"items": {
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
},
"write": {
"items": {
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
}
},
"type": "object"
},
"AdditionalNetworkPermissions": {
"properties": {
"enabled": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"AdditionalPermissionProfile": {
"properties": {
"fileSystem": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalFileSystemPermissions"
"$ref": "#/definitions/v2/AdditionalFileSystemPermissions"
},
{
"type": "null"
@@ -54,7 +20,7 @@
"network": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalNetworkPermissions"
"$ref": "#/definitions/v2/AdditionalNetworkPermissions"
},
{
"type": "null"
@@ -1999,6 +1965,12 @@
"callId": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
},
"threadId": {
"type": "string"
},
@@ -2383,7 +2355,7 @@
"fileSystem": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalFileSystemPermissions"
"$ref": "#/definitions/v2/AdditionalFileSystemPermissions"
},
{
"type": "null"
@@ -2393,7 +2365,7 @@
"network": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalNetworkPermissions"
"$ref": "#/definitions/v2/AdditionalNetworkPermissions"
},
{
"type": "null"
@@ -3392,7 +3364,7 @@
"type": "string"
},
"permissions": {
"$ref": "#/definitions/RequestPermissionProfile"
"$ref": "#/definitions/v2/RequestPermissionProfile"
},
"reason": {
"type": [
@@ -3450,32 +3422,6 @@
],
"title": "RequestId"
},
"RequestPermissionProfile": {
"additionalProperties": false,
"properties": {
"fileSystem": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalFileSystemPermissions"
},
{
"type": "null"
}
]
},
"network": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalNetworkPermissions"
},
{
"type": "null"
}
]
}
},
"type": "object"
},
"ReviewDecision": {
"description": "User's decision in response to an ExecApprovalRequest.",
"oneOf": [
@@ -4070,6 +4016,26 @@
"title": "Item/fileChange/outputDeltaNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"item/fileChange/patchUpdated"
],
"title": "Item/fileChange/patchUpdatedNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/FileChangePatchUpdatedNotification"
}
},
"required": [
"method",
"params"
],
"title": "Item/fileChange/patchUpdatedNotification",
"type": "object"
},
{
"properties": {
"method": {
@@ -5165,6 +5131,57 @@
],
"type": "string"
},
"AdditionalFileSystemPermissions": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/v2/FileSystemSandboxEntry"
},
"type": [
"array",
"null"
]
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"read": {
"items": {
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
},
"write": {
"items": {
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
}
},
"type": "object"
},
"AdditionalNetworkPermissions": {
"properties": {
"enabled": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"AgentMessageDeltaNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -7370,6 +7387,12 @@
"inputSchema": true,
"name": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
}
},
"required": [
@@ -7765,6 +7788,245 @@
"title": "FileChangeOutputDeltaNotification",
"type": "object"
},
"FileChangePatchUpdatedNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"changes": {
"items": {
"$ref": "#/definitions/v2/FileUpdateChange"
},
"type": "array"
},
"itemId": {
"type": "string"
},
"threadId": {
"type": "string"
},
"turnId": {
"type": "string"
}
},
"required": [
"changes",
"itemId",
"threadId",
"turnId"
],
"title": "FileChangePatchUpdatedNotification",
"type": "object"
},
"FileSystemAccessMode": {
"enum": [
"read",
"write",
"none"
],
"type": "string"
},
"FileSystemPath": {
"oneOf": [
{
"properties": {
"path": {
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
"type": {
"enum": [
"path"
],
"title": "PathFileSystemPathType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "PathFileSystemPath",
"type": "object"
},
{
"properties": {
"pattern": {
"type": "string"
},
"type": {
"enum": [
"glob_pattern"
],
"title": "GlobPatternFileSystemPathType",
"type": "string"
}
},
"required": [
"pattern",
"type"
],
"title": "GlobPatternFileSystemPath",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"special"
],
"title": "SpecialFileSystemPathType",
"type": "string"
},
"value": {
"$ref": "#/definitions/v2/FileSystemSpecialPath"
}
},
"required": [
"type",
"value"
],
"title": "SpecialFileSystemPath",
"type": "object"
}
]
},
"FileSystemSandboxEntry": {
"properties": {
"access": {
"$ref": "#/definitions/v2/FileSystemAccessMode"
},
"path": {
"$ref": "#/definitions/v2/FileSystemPath"
}
},
"required": [
"access",
"path"
],
"type": "object"
},
"FileSystemSpecialPath": {
"oneOf": [
{
"properties": {
"kind": {
"enum": [
"root"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "RootFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"minimal"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "MinimalFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"current_working_directory"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "CurrentWorkingDirectoryFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"project_roots"
],
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind"
],
"title": "KindFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"tmpdir"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "TmpdirFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"slash_tmp"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "SlashTmpFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"unknown"
],
"type": "string"
},
"path": {
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind",
"path"
],
"type": "object"
}
]
},
"FileUpdateChange": {
"properties": {
"diff": {
@@ -8558,6 +8820,32 @@
],
"title": "McpToolCallGuardianApprovalReviewAction",
"type": "object"
},
{
"properties": {
"permissions": {
"$ref": "#/definitions/v2/RequestPermissionProfile"
},
"reason": {
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"requestPermissions"
],
"title": "RequestPermissionsGuardianApprovalReviewActionType",
"type": "string"
}
},
"required": [
"permissions",
"type"
],
"title": "RequestPermissionsGuardianApprovalReviewAction",
"type": "object"
}
]
},
@@ -9409,7 +9697,10 @@
"type": "string"
},
"threadId": {
"type": "string"
"type": [
"string",
"null"
]
},
"uri": {
"type": "string"
@@ -9417,7 +9708,6 @@
},
"required": [
"server",
"threadId",
"uri"
],
"title": "McpResourceReadParams",
@@ -11363,6 +11653,32 @@
}
]
},
"RequestPermissionProfile": {
"additionalProperties": false,
"properties": {
"fileSystem": {
"anyOf": [
{
"$ref": "#/definitions/v2/AdditionalFileSystemPermissions"
},
{
"type": "null"
}
]
},
"network": {
"anyOf": [
{
"$ref": "#/definitions/v2/AdditionalNetworkPermissions"
},
{
"type": "null"
}
]
}
},
"type": "object"
},
"ResidencyRequirement": {
"enum": [
"us"
@@ -13781,6 +14097,12 @@
"id": {
"type": "string"
},
"namespace": {
"type": [
"null",
"string"
]
},
"status": {
"$ref": "#/definitions/v2/DynamicToolCallStatus"
},
@@ -16290,4 +16612,4 @@
},
"title": "CodexAppServerProtocol",
"type": "object"
}
}
@@ -128,6 +128,57 @@
],
"type": "string"
},
"AdditionalFileSystemPermissions": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/FileSystemSandboxEntry"
},
"type": [
"array",
"null"
]
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"read": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
},
"write": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
}
},
"type": "object"
},
"AdditionalNetworkPermissions": {
"properties": {
"enabled": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"AgentMessageDeltaNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -3947,6 +3998,12 @@
"inputSchema": true,
"name": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
}
},
"required": [
@@ -4342,6 +4399,245 @@
"title": "FileChangeOutputDeltaNotification",
"type": "object"
},
"FileChangePatchUpdatedNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"changes": {
"items": {
"$ref": "#/definitions/FileUpdateChange"
},
"type": "array"
},
"itemId": {
"type": "string"
},
"threadId": {
"type": "string"
},
"turnId": {
"type": "string"
}
},
"required": [
"changes",
"itemId",
"threadId",
"turnId"
],
"title": "FileChangePatchUpdatedNotification",
"type": "object"
},
"FileSystemAccessMode": {
"enum": [
"read",
"write",
"none"
],
"type": "string"
},
"FileSystemPath": {
"oneOf": [
{
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"path"
],
"title": "PathFileSystemPathType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "PathFileSystemPath",
"type": "object"
},
{
"properties": {
"pattern": {
"type": "string"
},
"type": {
"enum": [
"glob_pattern"
],
"title": "GlobPatternFileSystemPathType",
"type": "string"
}
},
"required": [
"pattern",
"type"
],
"title": "GlobPatternFileSystemPath",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"special"
],
"title": "SpecialFileSystemPathType",
"type": "string"
},
"value": {
"$ref": "#/definitions/FileSystemSpecialPath"
}
},
"required": [
"type",
"value"
],
"title": "SpecialFileSystemPath",
"type": "object"
}
]
},
"FileSystemSandboxEntry": {
"properties": {
"access": {
"$ref": "#/definitions/FileSystemAccessMode"
},
"path": {
"$ref": "#/definitions/FileSystemPath"
}
},
"required": [
"access",
"path"
],
"type": "object"
},
"FileSystemSpecialPath": {
"oneOf": [
{
"properties": {
"kind": {
"enum": [
"root"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "RootFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"minimal"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "MinimalFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"current_working_directory"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "CurrentWorkingDirectoryFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"project_roots"
],
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind"
],
"title": "KindFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"tmpdir"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "TmpdirFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"slash_tmp"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "SlashTmpFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"unknown"
],
"type": "string"
},
"path": {
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind",
"path"
],
"type": "object"
}
]
},
"FileUpdateChange": {
"properties": {
"diff": {
@@ -5246,6 +5542,32 @@
],
"title": "McpToolCallGuardianApprovalReviewAction",
"type": "object"
},
{
"properties": {
"permissions": {
"$ref": "#/definitions/RequestPermissionProfile"
},
"reason": {
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"requestPermissions"
],
"title": "RequestPermissionsGuardianApprovalReviewActionType",
"type": "string"
}
},
"required": [
"permissions",
"type"
],
"title": "RequestPermissionsGuardianApprovalReviewAction",
"type": "object"
}
]
},
@@ -6141,7 +6463,10 @@
"type": "string"
},
"threadId": {
"type": "string"
"type": [
"string",
"null"
]
},
"uri": {
"type": "string"
@@ -6149,7 +6474,6 @@
},
"required": [
"server",
"threadId",
"uri"
],
"title": "McpResourceReadParams",
@@ -8095,6 +8419,32 @@
}
]
},
"RequestPermissionProfile": {
"additionalProperties": false,
"properties": {
"fileSystem": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalFileSystemPermissions"
},
{
"type": "null"
}
]
},
"network": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalNetworkPermissions"
},
{
"type": "null"
}
]
}
},
"type": "object"
},
"ResidencyRequirement": {
"enum": [
"us"
@@ -9640,6 +9990,26 @@
"title": "Item/fileChange/outputDeltaNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"item/fileChange/patchUpdated"
],
"title": "Item/fileChange/patchUpdatedNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/FileChangePatchUpdatedNotification"
}
},
"required": [
"method",
"params"
],
"title": "Item/fileChange/patchUpdatedNotification",
"type": "object"
},
{
"properties": {
"method": {
@@ -11625,6 +11995,12 @@
"id": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
@@ -0,0 +1,107 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"FileUpdateChange": {
"properties": {
"diff": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/PatchChangeKind"
},
"path": {
"type": "string"
}
},
"required": [
"diff",
"kind",
"path"
],
"type": "object"
},
"PatchChangeKind": {
"oneOf": [
{
"properties": {
"type": {
"enum": [
"add"
],
"title": "AddPatchChangeKindType",
"type": "string"
}
},
"required": [
"type"
],
"title": "AddPatchChangeKind",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"delete"
],
"title": "DeletePatchChangeKindType",
"type": "string"
}
},
"required": [
"type"
],
"title": "DeletePatchChangeKind",
"type": "object"
},
{
"properties": {
"move_path": {
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"update"
],
"title": "UpdatePatchChangeKindType",
"type": "string"
}
},
"required": [
"type"
],
"title": "UpdatePatchChangeKind",
"type": "object"
}
]
}
},
"properties": {
"changes": {
"items": {
"$ref": "#/definitions/FileUpdateChange"
},
"type": "array"
},
"itemId": {
"type": "string"
},
"threadId": {
"type": "string"
},
"turnId": {
"type": "string"
}
},
"required": [
"changes",
"itemId",
"threadId",
"turnId"
],
"title": "FileChangePatchUpdatedNotification",
"type": "object"
}
@@ -854,6 +854,12 @@
"id": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
@@ -5,6 +5,57 @@
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"AdditionalFileSystemPermissions": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/FileSystemSandboxEntry"
},
"type": [
"array",
"null"
]
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"read": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
},
"write": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
}
},
"type": "object"
},
"AdditionalNetworkPermissions": {
"properties": {
"enabled": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"AutoReviewDecisionSource": {
"description": "[UNSTABLE] Source that produced a terminal approval auto-review decision.",
"enum": [
@@ -12,6 +63,217 @@
],
"type": "string"
},
"FileSystemAccessMode": {
"enum": [
"read",
"write",
"none"
],
"type": "string"
},
"FileSystemPath": {
"oneOf": [
{
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"path"
],
"title": "PathFileSystemPathType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "PathFileSystemPath",
"type": "object"
},
{
"properties": {
"pattern": {
"type": "string"
},
"type": {
"enum": [
"glob_pattern"
],
"title": "GlobPatternFileSystemPathType",
"type": "string"
}
},
"required": [
"pattern",
"type"
],
"title": "GlobPatternFileSystemPath",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"special"
],
"title": "SpecialFileSystemPathType",
"type": "string"
},
"value": {
"$ref": "#/definitions/FileSystemSpecialPath"
}
},
"required": [
"type",
"value"
],
"title": "SpecialFileSystemPath",
"type": "object"
}
]
},
"FileSystemSandboxEntry": {
"properties": {
"access": {
"$ref": "#/definitions/FileSystemAccessMode"
},
"path": {
"$ref": "#/definitions/FileSystemPath"
}
},
"required": [
"access",
"path"
],
"type": "object"
},
"FileSystemSpecialPath": {
"oneOf": [
{
"properties": {
"kind": {
"enum": [
"root"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "RootFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"minimal"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "MinimalFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"current_working_directory"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "CurrentWorkingDirectoryFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"project_roots"
],
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind"
],
"title": "KindFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"tmpdir"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "TmpdirFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"slash_tmp"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "SlashTmpFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"unknown"
],
"type": "string"
},
"path": {
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind",
"path"
],
"type": "object"
}
]
},
"GuardianApprovalReview": {
"description": "[UNSTABLE] Temporary approval auto-review payload used by `item/autoApprovalReview/*` notifications. This shape is expected to change soon.",
"properties": {
@@ -217,6 +479,32 @@
],
"title": "McpToolCallGuardianApprovalReviewAction",
"type": "object"
},
{
"properties": {
"permissions": {
"$ref": "#/definitions/RequestPermissionProfile"
},
"reason": {
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"requestPermissions"
],
"title": "RequestPermissionsGuardianApprovalReviewActionType",
"type": "string"
}
},
"required": [
"permissions",
"type"
],
"title": "RequestPermissionsGuardianApprovalReviewAction",
"type": "object"
}
]
},
@@ -266,6 +554,32 @@
"socks5Udp"
],
"type": "string"
},
"RequestPermissionProfile": {
"additionalProperties": false,
"properties": {
"fileSystem": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalFileSystemPermissions"
},
{
"type": "null"
}
]
},
"network": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalNetworkPermissions"
},
{
"type": "null"
}
]
}
},
"type": "object"
}
},
"description": "[UNSTABLE] Temporary notification payload for approval auto-review. This shape is expected to change soon.",
@@ -5,6 +5,268 @@
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"AdditionalFileSystemPermissions": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/FileSystemSandboxEntry"
},
"type": [
"array",
"null"
]
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"read": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
},
"write": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
}
},
"type": "object"
},
"AdditionalNetworkPermissions": {
"properties": {
"enabled": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"FileSystemAccessMode": {
"enum": [
"read",
"write",
"none"
],
"type": "string"
},
"FileSystemPath": {
"oneOf": [
{
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"path"
],
"title": "PathFileSystemPathType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "PathFileSystemPath",
"type": "object"
},
{
"properties": {
"pattern": {
"type": "string"
},
"type": {
"enum": [
"glob_pattern"
],
"title": "GlobPatternFileSystemPathType",
"type": "string"
}
},
"required": [
"pattern",
"type"
],
"title": "GlobPatternFileSystemPath",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"special"
],
"title": "SpecialFileSystemPathType",
"type": "string"
},
"value": {
"$ref": "#/definitions/FileSystemSpecialPath"
}
},
"required": [
"type",
"value"
],
"title": "SpecialFileSystemPath",
"type": "object"
}
]
},
"FileSystemSandboxEntry": {
"properties": {
"access": {
"$ref": "#/definitions/FileSystemAccessMode"
},
"path": {
"$ref": "#/definitions/FileSystemPath"
}
},
"required": [
"access",
"path"
],
"type": "object"
},
"FileSystemSpecialPath": {
"oneOf": [
{
"properties": {
"kind": {
"enum": [
"root"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "RootFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"minimal"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "MinimalFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"current_working_directory"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "CurrentWorkingDirectoryFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"project_roots"
],
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind"
],
"title": "KindFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"tmpdir"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "TmpdirFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"slash_tmp"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "SlashTmpFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"unknown"
],
"type": "string"
},
"path": {
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind",
"path"
],
"type": "object"
}
]
},
"GuardianApprovalReview": {
"description": "[UNSTABLE] Temporary approval auto-review payload used by `item/autoApprovalReview/*` notifications. This shape is expected to change soon.",
"properties": {
@@ -210,6 +472,32 @@
],
"title": "McpToolCallGuardianApprovalReviewAction",
"type": "object"
},
{
"properties": {
"permissions": {
"$ref": "#/definitions/RequestPermissionProfile"
},
"reason": {
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"requestPermissions"
],
"title": "RequestPermissionsGuardianApprovalReviewActionType",
"type": "string"
}
},
"required": [
"permissions",
"type"
],
"title": "RequestPermissionsGuardianApprovalReviewAction",
"type": "object"
}
]
},
@@ -259,6 +547,32 @@
"socks5Udp"
],
"type": "string"
},
"RequestPermissionProfile": {
"additionalProperties": false,
"properties": {
"fileSystem": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalFileSystemPermissions"
},
{
"type": "null"
}
]
},
"network": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalNetworkPermissions"
},
{
"type": "null"
}
]
}
},
"type": "object"
}
},
"description": "[UNSTABLE] Temporary notification payload for approval auto-review. This shape is expected to change soon.",
@@ -854,6 +854,12 @@
"id": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
@@ -5,7 +5,10 @@
"type": "string"
},
"threadId": {
"type": "string"
"type": [
"string",
"null"
]
},
"uri": {
"type": "string"
@@ -13,7 +16,6 @@
},
"required": [
"server",
"threadId",
"uri"
],
"title": "McpResourceReadParams",
@@ -997,6 +997,12 @@
"id": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
@@ -1511,6 +1511,12 @@
"id": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
@@ -1273,6 +1273,12 @@
"id": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
@@ -1273,6 +1273,12 @@
"id": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
@@ -1273,6 +1273,12 @@
"id": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
@@ -1511,6 +1511,12 @@
"id": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
@@ -1273,6 +1273,12 @@
"id": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
@@ -70,6 +70,12 @@
"inputSchema": true,
"name": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
}
},
"required": [
@@ -1511,6 +1511,12 @@
"id": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
@@ -1273,6 +1273,12 @@
"id": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
@@ -997,6 +997,12 @@
"id": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
@@ -1273,6 +1273,12 @@
"id": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
@@ -997,6 +997,12 @@
"id": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
@@ -997,6 +997,12 @@
"id": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
@@ -997,6 +997,12 @@
"id": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
},
"status": {
"$ref": "#/definitions/DynamicToolCallStatus"
},
@@ -16,6 +16,7 @@ import type { DeprecationNoticeNotification } from "./v2/DeprecationNoticeNotifi
import type { ErrorNotification } from "./v2/ErrorNotification";
import type { ExternalAgentConfigImportCompletedNotification } from "./v2/ExternalAgentConfigImportCompletedNotification";
import type { FileChangeOutputDeltaNotification } from "./v2/FileChangeOutputDeltaNotification";
import type { FileChangePatchUpdatedNotification } from "./v2/FileChangePatchUpdatedNotification";
import type { FsChangedNotification } from "./v2/FsChangedNotification";
import type { HookCompletedNotification } from "./v2/HookCompletedNotification";
import type { HookStartedNotification } from "./v2/HookStartedNotification";
@@ -61,4 +62,4 @@ import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldW
/**
* Notification sent from the server to the client.
*/
export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "externalAgentConfig/import/completed", "params": ExternalAgentConfigImportCompletedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "warning", "params": WarningNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/transcript/delta", "params": ThreadRealtimeTranscriptDeltaNotification } | { "method": "thread/realtime/transcript/done", "params": ThreadRealtimeTranscriptDoneNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification };
export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/fileChange/patchUpdated", "params": FileChangePatchUpdatedNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "externalAgentConfig/import/completed", "params": ExternalAgentConfigImportCompletedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "warning", "params": WarningNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/transcript/delta", "params": ThreadRealtimeTranscriptDeltaNotification } | { "method": "thread/realtime/transcript/done", "params": ThreadRealtimeTranscriptDoneNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification };
@@ -2,5 +2,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
import type { FileSystemSandboxEntry } from "./FileSystemSandboxEntry";
export type AdditionalFileSystemPermissions = { read: Array<AbsolutePathBuf> | null, write: Array<AbsolutePathBuf> | null, };
export type AdditionalFileSystemPermissions = { read: Array<AbsolutePathBuf> | null, write: Array<AbsolutePathBuf> | null, globScanMaxDepth?: number, entries?: Array<FileSystemSandboxEntry>, };
@@ -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 { JsonValue } from "../serde_json/JsonValue";
export type DynamicToolCallParams = { threadId: string, turnId: string, callId: string, tool: string, arguments: JsonValue, };
export type DynamicToolCallParams = { threadId: string, turnId: string, callId: string, namespace: string | null, tool: string, arguments: JsonValue, };
@@ -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 { JsonValue } from "../serde_json/JsonValue";
export type DynamicToolSpec = { name: string, description: string, inputSchema: JsonValue, deferLoading?: boolean, };
export type DynamicToolSpec = { namespace?: string, name: string, description: string, inputSchema: JsonValue, deferLoading?: boolean, };
@@ -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 { FileUpdateChange } from "./FileUpdateChange";
export type FileChangePatchUpdatedNotification = { threadId: string, turnId: string, itemId: string, changes: Array<FileUpdateChange>, };
@@ -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 FileSystemAccessMode = "read" | "write" | "none";
@@ -0,0 +1,7 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
import type { FileSystemSpecialPath } from "./FileSystemSpecialPath";
export type FileSystemPath = { "type": "path", path: AbsolutePathBuf, } | { "type": "glob_pattern", pattern: string, } | { "type": "special", value: FileSystemSpecialPath, };
@@ -0,0 +1,7 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { FileSystemAccessMode } from "./FileSystemAccessMode";
import type { FileSystemPath } from "./FileSystemPath";
export type FileSystemSandboxEntry = { path: FileSystemPath, access: FileSystemAccessMode, };
@@ -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 FileSystemSpecialPath = { "kind": "root" } | { "kind": "minimal" } | { "kind": "current_working_directory" } | { "kind": "project_roots", subpath: string | null, } | { "kind": "tmpdir" } | { "kind": "slash_tmp" } | { "kind": "unknown", path: string, subpath: string | null, };
@@ -4,5 +4,6 @@
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
import type { GuardianCommandSource } from "./GuardianCommandSource";
import type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol";
import type { RequestPermissionProfile } from "./RequestPermissionProfile";
export type GuardianApprovalReviewAction = { "type": "command", source: GuardianCommandSource, command: string, cwd: AbsolutePathBuf, } | { "type": "execve", source: GuardianCommandSource, program: string, argv: Array<string>, cwd: AbsolutePathBuf, } | { "type": "applyPatch", cwd: AbsolutePathBuf, files: Array<AbsolutePathBuf>, } | { "type": "networkAccess", target: string, host: string, protocol: NetworkApprovalProtocol, port: number, } | { "type": "mcpToolCall", server: string, toolName: string, connectorId: string | null, connectorName: string | null, toolTitle: string | null, };
export type GuardianApprovalReviewAction = { "type": "command", source: GuardianCommandSource, command: string, cwd: AbsolutePathBuf, } | { "type": "execve", source: GuardianCommandSource, program: string, argv: Array<string>, cwd: AbsolutePathBuf, } | { "type": "applyPatch", cwd: AbsolutePathBuf, files: Array<AbsolutePathBuf>, } | { "type": "networkAccess", target: string, host: string, protocol: NetworkApprovalProtocol, port: number, } | { "type": "mcpToolCall", server: string, toolName: string, connectorId: string | null, connectorName: string | null, toolTitle: string | null, } | { "type": "requestPermissions", reason: string | null, permissions: RequestPermissionProfile, };
@@ -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 McpResourceReadParams = { threadId: string, server: string, uri: string, };
export type McpResourceReadParams = { threadId?: string | null, server: string, uri: string, };
@@ -57,7 +57,7 @@ durationMs: number | null, } | { "type": "fileChange", id: string, changes: Arra
/**
* The duration of the MCP tool call in milliseconds.
*/
durationMs: number | null, } | { "type": "dynamicToolCall", id: string, tool: string, arguments: JsonValue, status: DynamicToolCallStatus, contentItems: Array<DynamicToolCallOutputContentItem> | null, success: boolean | null,
durationMs: number | null, } | { "type": "dynamicToolCall", id: string, namespace: string | null, tool: string, arguments: JsonValue, status: DynamicToolCallStatus, contentItems: Array<DynamicToolCallOutputContentItem> | null, success: boolean | null,
/**
* The duration of the dynamic tool call in milliseconds.
*/
@@ -98,8 +98,13 @@ export type { FeedbackUploadParams } from "./FeedbackUploadParams";
export type { FeedbackUploadResponse } from "./FeedbackUploadResponse";
export type { FileChangeApprovalDecision } from "./FileChangeApprovalDecision";
export type { FileChangeOutputDeltaNotification } from "./FileChangeOutputDeltaNotification";
export type { FileChangePatchUpdatedNotification } from "./FileChangePatchUpdatedNotification";
export type { FileChangeRequestApprovalParams } from "./FileChangeRequestApprovalParams";
export type { FileChangeRequestApprovalResponse } from "./FileChangeRequestApprovalResponse";
export type { FileSystemAccessMode } from "./FileSystemAccessMode";
export type { FileSystemPath } from "./FileSystemPath";
export type { FileSystemSandboxEntry } from "./FileSystemSandboxEntry";
export type { FileSystemSpecialPath } from "./FileSystemSpecialPath";
export type { FileUpdateChange } from "./FileUpdateChange";
export type { FsChangedNotification } from "./FsChangedNotification";
export type { FsCopyParams } from "./FsCopyParams";
@@ -1023,6 +1023,7 @@ server_notification_definitions! {
CommandExecutionOutputDelta => "item/commandExecution/outputDelta" (v2::CommandExecutionOutputDeltaNotification),
TerminalInteraction => "item/commandExecution/terminalInteraction" (v2::TerminalInteractionNotification),
FileChangeOutputDelta => "item/fileChange/outputDelta" (v2::FileChangeOutputDeltaNotification),
FileChangePatchUpdated => "item/fileChange/patchUpdated" (v2::FileChangePatchUpdatedNotification),
ServerRequestResolved => "serverRequest/resolved" (v2::ServerRequestResolvedNotification),
McpToolCallProgress => "item/mcpToolCall/progress" (v2::McpToolCallProgressNotification),
McpServerOauthLoginCompleted => "mcpServer/oauthLogin/completed" (v2::McpServerOauthLoginCompletedNotification),
@@ -2055,6 +2056,8 @@ mod tests {
file_system: Some(v2::AdditionalFileSystemPermissions {
read: Some(vec![absolute_path("/tmp/allowed")]),
write: None,
glob_scan_max_depth: None,
entries: None,
}),
}),
proposed_execpolicy_amendment: None,
@@ -199,7 +199,8 @@ pub fn build_item_from_guardian_event(
}
GuardianAssessmentAction::ApplyPatch { .. }
| GuardianAssessmentAction::NetworkAccess { .. }
| GuardianAssessmentAction::McpToolCall { .. } => None,
| GuardianAssessmentAction::McpToolCall { .. }
| GuardianAssessmentAction::RequestPermissions { .. } => None,
}
}
@@ -470,6 +470,7 @@ impl ThreadHistoryBuilder {
) {
let item = ThreadItem::DynamicToolCall {
id: payload.call_id.clone(),
namespace: payload.namespace.clone(),
tool: payload.tool.clone(),
arguments: payload.arguments.clone(),
status: DynamicToolCallStatus::InProgress,
@@ -493,6 +494,7 @@ impl ThreadHistoryBuilder {
let duration_ms = i64::try_from(payload.duration.as_millis()).ok();
let item = ThreadItem::DynamicToolCall {
id: payload.call_id.clone(),
namespace: payload.namespace.clone(),
tool: payload.tool.clone(),
arguments: payload.arguments.clone(),
status,
@@ -1975,6 +1977,7 @@ mod tests {
codex_protocol::dynamic_tools::DynamicToolCallRequest {
call_id: "dyn-1".into(),
turn_id: "turn-1".into(),
namespace: Some("codex_app".into()),
tool: "lookup_ticket".into(),
arguments: serde_json::json!({"id":"ABC-123"}),
},
@@ -1982,6 +1985,7 @@ mod tests {
EventMsg::DynamicToolCallResponse(DynamicToolCallResponseEvent {
call_id: "dyn-1".into(),
turn_id: "turn-1".into(),
namespace: Some("codex_app".into()),
tool: "lookup_ticket".into(),
arguments: serde_json::json!({"id":"ABC-123"}),
content_items: vec![CoreDynamicToolCallOutputContentItem::InputText {
@@ -2004,6 +2008,7 @@ mod tests {
turns[0].items[1],
ThreadItem::DynamicToolCall {
id: "dyn-1".into(),
namespace: Some("codex_app".into()),
tool: "lookup_ticket".into(),
arguments: serde_json::json!({"id":"ABC-123"}),
status: DynamicToolCallStatus::Completed,
+280 -16
View File
@@ -1,5 +1,6 @@
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::num::NonZeroUsize;
use std::path::PathBuf;
use crate::RequestId;
@@ -46,6 +47,10 @@ use codex_protocol::openai_models::ModelAvailabilityNux as CoreModelAvailability
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::openai_models::default_input_modalities;
use codex_protocol::parse_command::ParsedCommand as CoreParsedCommand;
use codex_protocol::permissions::FileSystemAccessMode as CoreFileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath as CoreFileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry as CoreFileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSpecialPath as CoreFileSystemSpecialPath;
use codex_protocol::plan_tool::PlanItemArg as CorePlanItemArg;
use codex_protocol::plan_tool::StepStatus as CorePlanStepStatus;
use codex_protocol::protocol::AgentStatus as CoreAgentStatus;
@@ -608,6 +613,8 @@ pub struct ToolsV2 {
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct DynamicToolSpec {
#[ts(optional)]
pub namespace: Option<String>,
pub name: String,
pub description: String,
pub input_schema: JsonValue,
@@ -618,6 +625,7 @@ pub struct DynamicToolSpec {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct DynamicToolSpecDe {
namespace: Option<String>,
name: String,
description: String,
input_schema: JsonValue,
@@ -631,6 +639,7 @@ impl<'de> Deserialize<'de> for DynamicToolSpec {
D: serde::Deserializer<'de>,
{
let DynamicToolSpecDe {
namespace,
name,
description,
input_schema,
@@ -639,6 +648,7 @@ impl<'de> Deserialize<'de> for DynamicToolSpec {
} = DynamicToolSpecDe::deserialize(deserializer)?;
Ok(Self {
namespace,
name,
description,
input_schema,
@@ -1156,23 +1166,55 @@ impl From<CoreNetworkApprovalContext> for NetworkApprovalContext {
pub struct AdditionalFileSystemPermissions {
pub read: Option<Vec<AbsolutePathBuf>>,
pub write: Option<Vec<AbsolutePathBuf>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub glob_scan_max_depth: Option<NonZeroUsize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub entries: Option<Vec<FileSystemSandboxEntry>>,
}
impl From<CoreFileSystemPermissions> for AdditionalFileSystemPermissions {
fn from(value: CoreFileSystemPermissions) -> Self {
Self {
read: value.read,
write: value.write,
if let Some((read, write)) = value.legacy_read_write_roots() {
Self {
read,
write,
glob_scan_max_depth: None,
entries: None,
}
} else {
Self {
read: None,
write: None,
glob_scan_max_depth: value.glob_scan_max_depth,
entries: Some(
value
.entries
.into_iter()
.map(FileSystemSandboxEntry::from)
.collect(),
),
}
}
}
}
impl From<AdditionalFileSystemPermissions> for CoreFileSystemPermissions {
fn from(value: AdditionalFileSystemPermissions) -> Self {
Self {
read: value.read,
write: value.write,
}
let mut permissions = if let Some(entries) = value.entries {
Self {
entries: entries
.into_iter()
.map(CoreFileSystemSandboxEntry::from)
.collect(),
glob_scan_max_depth: None,
}
} else {
CoreFileSystemPermissions::from_read_write_roots(value.read, value.write)
};
permissions.glob_scan_max_depth = value.glob_scan_max_depth;
permissions
}
}
@@ -1226,6 +1268,121 @@ impl From<RequestPermissionProfile> for CoreRequestPermissionProfile {
}
}
v2_enum_from_core!(
pub enum FileSystemAccessMode from CoreFileSystemAccessMode {
Read,
Write,
None
}
);
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(tag = "kind", rename_all = "snake_case")]
#[ts(tag = "kind")]
#[ts(export_to = "v2/")]
pub enum FileSystemSpecialPath {
Root,
Minimal,
CurrentWorkingDirectory,
ProjectRoots {
subpath: Option<PathBuf>,
},
Tmpdir,
SlashTmp,
Unknown {
path: String,
subpath: Option<PathBuf>,
},
}
impl From<CoreFileSystemSpecialPath> for FileSystemSpecialPath {
fn from(value: CoreFileSystemSpecialPath) -> Self {
match value {
CoreFileSystemSpecialPath::Root => Self::Root,
CoreFileSystemSpecialPath::Minimal => Self::Minimal,
CoreFileSystemSpecialPath::CurrentWorkingDirectory => Self::CurrentWorkingDirectory,
CoreFileSystemSpecialPath::ProjectRoots { subpath } => Self::ProjectRoots { subpath },
CoreFileSystemSpecialPath::Tmpdir => Self::Tmpdir,
CoreFileSystemSpecialPath::SlashTmp => Self::SlashTmp,
CoreFileSystemSpecialPath::Unknown { path, subpath } => Self::Unknown { path, subpath },
}
}
}
impl From<FileSystemSpecialPath> for CoreFileSystemSpecialPath {
fn from(value: FileSystemSpecialPath) -> Self {
match value {
FileSystemSpecialPath::Root => Self::Root,
FileSystemSpecialPath::Minimal => Self::Minimal,
FileSystemSpecialPath::CurrentWorkingDirectory => Self::CurrentWorkingDirectory,
FileSystemSpecialPath::ProjectRoots { subpath } => Self::ProjectRoots { subpath },
FileSystemSpecialPath::Tmpdir => Self::Tmpdir,
FileSystemSpecialPath::SlashTmp => Self::SlashTmp,
FileSystemSpecialPath::Unknown { path, subpath } => Self::Unknown { path, subpath },
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "snake_case")]
#[ts(tag = "type")]
#[ts(export_to = "v2/")]
pub enum FileSystemPath {
Path { path: AbsolutePathBuf },
GlobPattern { pattern: String },
Special { value: FileSystemSpecialPath },
}
impl From<CoreFileSystemPath> for FileSystemPath {
fn from(value: CoreFileSystemPath) -> Self {
match value {
CoreFileSystemPath::Path { path } => Self::Path { path },
CoreFileSystemPath::GlobPattern { pattern } => Self::GlobPattern { pattern },
CoreFileSystemPath::Special { value } => Self::Special {
value: value.into(),
},
}
}
}
impl From<FileSystemPath> for CoreFileSystemPath {
fn from(value: FileSystemPath) -> Self {
match value {
FileSystemPath::Path { path } => Self::Path { path },
FileSystemPath::GlobPattern { pattern } => Self::GlobPattern { pattern },
FileSystemPath::Special { value } => Self::Special {
value: value.into(),
},
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct FileSystemSandboxEntry {
pub path: FileSystemPath,
pub access: FileSystemAccessMode,
}
impl From<CoreFileSystemSandboxEntry> for FileSystemSandboxEntry {
fn from(value: CoreFileSystemSandboxEntry) -> Self {
Self {
path: value.path.into(),
access: value.access.into(),
}
}
}
impl From<FileSystemSandboxEntry> for CoreFileSystemSandboxEntry {
fn from(value: FileSystemSandboxEntry) -> Self {
Self {
path: value.path.into(),
access: value.access.to_core(),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -2089,7 +2246,8 @@ pub struct ListMcpServerStatusResponse {
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct McpResourceReadParams {
pub thread_id: String,
#[ts(optional = nullable)]
pub thread_id: Option<String>,
pub server: String,
pub uri: String,
}
@@ -4734,6 +4892,7 @@ pub enum ThreadItem {
#[ts(rename_all = "camelCase")]
DynamicToolCall {
id: String,
namespace: Option<String>,
tool: String,
arguments: JsonValue,
status: DynamicToolCallStatus,
@@ -4989,6 +5148,14 @@ pub struct GuardianMcpToolCallReviewAction {
pub tool_title: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct GuardianRequestPermissionsReviewAction {
pub reason: Option<String>,
pub permissions: RequestPermissionProfile,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "camelCase")]
#[ts(tag = "type", rename_all = "camelCase")]
@@ -5032,6 +5199,12 @@ pub enum GuardianApprovalReviewAction {
connector_name: Option<String>,
tool_title: Option<String>,
},
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
RequestPermissions {
reason: Option<String>,
permissions: RequestPermissionProfile,
},
}
impl From<CoreGuardianAssessmentAction> for GuardianApprovalReviewAction {
@@ -5084,6 +5257,13 @@ impl From<CoreGuardianAssessmentAction> for GuardianApprovalReviewAction {
connector_name,
tool_title,
},
CoreGuardianAssessmentAction::RequestPermissions {
reason,
permissions,
} => Self::RequestPermissions {
reason,
permissions: permissions.into(),
},
}
}
}
@@ -5138,6 +5318,13 @@ impl From<GuardianApprovalReviewAction> for CoreGuardianAssessmentAction {
connector_name,
tool_title,
},
GuardianApprovalReviewAction::RequestPermissions {
reason,
permissions,
} => Self::RequestPermissions {
reason,
permissions: permissions.into(),
},
}
}
}
@@ -5792,6 +5979,16 @@ pub struct FileChangeOutputDeltaNotification {
pub delta: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct FileChangePatchUpdatedNotification {
pub thread_id: String,
pub turn_id: String,
pub item_id: String,
pub changes: Vec<FileUpdateChange>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -6459,6 +6656,7 @@ pub struct DynamicToolCallParams {
pub thread_id: String,
pub turn_id: String,
pub call_id: String,
pub namespace: Option<String>,
pub tool: String,
pub arguments: JsonValue,
}
@@ -6797,6 +6995,7 @@ mod tests {
use codex_utils_absolute_path::test_support::test_path_buf;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::num::NonZeroUsize;
use std::path::PathBuf;
fn absolute_path_string(path: &str) -> String {
@@ -6931,6 +7130,8 @@ mod tests {
AbsolutePathBuf::try_from(PathBuf::from(read_write_path))
.expect("path must be absolute"),
]),
glob_scan_max_depth: None,
entries: None,
}),
}
);
@@ -6941,16 +7142,16 @@ mod tests {
network: Some(CoreNetworkPermissions {
enabled: Some(true),
}),
file_system: Some(CoreFileSystemPermissions {
read: Some(vec![
file_system: Some(CoreFileSystemPermissions::from_read_write_roots(
Some(vec![
AbsolutePathBuf::try_from(PathBuf::from(read_only_path))
.expect("path must be absolute"),
]),
write: Some(vec![
Some(vec![
AbsolutePathBuf::try_from(PathBuf::from(read_write_path))
.expect("path must be absolute"),
]),
}),
)),
}
);
}
@@ -6984,6 +7185,66 @@ mod tests {
);
}
#[test]
fn additional_file_system_permissions_preserves_canonical_entries() {
let core_permissions = CoreFileSystemPermissions {
entries: vec![
CoreFileSystemSandboxEntry {
path: CoreFileSystemPath::Special {
value: CoreFileSystemSpecialPath::Root,
},
access: CoreFileSystemAccessMode::Write,
},
CoreFileSystemSandboxEntry {
path: CoreFileSystemPath::GlobPattern {
pattern: "**/*.env".to_string(),
},
access: CoreFileSystemAccessMode::None,
},
],
glob_scan_max_depth: NonZeroUsize::new(2),
};
let permissions = AdditionalFileSystemPermissions::from(core_permissions.clone());
assert_eq!(
permissions,
AdditionalFileSystemPermissions {
read: None,
write: None,
glob_scan_max_depth: NonZeroUsize::new(2),
entries: Some(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Write,
},
FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: "**/*.env".to_string(),
},
access: FileSystemAccessMode::None,
},
]),
}
);
assert_eq!(
CoreFileSystemPermissions::from(permissions),
core_permissions
);
}
#[test]
fn additional_file_system_permissions_rejects_zero_glob_scan_depth() {
serde_json::from_value::<AdditionalFileSystemPermissions>(json!({
"read": null,
"write": null,
"globScanMaxDepth": 0,
"entries": [],
}))
.expect_err("zero glob scan depth should fail deserialization");
}
#[test]
fn permissions_request_approval_response_uses_granted_permission_profile_without_macos() {
let read_only_path = if cfg!(windows) {
@@ -7024,6 +7285,8 @@ mod tests {
AbsolutePathBuf::try_from(PathBuf::from(read_write_path))
.expect("path must be absolute"),
]),
glob_scan_max_depth: None,
entries: None,
}),
}
);
@@ -7034,16 +7297,16 @@ mod tests {
network: Some(CoreNetworkPermissions {
enabled: Some(true),
}),
file_system: Some(CoreFileSystemPermissions {
read: Some(vec![
file_system: Some(CoreFileSystemPermissions::from_read_write_roots(
Some(vec![
AbsolutePathBuf::try_from(PathBuf::from(read_only_path))
.expect("path must be absolute"),
]),
write: Some(vec![
Some(vec![
AbsolutePathBuf::try_from(PathBuf::from(read_write_path))
.expect("path must be absolute"),
]),
}),
)),
}
);
}
@@ -9047,6 +9310,7 @@ mod tests {
assert_eq!(
actual,
DynamicToolSpec {
namespace: None,
name: "lookup_ticket".to_string(),
description: "Fetch a ticket".to_string(),
input_schema: json!({
+1
View File
@@ -97,6 +97,7 @@ axum = { workspace = true, default-features = false, features = [
"tokio",
] }
core_test_support = { workspace = true }
codex-model-provider-info = { workspace = true }
codex-utils-cargo-bin = { workspace = true }
opentelemetry = { workspace = true }
opentelemetry_sdk = { workspace = true }
+4 -3
View File
@@ -147,7 +147,7 @@ Example with notification opt-out:
- `thread/memoryMode/set` — experimental; set a thread’s persisted memory eligibility to `"enabled"` or `"disabled"` for either a loaded thread or a stored rollout; returns `{}` on success.
- `memory/reset` — experimental; clear the current `CODEX_HOME/memories` directory and reset persisted memory stage data in sqlite while preserving existing thread memory modes; returns `{}` on success.
- `thread/status/changed` — notification emitted when a loaded thread’s status changes (`threadId` + new `status`).
- `thread/archive` — move a thread’s rollout file into the archived directory; returns `{}` on success and emits `thread/archived`.
- `thread/archive` — move a thread’s rollout file into the archived directory and attempt to move any spawned descendant thread rollout files; returns `{}` on success and emits `thread/archived` for each archived thread.
- `thread/unsubscribe` — unsubscribe this connection from thread turn/item events. If this was the last subscriber, the server keeps the thread loaded and unloads it only after it has had no subscribers and no thread activity for 30 minutes, then emits `thread/closed`.
- `thread/name/set` — set or update a thread’s user-facing name for either a loaded thread or a persisted rollout; returns `{}` on success and emits `thread/name/updated` to initialized, opted-in clients. Thread names are not required to be unique; name lookups resolve to the most recently updated thread.
- `thread/unarchive` — move an archived rollout file back into the sessions directory; returns the restored `thread` on success and emits `thread/unarchived`.
@@ -197,7 +197,7 @@ Example with notification opt-out:
- `tool/requestUserInput` — prompt the user with 1–3 short questions for a tool call and return their answers (experimental).
- `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 and auth status, plus resources/resource templates for `full` detail; supports cursor+limit pagination. If `detail` is omitted, the server defaults to `full`.
- `mcpServer/resource/read` — read a resource from a thread's configured MCP server by `threadId`, `server`, and `uri`, returning text/blob resource `contents`.
- `mcpServer/resource/read` — read a resource from a configured MCP server by optional `threadId`, `server`, and `uri`, returning text/blob resource `contents`. If `threadId` is omitted, the server reads from the latest MCP config directly.
- `mcpServer/tool/call` — call a tool on a thread's configured MCP server by `threadId`, `server`, `tool`, optional `arguments`, and optional `_meta`, returning the MCP tool result.
- `windowsSandbox/setupStart` — start Windows sandbox setup for the selected mode (`elevated` or `unelevated`); accepts an optional absolute `cwd` to target setup for a specific workspace, returns `{ started: true }` immediately, and later emits `windowsSandbox/setupCompleted`.
- `feedback/upload` — submit a feedback report (classification + optional reason/logs, conversation_id, and optional `extraLogFiles` attachments array); returns the tracking thread id.
@@ -448,7 +448,7 @@ Experimental: use `memory/reset` to clear local memory artifacts and sqlite-back
### Example: Archive a thread
Use `thread/archive` to move the persisted rollout (stored as a JSONL file on disk) into the archived sessions directory.
Use `thread/archive` to move the persisted rollout (stored as a JSONL file on disk) into the archived sessions directory and attempt to move any spawned descendant thread rollouts.
```json
{ "method": "thread/archive", "id": 21, "params": { "threadId": "thr_b" } }
@@ -1069,6 +1069,7 @@ There are additional item-specific events:
#### fileChange
- `item/fileChange/patchUpdated` - when `features.apply_patch_streaming_events` is enabled, streams structured file-change snapshots parsed from the model-generated patch before it is executed.
- `item/fileChange/outputDelta` - contains the tool call response of the underlying `apply_patch` tool call.
### Errors
@@ -40,6 +40,7 @@ use codex_app_server_protocol::ExecCommandApprovalResponse;
use codex_app_server_protocol::ExecPolicyAmendment as V2ExecPolicyAmendment;
use codex_app_server_protocol::FileChangeApprovalDecision;
use codex_app_server_protocol::FileChangeOutputDeltaNotification;
use codex_app_server_protocol::FileChangePatchUpdatedNotification;
use codex_app_server_protocol::FileChangeRequestApprovalParams;
use codex_app_server_protocol::FileChangeRequestApprovalResponse;
use codex_app_server_protocol::FileUpdateChange;
@@ -509,7 +510,8 @@ pub(crate) async fn apply_bespoke_event_handling(
))
.await;
}
RealtimeEvent::ConversationItemDone { .. } => {}
RealtimeEvent::ConversationItemDone { .. }
| RealtimeEvent::NoopRequested(_) => {}
RealtimeEvent::HandoffRequested(handoff) => {
let notification = ThreadRealtimeItemAddedNotification {
thread_id: conversation_id.to_string(),
@@ -952,10 +954,12 @@ pub(crate) async fn apply_bespoke_event_handling(
if matches!(api_version, ApiVersion::V2) {
let call_id = request.call_id;
let turn_id = request.turn_id;
let namespace = request.namespace;
let tool = request.tool;
let arguments = request.arguments;
let item = ThreadItem::DynamicToolCall {
id: call_id.clone(),
namespace: namespace.clone(),
tool: tool.clone(),
arguments: arguments.clone(),
status: DynamicToolCallStatus::InProgress,
@@ -975,6 +979,7 @@ pub(crate) async fn apply_bespoke_event_handling(
thread_id: conversation_id.to_string(),
turn_id: turn_id.clone(),
call_id: call_id.clone(),
namespace,
tool: tool.clone(),
arguments: arguments.clone(),
};
@@ -1013,6 +1018,7 @@ pub(crate) async fn apply_bespoke_event_handling(
let duration_ms = i64::try_from(response.duration.as_millis()).ok();
let item = ThreadItem::DynamicToolCall {
id: response.call_id,
namespace: response.namespace,
tool: response.tool,
arguments: response.arguments,
status,
@@ -1636,6 +1642,17 @@ pub(crate) async fn apply_bespoke_event_handling(
.await;
}
}
EventMsg::PatchApplyUpdated(event) => {
let notification = FileChangePatchUpdatedNotification {
thread_id: conversation_id.to_string(),
turn_id: event_turn_id.clone(),
item_id: event.call_id,
changes: convert_patch_changes(&event.changes),
};
outgoing
.send_server_notification(ServerNotification::FileChangePatchUpdated(notification))
.await;
}
EventMsg::PatchApplyEnd(patch_end_event) => {
// Until we migrate the core to be aware of a first class FileChangeItem
// and emit the corresponding EventMsg, we repurpose the call_id as the item_id.
@@ -3723,10 +3740,10 @@ mod tests {
network: Some(CoreNetworkPermissions {
enabled: Some(true),
}),
file_system: Some(CoreFileSystemPermissions {
read: Some(vec![absolute_path(input_path)]),
write: Some(vec![absolute_path(output_path)]),
}),
file_system: Some(CoreFileSystemPermissions::from_read_write_roots(
Some(vec![absolute_path(input_path)]),
Some(vec![absolute_path(output_path)]),
)),
};
let cases = vec![
(
@@ -3753,10 +3770,10 @@ mod tests {
},
}),
CoreRequestPermissionProfile {
file_system: Some(CoreFileSystemPermissions {
read: None,
write: Some(vec![absolute_path(output_path)]),
}),
file_system: Some(CoreFileSystemPermissions::from_read_write_roots(
/*read*/ None,
Some(vec![absolute_path(output_path)]),
)),
..CoreRequestPermissionProfile::default()
},
),
@@ -3771,10 +3788,10 @@ mod tests {
},
}),
CoreRequestPermissionProfile {
file_system: Some(CoreFileSystemPermissions {
read: Some(vec![absolute_path(input_path)]),
write: Some(vec![absolute_path(output_path)]),
}),
file_system: Some(CoreFileSystemPermissions::from_read_write_roots(
Some(vec![absolute_path(input_path)]),
Some(vec![absolute_path(output_path)]),
)),
..CoreRequestPermissionProfile::default()
},
),
File diff suppressed because it is too large Load Diff
+4
View File
@@ -108,6 +108,7 @@ impl ConfigApi {
self.current_cli_overrides(),
self.loader_overrides.clone(),
self.current_cloud_requirements(),
codex_config::host_name(),
)
}
@@ -563,6 +564,7 @@ mod tests {
CoreSandboxModeRequirement::ReadOnly,
CoreSandboxModeRequirement::ExternalSandbox,
]),
remote_sandbox_config: None,
allowed_web_search_modes: Some(vec![
codex_core::config_loader::WebSearchModeRequirement::Cached,
]),
@@ -675,6 +677,7 @@ mod tests {
allowed_approval_policies: None,
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -733,6 +736,7 @@ mod tests {
allowed_approval_policies: None,
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: Some(Vec::new()),
guardian_policy_config: None,
feature_requirements: None,
+5
View File
@@ -75,6 +75,7 @@ use codex_app_server_protocol::Result;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerRequest;
use codex_arg0::Arg0DispatchPaths;
use codex_config::ThreadConfigLoader;
use codex_core::config::Config;
use codex_core::config_loader::CloudRequirementsLoader;
use codex_core::config_loader::LoaderOverrides;
@@ -116,6 +117,8 @@ pub struct InProcessStartArgs {
pub loader_overrides: LoaderOverrides,
/// Preloaded cloud requirements provider.
pub cloud_requirements: CloudRequirementsLoader,
/// Loader used to fetch typed thread config sources before a thread starts.
pub thread_config_loader: Arc<dyn ThreadConfigLoader>,
/// Feedback sink used by app-server/core telemetry and logs.
pub feedback: CodexFeedback,
/// SQLite tracing layer used to flush recently emitted logs before feedback upload.
@@ -397,6 +400,7 @@ fn start_uninitialized(args: InProcessStartArgs) -> InProcessClientHandle {
cli_overrides: args.cli_overrides,
loader_overrides: args.loader_overrides,
cloud_requirements: args.cloud_requirements,
thread_config_loader: args.thread_config_loader,
feedback: args.feedback,
log_db: args.log_db,
config_warnings: args.config_warnings,
@@ -731,6 +735,7 @@ mod tests {
cli_overrides: Vec::new(),
loader_overrides: LoaderOverrides::default(),
cloud_requirements: CloudRequirementsLoader::default(),
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
feedback: CodexFeedback::new(),
log_db: None,
environment_manager: Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)),
+16 -11
View File
@@ -2,6 +2,7 @@
use codex_arg0::Arg0DispatchPaths;
use codex_cloud_requirements::cloud_requirements_loader;
use codex_config::NoopThreadConfigLoader;
use codex_core::config::Config;
use codex_core::config::ConfigBuilder;
use codex_core::config_loader::CloudRequirementsLoader;
@@ -27,10 +28,11 @@ use crate::outgoing_message::QueuedOutgoingMessage;
use crate::transport::CHANNEL_CAPACITY;
use crate::transport::ConnectionState;
use crate::transport::OutboundConnectionState;
use crate::transport::RemoteControlStartOptions;
use crate::transport::TransportEvent;
use crate::transport::auth::policy_from_settings;
use crate::transport::route_outgoing_envelope;
use crate::transport::start_remote_control;
use crate::transport::start_remote_control_with_options;
use crate::transport::start_stdio_connection;
use crate::transport::start_websocket_acceptor;
use codex_analytics::AppServerRpcTransport;
@@ -415,6 +417,7 @@ pub async fn run_main_with_transport(
}
};
let loader_overrides_for_config_api = loader_overrides.clone();
let thread_config_loader = Arc::new(NoopThreadConfigLoader);
let mut config_warnings = Vec::new();
let config = match ConfigBuilder::default()
.cli_overrides(cli_kv_overrides.clone())
@@ -575,16 +578,17 @@ pub async fn run_main_with_transport(
));
}
let (remote_control_accept_handle, remote_control_handle) = start_remote_control(
config.chatgpt_base_url.clone(),
state_db.clone(),
auth_manager.clone(),
transport_event_tx.clone(),
transport_shutdown_token.clone(),
app_server_client_name_rx,
remote_control_enabled,
)
.await?;
let (remote_control_accept_handle, remote_control_handle) =
start_remote_control_with_options(RemoteControlStartOptions {
remote_control_url: config.chatgpt_base_url.clone(),
state_db: state_db.clone(),
auth_manager: auth_manager.clone(),
transport_event_tx: transport_event_tx.clone(),
shutdown_token: transport_shutdown_token.clone(),
app_server_client_name_rx,
initial_enabled: remote_control_enabled,
})
.await?;
transport_accept_handles.push(remote_control_accept_handle);
let outbound_handle = tokio::spawn(async move {
@@ -657,6 +661,7 @@ pub async fn run_main_with_transport(
cli_overrides,
loader_overrides,
cloud_requirements: cloud_requirements.clone(),
thread_config_loader,
feedback: feedback.clone(),
log_db,
config_warnings,
@@ -63,6 +63,7 @@ use codex_app_server_protocol::ServerRequestPayload;
use codex_app_server_protocol::experimental_required_message;
use codex_arg0::Arg0DispatchPaths;
use codex_chatgpt::connectors;
use codex_config::ThreadConfigLoader;
use codex_core::ThreadManager;
use codex_core::config::Config;
use codex_core::config_loader::CloudRequirementsLoader;
@@ -235,6 +236,7 @@ pub(crate) struct MessageProcessorArgs {
pub(crate) cli_overrides: Vec<(String, TomlValue)>,
pub(crate) loader_overrides: LoaderOverrides,
pub(crate) cloud_requirements: CloudRequirementsLoader,
pub(crate) thread_config_loader: Arc<dyn ThreadConfigLoader>,
pub(crate) feedback: CodexFeedback,
pub(crate) log_db: Option<LogDbLayer>,
pub(crate) config_warnings: Vec<ConfigWarningNotification>,
@@ -256,6 +258,7 @@ impl MessageProcessor {
cli_overrides,
loader_overrides,
cloud_requirements,
thread_config_loader,
feedback,
log_db,
config_warnings,
@@ -301,6 +304,7 @@ impl MessageProcessor {
cli_overrides: cli_overrides.clone(),
runtime_feature_enablement: runtime_feature_enablement.clone(),
cloud_requirements: cloud_requirements.clone(),
thread_config_loader,
feedback,
log_db,
});
@@ -245,6 +245,7 @@ fn build_test_processor(
cli_overrides: Vec::new(),
loader_overrides: LoaderOverrides::default(),
cloud_requirements: CloudRequirementsLoader::default(),
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
feedback: CodexFeedback::new(),
log_db: None,
config_warnings: Vec::new(),
@@ -504,6 +505,10 @@ where
}
#[tokio::test(flavor = "current_thread")]
#[expect(
clippy::await_holding_invalid_type,
reason = "test serializes access to global tracing state for its full duration"
)]
async fn thread_start_jsonrpc_span_exports_server_span_and_parents_children() -> Result<()> {
let _guard = tracing_test_guard().lock().await;
let mut harness = TracingHarness::new().await?;
@@ -583,6 +588,10 @@ async fn thread_start_jsonrpc_span_exports_server_span_and_parents_children() ->
}
#[tokio::test(flavor = "current_thread")]
#[expect(
clippy::await_holding_invalid_type,
reason = "test serializes access to global tracing state for its full duration"
)]
async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> {
let _guard = tracing_test_guard().lock().await;
let mut harness = TracingHarness::new().await?;
@@ -1067,6 +1067,7 @@ mod tests {
thread_id: thread_id.to_string(),
turn_id: "turn-1".to_string(),
call_id: "call-0".to_string(),
namespace: None,
tool: "tool".to_string(),
arguments: json!({}),
},
@@ -1124,6 +1125,7 @@ mod tests {
thread_id: thread_id.to_string(),
turn_id: "turn-1".to_string(),
call_id: "call-0".to_string(),
namespace: None,
tool: "tool".to_string(),
arguments: json!({}),
},
+6 -1
View File
@@ -34,7 +34,8 @@ mod stdio;
mod websocket;
pub(crate) use remote_control::RemoteControlHandle;
pub(crate) use remote_control::start_remote_control;
pub(crate) use remote_control::RemoteControlStartOptions;
pub(crate) use remote_control::start_remote_control_with_options;
pub(crate) use stdio::start_stdio_connection;
pub(crate) use websocket::start_websocket_acceptor;
@@ -780,6 +781,8 @@ mod tests {
codex_app_server_protocol::AdditionalFileSystemPermissions {
read: Some(vec![absolute_path("/tmp/allowed")]),
write: None,
glob_scan_max_depth: None,
entries: None,
},
),
},
@@ -842,6 +845,8 @@ mod tests {
codex_app_server_protocol::AdditionalFileSystemPermissions {
read: Some(vec![absolute_path("/tmp/allowed")]),
write: None,
glob_scan_max_depth: None,
entries: None,
},
),
},
@@ -17,6 +17,7 @@ const REMOTE_CONTROL_RESPONSE_BODY_MAX_BYTES: usize = 4096;
const REQUEST_ID_HEADER: &str = "x-request-id";
const OAI_REQUEST_ID_HEADER: &str = "x-oai-request-id";
const CF_RAY_HEADER: &str = "cf-ray";
const REMOTE_CONTROL_FEDRAMP_HEADER: &str = "X-OpenAI-Fedramp";
pub(super) const REMOTE_CONTROL_ACCOUNT_ID_HEADER: &str = "chatgpt-account-id";
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -29,8 +30,9 @@ pub(super) struct RemoteControlEnrollment {
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct RemoteControlConnectionAuth {
pub(super) bearer_token: String,
pub(super) authorization_header_value: String,
pub(super) account_id: String,
pub(super) is_fedramp_account: bool,
}
pub(super) async fn load_persisted_remote_control_enrollment(
@@ -199,12 +201,15 @@ pub(super) async fn enroll_remote_control_server(
app_server_version: env!("CARGO_PKG_VERSION"),
};
let client = build_reqwest_client();
let http_request = client
let mut http_request = client
.post(enroll_url)
.timeout(REMOTE_CONTROL_ENROLL_TIMEOUT)
.bearer_auth(&auth.bearer_token)
.header(REMOTE_CONTROL_ACCOUNT_ID_HEADER, &auth.account_id)
.json(&request);
.header("authorization", &auth.authorization_header_value)
.header(REMOTE_CONTROL_ACCOUNT_ID_HEADER, &auth.account_id);
if auth.is_fedramp_account {
http_request = http_request.header(REMOTE_CONTROL_FEDRAMP_HEADER, "true");
}
let http_request = http_request.json(&request);
let response = http_request.send().await.map_err(|err| {
io::Error::other(format!(
@@ -445,8 +450,9 @@ mod tests {
let err = enroll_remote_control_server(
&remote_control_target,
&RemoteControlConnectionAuth {
bearer_token: "Access Token".to_string(),
authorization_header_value: "Bearer Access Token".to_string(),
account_id: "account_id".to_string(),
is_fedramp_account: false,
},
)
.await
@@ -4,6 +4,7 @@ mod protocol;
mod websocket;
use crate::transport::remote_control::websocket::RemoteControlWebsocket;
use crate::transport::remote_control::websocket::RemoteControlWebsocketOptions;
pub use self::protocol::ClientId;
use self::protocol::ServerEvent;
@@ -44,6 +45,17 @@ impl RemoteControlHandle {
}
}
pub(crate) struct RemoteControlStartOptions {
pub(crate) remote_control_url: String,
pub(crate) state_db: Option<Arc<StateRuntime>>,
pub(crate) auth_manager: Arc<AuthManager>,
pub(crate) transport_event_tx: mpsc::Sender<TransportEvent>,
pub(crate) shutdown_token: CancellationToken,
pub(crate) app_server_client_name_rx: Option<oneshot::Receiver<String>>,
pub(crate) initial_enabled: bool,
}
#[cfg(test)]
pub(crate) async fn start_remote_control(
remote_control_url: String,
state_db: Option<Arc<StateRuntime>>,
@@ -53,15 +65,38 @@ pub(crate) async fn start_remote_control(
app_server_client_name_rx: Option<oneshot::Receiver<String>>,
initial_enabled: bool,
) -> io::Result<(JoinHandle<()>, RemoteControlHandle)> {
start_remote_control_with_options(RemoteControlStartOptions {
remote_control_url,
state_db,
auth_manager,
transport_event_tx,
shutdown_token,
app_server_client_name_rx,
initial_enabled,
})
.await
}
pub(crate) async fn start_remote_control_with_options(
options: RemoteControlStartOptions,
) -> io::Result<(JoinHandle<()>, RemoteControlHandle)> {
let RemoteControlStartOptions {
remote_control_url,
state_db,
auth_manager,
transport_event_tx,
shutdown_token,
app_server_client_name_rx,
initial_enabled,
} = options;
let remote_control_target = if initial_enabled {
Some(normalize_remote_control_url(&remote_control_url)?)
} else {
None
};
let (enabled_tx, enabled_rx) = watch::channel(initial_enabled);
let join_handle = tokio::spawn(async move {
RemoteControlWebsocket::new(
RemoteControlWebsocket::from_options(RemoteControlWebsocketOptions {
remote_control_url,
remote_control_target,
state_db,
@@ -69,7 +104,7 @@ pub(crate) async fn start_remote_control(
transport_event_tx,
shutdown_token,
enabled_rx,
)
})
.run(app_server_client_name_rx)
.await;
});
@@ -49,6 +49,7 @@ use tracing::warn;
pub(super) const REMOTE_CONTROL_PROTOCOL_VERSION: &str = "2";
pub(super) const REMOTE_CONTROL_ACCOUNT_ID_HEADER: &str = "chatgpt-account-id";
const REMOTE_CONTROL_FEDRAMP_HEADER: &str = "X-OpenAI-Fedramp";
const REMOTE_CONTROL_SUBSCRIBE_CURSOR_HEADER: &str = "x-codex-subscribe-cursor";
const REMOTE_CONTROL_WEBSOCKET_PING_INTERVAL: std::time::Duration =
std::time::Duration::from_secs(10);
@@ -128,6 +129,16 @@ pub(crate) struct RemoteControlWebsocket {
enabled_rx: watch::Receiver<bool>,
}
pub(crate) struct RemoteControlWebsocketOptions {
pub(crate) remote_control_url: String,
pub(crate) remote_control_target: Option<RemoteControlTarget>,
pub(crate) state_db: Option<Arc<StateRuntime>>,
pub(crate) auth_manager: Arc<AuthManager>,
pub(crate) transport_event_tx: mpsc::Sender<TransportEvent>,
pub(crate) shutdown_token: CancellationToken,
pub(crate) enabled_rx: watch::Receiver<bool>,
}
enum ConnectOutcome {
Connected(Box<WebSocketStream<MaybeTlsStream<TcpStream>>>),
Disabled,
@@ -135,6 +146,7 @@ enum ConnectOutcome {
}
impl RemoteControlWebsocket {
#[cfg(test)]
pub(crate) fn new(
remote_control_url: String,
remote_control_target: Option<RemoteControlTarget>,
@@ -144,6 +156,27 @@ impl RemoteControlWebsocket {
shutdown_token: CancellationToken,
enabled_rx: watch::Receiver<bool>,
) -> Self {
Self::from_options(RemoteControlWebsocketOptions {
remote_control_url,
remote_control_target,
state_db,
auth_manager,
transport_event_tx,
shutdown_token,
enabled_rx,
})
}
pub(crate) fn from_options(options: RemoteControlWebsocketOptions) -> Self {
let RemoteControlWebsocketOptions {
remote_control_url,
remote_control_target,
state_db,
auth_manager,
transport_event_tx,
shutdown_token,
enabled_rx,
} = options;
let shutdown_token = shutdown_token.child_token();
let (server_event_tx, server_event_rx) = mpsc::channel(super::CHANNEL_CAPACITY);
let client_tracker =
@@ -172,6 +205,10 @@ impl RemoteControlWebsocket {
}
}
#[expect(
clippy::await_holding_invalid_type,
reason = "remote-control client shutdown must serialize tracker state"
)]
pub(crate) async fn run(
mut self,
app_server_client_name_rx: Option<oneshot::Receiver<String>>,
@@ -271,14 +308,16 @@ impl RemoteControlWebsocket {
}
return ConnectOutcome::Disabled;
}
connect_result = connect_remote_control_websocket(
&remote_control_target,
self.state_db.as_deref(),
&self.auth_manager,
&mut self.auth_recovery,
&mut self.enrollment,
subscribe_cursor.as_deref(),
app_server_client_name,
connect_result = connect_remote_control_websocket_with_options(
ConnectRemoteControlWebsocketOptions {
remote_control_target: &remote_control_target,
state_db: self.state_db.as_deref(),
auth_manager: &self.auth_manager,
auth_recovery: &mut self.auth_recovery,
enrollment: &mut self.enrollment,
subscribe_cursor: subscribe_cursor.as_deref(),
app_server_client_name,
},
) => connect_result,
};
@@ -381,6 +420,10 @@ impl RemoteControlWebsocket {
}
}
#[expect(
clippy::await_holding_invalid_type,
reason = "remote-control server event receiver is shared across reconnects"
)]
async fn run_server_writer_inner(
state: Arc<Mutex<WebsocketState>>,
server_event_rx: Arc<Mutex<mpsc::Receiver<super::QueuedServerEnvelope>>>,
@@ -522,6 +565,10 @@ impl RemoteControlWebsocket {
}
}
#[expect(
clippy::await_holding_invalid_type,
reason = "remote-control client tracking must stay serialized while processing inbound events"
)]
async fn run_websocket_reader_inner(
client_tracker: Arc<Mutex<ClientTracker>>,
state: Arc<Mutex<WebsocketState>>,
@@ -668,12 +715,11 @@ fn build_remote_control_websocket_request(
"x-codex-protocol-version",
REMOTE_CONTROL_PROTOCOL_VERSION,
)?;
set_remote_control_header(
headers,
"authorization",
&format!("Bearer {}", auth.bearer_token),
)?;
set_remote_control_header(headers, "authorization", &auth.authorization_header_value)?;
set_remote_control_header(headers, REMOTE_CONTROL_ACCOUNT_ID_HEADER, &auth.account_id)?;
if auth.is_fedramp_account {
set_remote_control_header(headers, REMOTE_CONTROL_FEDRAMP_HEADER, "true")?;
}
if let Some(subscribe_cursor) = subscribe_cursor {
set_remote_control_header(
headers,
@@ -718,8 +764,19 @@ pub(crate) async fn load_remote_control_auth(
));
}
let authorization_header_value = auth_manager
.chatgpt_authorization_header_for_auth(&auth)
.await
.ok_or_else(|| {
io::Error::new(
ErrorKind::PermissionDenied,
"remote control requires ChatGPT authentication",
)
})?;
Ok(RemoteControlConnectionAuth {
bearer_token: auth.get_token().map_err(io::Error::other)?,
authorization_header_value,
is_fedramp_account: auth.is_fedramp_account(),
account_id: auth.get_account_id().ok_or_else(|| {
io::Error::new(
ErrorKind::WouldBlock,
@@ -729,6 +786,7 @@ pub(crate) async fn load_remote_control_auth(
})
}
#[cfg(test)]
pub(super) async fn connect_remote_control_websocket(
remote_control_target: &RemoteControlTarget,
state_db: Option<&StateRuntime>,
@@ -741,6 +799,44 @@ pub(super) async fn connect_remote_control_websocket(
WebSocketStream<MaybeTlsStream<TcpStream>>,
tungstenite::http::Response<()>,
)> {
connect_remote_control_websocket_with_options(ConnectRemoteControlWebsocketOptions {
remote_control_target,
state_db,
auth_manager,
auth_recovery,
enrollment,
subscribe_cursor,
app_server_client_name,
})
.await
}
struct ConnectRemoteControlWebsocketOptions<'a> {
remote_control_target: &'a RemoteControlTarget,
state_db: Option<&'a StateRuntime>,
auth_manager: &'a Arc<AuthManager>,
auth_recovery: &'a mut UnauthorizedRecovery,
enrollment: &'a mut Option<RemoteControlEnrollment>,
subscribe_cursor: Option<&'a str>,
app_server_client_name: Option<&'a str>,
}
async fn connect_remote_control_websocket_with_options(
options: ConnectRemoteControlWebsocketOptions<'_>,
) -> io::Result<(
WebSocketStream<MaybeTlsStream<TcpStream>>,
tungstenite::http::Response<()>,
)> {
let ConnectRemoteControlWebsocketOptions {
remote_control_target,
state_db,
auth_manager,
auth_recovery,
enrollment,
subscribe_cursor,
app_server_client_name,
} = options;
ensure_rustls_crypto_provider();
let auth = load_remote_control_auth(auth_manager).await?;
@@ -1003,6 +1099,34 @@ mod tests {
}
}
#[test]
fn build_remote_control_websocket_request_includes_fedramp_header() {
let request = build_remote_control_websocket_request(
"ws://127.0.0.1/backend-api/wham/remote/control/server",
&RemoteControlEnrollment {
account_id: "account_id".to_string(),
environment_id: "env_test".to_string(),
server_id: "srv_e_test".to_string(),
server_name: "test-server".to_string(),
},
&RemoteControlConnectionAuth {
authorization_header_value: "AgentAssertion assertion".to_string(),
account_id: "account_id".to_string(),
is_fedramp_account: true,
},
/*subscribe_cursor*/ None,
)
.expect("request should build");
assert_eq!(
request
.headers()
.get(REMOTE_CONTROL_FEDRAMP_HEADER)
.and_then(|value| value.to_str().ok()),
Some("true")
);
}
#[tokio::test]
async fn connect_remote_control_websocket_includes_http_error_details() {
let listener = TcpListener::bind("127.0.0.1:0")
@@ -6,6 +6,7 @@ use app_test_support::to_response;
use codex_app_server_protocol::ConversationSummary;
use codex_app_server_protocol::GetConversationSummaryParams;
use codex_app_server_protocol::GetConversationSummaryResponse;
use codex_app_server_protocol::JSONRPCError;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_protocol::ThreadId;
@@ -18,16 +19,18 @@ use tokio::time::timeout;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
const FILENAME_TS: &str = "2025-01-02T12-00-00";
const META_RFC3339: &str = "2025-01-02T12:00:00Z";
const CREATED_AT_RFC3339: &str = "2025-01-02T12:00:00.000Z";
const UPDATED_AT_RFC3339: &str = "2025-01-02T12:00:00.000Z";
const PREVIEW: &str = "Summarize this conversation";
const MODEL_PROVIDER: &str = "openai";
const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
fn expected_summary(conversation_id: ThreadId, path: PathBuf) -> ConversationSummary {
ConversationSummary {
conversation_id,
path,
preview: PREVIEW.to_string(),
timestamp: Some(META_RFC3339.to_string()),
timestamp: Some(CREATED_AT_RFC3339.to_string()),
updated_at: Some(UPDATED_AT_RFC3339.to_string()),
model_provider: MODEL_PROVIDER.to_string(),
cwd: PathBuf::from("/"),
@@ -77,6 +80,37 @@ async fn get_conversation_summary_by_thread_id_reads_rollout() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn get_conversation_summary_by_rollout_path_rejects_remote_thread_store() -> Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join("config.toml"),
r#"experimental_thread_store_endpoint = "http://127.0.0.1:1"
"#,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_get_conversation_summary_request(GetConversationSummaryParams::RolloutPath {
rollout_path: PathBuf::from("sessions/2025/01/02/rollout.jsonl"),
})
.await?;
let error: JSONRPCError = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert_eq!(error.error.code, INVALID_REQUEST_ERROR_CODE);
assert_eq!(
error.error.message,
"rollout path queries are only supported with the local thread store"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn get_conversation_summary_by_relative_rollout_path_resolves_from_codex_home() -> Result<()>
{
@@ -64,6 +64,7 @@ async fn thread_start_injects_dynamic_tools_into_model_requests() -> Result<()>
"additionalProperties": false,
});
let dynamic_tool = DynamicToolSpec {
namespace: None,
name: "demo_tool".to_string(),
description: "Demo dynamic tool".to_string(),
input_schema: input_schema.clone(),
@@ -137,6 +138,7 @@ async fn thread_start_keeps_hidden_dynamic_tools_out_of_model_requests() -> Resu
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let dynamic_tool = DynamicToolSpec {
namespace: Some("codex_app".to_string()),
name: "hidden_tool".to_string(),
description: "Hidden dynamic tool".to_string(),
input_schema: json!({
@@ -197,10 +199,51 @@ async fn thread_start_keeps_hidden_dynamic_tools_out_of_model_requests() -> Resu
Ok(())
}
#[tokio::test]
async fn thread_start_rejects_hidden_dynamic_tools_without_namespace() -> Result<()> {
let server = MockServer::start().await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let dynamic_tool = DynamicToolSpec {
namespace: None,
name: "hidden_tool".to_string(),
description: "Hidden dynamic tool".to_string(),
input_schema: json!({
"type": "object",
"properties": {},
"additionalProperties": false,
}),
defer_loading: true,
};
let thread_req = mcp
.send_thread_start_request(ThreadStartParams {
dynamic_tools: Some(vec![dynamic_tool]),
..Default::default()
})
.await?;
let error = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(thread_req)),
)
.await??;
assert_eq!(error.error.code, -32600);
assert!(error.error.message.contains("hidden_tool"));
assert!(error.error.message.contains("namespace"));
Ok(())
}
/// Exercises the full dynamic tool call path (server request, client response, model output).
#[tokio::test]
async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Result<()> {
let call_id = "dyn-call-1";
let tool_namespace = "codex_app";
let tool_name = "demo_tool";
let tool_args = json!({ "city": "Paris" });
let tool_call_arguments = serde_json::to_string(&tool_args)?;
@@ -209,7 +252,16 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res
let responses = vec![
responses::sse(vec![
responses::ev_response_created("resp-1"),
responses::ev_function_call(call_id, tool_name, &tool_call_arguments),
json!({
"type": "response.output_item.done",
"item": {
"type": "function_call",
"call_id": call_id,
"namespace": tool_namespace,
"name": tool_name,
"arguments": tool_call_arguments,
}
}),
responses::ev_completed("resp-1"),
]),
create_final_assistant_message_sse_response("Done")?,
@@ -223,6 +275,7 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let dynamic_tool = DynamicToolSpec {
namespace: Some(tool_namespace.to_string()),
name: tool_name.to_string(),
description: "Demo dynamic tool".to_string(),
input_schema: json!({
@@ -274,6 +327,7 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res
assert_eq!(started.turn_id, turn_id.clone());
let ThreadItem::DynamicToolCall {
id,
namespace,
tool,
arguments,
status,
@@ -285,6 +339,7 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res
panic!("expected dynamic tool call item");
};
assert_eq!(id, call_id);
assert_eq!(namespace.as_deref(), Some(tool_namespace));
assert_eq!(tool, tool_name);
assert_eq!(arguments, tool_args);
assert_eq!(status, DynamicToolCallStatus::InProgress);
@@ -307,6 +362,7 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res
thread_id: thread_id.clone(),
turn_id: turn_id.clone(),
call_id: call_id.to_string(),
namespace: Some(tool_namespace.to_string()),
tool: tool_name.to_string(),
arguments: tool_args.clone(),
};
@@ -327,6 +383,7 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res
assert_eq!(completed.turn_id, turn_id);
let ThreadItem::DynamicToolCall {
id,
namespace,
tool,
arguments,
status,
@@ -338,6 +395,7 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res
panic!("expected dynamic tool call item");
};
assert_eq!(id, call_id);
assert_eq!(namespace.as_deref(), Some(tool_namespace));
assert_eq!(tool, tool_name);
assert_eq!(arguments, tool_args);
assert_eq!(status, DynamicToolCallStatus::Completed);
@@ -392,6 +450,7 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<(
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let dynamic_tool = DynamicToolSpec {
namespace: None,
name: tool_name.to_string(),
description: "Demo dynamic tool".to_string(),
input_schema: json!({
@@ -455,6 +514,7 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<(
thread_id,
turn_id: turn_id.clone(),
call_id: call_id.to_string(),
namespace: None,
tool: tool_name.to_string(),
arguments: tool_args,
};
@@ -24,7 +24,7 @@ use std::collections::BTreeMap;
use tempfile::TempDir;
use tokio::time::timeout;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
#[tokio::test]
async fn experimental_feature_list_returns_feature_metadata_with_stage() -> Result<()> {
@@ -43,6 +43,7 @@ use rmcp::transport::StreamableHttpService;
use rmcp::transport::streamable_http_server::session::local::LocalSessionManager;
use tempfile::TempDir;
use tokio::net::TcpListener;
use tokio::task::JoinHandle;
use tokio::time::timeout;
const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10);
@@ -54,19 +55,7 @@ const TEST_RESOURCE_TEXT: &str = "Resource body from the MCP server.";
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn mcp_resource_read_returns_resource_contents() -> Result<()> {
let responses_server = responses::start_mock_server().await;
let listener = TcpListener::bind("127.0.0.1:0").await?;
let addr = listener.local_addr()?;
let apps_server_url = format!("http://{addr}");
let mcp_service = StreamableHttpService::new(
move || Ok(ResourceAppsMcpServer),
Arc::new(LocalSessionManager::default()),
StreamableHttpServerConfig::default(),
);
let router = Router::new().nest_service("/api/codex/apps", mcp_service);
let apps_server_handle = tokio::spawn(async move {
let _ = axum::serve(listener, router).await;
});
let (apps_server_url, apps_server_handle) = start_resource_apps_mcp_server().await?;
let codex_home = TempDir::new()?;
let responses_server_uri = responses_server.uri();
@@ -121,7 +110,7 @@ stream_max_retries = 0
let read_request_id = mcp
.send_mcp_resource_read_request(McpResourceReadParams {
thread_id: thread.id,
thread_id: Some(thread.id),
server: "codex_apps".to_string(),
uri: TEST_RESOURCE_URI.to_string(),
})
@@ -134,22 +123,59 @@ stream_max_retries = 0
assert_eq!(
to_response::<McpResourceReadResponse>(read_response)?,
McpResourceReadResponse {
contents: vec![
McpResourceContent::Text {
uri: TEST_RESOURCE_URI.to_string(),
mime_type: Some("text/markdown".to_string()),
text: TEST_RESOURCE_TEXT.to_string(),
meta: None,
},
McpResourceContent::Blob {
uri: TEST_BLOB_RESOURCE_URI.to_string(),
mime_type: Some("application/octet-stream".to_string()),
blob: TEST_RESOURCE_BLOB.to_string(),
meta: None,
},
],
}
expected_resource_read_response()
);
apps_server_handle.abort();
let _ = apps_server_handle.await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn mcp_resource_read_returns_resource_contents_without_thread() -> Result<()> {
let (apps_server_url, apps_server_handle) = start_resource_apps_mcp_server().await?;
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join("config.toml"),
format!(
r#"
chatgpt_base_url = "{apps_server_url}"
mcp_oauth_credentials_store = "file"
[features]
apps = true
"#
),
)?;
write_chatgpt_auth(
codex_home.path(),
ChatGptAuthFixture::new("chatgpt-token")
.account_id("account-123")
.chatgpt_user_id("user-123")
.chatgpt_account_id("account-123"),
AuthCredentialsStoreMode::File,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let read_request_id = mcp
.send_mcp_resource_read_request(McpResourceReadParams {
thread_id: None,
server: "codex_apps".to_string(),
uri: TEST_RESOURCE_URI.to_string(),
})
.await?;
let read_response: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(read_request_id)),
)
.await??;
assert_eq!(
to_response::<McpResourceReadResponse>(read_response)?,
expected_resource_read_response()
);
apps_server_handle.abort();
@@ -175,6 +201,7 @@ async fn mcp_resource_read_returns_error_for_unknown_thread() -> Result<()> {
cli_overrides: Vec::new(),
loader_overrides,
cloud_requirements: CloudRequirementsLoader::default(),
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
feedback: CodexFeedback::new(),
log_db: None,
environment_manager: Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)),
@@ -197,7 +224,7 @@ async fn mcp_resource_read_returns_error_for_unknown_thread() -> Result<()> {
.request(ClientRequest::McpResourceRead {
request_id: RequestId::Integer(1),
params: McpResourceReadParams {
thread_id: "00000000-0000-4000-8000-000000000000".to_string(),
thread_id: Some("00000000-0000-4000-8000-000000000000".to_string()),
server: "codex_apps".to_string(),
uri: TEST_RESOURCE_URI.to_string(),
},
@@ -217,6 +244,43 @@ async fn mcp_resource_read_returns_error_for_unknown_thread() -> Result<()> {
Ok(())
}
async fn start_resource_apps_mcp_server() -> Result<(String, JoinHandle<()>)> {
let listener = TcpListener::bind("127.0.0.1:0").await?;
let addr = listener.local_addr()?;
let apps_server_url = format!("http://{addr}");
let mcp_service = StreamableHttpService::new(
move || Ok(ResourceAppsMcpServer),
Arc::new(LocalSessionManager::default()),
StreamableHttpServerConfig::default(),
);
let router = Router::new().nest_service("/api/codex/apps", mcp_service);
let apps_server_handle = tokio::spawn(async move {
let _ = axum::serve(listener, router).await;
});
Ok((apps_server_url, apps_server_handle))
}
fn expected_resource_read_response() -> McpResourceReadResponse {
McpResourceReadResponse {
contents: vec![
McpResourceContent::Text {
uri: TEST_RESOURCE_URI.to_string(),
mime_type: Some("text/markdown".to_string()),
text: TEST_RESOURCE_TEXT.to_string(),
meta: None,
},
McpResourceContent::Blob {
uri: TEST_BLOB_RESOURCE_URI.to_string(),
mime_type: Some("application/octet-stream".to_string()),
blob: TEST_RESOURCE_BLOB.to_string(),
meta: None,
},
],
}
}
#[derive(Clone, Default)]
struct ResourceAppsMcpServer;
@@ -458,7 +458,10 @@ fn v2_background_agent_tool_call(call_id: &str, prompt: &str) -> Value {
async fn realtime_conversation_streams_v2_notifications() -> Result<()> {
skip_if_no_network!(Ok(()));
let responses_server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
let responses_server = create_mock_responses_server_sequence_unchecked(vec![
create_final_assistant_message_sse_response("delegated")?,
])
.await;
let realtime_server = start_websocket_server(vec![vec![
vec![json!({
"type": "session.updated",
@@ -489,6 +492,10 @@ async fn realtime_conversation_streams_v2_notifications() -> Result<()> {
"type": "response.output_text.delta",
"delta": "working"
}),
json!({
"type": "response.output_text.done",
"text": "working on it"
}),
json!({
"type": "conversation.item.done",
"item": {
@@ -677,7 +684,13 @@ async fn realtime_conversation_streams_v2_notifications() -> Result<()> {
handoff_item_added.item["input_transcript"],
json!("delegate now")
);
assert_eq!(handoff_item_added.item["active_transcript"], json!([]));
assert_eq!(
handoff_item_added.item["active_transcript"],
json!([
{"role": "user", "text": "delegate now"},
{"role": "assistant", "text": "working on it"}
])
);
let realtime_error =
read_notification::<ThreadRealtimeErrorNotification>(&mut mcp, "thread/realtime/error")
@@ -1142,7 +1155,7 @@ async fn realtime_webrtc_start_emits_sdp_notification() -> Result<()> {
Some("multipart/form-data; boundary=codex-realtime-call-boundary")
);
let body = String::from_utf8(request.body).context("multipart body should be utf-8")?;
let session = r#"{"tool_choice":"auto","type":"realtime","model":"gpt-realtime-1.5","instructions":"backend prompt\n\nstartup context","output_modalities":["audio"],"audio":{"input":{"format":{"type":"audio/pcm","rate":24000},"noise_reduction":{"type":"near_field"},"turn_detection":{"type":"server_vad","interrupt_response":true,"create_response":true,"silence_duration_ms":500}},"output":{"format":{"type":"audio/pcm","rate":24000},"voice":"marin"}},"tools":[{"type":"function","name":"background_agent","description":"Send a user request to the background agent. Use this as the default action. Do not rephrase the user's ask or rewrite it in your own words; pass along the user's own words. If the background agent is idle, this starts a new task and returns the final result to the user. If the background agent is already working on a task, this sends the request as guidance to steer that previous task. If the user asks to do something next, later, after this, or once current work finishes, call this tool so the work is actually queued instead of merely promising to do it later.","parameters":{"type":"object","properties":{"prompt":{"type":"string","description":"The user request to delegate to the background agent."}},"required":["prompt"],"additionalProperties":false}}]}"#;
let session = r#"{"tool_choice":"auto","type":"realtime","model":"gpt-realtime-1.5","instructions":"backend prompt\n\nstartup context","output_modalities":["audio"],"audio":{"input":{"format":{"type":"audio/pcm","rate":24000},"noise_reduction":{"type":"near_field"},"transcription":{"model":"gpt-4o-mini-transcribe"},"turn_detection":{"type":"server_vad","interrupt_response":true,"create_response":true,"silence_duration_ms":500}},"output":{"format":{"type":"audio/pcm","rate":24000},"voice":"marin"}},"tools":[{"type":"function","name":"background_agent","description":"Send a user request to the background agent. Use this as the default action. Do not rephrase the user's ask or rewrite it in your own words; pass along the user's own words. If the background agent is idle, this starts a new task and returns the final result to the user. If the background agent is already working on a task, this sends the request as guidance to steer that previous task. If the user asks to do something next, later, after this, or once current work finishes, call this tool so the work is actually queued instead of merely promising to do it later.","parameters":{"type":"object","properties":{"prompt":{"type":"string","description":"The user request to delegate to the background agent."}},"required":["prompt"],"additionalProperties":false}},{"type":"function","name":"remain_silent","description":"Call this when the best response is to say nothing. Use it instead of speaking after hidden system/control messages, after background agent updates in silent modes, or whenever acknowledging aloud would be distracting. This tool has no user-visible effect.","parameters":{"type":"object","properties":{},"additionalProperties":false}}]}"#;
let session = normalized_json_string(session)?;
assert_eq!(
body,
@@ -1241,8 +1254,16 @@ async fn webrtc_v1_handoff_request_delegates_and_appends_result() -> Result<()>
vec![
session_updated("sess_v1_handoff"),
json!({
"type": "conversation.input_transcript.delta",
"delta": "delegate from v1"
"type": "conversation.item.input_audio_transcription.completed",
"transcript": "delegate from v1"
}),
json!({
"type": "response.output_audio_transcript.delta",
"delta": "the secret word is "
}),
json!({
"type": "response.output_audio_transcript.delta",
"delta": "kumquat"
}),
json!({
"type": "conversation.handoff.requested",
@@ -1258,6 +1279,12 @@ async fn webrtc_v1_handoff_request_delegates_and_appends_result() -> Result<()>
let started = harness.start_webrtc_realtime("v=offer\r\n").await?;
assert_eq!(started.started.version, RealtimeConversationVersion::V1);
assert_call_create_multipart(
harness.call_capture.single_request(),
"v=offer\r\n",
v1_session_create_json(),
)?;
assert_v1_session_update(&harness.sideband_outbound_request(/*request_index*/ 0).await)?;
// Phase 2: wait for the delegated background agent turn that is launched by the handoff request.
let turn_started = harness
@@ -1274,11 +1301,13 @@ async fn webrtc_v1_handoff_request_delegates_and_appends_result() -> Result<()>
let requests = harness.main_loop_responses_requests().await?;
assert_eq!(requests.len(), 1);
assert!(
response_request_contains_text(&requests[0], "user: delegate from v1"),
"delegated Responses request should contain realtime text: {}",
response_request_contains_text(
&requests[0],
"<realtime_delegation>\n <input>delegate from v1</input>\n <transcript_delta>user: delegate from v1\nassistant: the secret word is kumquat</transcript_delta>\n</realtime_delegation>",
),
"delegated Responses request should contain realtime delegation envelope: {}",
requests[0]
);
let handoff_append = harness.sideband_outbound_request(/*request_index*/ 1).await;
assert_eq!(
handoff_append,
@@ -1526,7 +1555,34 @@ async fn webrtc_v2_background_agent_tool_call_delegates_and_returns_function_out
realtime_sideband(vec![realtime_sideband_connection(vec![
vec![
session_updated("sess_v2_tool"),
v2_background_agent_tool_call("call_v2", "delegate from v2"),
json!({
"type": "conversation.item.input_audio_transcription.completed",
"transcript": "Hi how are you"
}),
json!({
"type": "response.output_audio_transcript.done",
"transcript": "Doing well, what can I help you with?"
}),
json!({
"type": "conversation.item.input_audio_transcription.completed",
"transcript": "The secret word is strawberry"
}),
json!({
"type": "conversation.item.created",
"item": {
"type": "message",
"role": "user",
"content": [{
"type": "input_text",
"text": "<realtime_collaboration_update><voice_policy>silent_delegate</voice_policy></realtime_collaboration_update>"
}]
}
}),
json!({
"type": "response.output_audio_transcript.delta",
"delta": "Got it-strawberry. What's next on the menu?"
}),
v2_background_agent_tool_call("call_v2", "run ls"),
],
vec![],
vec![],
@@ -1553,8 +1609,16 @@ async fn webrtc_v2_background_agent_tool_call_delegates_and_returns_function_out
let requests = harness.main_loop_responses_requests().await?;
assert_eq!(requests.len(), 1);
assert!(
response_request_contains_text(&requests[0], "delegate from v2"),
"delegated Responses request should contain tool prompt: {}",
response_request_contains_text(
&requests[0],
"<realtime_delegation>\n <input>run ls</input>\n <transcript_delta>user: Hi how are you\nassistant: Doing well, what can I help you with?\nuser: The secret word is strawberry\nassistant: Got it-strawberry. What's next on the menu?\nuser: run ls</transcript_delta>\n</realtime_delegation>",
),
"delegated Responses request should contain realtime delegation envelope: {}",
requests[0]
);
assert!(
!response_request_contains_text(&requests[0], "<realtime_collaboration_update>"),
"delegated Responses request should not include realtime control injects: {}",
requests[0]
);
@@ -2068,7 +2132,16 @@ async fn responses_requests(server: &MockServer) -> Result<Vec<Value>> {
}
fn response_request_contains_text(request: &Value, text: &str) -> bool {
request.to_string().contains(text)
match request {
Value::String(value) => value.contains(text),
Value::Array(values) => values
.iter()
.any(|value| response_request_contains_text(value, text)),
Value::Object(map) => map
.values()
.any(|value| response_request_contains_text(value, text)),
Value::Null | Value::Bool(_) | Value::Number(_) => false,
}
}
fn realtime_tool_ok_command() -> Vec<String> {
@@ -2187,6 +2260,14 @@ fn assert_v2_session_update(request: &Value) -> Result<()> {
request["session"]["tools"][0]["name"].as_str(),
Some("background_agent")
);
assert_eq!(
request["session"]["tools"][1]["name"].as_str(),
Some("remain_silent")
);
assert_eq!(
request["session"]["audio"]["input"]["transcription"]["model"].as_str(),
Some("gpt-4o-mini-transcribe")
);
Ok(())
}
@@ -93,6 +93,8 @@ async fn request_permissions_round_trip() -> Result<()> {
file_system: Some(codex_app_server_protocol::AdditionalFileSystemPermissions {
read: None,
write: Some(vec![requested_writes[0].clone()]),
glob_scan_max_depth: None,
entries: None,
}),
},
scope: PermissionGrantScope::Turn,
@@ -21,8 +21,8 @@ use tempfile::TempDir;
use tokio::time::timeout;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
const REQUESTED_MODEL: &str = "gpt-5.1-codex-max";
const SERVER_MODEL: &str = "gpt-5.2-codex";
const REQUESTED_MODEL: &str = "gpt-5.4";
const SERVER_MODEL: &str = "gpt-5.3-codex";
#[tokio::test]
async fn openai_model_header_mismatch_emits_model_rerouted_notification_v2() -> Result<()> {
@@ -1,5 +1,6 @@
use anyhow::Result;
use app_test_support::McpProcess;
use app_test_support::create_fake_rollout;
use app_test_support::create_mock_responses_server_repeating_assistant;
use app_test_support::to_response;
use codex_app_server_protocol::JSONRPCError;
@@ -19,7 +20,11 @@ use codex_app_server_protocol::TurnStartParams;
use codex_app_server_protocol::TurnStartResponse;
use codex_app_server_protocol::UserInput;
use codex_core::ARCHIVED_SESSIONS_SUBDIR;
use codex_core::find_archived_thread_path_by_id_str;
use codex_core::find_thread_path_by_id_str;
use codex_protocol::ThreadId;
use codex_state::DirectionalThreadSpawnEdgeStatus;
use codex_state::StateRuntime;
use pretty_assertions::assert_eq;
use std::path::Path;
use tempfile::TempDir;
@@ -160,6 +165,311 @@ async fn thread_archive_requires_materialized_rollout() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn thread_archive_archives_spawned_descendants() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
let parent_id = create_fake_rollout(
codex_home.path(),
"2025-01-01T00-00-00",
"2025-01-01T00:00:00Z",
"parent",
Some("mock_provider"),
/*git_info*/ None,
)?;
let child_id = create_fake_rollout(
codex_home.path(),
"2025-01-01T00-01-00",
"2025-01-01T00:01:00Z",
"child",
Some("mock_provider"),
/*git_info*/ None,
)?;
let grandchild_id = create_fake_rollout(
codex_home.path(),
"2025-01-01T00-02-00",
"2025-01-01T00:02:00Z",
"grandchild",
Some("mock_provider"),
/*git_info*/ None,
)?;
let parent_thread_id = ThreadId::from_string(&parent_id)?;
let child_thread_id = ThreadId::from_string(&child_id)?;
let grandchild_thread_id = ThreadId::from_string(&grandchild_id)?;
let state_db =
StateRuntime::init(codex_home.path().to_path_buf(), "mock_provider".into()).await?;
state_db
.mark_backfill_complete(/*last_watermark*/ None)
.await?;
state_db
.upsert_thread_spawn_edge(
parent_thread_id,
child_thread_id,
DirectionalThreadSpawnEdgeStatus::Closed,
)
.await?;
state_db
.upsert_thread_spawn_edge(
child_thread_id,
grandchild_thread_id,
DirectionalThreadSpawnEdgeStatus::Open,
)
.await?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let archive_id = mcp
.send_thread_archive_request(ThreadArchiveParams {
thread_id: parent_id.clone(),
})
.await?;
let archive_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(archive_id)),
)
.await??;
let _: ThreadArchiveResponse = to_response::<ThreadArchiveResponse>(archive_resp)?;
let mut archived_ids = Vec::new();
for _ in 0..3 {
let notification = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("thread/archived"),
)
.await??;
let archived_notification: ThreadArchivedNotification = serde_json::from_value(
notification
.params
.expect("thread/archived notification params"),
)?;
archived_ids.push(archived_notification.thread_id);
}
assert_eq!(archived_ids, vec![parent_id, grandchild_id, child_id]);
for thread_id in [parent_thread_id, child_thread_id, grandchild_thread_id] {
assert!(
find_thread_path_by_id_str(codex_home.path(), &thread_id.to_string())
.await?
.is_none(),
"expected active rollout for {thread_id} to be archived"
);
assert!(
find_archived_thread_path_by_id_str(codex_home.path(), &thread_id.to_string())
.await?
.is_some(),
"expected archived rollout for {thread_id} to exist"
);
}
Ok(())
}
#[tokio::test]
async fn thread_archive_succeeds_when_descendant_archive_fails() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
let parent_id = create_fake_rollout(
codex_home.path(),
"2025-01-01T00-00-00",
"2025-01-01T00:00:00Z",
"parent",
Some("mock_provider"),
/*git_info*/ None,
)?;
let child_id = create_fake_rollout(
codex_home.path(),
"2025-01-01T00-01-00",
"2025-01-01T00:01:00Z",
"child",
Some("mock_provider"),
/*git_info*/ None,
)?;
let grandchild_id = create_fake_rollout(
codex_home.path(),
"2025-01-01T00-02-00",
"2025-01-01T00:02:00Z",
"grandchild",
Some("mock_provider"),
/*git_info*/ None,
)?;
let parent_thread_id = ThreadId::from_string(&parent_id)?;
let child_thread_id = ThreadId::from_string(&child_id)?;
let grandchild_thread_id = ThreadId::from_string(&grandchild_id)?;
let state_db =
StateRuntime::init(codex_home.path().to_path_buf(), "mock_provider".into()).await?;
state_db
.mark_backfill_complete(/*last_watermark*/ None)
.await?;
state_db
.upsert_thread_spawn_edge(
parent_thread_id,
child_thread_id,
DirectionalThreadSpawnEdgeStatus::Closed,
)
.await?;
state_db
.upsert_thread_spawn_edge(
child_thread_id,
grandchild_thread_id,
DirectionalThreadSpawnEdgeStatus::Open,
)
.await?;
let child_rollout_path = find_thread_path_by_id_str(codex_home.path(), &child_id)
.await?
.expect("child rollout path");
let archived_child_path = codex_home
.path()
.join(ARCHIVED_SESSIONS_SUBDIR)
.join(child_rollout_path.file_name().expect("rollout file name"));
std::fs::create_dir_all(&archived_child_path)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let archive_id = mcp
.send_thread_archive_request(ThreadArchiveParams {
thread_id: parent_id.clone(),
})
.await?;
let archive_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(archive_id)),
)
.await??;
let _: ThreadArchiveResponse = to_response::<ThreadArchiveResponse>(archive_resp)?;
let mut archived_ids = Vec::new();
for _ in 0..2 {
let notification = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("thread/archived"),
)
.await??;
let archived_notification: ThreadArchivedNotification = serde_json::from_value(
notification
.params
.expect("thread/archived notification params"),
)?;
archived_ids.push(archived_notification.thread_id);
}
assert_eq!(archived_ids, vec![parent_id, grandchild_id]);
assert!(
timeout(
std::time::Duration::from_millis(250),
mcp.read_stream_until_notification_message("thread/archived"),
)
.await
.is_err()
);
assert!(
child_rollout_path.exists(),
"child should stay active after descendant archive failure"
);
assert!(
archived_child_path.is_dir(),
"test conflict should remain in archived sessions"
);
for thread_id in [parent_thread_id, grandchild_thread_id] {
assert!(
find_thread_path_by_id_str(codex_home.path(), &thread_id.to_string())
.await?
.is_none(),
"expected active rollout for {thread_id} to be archived"
);
assert!(
find_archived_thread_path_by_id_str(codex_home.path(), &thread_id.to_string())
.await?
.is_some(),
"expected archived rollout for {thread_id} to exist"
);
}
Ok(())
}
#[tokio::test]
async fn thread_archive_succeeds_when_spawned_descendant_is_missing() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
let parent_id = create_fake_rollout(
codex_home.path(),
"2025-01-01T00-00-00",
"2025-01-01T00:00:00Z",
"parent",
Some("mock_provider"),
/*git_info*/ None,
)?;
let parent_thread_id = ThreadId::from_string(&parent_id)?;
let missing_child_thread_id = ThreadId::from_string("00000000-0000-0000-0000-000000000901")?;
let state_db =
StateRuntime::init(codex_home.path().to_path_buf(), "mock_provider".into()).await?;
state_db
.mark_backfill_complete(/*last_watermark*/ None)
.await?;
state_db
.upsert_thread_spawn_edge(
parent_thread_id,
missing_child_thread_id,
DirectionalThreadSpawnEdgeStatus::Closed,
)
.await?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let archive_id = mcp
.send_thread_archive_request(ThreadArchiveParams {
thread_id: parent_id.clone(),
})
.await?;
let archive_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(archive_id)),
)
.await??;
let _: ThreadArchiveResponse = to_response::<ThreadArchiveResponse>(archive_resp)?;
let notification = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("thread/archived"),
)
.await??;
let archived_notification: ThreadArchivedNotification = serde_json::from_value(
notification
.params
.expect("thread/archived notification params"),
)?;
assert_eq!(archived_notification.thread_id, parent_id);
assert!(
find_thread_path_by_id_str(codex_home.path(), &parent_id)
.await?
.is_none(),
"parent should be archived even when a descendant is missing"
);
assert!(
find_archived_thread_path_by_id_str(codex_home.path(), &parent_id)
.await?
.is_some(),
"parent should be moved into archived sessions"
);
Ok(())
}
#[tokio::test]
async fn thread_archive_clears_stale_subscriptions_before_resume() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
@@ -125,7 +125,7 @@ stream_max_retries = 0
async fn start_thread(mcp: &mut McpProcess) -> Result<String> {
let req_id = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.1".to_string()),
model: Some("gpt-5.2".to_string()),
..Default::default()
})
.await?;
@@ -131,7 +131,7 @@ async fn thread_resume_rejects_unmaterialized_thread() -> Result<()> {
// Start a thread.
let start_id = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.1-codex-max".to_string()),
model: Some("gpt-5.4".to_string()),
..Default::default()
})
.await?;
@@ -207,7 +207,7 @@ async fn thread_resume_tracks_thread_initialized_analytics() -> Result<()> {
let payload = wait_for_analytics_payload(&server, DEFAULT_READ_TIMEOUT).await?;
let event = thread_initialized_event(&payload)?;
assert_basic_thread_initialized_event(event, &thread.id, "gpt-5.2-codex", "resumed");
assert_basic_thread_initialized_event(event, &thread.id, "gpt-5.3-codex", "resumed");
Ok(())
}
@@ -556,7 +556,7 @@ async fn thread_resume_prefers_persisted_git_metadata_for_local_threads() -> Res
&config_toml,
format!(
r#"
model = "gpt-5.2-codex"
model = "gpt-5.3-codex"
approval_policy = "never"
sandbox_mode = "read-only"
@@ -922,7 +922,7 @@ async fn thread_resume_keeps_in_flight_turn_streaming() -> Result<()> {
let start_id = primary
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.1-codex-max".to_string()),
model: Some("gpt-5.4".to_string()),
..Default::default()
})
.await?;
@@ -1029,7 +1029,7 @@ async fn thread_resume_rejects_history_when_thread_is_running() -> Result<()> {
let start_id = primary
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.1-codex-max".to_string()),
model: Some("gpt-5.4".to_string()),
..Default::default()
})
.await?;
@@ -1145,7 +1145,7 @@ async fn thread_resume_rejects_mismatched_path_when_thread_is_running() -> Resul
let start_id = primary
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.1-codex-max".to_string()),
model: Some("gpt-5.4".to_string()),
..Default::default()
})
.await?;
@@ -1251,7 +1251,7 @@ async fn thread_resume_rejoins_running_thread_even_with_override_mismatch() -> R
let start_id = primary
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.1-codex-max".to_string()),
model: Some("gpt-5.4".to_string()),
..Default::default()
})
.await?;
@@ -1320,7 +1320,7 @@ async fn thread_resume_rejoins_running_thread_even_with_override_mismatch() -> R
.await??;
let ThreadResumeResponse { thread, model, .. } =
to_response::<ThreadResumeResponse>(resume_resp)?;
assert_eq!(model, "gpt-5.1-codex-max");
assert_eq!(model, "gpt-5.4");
// The running-thread resume response is queued onto the thread listener task.
// If the in-flight turn completes before that queued command runs, the response
// can legitimately observe the thread as idle.
@@ -1364,7 +1364,7 @@ async fn thread_resume_replays_pending_command_execution_request_approval() -> R
let start_id = primary
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.1-codex-max".to_string()),
model: Some("gpt-5.4".to_string()),
..Default::default()
})
.await?;
@@ -1501,7 +1501,7 @@ async fn thread_resume_replays_pending_file_change_request_approval() -> Result<
let start_id = primary
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.1-codex-max".to_string()),
model: Some("gpt-5.4".to_string()),
cwd: Some(workspace.to_string_lossy().into_owned()),
..Default::default()
})
@@ -1851,7 +1851,7 @@ async fn thread_resume_prefers_path_over_thread_id() -> Result<()> {
let start_id = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.1-codex-max".to_string()),
model: Some("gpt-5.4".to_string()),
..Default::default()
})
.await?;
@@ -1971,7 +1971,7 @@ async fn start_materialized_thread_and_restart(
let start_id = first_mcp
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.1-codex-max".to_string()),
model: Some("gpt-5.4".to_string()),
..Default::default()
})
.await?;
@@ -2045,7 +2045,7 @@ async fn thread_resume_accepts_personality_override() -> Result<()> {
let start_id = primary
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.2-codex".to_string()),
model: Some("gpt-5.3-codex".to_string()),
..Default::default()
})
.await?;
@@ -2083,7 +2083,7 @@ async fn thread_resume_accepts_personality_override() -> Result<()> {
let resume_id = secondary
.send_thread_resume_request(ThreadResumeParams {
thread_id: thread.id,
model: Some("gpt-5.2-codex".to_string()),
model: Some("gpt-5.3-codex".to_string()),
personality: Some(Personality::Friendly),
..Default::default()
})
@@ -2145,7 +2145,7 @@ fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io
config_toml,
format!(
r#"
model = "gpt-5.2-codex"
model = "gpt-5.3-codex"
approval_policy = "never"
sandbox_mode = "read-only"
@@ -2182,7 +2182,7 @@ fn create_config_toml_with_chatgpt_base_url(
config_toml,
format!(
r#"
model = "gpt-5.2-codex"
model = "gpt-5.3-codex"
approval_policy = "never"
sandbox_mode = "read-only"
chatgpt_base_url = "{chatgpt_base_url}"
@@ -2213,7 +2213,7 @@ fn create_config_toml_with_required_broken_mcp(
config_toml,
format!(
r#"
model = "gpt-5.2-codex"
model = "gpt-5.3-codex"
approval_policy = "never"
sandbox_mode = "read-only"
@@ -64,7 +64,7 @@ async fn thread_start_creates_thread_and_emits_started() -> Result<()> {
// Start a v2 thread with an explicit model override.
let req_id = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.1".to_string()),
model: Some("gpt-5.2".to_string()),
..Default::default()
})
.await?;
@@ -414,7 +414,7 @@ async fn thread_start_ephemeral_remains_pathless() -> Result<()> {
let req_id = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.1".to_string()),
model: Some("gpt-5.2".to_string()),
ephemeral: Some(true),
..Default::default()
})
@@ -127,6 +127,7 @@ async fn thread_unsubscribe_during_turn_keeps_turn_running() -> Result<()> {
.send_thread_start_request(ThreadStartParams {
model: Some("mock-model".to_string()),
dynamic_tools: Some(vec![DynamicToolSpec {
namespace: None,
name: tool_name.to_string(),
description: "Deterministic wait tool".to_string(),
input_schema: json!({
@@ -194,6 +195,7 @@ async fn thread_unsubscribe_during_turn_keeps_turn_running() -> Result<()> {
thread_id: thread_id.clone(),
turn_id: started.turn_id,
call_id: call_id.to_string(),
namespace: None,
tool: tool_name.to_string(),
arguments: tool_args,
}
@@ -24,6 +24,7 @@ use codex_app_server_protocol::CommandExecutionRequestApprovalResponse;
use codex_app_server_protocol::CommandExecutionStatus;
use codex_app_server_protocol::FileChangeApprovalDecision;
use codex_app_server_protocol::FileChangeOutputDeltaNotification;
use codex_app_server_protocol::FileChangePatchUpdatedNotification;
use codex_app_server_protocol::FileChangeRequestApprovalResponse;
use codex_app_server_protocol::ItemCompletedNotification;
use codex_app_server_protocol::ItemStartedNotification;
@@ -894,7 +895,7 @@ async fn turn_start_accepts_collaboration_mode_override_v2() -> Result<()> {
let thread_req = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.2-codex".to_string()),
model: Some("gpt-5.3-codex".to_string()),
..Default::default()
})
.await?;
@@ -977,7 +978,7 @@ async fn turn_start_uses_thread_feature_overrides_for_collaboration_mode_instruc
let thread_req = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.2-codex".to_string()),
model: Some("gpt-5.3-codex".to_string()),
config: Some(HashMap::from([(
"features.default_mode_request_user_input".to_string(),
json!(true),
@@ -1268,7 +1269,7 @@ async fn turn_start_uses_migrated_pragmatic_personality_without_override_v2() ->
let thread_req = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.2-codex".to_string()),
model: Some("gpt-5.3-codex".to_string()),
..Default::default()
})
.await?;
@@ -2026,6 +2027,248 @@ async fn turn_start_file_change_approval_v2() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn turn_start_does_not_stream_apply_patch_change_updates_without_feature_v2() -> Result<()> {
skip_if_no_network!(Ok(()));
let tmp = TempDir::new()?;
let codex_home = tmp.path().join("codex_home");
std::fs::create_dir(&codex_home)?;
let workspace = tmp.path().join("workspace");
std::fs::create_dir(&workspace)?;
let call_id = "patch-call";
let item_id = "fc-patch-call";
let patch = "*** Begin Patch\n*** Add File: live.txt\n+live line\n*** End Patch\n";
let patch_delta_1 = "*** Begin Patch\n*** Add File: live.txt\n+live";
let patch_delta_2 = " line\n*** End Patch\n";
let responses = vec![
responses::sse(vec![
responses::ev_response_created("resp-1"),
serde_json::json!({
"type": "response.output_item.added",
"item": {
"type": "custom_tool_call",
"id": item_id,
"call_id": call_id,
"name": "apply_patch",
"input": "",
"status": "in_progress"
}
}),
serde_json::json!({
"type": "response.custom_tool_call_input.delta",
"item_id": item_id,
"call_id": call_id,
"delta": patch_delta_1,
}),
serde_json::json!({
"type": "response.custom_tool_call_input.delta",
"item_id": item_id,
"call_id": call_id,
"delta": patch_delta_2,
}),
responses::ev_apply_patch_custom_tool_call(call_id, patch),
responses::ev_completed("resp-1"),
]),
create_final_assistant_message_sse_response("patch applied")?,
];
let server = create_mock_responses_server_sequence(responses).await;
create_config_toml(&codex_home, &server.uri(), "never", &BTreeMap::default())?;
let mut mcp = McpProcess::new(&codex_home).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let start_req = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("mock-model".to_string()),
cwd: Some(workspace.to_string_lossy().into_owned()),
..Default::default()
})
.await?;
let start_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(start_req)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
let turn_req = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id,
input: vec![V2UserInput::Text {
text: "apply patch".into(),
text_elements: Vec::new(),
}],
cwd: Some(workspace),
..Default::default()
})
.await?;
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
)
.await??;
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
)
.await??;
assert!(
!mcp.pending_notification_methods()
.iter()
.any(|method| method == "item/fileChange/patchUpdated")
);
Ok(())
}
#[tokio::test]
async fn turn_start_streams_apply_patch_change_updates_v2() -> Result<()> {
skip_if_no_network!(Ok(()));
let tmp = TempDir::new()?;
let codex_home = tmp.path().join("codex_home");
std::fs::create_dir(&codex_home)?;
let workspace = tmp.path().join("workspace");
std::fs::create_dir(&workspace)?;
let call_id = "patch-call";
let item_id = "fc-patch-call";
let patch = "*** Begin Patch\n*** Add File: live.txt\n+live line\n*** End Patch\n";
let patch_delta_1 = "*** Begin Patch\n*** Add File: live.txt\n+live";
let patch_delta_2 = " line\n*** End Patch\n";
let responses = vec![
responses::sse(vec![
responses::ev_response_created("resp-1"),
serde_json::json!({
"type": "response.output_item.added",
"item": {
"type": "function_call",
"id": "fc-other-call",
"call_id": "other-call",
"name": "not_apply_patch",
"arguments": "",
"status": "in_progress"
}
}),
serde_json::json!({
"type": "response.function_call_arguments.delta",
"item_id": "fc-other-call",
"delta": r#"{"input":"*** Begin Patch\n*** Add File: ignored.txt\n+ignored"#,
}),
serde_json::json!({
"type": "response.output_item.added",
"item": {
"type": "custom_tool_call",
"id": item_id,
"call_id": call_id,
"name": "apply_patch",
"input": "",
"status": "in_progress"
}
}),
serde_json::json!({
"type": "response.custom_tool_call_input.delta",
"item_id": item_id,
"call_id": call_id,
"delta": patch_delta_1,
}),
serde_json::json!({
"type": "response.custom_tool_call_input.delta",
"item_id": item_id,
"call_id": call_id,
"delta": patch_delta_2,
}),
responses::ev_apply_patch_custom_tool_call(call_id, patch),
responses::ev_completed("resp-1"),
]),
create_final_assistant_message_sse_response("patch applied")?,
];
let server = create_mock_responses_server_sequence(responses).await;
create_config_toml(
&codex_home,
&server.uri(),
"never",
&BTreeMap::from([
(Feature::ApplyPatchFreeform, true),
(Feature::ApplyPatchStreamingEvents, true),
(Feature::Plugins, false),
(Feature::RemoteModels, false),
(Feature::ShellSnapshot, false),
]),
)?;
let mut mcp = McpProcess::new(&codex_home).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let start_req = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("mock-model".to_string()),
cwd: Some(workspace.to_string_lossy().into_owned()),
..Default::default()
})
.await?;
let start_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(start_req)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
let turn_req = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "apply patch".into(),
text_elements: Vec::new(),
}],
cwd: Some(workspace.clone()),
..Default::default()
})
.await?;
let turn_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
)
.await??;
let TurnStartResponse { turn } = to_response::<TurnStartResponse>(turn_resp)?;
let mut streamed_content = String::new();
while streamed_content != "live line\n" {
let delta_notif = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("item/fileChange/patchUpdated"),
)
.await??;
let delta: FileChangePatchUpdatedNotification = serde_json::from_value(
delta_notif
.params
.clone()
.expect("item/fileChange/patchUpdated params"),
)?;
assert_eq!(delta.thread_id, thread.id);
assert_eq!(delta.turn_id, turn.id);
assert_eq!(delta.item_id, call_id);
let change = delta
.changes
.iter()
.find(|change| change.path == "live.txt")
.expect("live.txt change");
assert!(matches!(change.kind, PatchChangeKind::Add));
streamed_content = change.diff.clone();
}
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
)
.await??;
Ok(())
}
#[tokio::test]
async fn turn_start_emits_spawn_agent_item_with_model_metadata_v2() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -2033,7 +2276,7 @@ async fn turn_start_emits_spawn_agent_item_with_model_metadata_v2() -> Result<()
const CHILD_PROMPT: &str = "child: do work";
const PARENT_PROMPT: &str = "spawn a child and continue";
const SPAWN_CALL_ID: &str = "spawn-call-1";
const REQUESTED_MODEL: &str = "gpt-5.1";
const REQUESTED_MODEL: &str = "gpt-5.2";
const REQUESTED_REASONING_EFFORT: ReasoningEffort = ReasoningEffort::Low;
let server = responses::start_mock_server().await;
@@ -2088,7 +2331,7 @@ async fn turn_start_emits_spawn_agent_item_with_model_metadata_v2() -> Result<()
let thread_req = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.2-codex".to_string()),
model: Some("gpt-5.3-codex".to_string()),
..Default::default()
})
.await?;
@@ -2227,9 +2470,9 @@ async fn turn_start_emits_spawn_agent_item_with_effective_role_model_metadata_v2
const CHILD_PROMPT: &str = "child: do work";
const PARENT_PROMPT: &str = "spawn a child and continue";
const SPAWN_CALL_ID: &str = "spawn-call-1";
const REQUESTED_MODEL: &str = "gpt-5.1";
const REQUESTED_MODEL: &str = "gpt-5.2";
const REQUESTED_REASONING_EFFORT: ReasoningEffort = ReasoningEffort::Low;
const ROLE_MODEL: &str = "gpt-5.1-codex-max";
const ROLE_MODEL: &str = "gpt-5.4";
const ROLE_REASONING_EFFORT: ReasoningEffort = ReasoningEffort::High;
let server = responses::start_mock_server().await;
@@ -2302,7 +2545,7 @@ config_file = "./custom-role.toml"
let thread_req = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.2-codex".to_string()),
model: Some("gpt-5.3-codex".to_string()),
..Default::default()
})
.await?;
+14 -10
View File
@@ -116,7 +116,7 @@ impl PathStyle {
pub struct Client {
base_url: String,
http: reqwest::Client,
bearer_token: Option<String>,
authorization_header_value: Option<String>,
user_agent: Option<HeaderValue>,
chatgpt_account_id: Option<String>,
chatgpt_account_is_fedramp: bool,
@@ -142,7 +142,7 @@ impl Client {
Ok(Self {
base_url,
http,
bearer_token: None,
authorization_header_value: None,
user_agent: None,
chatgpt_account_id: None,
chatgpt_account_is_fedramp: false,
@@ -165,7 +165,12 @@ impl Client {
}
pub fn with_bearer_token(mut self, token: impl Into<String>) -> Self {
self.bearer_token = Some(token.into());
self.authorization_header_value = Some(format!("Bearer {}", token.into()));
self
}
pub fn with_authorization_header_value(mut self, value: impl Into<String>) -> Self {
self.authorization_header_value = Some(value.into());
self
}
@@ -198,11 +203,10 @@ impl Client {
} else {
h.insert(USER_AGENT, HeaderValue::from_static("codex-cli"));
}
if let Some(token) = &self.bearer_token {
let value = format!("Bearer {token}");
if let Ok(hv) = HeaderValue::from_str(&value) {
h.insert(AUTHORIZATION, hv);
}
if let Some(value) = &self.authorization_header_value
&& let Ok(hv) = HeaderValue::from_str(value)
{
h.insert(AUTHORIZATION, hv);
}
if let Some(acc) = &self.chatgpt_account_id
&& let Ok(name) = HeaderName::from_bytes(b"ChatGPT-Account-Id")
@@ -816,7 +820,7 @@ mod tests {
let codex_client = Client {
base_url: "https://example.test".to_string(),
http: reqwest::Client::new(),
bearer_token: None,
authorization_header_value: None,
user_agent: None,
chatgpt_account_id: None,
chatgpt_account_is_fedramp: false,
@@ -830,7 +834,7 @@ mod tests {
let chatgpt_client = Client {
base_url: "https://chatgpt.com/backend-api".to_string(),
http: reqwest::Client::new(),
bearer_token: None,
authorization_header_value: None,
user_agent: None,
chatgpt_account_id: None,
chatgpt_account_is_fedramp: false,
+20 -3
View File
@@ -1,4 +1,5 @@
use codex_core::config::Config;
use codex_login::AuthManager;
use codex_login::default_client::create_client;
use crate::chatgpt_token::get_chatgpt_token_data;
@@ -31,16 +32,32 @@ pub(crate) async fn chatgpt_get_request_with_timeout<T: DeserializeOwned>(
let token =
get_chatgpt_token_data().ok_or_else(|| anyhow::anyhow!("ChatGPT token not available"))?;
let auth_manager =
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false);
let auth = auth_manager.auth().await;
let is_fedramp_account = auth
.as_ref()
.is_some_and(codex_login::CodexAuth::is_fedramp_account);
let authorization_header_value = match auth.as_ref() {
Some(auth) if auth.is_chatgpt_auth() => auth_manager
.chatgpt_authorization_header_for_auth(auth)
.await
.unwrap_or_else(|| format!("Bearer {}", token.access_token)),
_ => format!("Bearer {}", token.access_token),
};
let account_id = token.account_id.ok_or_else(|| {
anyhow::anyhow!("ChatGPT account ID not available, please re-run `codex login`")
});
})?;
let mut request = client
.get(&url)
.bearer_auth(&token.access_token)
.header("chatgpt-account-id", account_id?)
.header("authorization", authorization_header_value)
.header("chatgpt-account-id", account_id)
.header("Content-Type", "application/json");
if is_fedramp_account {
request = request.header("X-OpenAI-Fedramp", "true");
}
if let Some(timeout) = timeout {
request = request.timeout(timeout);
+44 -44
View File
@@ -717,6 +717,9 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
root_remote_auth_token_env.as_deref(),
"exec",
)?;
exec_cli
.shared
.inherit_exec_root_options(&interactive.shared);
prepend_config_flags(
&mut exec_cli.config_overrides,
root_config_overrides.clone(),
@@ -1090,8 +1093,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
"stdio-to-uds",
)?;
let socket_path = cmd.socket_path;
tokio::task::spawn_blocking(move || codex_stdio_to_uds::run(socket_path.as_path()))
.await??;
codex_stdio_to_uds::run(socket_path.as_path()).await?;
}
Some(Subcommand::ExecServer(cmd)) => {
reject_remote_mode_for_subcommand(
@@ -1242,6 +1244,7 @@ async fn run_debug_prompt_input_command(
interactive: TuiCli,
arg0_paths: Arg0DispatchPaths,
) -> anyhow::Result<()> {
let shared = interactive.shared.into_inner();
let mut cli_kv_overrides = root_config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?;
@@ -1252,38 +1255,38 @@ async fn run_debug_prompt_input_command(
));
}
let approval_policy = if interactive.full_auto {
let approval_policy = if shared.full_auto {
Some(AskForApproval::OnRequest)
} else if interactive.dangerously_bypass_approvals_and_sandbox {
} else if shared.dangerously_bypass_approvals_and_sandbox {
Some(AskForApproval::Never)
} else {
interactive.approval_policy.map(Into::into)
};
let sandbox_mode = if interactive.full_auto {
let sandbox_mode = if shared.full_auto {
Some(codex_protocol::config_types::SandboxMode::WorkspaceWrite)
} else if interactive.dangerously_bypass_approvals_and_sandbox {
} else if shared.dangerously_bypass_approvals_and_sandbox {
Some(codex_protocol::config_types::SandboxMode::DangerFullAccess)
} else {
interactive.sandbox_mode.map(Into::into)
shared.sandbox_mode.map(Into::into)
};
let overrides = ConfigOverrides {
model: interactive.model,
config_profile: interactive.config_profile,
model: shared.model,
config_profile: shared.config_profile,
approval_policy,
sandbox_mode,
cwd: interactive.cwd,
cwd: shared.cwd,
codex_self_exe: arg0_paths.codex_self_exe,
codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe,
main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe,
show_raw_agent_reasoning: interactive.oss.then_some(true),
show_raw_agent_reasoning: shared.oss.then_some(true),
ephemeral: Some(true),
additional_writable_roots: interactive.add_dir,
additional_writable_roots: shared.add_dir,
..Default::default()
};
let config =
Config::load_with_cli_overrides_and_harness_overrides(cli_kv_overrides, overrides).await?;
let mut input = interactive
let mut input = shared
.images
.into_iter()
.chain(cmd.images)
@@ -1554,40 +1557,24 @@ fn finalize_fork_interactive(
/// root-level flags. Only overrides fields explicitly set on the subcommand-scoped
/// CLI. Also appends `-c key=value` overrides with highest precedence.
fn merge_interactive_cli_flags(interactive: &mut TuiCli, subcommand_cli: TuiCli) {
if let Some(model) = subcommand_cli.model {
interactive.model = Some(model);
}
if subcommand_cli.oss {
interactive.oss = true;
}
if let Some(profile) = subcommand_cli.config_profile {
interactive.config_profile = Some(profile);
}
if let Some(sandbox) = subcommand_cli.sandbox_mode {
interactive.sandbox_mode = Some(sandbox);
}
if let Some(approval) = subcommand_cli.approval_policy {
let TuiCli {
shared,
approval_policy,
web_search,
prompt,
config_overrides,
..
} = subcommand_cli;
interactive
.shared
.apply_subcommand_overrides(shared.into_inner());
if let Some(approval) = approval_policy {
interactive.approval_policy = Some(approval);
}
if subcommand_cli.full_auto {
interactive.full_auto = true;
}
if subcommand_cli.dangerously_bypass_approvals_and_sandbox {
interactive.dangerously_bypass_approvals_and_sandbox = true;
}
if let Some(cwd) = subcommand_cli.cwd {
interactive.cwd = Some(cwd);
}
if subcommand_cli.web_search {
if web_search {
interactive.web_search = true;
}
if !subcommand_cli.images.is_empty() {
interactive.images = subcommand_cli.images;
}
if !subcommand_cli.add_dir.is_empty() {
interactive.add_dir.extend(subcommand_cli.add_dir);
}
if let Some(prompt) = subcommand_cli.prompt {
if let Some(prompt) = prompt {
// Normalize CRLF/CR to LF so CLI-provided text can't leak `\r` into TUI state.
interactive.prompt = Some(prompt.replace("\r\n", "\n").replace('\r', "\n"));
}
@@ -1595,7 +1582,7 @@ fn merge_interactive_cli_flags(interactive: &mut TuiCli, subcommand_cli: TuiCli)
interactive
.config_overrides
.raw_overrides
.extend(subcommand_cli.config_overrides.raw_overrides);
.extend(config_overrides.raw_overrides);
}
fn print_completion(cmd: CompletionCommand) {
@@ -1715,6 +1702,19 @@ mod tests {
assert_eq!(args.prompt.as_deref(), Some("re-review"));
}
#[test]
fn dangerous_bypass_conflicts_with_approval_policy() {
let err = MultitoolCli::try_parse_from([
"codex",
"--dangerously-bypass-approvals-and-sandbox",
"--ask-for-approval",
"on-request",
])
.expect_err("conflicting permission flags should be rejected");
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
}
fn app_server_from_args(args: &[&str]) -> AppServerCommand {
let cli = MultitoolCli::try_parse_from(args).expect("parse");
let Subcommand::AppServer(app_server) = cli.subcommand.expect("app-server present") else {
+5
View File
@@ -1,5 +1,10 @@
allow-expect-in-tests = true
allow-unwrap-in-tests = true
await-holding-invalid-types = [
"tokio::sync::MutexGuard",
"tokio::sync::RwLockReadGuard",
"tokio::sync::RwLockWriteGuard",
]
disallowed-methods = [
{ path = "ratatui::style::Color::Rgb", reason = "Use ANSI colors, which work better in various terminal themes." },
{ path = "ratatui::style::Color::Indexed", reason = "Use ANSI colors, which work better in various terminal themes." },
+44 -5
View File
@@ -195,11 +195,15 @@ trait RequirementsFetcher: Send + Sync {
struct BackendRequirementsFetcher {
base_url: String,
auth_manager: Arc<AuthManager>,
}
impl BackendRequirementsFetcher {
fn new(base_url: String) -> Self {
Self { base_url }
fn new(auth_manager: Arc<AuthManager>, base_url: String) -> Self {
Self {
base_url,
auth_manager,
}
}
}
@@ -209,7 +213,14 @@ impl RequirementsFetcher for BackendRequirementsFetcher {
&self,
auth: &CodexAuth,
) -> Result<Option<String>, FetchAttemptError> {
let client = BackendClient::from_auth(self.base_url.clone(), auth)
let authorization_header_value = self
.auth_manager
.chatgpt_authorization_header_for_auth(auth)
.await;
let mut client = BackendClient::new(self.base_url.clone())
.map(|client| {
client.with_user_agent(codex_login::default_client::get_codex_user_agent())
})
.inspect_err(|err| {
tracing::warn!(
error = %err,
@@ -217,6 +228,15 @@ impl RequirementsFetcher for BackendRequirementsFetcher {
);
})
.map_err(|_| FetchAttemptError::Retryable(RetryableFailureKind::BackendClientInit))?;
if let Some(authorization_header_value) = authorization_header_value {
client = client.with_authorization_header_value(authorization_header_value);
}
if let Some(account_id) = auth.get_account_id() {
client = client.with_chatgpt_account_id(account_id);
}
if auth.is_fedramp_account() {
client = client.with_fedramp_routing_header();
}
let response = client
.get_config_requirements_file()
@@ -693,8 +713,11 @@ pub fn cloud_requirements_loader(
codex_home: PathBuf,
) -> CloudRequirementsLoader {
let service = CloudRequirementsService::new(
auth_manager,
Arc::new(BackendRequirementsFetcher::new(chatgpt_base_url)),
auth_manager.clone(),
Arc::new(BackendRequirementsFetcher::new(
auth_manager,
chatgpt_base_url,
)),
codex_home,
CLOUD_REQUIREMENTS_TIMEOUT,
);
@@ -1164,6 +1187,7 @@ mod tests {
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1194,6 +1218,7 @@ mod tests {
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1224,6 +1249,7 @@ mod tests {
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1271,6 +1297,7 @@ mod tests {
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1354,6 +1381,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1427,6 +1455,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1498,6 +1527,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1696,6 +1726,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1732,6 +1763,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1788,6 +1820,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1839,6 +1872,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1894,6 +1928,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -1950,6 +1985,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -2006,6 +2042,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -2095,6 +2132,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
@@ -2123,6 +2161,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
remote_sandbox_config: None,
allowed_web_search_modes: None,
guardian_policy_config: None,
feature_requirements: None,
+10
View File
@@ -37,6 +37,11 @@ impl HttpClient {
self
}
pub fn with_authorization_header_value(mut self, value: impl Into<String>) -> Self {
self.backend = self.backend.clone().with_authorization_header_value(value);
self
}
pub fn with_user_agent(mut self, ua: impl Into<String>) -> Self {
self.backend = self.backend.clone().with_user_agent(ua);
self
@@ -47,6 +52,11 @@ impl HttpClient {
self
}
pub fn with_fedramp_routing_header(mut self) -> Self {
self.backend = self.backend.clone().with_fedramp_routing_header();
self
}
fn tasks_api(&self) -> api::Tasks<'_> {
api::Tasks::new(self)
}
+27 -25
View File
@@ -68,43 +68,45 @@ async fn init_backend(user_agent_suffix: &str) -> anyhow::Result<BackendContext>
};
append_error_log(format!("startup: base_url={base_url} path_style={style}"));
let auth_manager = util::load_auth_manager().await;
let auth = match auth_manager.as_ref() {
Some(manager) => manager.auth().await,
None => None,
let Some(auth_manager) = util::load_auth_manager(Some(base_url.clone())).await else {
eprintln!(
"Not signed in. Please run 'codex login' to sign in with ChatGPT, then re-run 'codex cloud'."
);
std::process::exit(1);
};
let auth = match auth {
Some(auth) => auth,
None => {
eprintln!(
"Not signed in. Please run 'codex login' to sign in with ChatGPT, then re-run 'codex cloud'."
);
std::process::exit(1);
}
let Some(auth) = auth_manager.auth().await else {
eprintln!(
"Not signed in. Please run 'codex login' to sign in with ChatGPT, then re-run 'codex cloud'."
);
std::process::exit(1);
};
if let Some(acc) = auth.get_account_id() {
append_error_log(format!("auth: mode=ChatGPT account_id={acc}"));
}
let token = match auth.get_token() {
Ok(t) if !t.is_empty() => t,
_ => {
eprintln!(
"Not signed in. Please run 'codex login' to sign in with ChatGPT, then re-run 'codex cloud'."
);
std::process::exit(1);
}
let authorization_header_value = auth_manager
.chatgpt_authorization_header_for_auth(&auth)
.await;
let Some(authorization_header_value) = authorization_header_value else {
eprintln!(
"Not signed in. Please run 'codex login' to sign in with ChatGPT, then re-run 'codex cloud'."
);
std::process::exit(1);
};
http = http.with_bearer_token(token.clone());
if let Some(acc) = auth
.get_account_id()
.or_else(|| util::extract_chatgpt_account_id(&token))
{
http = http.with_authorization_header_value(authorization_header_value);
if let Some(acc) = auth.get_account_id().or_else(|| {
auth.get_token()
.ok()
.and_then(|token| util::extract_chatgpt_account_id(&token))
}) {
append_error_log(format!("auth: set ChatGPT-Account-Id header: {acc}"));
http = http.with_chatgpt_account_id(acc);
}
if auth.is_fedramp_account() {
http = http.with_fedramp_routing_header();
}
Ok(BackendContext {
backend: Arc::new(http),
+30 -18
View File
@@ -3,6 +3,7 @@ use chrono::DateTime;
use chrono::Local;
use chrono::Utc;
use reqwest::header::HeaderMap;
use std::sync::Arc;
use codex_core::config::Config;
use codex_login::AuthManager;
@@ -59,18 +60,18 @@ pub fn extract_chatgpt_account_id(token: &str) -> Option<String> {
.map(str::to_string)
}
pub async fn load_auth_manager() -> Option<AuthManager> {
pub async fn load_auth_manager(chatgpt_base_url: Option<String>) -> Option<Arc<AuthManager>> {
// TODO: pass in cli overrides once cloud tasks properly support them.
let config = Config::load_with_cli_overrides(Vec::new()).await.ok()?;
Some(AuthManager::new(
config.codex_home.to_path_buf(),
/*enable_codex_api_key_env*/ false,
config.cli_auth_credentials_store_mode,
))
let auth_manager =
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false);
if let Some(chatgpt_base_url) = chatgpt_base_url {
auth_manager.set_chatgpt_backend_base_url(Some(chatgpt_base_url));
}
Some(auth_manager)
}
/// Build headers for ChatGPT-backed requests: `User-Agent`, optional `Authorization`,
/// and optional `ChatGPT-Account-Id`.
/// Build headers for ChatGPT-backed requests.
pub async fn build_chatgpt_headers() -> HeaderMap {
use reqwest::header::AUTHORIZATION;
use reqwest::header::HeaderName;
@@ -84,23 +85,34 @@ pub async fn build_chatgpt_headers() -> HeaderMap {
USER_AGENT,
HeaderValue::from_str(&ua).unwrap_or(HeaderValue::from_static("codex-cli")),
);
if let Some(am) = load_auth_manager().await
&& let Some(auth) = am.auth().await
&& let Ok(tok) = auth.get_token()
&& !tok.is_empty()
let base_url = normalize_base_url(
&std::env::var("CODEX_CLOUD_TASKS_BASE_URL")
.unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()),
);
if let Some(auth_manager) = load_auth_manager(Some(base_url)).await
&& let Some(auth) = auth_manager.auth().await
{
let v = format!("Bearer {tok}");
if let Ok(hv) = HeaderValue::from_str(&v) {
if let Some(authorization_header_value) = auth_manager
.chatgpt_authorization_header_for_auth(&auth)
.await
&& let Ok(hv) = HeaderValue::from_str(&authorization_header_value)
{
headers.insert(AUTHORIZATION, hv);
}
if let Some(acc) = auth
.get_account_id()
.or_else(|| extract_chatgpt_account_id(&tok))
&& let Ok(name) = HeaderName::from_bytes(b"ChatGPT-Account-Id")
if let Some(acc) = auth.get_account_id().or_else(|| {
auth.get_token()
.ok()
.and_then(|token| extract_chatgpt_account_id(&token))
}) && let Ok(name) = HeaderName::from_bytes(b"ChatGPT-Account-Id")
&& let Ok(hv) = HeaderValue::from_str(&acc)
{
headers.insert(name, hv);
}
if auth.is_fedramp_account()
&& let Ok(name) = HeaderName::from_bytes(b"X-OpenAI-Fedramp")
{
headers.insert(name, HeaderValue::from_static("true"));
}
}
headers
}
@@ -1,4 +1,4 @@
use crate::endpoint::realtime_websocket::methods_common::conversation_handoff_append_message;
use crate::endpoint::realtime_websocket::methods_common::conversation_function_call_output_message;
use crate::endpoint::realtime_websocket::methods_common::conversation_item_create_message;
use crate::endpoint::realtime_websocket::methods_common::normalized_session_mode;
use crate::endpoint::realtime_websocket::methods_common::session_update_session;
@@ -217,6 +217,9 @@ pub struct RealtimeWebsocketEvents {
#[derive(Default)]
struct ActiveTranscriptState {
entries: Vec<RealtimeTranscriptEntry>,
last_handoff_entry_count: usize,
new_input_entry: bool,
new_output_entry: bool,
}
impl RealtimeWebsocketConnection {
@@ -228,13 +231,13 @@ impl RealtimeWebsocketConnection {
self.writer.send_conversation_item_create(text).await
}
pub async fn send_conversation_handoff_append(
pub async fn send_conversation_function_call_output(
&self,
handoff_id: String,
call_id: String,
output_text: String,
) -> Result<(), ApiError> {
self.writer
.send_conversation_handoff_append(handoff_id, output_text)
.send_conversation_function_call_output(call_id, output_text)
.await
}
@@ -288,14 +291,14 @@ impl RealtimeWebsocketWriter {
.await
}
pub async fn send_conversation_handoff_append(
pub async fn send_conversation_function_call_output(
&self,
handoff_id: String,
call_id: String,
output_text: String,
) -> Result<(), ApiError> {
self.send_json(&conversation_handoff_append_message(
self.send_json(&conversation_function_call_output_message(
self.event_parser,
handoff_id,
call_id,
output_text,
))
.await
@@ -417,38 +420,80 @@ impl RealtimeWebsocketEvents {
async fn update_active_transcript(&self, event: &mut RealtimeEvent) {
let mut active_transcript = self.active_transcript.lock().await;
match event {
RealtimeEvent::InputAudioSpeechStarted(_) => {}
RealtimeEvent::InputAudioSpeechStarted(_) => {
active_transcript.new_input_entry = true;
}
RealtimeEvent::InputTranscriptDelta(RealtimeTranscriptDelta { delta, .. }) => {
append_transcript_delta(&mut active_transcript.entries, "user", delta);
let force_new = active_transcript.new_input_entry;
append_transcript_delta(&mut active_transcript.entries, "user", delta, force_new);
active_transcript.new_input_entry = false;
}
RealtimeEvent::OutputTranscriptDelta(RealtimeTranscriptDelta { delta, .. }) => {
append_transcript_delta(&mut active_transcript.entries, "assistant", delta);
let force_new = active_transcript.new_output_entry;
append_transcript_delta(
&mut active_transcript.entries,
"assistant",
delta,
force_new,
);
active_transcript.new_output_entry = false;
}
RealtimeEvent::InputTranscriptDone(done) => {
let force_new = active_transcript.new_input_entry;
apply_transcript_done(
&mut active_transcript.entries,
"user",
&done.text,
force_new,
);
active_transcript.new_input_entry = false;
}
RealtimeEvent::OutputTranscriptDone(done) => {
let force_new = active_transcript.new_output_entry;
apply_transcript_done(
&mut active_transcript.entries,
"assistant",
&done.text,
force_new,
);
active_transcript.new_output_entry = false;
}
RealtimeEvent::HandoffRequested(handoff) => {
if self.event_parser == RealtimeEventParser::V1 {
handoff.active_transcript = std::mem::take(&mut active_transcript.entries);
}
append_handoff_input(&mut active_transcript.entries, &handoff.input_transcript);
handoff.active_transcript = active_transcript.entries
[active_transcript.last_handoff_entry_count..]
.to_vec();
active_transcript.last_handoff_entry_count = active_transcript.entries.len();
active_transcript.new_input_entry = true;
active_transcript.new_output_entry = true;
}
RealtimeEvent::ResponseCreated(_) => {
active_transcript.new_output_entry = true;
}
RealtimeEvent::SessionUpdated { .. }
| RealtimeEvent::InputTranscriptDone(_)
| RealtimeEvent::OutputTranscriptDone(_)
| RealtimeEvent::AudioOut(_)
| RealtimeEvent::ResponseCreated(_)
| RealtimeEvent::ResponseCancelled(_)
| RealtimeEvent::ResponseDone(_)
| RealtimeEvent::ConversationItemAdded(_)
| RealtimeEvent::ConversationItemDone { .. }
| RealtimeEvent::NoopRequested(_)
| RealtimeEvent::ConversationItemAdded(_)
| RealtimeEvent::Error(_) => {}
}
}
}
fn append_transcript_delta(entries: &mut Vec<RealtimeTranscriptEntry>, role: &str, delta: &str) {
fn append_transcript_delta(
entries: &mut Vec<RealtimeTranscriptEntry>,
role: &str,
delta: &str,
force_new: bool,
) {
if delta.is_empty() {
return;
}
if let Some(last_entry) = entries.last_mut()
if !force_new
&& let Some(last_entry) = entries.last_mut()
&& last_entry.role == role
{
last_entry.text.push_str(delta);
@@ -461,6 +506,48 @@ fn append_transcript_delta(entries: &mut Vec<RealtimeTranscriptEntry>, role: &st
});
}
fn apply_transcript_done(
entries: &mut Vec<RealtimeTranscriptEntry>,
role: &str,
text: &str,
force_new: bool,
) {
if text.is_empty() {
return;
}
if !force_new
&& let Some(last_entry) = entries.last_mut()
&& last_entry.role == role
{
last_entry.text = text.to_string();
return;
}
entries.push(RealtimeTranscriptEntry {
role: role.to_string(),
text: text.to_string(),
});
}
fn append_handoff_input(entries: &mut Vec<RealtimeTranscriptEntry>, input: &str) {
let input = input.trim();
if input.is_empty() || contains_transcript_entry(entries, "user", input) {
return;
}
entries.push(RealtimeTranscriptEntry {
role: "user".to_string(),
text: input.to_string(),
});
}
fn contains_transcript_entry(entries: &[RealtimeTranscriptEntry], role: &str, text: &str) -> bool {
entries
.iter()
.any(|entry| entry.role == role && entry.text.trim() == text.trim())
}
pub struct RealtimeWebsocketClient {
provider: Provider,
}
@@ -743,6 +830,7 @@ mod tests {
use crate::endpoint::realtime_websocket::protocol::RealtimeTranscriptEntry;
use codex_protocol::protocol::RealtimeHandoffRequested;
use codex_protocol::protocol::RealtimeInputAudioSpeechStarted;
use codex_protocol::protocol::RealtimeNoopRequested;
use codex_protocol::protocol::RealtimeResponseCancelled;
use codex_protocol::protocol::RealtimeResponseCreated;
use codex_protocol::protocol::RealtimeResponseDone;
@@ -867,6 +955,44 @@ mod tests {
);
}
#[test]
fn parse_v1_input_audio_transcription_delta_event() {
let payload = json!({
"type": "conversation.item.input_audio_transcription.delta",
"item_id": "item_input_1",
"content_index": 0,
"delta": "hello"
})
.to_string();
assert_eq!(
parse_realtime_event(payload.as_str(), RealtimeEventParser::V1),
Some(RealtimeEvent::InputTranscriptDelta(
RealtimeTranscriptDelta {
delta: "hello".to_string(),
}
))
);
}
#[test]
fn parse_v1_input_audio_transcription_completed_event() {
let payload = json!({
"type": "conversation.item.input_audio_transcription.completed",
"item_id": "item_input_1",
"content_index": 0,
"transcript": "hello world"
})
.to_string();
assert_eq!(
parse_realtime_event(payload.as_str(), RealtimeEventParser::V1),
Some(RealtimeEvent::InputTranscriptDone(RealtimeTranscriptDone {
text: "hello world".to_string(),
}))
);
}
#[test]
fn parse_output_transcript_delta_event() {
let payload = json!({
@@ -885,6 +1011,66 @@ mod tests {
);
}
#[test]
fn parse_v1_output_audio_transcript_delta_event() {
let payload = json!({
"type": "response.output_audio_transcript.delta",
"delta": "hi"
})
.to_string();
assert_eq!(
parse_realtime_event(payload.as_str(), RealtimeEventParser::V1),
Some(RealtimeEvent::OutputTranscriptDelta(
RealtimeTranscriptDelta {
delta: "hi".to_string(),
}
))
);
}
#[test]
fn parse_v1_output_audio_transcript_done_event() {
let payload = json!({
"type": "response.output_audio_transcript.done",
"transcript": "hi there"
})
.to_string();
assert_eq!(
parse_realtime_event(payload.as_str(), RealtimeEventParser::V1),
Some(RealtimeEvent::OutputTranscriptDone(
RealtimeTranscriptDone {
text: "hi there".to_string(),
}
))
);
}
#[test]
fn parse_v1_item_done_output_text_event() {
let payload = json!({
"type": "conversation.item.done",
"item": {
"id": "item_output_1",
"type": "message",
"role": "assistant",
"content": [
{"type": "output_text", "text": "hello"},
{"type": "output_text", "text": " world"}
]
}
})
.to_string();
assert_eq!(
parse_realtime_event(payload.as_str(), RealtimeEventParser::V1),
Some(RealtimeEvent::ConversationItemDone {
item_id: "item_output_1".to_string(),
})
);
}
#[test]
fn parse_realtime_v2_handoff_tool_call_event() {
let payload = json!({
@@ -910,6 +1096,29 @@ mod tests {
);
}
#[test]
fn parse_realtime_v2_noop_tool_call_event() {
let payload = json!({
"type": "conversation.item.done",
"item": {
"id": "item_silent",
"type": "function_call",
"name": "remain_silent",
"call_id": "call_silent",
"arguments": "{}"
}
})
.to_string();
assert_eq!(
parse_realtime_event(payload.as_str(), RealtimeEventParser::RealtimeV2),
Some(RealtimeEvent::NoopRequested(RealtimeNoopRequested {
call_id: "call_silent".to_string(),
item_id: "item_silent".to_string(),
}))
);
}
#[test]
fn parse_realtime_v2_input_audio_transcription_delta_event() {
let payload = json!({
@@ -930,6 +1139,58 @@ mod tests {
);
}
#[test]
fn parse_realtime_v2_output_audio_transcript_done_event() {
let payload = json!({
"type": "response.output_audio_transcript.done",
"transcript": "hello there"
})
.to_string();
assert_eq!(
parse_realtime_event(payload.as_str(), RealtimeEventParser::RealtimeV2),
Some(RealtimeEvent::OutputTranscriptDone(
RealtimeTranscriptDone {
text: "hello there".to_string(),
}
))
);
}
#[test]
fn parse_realtime_v2_output_text_done_event() {
let payload = json!({
"type": "response.output_text.done",
"text": "hello there"
})
.to_string();
assert_eq!(
parse_realtime_event(payload.as_str(), RealtimeEventParser::RealtimeV2),
Some(RealtimeEvent::OutputTranscriptDone(
RealtimeTranscriptDone {
text: "hello there".to_string(),
}
))
);
}
#[test]
fn parse_realtime_v2_conversation_item_created_event() {
let payload = json!({
"type": "conversation.item.created",
"item": {"type": "message", "role": "user"}
})
.to_string();
assert_eq!(
parse_realtime_event(payload.as_str(), RealtimeEventParser::RealtimeV2),
Some(RealtimeEvent::ConversationItemAdded(
json!({"type": "message", "role": "user"})
))
);
}
#[test]
fn parse_realtime_v2_item_done_output_text_event() {
let payload = json!({
@@ -948,11 +1209,9 @@ mod tests {
assert_eq!(
parse_realtime_event(payload.as_str(), RealtimeEventParser::RealtimeV2),
Some(RealtimeEvent::OutputTranscriptDone(
RealtimeTranscriptDone {
text: "hello world".to_string(),
}
))
Some(RealtimeEvent::ConversationItemDone {
item_id: "item_output_1".to_string(),
})
);
}
@@ -1459,7 +1718,7 @@ mod tests {
.await
.expect("send item");
connection
.send_conversation_handoff_append(
.send_conversation_function_call_output(
"handoff_1".to_string(),
"hello from background agent".to_string(),
)
@@ -1582,6 +1841,12 @@ mod tests {
"type": "near_field",
})
);
assert_eq!(
first_json["session"]["audio"]["input"]["transcription"],
json!({
"model": "gpt-4o-mini-transcribe",
})
);
assert_eq!(
first_json["session"]["audio"]["input"]["turn_detection"],
json!({
@@ -1614,6 +1879,18 @@ mod tests {
first_json["session"]["tools"][0]["parameters"]["required"],
json!(["prompt"])
);
assert_eq!(
first_json["session"]["tools"][1]["type"],
Value::String("function".to_string())
);
assert_eq!(
first_json["session"]["tools"][1]["name"],
Value::String("remain_silent".to_string())
);
assert_eq!(
first_json["session"]["tools"][1]["parameters"]["properties"],
json!({})
);
assert_eq!(
first_json["session"]["tool_choice"],
Value::String("auto".to_string())
@@ -1725,7 +2002,10 @@ mod tests {
.await
.expect("send text item");
connection
.send_conversation_handoff_append("call_1".to_string(), "delegated result".to_string())
.send_conversation_function_call_output(
"call_1".to_string(),
"delegated result".to_string(),
)
.await
.expect("send handoff output");
@@ -1756,6 +2036,12 @@ mod tests {
Value::String("transcription".to_string())
);
assert!(first_json["session"].get("instructions").is_none());
assert_eq!(
first_json["session"]["audio"]["input"]["transcription"],
json!({
"model": "gpt-4o-mini-transcribe",
})
);
assert!(first_json["session"]["audio"].get("output").is_none());
assert!(first_json["session"].get("tools").is_none());
@@ -2,7 +2,7 @@ use crate::endpoint::realtime_websocket::methods_v1::conversation_handoff_append
use crate::endpoint::realtime_websocket::methods_v1::conversation_item_create_message as v1_conversation_item_create_message;
use crate::endpoint::realtime_websocket::methods_v1::session_update_session as v1_session_update_session;
use crate::endpoint::realtime_websocket::methods_v1::websocket_intent as v1_websocket_intent;
use crate::endpoint::realtime_websocket::methods_v2::conversation_handoff_append_message as v2_conversation_handoff_append_message;
use crate::endpoint::realtime_websocket::methods_v2::conversation_function_call_output_message as v2_conversation_function_call_output_message;
use crate::endpoint::realtime_websocket::methods_v2::conversation_item_create_message as v2_conversation_item_create_message;
use crate::endpoint::realtime_websocket::methods_v2::session_update_session as v2_session_update_session;
use crate::endpoint::realtime_websocket::methods_v2::websocket_intent as v2_websocket_intent;
@@ -40,18 +40,18 @@ pub(super) fn conversation_item_create_message(
}
}
pub(super) fn conversation_handoff_append_message(
pub(super) fn conversation_function_call_output_message(
event_parser: RealtimeEventParser,
handoff_id: String,
call_id: String,
output_text: String,
) -> RealtimeOutboundMessage {
match event_parser {
RealtimeEventParser::V1 => v1_conversation_handoff_append_message(
handoff_id,
call_id,
format!("{AGENT_FINAL_MESSAGE_PREFIX}{output_text}"),
),
RealtimeEventParser::RealtimeV2 => {
v2_conversation_handoff_append_message(handoff_id, output_text)
v2_conversation_function_call_output_message(call_id, output_text)
}
}
}
@@ -55,6 +55,7 @@ pub(super) fn session_update_session(
rate: REALTIME_AUDIO_SAMPLE_RATE,
},
noise_reduction: None,
transcription: None,
turn_detection: None,
},
output: Some(SessionAudioOutput {
@@ -18,6 +18,7 @@ use crate::endpoint::realtime_websocket::protocol::SessionAudioInput;
use crate::endpoint::realtime_websocket::protocol::SessionAudioOutput;
use crate::endpoint::realtime_websocket::protocol::SessionAudioOutputFormat;
use crate::endpoint::realtime_websocket::protocol::SessionFunctionTool;
use crate::endpoint::realtime_websocket::protocol::SessionInputAudioTranscription;
use crate::endpoint::realtime_websocket::protocol::SessionNoiseReduction;
use crate::endpoint::realtime_websocket::protocol::SessionToolType;
use crate::endpoint::realtime_websocket::protocol::SessionTurnDetection;
@@ -31,6 +32,9 @@ const REALTIME_V2_OUTPUT_MODALITY_TEXT: &str = "text";
const REALTIME_V2_TOOL_CHOICE: &str = "auto";
const REALTIME_V2_BACKGROUND_AGENT_TOOL_NAME: &str = "background_agent";
const REALTIME_V2_BACKGROUND_AGENT_TOOL_DESCRIPTION: &str = "Send a user request to the background agent. Use this as the default action. Do not rephrase the user's ask or rewrite it in your own words; pass along the user's own words. If the background agent is idle, this starts a new task and returns the final result to the user. If the background agent is already working on a task, this sends the request as guidance to steer that previous task. If the user asks to do something next, later, after this, or once current work finishes, call this tool so the work is actually queued instead of merely promising to do it later.";
const REALTIME_V2_SILENCE_TOOL_NAME: &str = "remain_silent";
const REALTIME_V2_SILENCE_TOOL_DESCRIPTION: &str = "Call this when the best response is to say nothing. Use it instead of speaking after hidden system/control messages, after background agent updates in silent modes, or whenever acknowledging aloud would be distracting. This tool has no user-visible effect.";
const REALTIME_V2_INPUT_TRANSCRIPTION_MODEL: &str = "gpt-4o-mini-transcribe";
pub(super) fn conversation_item_create_message(text: String) -> RealtimeOutboundMessage {
RealtimeOutboundMessage::ConversationItemCreate {
@@ -45,14 +49,14 @@ pub(super) fn conversation_item_create_message(text: String) -> RealtimeOutbound
}
}
pub(super) fn conversation_handoff_append_message(
handoff_id: String,
pub(super) fn conversation_function_call_output_message(
call_id: String,
output_text: String,
) -> RealtimeOutboundMessage {
RealtimeOutboundMessage::ConversationItemCreate {
item: ConversationItemPayload::FunctionCallOutput(ConversationFunctionCallOutputItem {
r#type: ConversationItemType::FunctionCallOutput,
call_id: handoff_id,
call_id,
output: output_text,
}),
}
@@ -80,6 +84,9 @@ pub(super) fn session_update_session(
noise_reduction: Some(SessionNoiseReduction {
r#type: NoiseReductionType::NearField,
}),
transcription: Some(SessionInputAudioTranscription {
model: REALTIME_V2_INPUT_TRANSCRIPTION_MODEL.to_string(),
}),
turn_detection: Some(SessionTurnDetection {
r#type: TurnDetectionType::ServerVad,
interrupt_response: true,
@@ -95,22 +102,34 @@ pub(super) fn session_update_session(
voice,
}),
},
tools: Some(vec![SessionFunctionTool {
r#type: SessionToolType::Function,
name: REALTIME_V2_BACKGROUND_AGENT_TOOL_NAME.to_string(),
description: REALTIME_V2_BACKGROUND_AGENT_TOOL_DESCRIPTION.to_string(),
parameters: json!({
"type": "object",
"properties": {
"prompt": {
"type": "string",
"description": "The user request to delegate to the background agent."
}
},
"required": ["prompt"],
"additionalProperties": false
}),
}]),
tools: Some(vec![
SessionFunctionTool {
r#type: SessionToolType::Function,
name: REALTIME_V2_BACKGROUND_AGENT_TOOL_NAME.to_string(),
description: REALTIME_V2_BACKGROUND_AGENT_TOOL_DESCRIPTION.to_string(),
parameters: json!({
"type": "object",
"properties": {
"prompt": {
"type": "string",
"description": "The user request to delegate to the background agent."
}
},
"required": ["prompt"],
"additionalProperties": false
}),
},
SessionFunctionTool {
r#type: SessionToolType::Function,
name: REALTIME_V2_SILENCE_TOOL_NAME.to_string(),
description: REALTIME_V2_SILENCE_TOOL_DESCRIPTION.to_string(),
parameters: json!({
"type": "object",
"properties": {},
"additionalProperties": false
}),
},
]),
tool_choice: Some(REALTIME_V2_TOOL_CHOICE.to_string()),
},
RealtimeSessionMode::Transcription => SessionUpdateSession {
@@ -126,6 +145,9 @@ pub(super) fn session_update_session(
rate: REALTIME_AUDIO_SAMPLE_RATE,
},
noise_reduction: None,
transcription: Some(SessionInputAudioTranscription {
model: REALTIME_V2_INPUT_TRANSCRIPTION_MODEL.to_string(),
}),
turn_detection: None,
},
output: None,
@@ -89,9 +89,16 @@ pub(super) struct SessionAudioInput {
#[serde(skip_serializing_if = "Option::is_none")]
pub(super) noise_reduction: Option<SessionNoiseReduction>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(super) transcription: Option<SessionInputAudioTranscription>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(super) turn_detection: Option<SessionTurnDetection>,
}
#[derive(Debug, Clone, Serialize)]
pub(super) struct SessionInputAudioTranscription {
pub(super) model: String,
}
#[derive(Debug, Clone, Serialize)]
pub(super) struct SessionAudioFormat {
#[serde(rename = "type")]
@@ -2,6 +2,7 @@ use crate::endpoint::realtime_websocket::protocol_common::parse_error_event;
use crate::endpoint::realtime_websocket::protocol_common::parse_realtime_payload;
use crate::endpoint::realtime_websocket::protocol_common::parse_session_updated_event;
use crate::endpoint::realtime_websocket::protocol_common::parse_transcript_delta_event;
use crate::endpoint::realtime_websocket::protocol_common::parse_transcript_done_event;
use codex_protocol::protocol::RealtimeAudioFrame;
use codex_protocol::protocol::RealtimeEvent;
use codex_protocol::protocol::RealtimeHandoffRequested;
@@ -38,23 +39,28 @@ pub(super) fn parse_realtime_event_v1(payload: &str) -> Option<RealtimeEvent> {
item_id: None,
}))
}
"conversation.input_transcript.delta" => {
"conversation.input_transcript.delta"
| "conversation.item.input_audio_transcription.delta" => {
parse_transcript_delta_event(&parsed, "delta").map(RealtimeEvent::InputTranscriptDelta)
}
"conversation.output_transcript.delta" => {
"conversation.item.input_audio_transcription.completed" => {
parse_transcript_done_event(&parsed, "transcript")
.map(RealtimeEvent::InputTranscriptDone)
}
"conversation.output_transcript.delta"
| "response.output_text.delta"
| "response.output_audio_transcript.delta" => {
parse_transcript_delta_event(&parsed, "delta").map(RealtimeEvent::OutputTranscriptDelta)
}
"response.output_audio_transcript.done" => {
parse_transcript_done_event(&parsed, "transcript")
.map(RealtimeEvent::OutputTranscriptDone)
}
"conversation.item.added" => parsed
.get("item")
.cloned()
.map(RealtimeEvent::ConversationItemAdded),
"conversation.item.done" => parsed
.get("item")
.and_then(Value::as_object)
.and_then(|item| item.get("id"))
.and_then(Value::as_str)
.map(str::to_string)
.map(|item_id| RealtimeEvent::ConversationItemDone { item_id }),
"conversation.item.done" => parse_conversation_item_done_event(&parsed),
"conversation.handoff.requested" => {
let handoff_id = parsed
.get("handoff_id")
@@ -82,3 +88,11 @@ pub(super) fn parse_realtime_event_v1(payload: &str) -> Option<RealtimeEvent> {
}
}
}
fn parse_conversation_item_done_event(parsed: &Value) -> Option<RealtimeEvent> {
let item = parsed.get("item")?.as_object()?;
item.get("id")
.and_then(Value::as_str)
.map(str::to_string)
.map(|item_id| RealtimeEvent::ConversationItemDone { item_id })
}
@@ -7,15 +7,16 @@ use codex_protocol::protocol::RealtimeAudioFrame;
use codex_protocol::protocol::RealtimeEvent;
use codex_protocol::protocol::RealtimeHandoffRequested;
use codex_protocol::protocol::RealtimeInputAudioSpeechStarted;
use codex_protocol::protocol::RealtimeNoopRequested;
use codex_protocol::protocol::RealtimeResponseCancelled;
use codex_protocol::protocol::RealtimeResponseCreated;
use codex_protocol::protocol::RealtimeResponseDone;
use codex_protocol::protocol::RealtimeTranscriptDone;
use serde_json::Map as JsonMap;
use serde_json::Value;
use tracing::debug;
const BACKGROUND_AGENT_TOOL_NAME: &str = "background_agent";
const SILENCE_TOOL_NAME: &str = "remain_silent";
const DEFAULT_AUDIO_SAMPLE_RATE: u32 = 24_000;
const DEFAULT_AUDIO_CHANNELS: u16 = 1;
const TOOL_ARGUMENT_KEYS: [&str; 5] = ["input_transcript", "input", "text", "prompt", "query"];
@@ -38,6 +39,13 @@ pub(super) fn parse_realtime_event_v2(payload: &str) -> Option<RealtimeEvent> {
"response.output_text.delta" | "response.output_audio_transcript.delta" => {
parse_transcript_delta_event(&parsed, "delta").map(RealtimeEvent::OutputTranscriptDelta)
}
"response.output_text.done" => {
parse_transcript_done_event(&parsed, "text").map(RealtimeEvent::OutputTranscriptDone)
}
"response.output_audio_transcript.done" => {
parse_transcript_done_event(&parsed, "transcript")
.map(RealtimeEvent::OutputTranscriptDone)
}
"input_audio_buffer.speech_started" => Some(RealtimeEvent::InputAudioSpeechStarted(
RealtimeInputAudioSpeechStarted {
item_id: parsed
@@ -46,7 +54,7 @@ pub(super) fn parse_realtime_event_v2(payload: &str) -> Option<RealtimeEvent> {
.map(str::to_string),
},
)),
"conversation.item.added" => parsed
"conversation.item.added" | "conversation.item.created" => parsed
.get("item")
.cloned()
.map(RealtimeEvent::ConversationItemAdded),
@@ -121,9 +129,8 @@ fn parse_conversation_item_done_event(parsed: &Value) -> Option<RealtimeEvent> {
if let Some(handoff) = parse_handoff_requested_event(item) {
return Some(handoff);
}
if let Some(transcript_done) = parse_item_done_transcript(item) {
return Some(transcript_done);
if let Some(noop) = parse_noop_requested_event(item) {
return Some(noop);
}
item.get("id")
@@ -132,33 +139,6 @@ fn parse_conversation_item_done_event(parsed: &Value) -> Option<RealtimeEvent> {
.map(|item_id| RealtimeEvent::ConversationItemDone { item_id })
}
fn parse_item_done_transcript(item: &JsonMap<String, Value>) -> Option<RealtimeEvent> {
let role = item.get("role").and_then(Value::as_str)?;
let text = item
.get("content")
.and_then(Value::as_array)?
.iter()
.filter_map(item_content_text)
.collect::<String>();
if text.is_empty() {
return None;
}
let done = RealtimeTranscriptDone { text };
match role {
"user" => Some(RealtimeEvent::InputTranscriptDone(done)),
"assistant" => Some(RealtimeEvent::OutputTranscriptDone(done)),
_ => None,
}
}
fn item_content_text(content: &Value) -> Option<&str> {
content
.get("text")
.or_else(|| content.get("transcript"))
.and_then(Value::as_str)
}
fn parse_handoff_requested_event(item: &JsonMap<String, Value>) -> Option<RealtimeEvent> {
let item_type = item.get("type").and_then(Value::as_str);
let item_name = item.get("name").and_then(Value::as_str);
@@ -185,6 +165,29 @@ fn parse_handoff_requested_event(item: &JsonMap<String, Value>) -> Option<Realti
}))
}
fn parse_noop_requested_event(item: &JsonMap<String, Value>) -> Option<RealtimeEvent> {
let item_type = item.get("type").and_then(Value::as_str);
let item_name = item.get("name").and_then(Value::as_str);
if item_type != Some("function_call") || item_name != Some(SILENCE_TOOL_NAME) {
return None;
}
let call_id = item
.get("call_id")
.and_then(Value::as_str)
.or_else(|| item.get("id").and_then(Value::as_str))?;
let item_id = item
.get("id")
.and_then(Value::as_str)
.unwrap_or(call_id)
.to_string();
Some(RealtimeEvent::NoopRequested(RealtimeNoopRequested {
call_id: call_id.to_string(),
item_id,
}))
}
fn extract_input_transcript(arguments: &str) -> String {
if arguments.is_empty() {
return String::new();
@@ -229,6 +229,10 @@ impl ResponsesWebsocketConnection {
let current_span = Span::current();
tokio::spawn(
#[expect(
clippy::await_holding_invalid_type,
reason = "the guard serializes exclusive use of the websocket stream for the lifetime of the response stream"
)]
async move {
if let Some(model) = server_model {
let _ = tx_event.send(Ok(ResponseEvent::ServerModel(model))).await;
@@ -12,6 +12,9 @@ use codex_api::RealtimeSessionMode;
use codex_api::RealtimeWebsocketClient;
use codex_api::RetryConfig;
use codex_protocol::protocol::RealtimeHandoffRequested;
use codex_protocol::protocol::RealtimeTranscriptDelta;
use codex_protocol::protocol::RealtimeTranscriptDone;
use codex_protocol::protocol::RealtimeTranscriptEntry;
use codex_protocol::protocol::RealtimeVoice;
use futures::SinkExt;
use futures::StreamExt;
@@ -493,6 +496,46 @@ async fn realtime_ws_e2e_realtime_v2_parser_emits_handoff_requested() {
let first_json: Value = serde_json::from_str(&first).expect("json");
assert_eq!(first_json["type"], "session.update");
ws.send(Message::Text(
json!({
"type": "conversation.item.input_audio_transcription.completed",
"transcript": "delegate now"
})
.to_string()
.into(),
))
.await
.expect("send input transcript");
ws.send(Message::Text(
json!({
"type": "response.output_audio_transcript.delta",
"delta": "secret context"
})
.to_string()
.into(),
))
.await
.expect("send output transcript");
ws.send(Message::Text(
json!({
"type": "conversation.item.created",
"item": {
"type": "message",
"role": "user",
"content": [{
"type": "input_text",
"text": "<realtime_collaboration_update><voice_policy>silent_delegate</voice_policy></realtime_collaboration_update>"
}]
}
})
.to_string()
.into(),
))
.await
.expect("send control item echo");
ws.send(Message::Text(
json!({
"type": "conversation.item.done",
@@ -530,6 +573,37 @@ async fn realtime_ws_e2e_realtime_v2_parser_emits_handoff_requested() {
.await
.expect("connect");
let event = connection
.next_event()
.await
.expect("next event")
.expect("event");
assert_eq!(
event,
RealtimeEvent::InputTranscriptDone(RealtimeTranscriptDone {
text: "delegate now".to_string()
})
);
let event = connection
.next_event()
.await
.expect("next event")
.expect("event");
assert_eq!(
event,
RealtimeEvent::OutputTranscriptDelta(RealtimeTranscriptDelta {
delta: "secret context".to_string()
})
);
let event = connection
.next_event()
.await
.expect("next event")
.expect("event");
assert!(matches!(event, RealtimeEvent::ConversationItemAdded(_)));
let event = connection
.next_event()
.await
@@ -541,7 +615,16 @@ async fn realtime_ws_e2e_realtime_v2_parser_emits_handoff_requested() {
handoff_id: "call_123".to_string(),
item_id: "item_123".to_string(),
input_transcript: "delegate now".to_string(),
active_transcript: Vec::new(),
active_transcript: vec![
RealtimeTranscriptEntry {
role: "user".to_string(),
text: "delegate now".to_string(),
},
RealtimeTranscriptEntry {
role: "assistant".to_string(),
text: "secret context".to_string(),
},
],
})
);
+5
View File
@@ -16,24 +16,29 @@ pub use mcp::ToolPluginProvenance;
pub use mcp::canonical_mcp_server_key;
pub use mcp::collect_mcp_server_status_snapshot;
pub use mcp::collect_mcp_server_status_snapshot_with_detail;
pub use mcp::collect_mcp_server_status_snapshot_with_detail_and_authorization_header;
pub use mcp::collect_mcp_snapshot;
pub use mcp::collect_mcp_snapshot_from_manager;
pub use mcp::collect_mcp_snapshot_from_manager_with_detail;
pub use mcp::collect_mcp_snapshot_with_detail;
pub use mcp::collect_mcp_snapshot_with_detail_and_authorization_header;
pub use mcp::collect_missing_mcp_dependencies;
pub use mcp::compute_auth_statuses;
pub use mcp::configured_mcp_servers;
pub use mcp::discover_supported_scopes;
pub use mcp::effective_mcp_servers;
pub use mcp::effective_mcp_servers_with_authorization_header;
pub use mcp::group_tools_by_server;
pub use mcp::mcp_permission_prompt_is_auto_approved;
pub use mcp::oauth_login_support;
pub use mcp::qualified_mcp_tool_name_prefix;
pub use mcp::read_mcp_resource;
pub use mcp::resolve_oauth_scopes;
pub use mcp::should_retry_without_scopes;
pub use mcp::split_qualified_tool_name;
pub use mcp::tool_plugin_provenance;
pub use mcp::with_codex_apps_mcp;
pub use mcp::with_codex_apps_mcp_with_authorization_header;
pub use mcp_connection_manager::CodexAppsToolsCacheKey;
pub use mcp_connection_manager::DEFAULT_STARTUP_TIMEOUT;
pub use mcp_connection_manager::MCP_SANDBOX_STATE_META_CAPABILITY;

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