Merge commit '152e000b1b933fc7285fe3fc7fb204106ac01c6b' into HEAD

This commit is contained in:
Winston Howes
2026-06-18 11:39:28 -07:00
462 changed files with 19132 additions and 6630 deletions
+25 -1
View File
@@ -9,10 +9,34 @@ Apply this guidance when defining new types. Change existing code only when expl
and keep edits minimal and proportional. Treat these rules as the target state of an ongoing
migration; if compliance is difficult, ask the user how to proceed.
- In app-server protocol types, use `ApiPathString` for backwards compatibility during the URI
- In app-server protocol types, use `LegacyAppPathString` for backwards compatibility during the URI
migration. At the protocol boundary, convert it to `PathUri` and use `PathUri` internally. For
host-local logic, such as some config values, use `AbsolutePathBuf` or `PathBuf` instead.
- In exec-server protocol types, use `PathUri`. Internally, use `PathUri` or `AbsolutePathBuf` as
appropriate.
- In dependencies shared by both servers, use `PathUri` or separate APIs that decouple their use
cases.
- Tool call arguments that the model is expected to generate should be deserialized as regular
`String`s with feature-specific path handling code.
## Migration requirements
Keep these requirements in mind while migrating code to conform with the above guidelines:
* existing app-server clients keep sending and receiving legacy native-path strings
* app-server can retain and manipulate foreign-platform path URIs
* exec-server APIs use file:// URIs
* local-only operation must not change model-visible text
* model tool arguments may contain raw relative or absolute paths for any OS
* path reasoning must work before the related environment has come online
* URIs cannot explicitly encode the executor’s path convention or operating system
* users must not configure the environment’s OS/path convention explicitly
* URIs should not yet be stored in rollouts, databases, or other persistent storage
* path conversion errors: fail-closed for security-relevant paths, fail-open for UI/diagnostics
* prefer small focused methods on `PathUri` or `LegacyAppPathString` over local helpers
It is OK if the conversion between paths and URIs is somewhat lossy as long as it will do the right
thing for real users.
Migrating to URIs should not add significant new failure modes. We will need to surface errors in
some places that were previously infallible but it should be kept to a minimum.
+1 -4
View File
@@ -173,11 +173,8 @@ jobs:
runner: macos-15-xlarge
timeout_minutes: 30
- name: Windows
runner: windows-x64
runner: windows-2022
timeout_minutes: 30
runs_on:
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-x64
steps:
- name: Check whether argument comment lint should run
id: argument_comment_lint_gate
+3 -3
View File
@@ -31,11 +31,11 @@ register_toolchains("@llvm//toolchain:all")
osx = use_extension("@llvm//extensions:osx.bzl", "osx")
osx.from_archive(
sha256 = "1bde70c0b1c2ab89ff454acbebf6741390d7b7eb149ca2a3ca24cc9203a408b7",
strip_prefix = "Payload/Library/Developer/CommandLineTools/SDKs/MacOSX26.4.sdk",
sha256 = "5f044578cd78a3a9b9c965a42d56bad609ee5d252e1d4e6aa7c42fc3f35fee7b",
strip_prefix = "Payload/Library/Developer/CommandLineTools/SDKs/MacOSX26.5.sdk",
type = "pkg",
urls = [
"https://swcdn.apple.com/content/downloads/32/53/047-96692-A_OAHIHT53YB/ybtshxmrcju8m2qvw3w5elr4rajtg1x3y3/CLTools_macOSNMOS_SDK.pkg",
"https://swcdn.apple.com/content/downloads/09/08/047-91568-A_Y1CFZWQCD4/4xekpyz43i26dbp4enxfro8eb1q7wiujh5/CLTools_macOSNMOS_SDK.pkg",
],
)
osx.frameworks(names = [
+4
View File
@@ -2760,6 +2760,7 @@ dependencies = [
"codex-otel",
"codex-plugin",
"codex-protocol",
"codex-tools",
"codex-utils-absolute-path",
"codex-utils-path-uri",
"codex-utils-plugins",
@@ -3288,6 +3289,7 @@ dependencies = [
"codex-plugin",
"codex-protocol",
"codex-rmcp-client",
"codex-utils-path-uri",
"codex-utils-plugins",
"futures",
"pretty_assertions",
@@ -3793,7 +3795,9 @@ dependencies = [
"codex-network-proxy",
"codex-protocol",
"codex-utils-absolute-path",
"codex-utils-home-dir",
"codex-utils-path-uri",
"codex-windows-sandbox",
"dunce",
"libc",
"pretty_assertions",
+322 -82
View File
@@ -1,4 +1,6 @@
use std::collections::BTreeMap;
use std::error::Error as StdError;
use std::fmt;
use std::time::Duration;
use anyhow::Context;
@@ -34,21 +36,66 @@ const AGENT_TASK_REGISTRATION_TIMEOUT: Duration = Duration::from_secs(30);
const AGENT_IDENTITY_JWKS_TIMEOUT: Duration = Duration::from_secs(10);
const AGENT_IDENTITY_JWT_AUDIENCE: &str = "codex-app-server";
const AGENT_IDENTITY_JWT_ISSUER: &str = "https://chatgpt.com/codex-backend/agent-identity";
const AGENT_REGISTRATION_TIMEOUT: Duration = Duration::from_secs(15);
const PROD_AGENT_IDENTITY_AUTHAPI_BASE_URL: &str = "https://auth.openai.com/api/accounts";
const STAGING_AGENT_IDENTITY_AUTHAPI_BASE_URL: &str = "https://auth.api.openai.org/api/accounts";
const AGENT_IDENTITY_KEY_SEED_BYTES: usize = 64;
const AGENT_IDENTITY_KEY_DERIVATION_CONTEXT: &[u8] = b"codex-agent-identity-ed25519-v1";
/// Stored key material for a registered agent identity.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ChatGptEnvironment {
#[default]
Production,
Staging,
}
impl ChatGptEnvironment {
pub fn from_chatgpt_base_url(chatgpt_base_url: &str) -> Result<Self> {
match chatgpt_base_url.trim_end_matches('/') {
"https://chatgpt.com"
| "https://chatgpt.com/backend-api"
| "https://chatgpt.com/codex"
| "https://chatgpt.com/backend-api/codex"
| "https://chat.openai.com"
| "https://chat.openai.com/backend-api"
| "https://chat.openai.com/codex"
| "https://chat.openai.com/backend-api/codex" => Ok(Self::Production),
"https://chatgpt-staging.com"
| "https://chatgpt-staging.com/backend-api"
| "https://chatgpt-staging.com/codex"
| "https://chatgpt-staging.com/backend-api/codex" => Ok(Self::Staging),
_ => anyhow::bail!(
"Agent Identity only supports production and staging ChatGPT environments"
),
}
}
pub fn chatgpt_base_url(self) -> &'static str {
match self {
Self::Production => "https://chatgpt.com/backend-api",
Self::Staging => "https://chatgpt-staging.com/backend-api",
}
}
pub fn agent_identity_authapi_base_url(self) -> &'static str {
match self {
Self::Production => PROD_AGENT_IDENTITY_AUTHAPI_BASE_URL,
Self::Staging => STAGING_AGENT_IDENTITY_AUTHAPI_BASE_URL,
}
}
}
/// Borrowed durable signing material for a registered agent identity.
///
/// This intentionally does not include a task id. Task ids are scoped to a
/// single Codex run, while the agent runtime id and private key are the
/// reusable identity material used to register and sign that run task.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AgentIdentityKey<'a> {
pub agent_runtime_id: &'a str,
pub private_key_pkcs8_base64: &'a str,
}
/// Task binding to use when constructing a task-scoped AgentAssertion.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AgentTaskAuthorizationTarget<'a> {
pub agent_runtime_id: &'a str,
pub task_id: &'a str,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct AgentBillOfMaterials {
pub agent_version: String,
@@ -103,23 +150,92 @@ struct RegisterTaskResponse {
encrypted_task_id_camel: Option<String>,
}
#[derive(Debug, Serialize)]
struct RegisterAgentRequest {
abom: AgentBillOfMaterials,
agent_public_key: String,
capabilities: Vec<String>,
ttl: Option<u64>,
}
#[derive(Debug, Deserialize)]
struct RegisterAgentResponse {
agent_runtime_id: String,
}
/// HTTP status failure returned by Agent Identity registration endpoints.
#[derive(Debug)]
pub struct AgentIdentityRegistrationHttpError {
operation: &'static str,
status: reqwest::StatusCode,
body: String,
}
impl AgentIdentityRegistrationHttpError {
fn new(operation: &'static str, status: reqwest::StatusCode, body: String) -> Self {
Self {
operation,
status,
body,
}
}
/// HTTP status returned by the registration endpoint.
pub fn status(&self) -> reqwest::StatusCode {
self.status
}
}
impl fmt::Display for AgentIdentityRegistrationHttpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.body.is_empty() {
write!(f, "{} failed with status {}", self.operation, self.status)
} else {
write!(
f,
"{} failed with status {}: {}",
self.operation, self.status, self.body
)
}
}
}
impl StdError for AgentIdentityRegistrationHttpError {}
/// Returns whether an Agent Identity registration error is safe to retry.
pub fn is_retryable_registration_error(error: &anyhow::Error) -> bool {
error.chain().any(is_retryable_registration_cause)
}
fn is_retryable_registration_cause(cause: &(dyn StdError + 'static)) -> bool {
if let Some(error) = cause.downcast_ref::<AgentIdentityRegistrationHttpError>() {
return is_retryable_registration_status(error.status());
}
if let Some(error) = cause.downcast_ref::<reqwest::Error>() {
if let Some(status) = error.status() {
return is_retryable_registration_status(status);
}
return error.is_timeout() || error.is_connect() || error.is_request();
}
false
}
fn is_retryable_registration_status(status: reqwest::StatusCode) -> bool {
status == reqwest::StatusCode::TOO_MANY_REQUESTS || status.is_server_error()
}
pub fn authorization_header_for_agent_task(
key: AgentIdentityKey<'_>,
target: AgentTaskAuthorizationTarget<'_>,
task_id: &str,
) -> Result<String> {
anyhow::ensure!(
key.agent_runtime_id == target.agent_runtime_id,
"agent task runtime {} does not match stored agent identity {}",
target.agent_runtime_id,
key.agent_runtime_id
);
let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
let envelope = AgentAssertionEnvelope {
agent_runtime_id: target.agent_runtime_id.to_string(),
task_id: target.task_id.to_string(),
agent_runtime_id: key.agent_runtime_id.to_string(),
task_id: task_id.to_string(),
timestamp: timestamp.clone(),
signature: sign_agent_assertion_payload(key, target.task_id, &timestamp)?,
signature: sign_agent_assertion_payload(key, task_id, &timestamp)?,
};
let serialized_assertion = serialize_agent_assertion(&envelope)?;
Ok(format!("AgentAssertion {serialized_assertion}"))
@@ -127,10 +243,10 @@ pub fn authorization_header_for_agent_task(
pub async fn fetch_agent_identity_jwks(
client: &reqwest::Client,
chatgpt_base_url: &str,
agent_identity_jwt_base_url: &str,
) -> Result<JwkSet> {
let response = client
.get(agent_identity_jwks_url(chatgpt_base_url))
.get(agent_identity_jwks_url(agent_identity_jwt_base_url))
.timeout(AGENT_IDENTITY_JWKS_TIMEOUT)
.send()
.await
@@ -195,7 +311,7 @@ pub fn sign_task_registration_payload(
pub async fn register_agent_task(
client: &reqwest::Client,
chatgpt_base_url: &str,
agent_identity_authapi_base_url: &str,
key: AgentIdentityKey<'_>,
) -> Result<String> {
let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
@@ -203,7 +319,7 @@ pub async fn register_agent_task(
signature: sign_task_registration_payload(key, &timestamp)?,
timestamp,
};
let url = agent_task_registration_url(chatgpt_base_url, key.agent_runtime_id);
let url = agent_task_registration_url(agent_identity_authapi_base_url, key.agent_runtime_id);
let response = client
.post(url)
@@ -220,7 +336,12 @@ pub async fn register_agent_task(
} else {
body
};
anyhow::bail!("failed to register agent task with status {status}: {body}");
return Err(AgentIdentityRegistrationHttpError::new(
"agent task registration",
status,
body,
)
.into());
}
let response = response
@@ -231,6 +352,45 @@ pub async fn register_agent_task(
task_id_from_register_task_response(key, response)
}
pub async fn register_agent_identity(
client: &reqwest::Client,
agent_identity_authapi_base_url: &str,
access_token: &str,
is_fedramp_account: bool,
key_material: &GeneratedAgentKeyMaterial,
abom: AgentBillOfMaterials,
capabilities: Vec<String>,
) -> Result<String> {
let url = agent_registration_url(agent_identity_authapi_base_url);
let request = RegisterAgentRequest {
abom,
agent_public_key: key_material.public_key_ssh.clone(),
capabilities,
ttl: None,
};
let mut request_builder = client
.post(&url)
.bearer_auth(access_token)
.json(&request)
.timeout(AGENT_REGISTRATION_TIMEOUT);
if is_fedramp_account {
request_builder = request_builder.header("X-OpenAI-Fedramp", "true");
}
let response = request_builder
.send()
.await
.with_context(|| format!("failed to send agent identity registration request to {url}"))?
.error_for_status()
.with_context(|| format!("agent identity registration failed for {url}"))?
.json::<RegisterAgentResponse>()
.await
.with_context(|| format!("failed to parse agent identity response from {url}"))?;
Ok(response.agent_runtime_id)
}
fn task_id_from_register_task_response(
key: AgentIdentityKey<'_>,
response: RegisterTaskResponse,
@@ -260,10 +420,17 @@ pub fn decrypt_task_id_response(
}
pub fn generate_agent_key_material() -> Result<GeneratedAgentKeyMaterial> {
let mut secret_key_bytes = [0u8; 32];
let mut seed_material = [0u8; AGENT_IDENTITY_KEY_SEED_BYTES];
OsRng
.try_fill_bytes(&mut secret_key_bytes)
.context("failed to generate agent identity private key bytes")?;
.try_fill_bytes(&mut seed_material)
.context("failed to generate agent identity private key seed material")?;
// Ed25519 stores a 32-byte seed, so derive it from all sampled seed material.
let mut digest = Sha512::new();
digest.update(AGENT_IDENTITY_KEY_DERIVATION_CONTEXT);
digest.update(seed_material);
let digest = digest.finalize();
let mut secret_key_bytes = [0u8; 32];
secret_key_bytes.copy_from_slice(&digest[..32]);
let signing_key = SigningKey::from_bytes(&secret_key_bytes);
let private_key_pkcs8 = signing_key
.to_pkcs8_der()
@@ -296,23 +463,22 @@ pub fn curve25519_secret_key_from_private_key_pkcs8_base64(
Ok(curve25519_secret_key_from_signing_key(&signing_key))
}
pub fn agent_registration_url(chatgpt_base_url: &str) -> String {
let trimmed = chatgpt_base_url.trim_end_matches('/');
format!("{trimmed}/v1/agent/register")
pub fn agent_registration_url(agent_identity_authapi_base_url: &str) -> String {
agent_identity_authapi_url(agent_identity_authapi_base_url, "/v1/agent/register")
}
pub fn agent_task_registration_url(chatgpt_base_url: &str, agent_runtime_id: &str) -> String {
let trimmed = chatgpt_base_url.trim_end_matches('/');
format!("{trimmed}/v1/agent/{agent_runtime_id}/task/register")
pub fn agent_task_registration_url(
agent_identity_authapi_base_url: &str,
agent_runtime_id: &str,
) -> String {
agent_identity_authapi_url(
agent_identity_authapi_base_url,
&format!("/v1/agent/{agent_runtime_id}/task/register"),
)
}
pub fn agent_identity_biscuit_url(chatgpt_base_url: &str) -> String {
let trimmed = chatgpt_base_url.trim_end_matches('/');
format!("{trimmed}/authenticate_app_v2")
}
pub fn agent_identity_jwks_url(chatgpt_base_url: &str) -> String {
let trimmed = chatgpt_base_url.trim_end_matches('/');
pub fn agent_identity_jwks_url(agent_identity_jwt_base_url: &str) -> String {
let trimmed = agent_identity_jwt_base_url.trim_end_matches('/');
if trimmed.contains("/backend-api") {
format!("{trimmed}/wham/agent-identities/jwks")
} else {
@@ -320,15 +486,9 @@ pub fn agent_identity_jwks_url(chatgpt_base_url: &str) -> String {
}
}
pub fn agent_identity_request_id() -> Result<String> {
let mut request_id_bytes = [0u8; 16];
OsRng
.try_fill_bytes(&mut request_id_bytes)
.context("failed to generate agent identity request id")?;
Ok(format!(
"codex-agent-identity-{}",
URL_SAFE_NO_PAD.encode(request_id_bytes)
))
fn agent_identity_authapi_url(agent_identity_authapi_base_url: &str, api_path: &str) -> String {
let base_url = agent_identity_authapi_base_url.trim_end_matches('/');
format!("{base_url}{api_path}")
}
pub fn build_abom(session_source: SessionSource) -> AgentBillOfMaterials {
@@ -412,6 +572,24 @@ mod tests {
use super::*;
#[test]
fn register_task_request_uses_single_run_task_shape() {
let request = RegisterTaskRequest {
timestamp: "2026-04-23T00:00:00Z".to_string(),
signature: "signature".to_string(),
};
let serialized = serde_json::to_value(request).expect("serialize request");
assert_eq!(
serialized,
serde_json::json!({
"timestamp": "2026-04-23T00:00:00Z",
"signature": "signature",
})
);
}
#[test]
fn authorization_header_for_agent_task_serializes_signed_agent_assertion() {
let signing_key = SigningKey::from_bytes(&[7u8; 32]);
@@ -422,13 +600,9 @@ mod tests {
agent_runtime_id: "agent-123",
private_key_pkcs8_base64: &BASE64_STANDARD.encode(private_key.as_bytes()),
};
let target = AgentTaskAuthorizationTarget {
agent_runtime_id: "agent-123",
task_id: "task-123",
};
let header =
authorization_header_for_agent_task(key, target).expect("build agent assertion header");
let header = authorization_header_for_agent_task(key, "task-123")
.expect("build agent assertion header");
let token = header
.strip_prefix("AgentAssertion ")
.expect("agent assertion scheme");
@@ -464,31 +638,6 @@ mod tests {
.expect("signature should verify");
}
#[test]
fn authorization_header_for_agent_task_rejects_mismatched_runtime() {
let signing_key = SigningKey::from_bytes(&[7u8; 32]);
let private_key = signing_key
.to_pkcs8_der()
.expect("encode test key material");
let private_key_pkcs8_base64 = BASE64_STANDARD.encode(private_key.as_bytes());
let key = AgentIdentityKey {
agent_runtime_id: "agent-123",
private_key_pkcs8_base64: &private_key_pkcs8_base64,
};
let target = AgentTaskAuthorizationTarget {
agent_runtime_id: "agent-456",
task_id: "task-123",
};
let error = authorization_header_for_agent_task(key, target)
.expect_err("runtime mismatch should fail");
assert_eq!(
error.to_string(),
"agent task runtime agent-456 does not match stored agent identity agent-123"
);
}
#[test]
fn decode_agent_identity_jwt_reads_claims() {
let jwt = jwt_with_payload(serde_json::json!({
@@ -704,7 +853,98 @@ J1bwkqKZTB5dHolX9A58e/xXnfZ5P8f3Z83+Izap3FwqQulk7b1WO1MQcHuVg2NN
}
#[test]
fn agent_identity_jwks_url_uses_backend_api_base_url() {
fn chatgpt_environment_maps_known_urls_to_authapi() -> anyhow::Result<()> {
assert_eq!(
ChatGptEnvironment::from_chatgpt_base_url("https://chatgpt.com/backend-api/codex")?,
ChatGptEnvironment::Production
);
assert_eq!(
ChatGptEnvironment::Production.agent_identity_authapi_base_url(),
"https://auth.openai.com/api/accounts"
);
assert_eq!(
ChatGptEnvironment::from_chatgpt_base_url("https://chatgpt-staging.com/backend-api")?,
ChatGptEnvironment::Staging
);
assert_eq!(
ChatGptEnvironment::Staging.agent_identity_authapi_base_url(),
"https://auth.api.openai.org/api/accounts"
);
Ok(())
}
#[test]
fn chatgpt_environment_rejects_custom_urls() {
assert!(ChatGptEnvironment::from_chatgpt_base_url("http://localhost:8080").is_err(),);
}
#[test]
fn agent_registration_url_appends_to_authapi_base_url() {
assert_eq!(
agent_registration_url("https://auth.openai.com/api/accounts"),
"https://auth.openai.com/api/accounts/v1/agent/register"
);
assert_eq!(
agent_registration_url("http://localhost:8080"),
"http://localhost:8080/v1/agent/register"
);
assert_eq!(
agent_registration_url("http://localhost:8080/backend-api"),
"http://localhost:8080/backend-api/v1/agent/register"
);
}
#[test]
fn agent_task_registration_url_appends_to_authapi_base_url() {
assert_eq!(
agent_task_registration_url("https://auth.openai.com/api/accounts", "agent-runtime-id"),
"https://auth.openai.com/api/accounts/v1/agent/agent-runtime-id/task/register"
);
assert_eq!(
agent_task_registration_url(
"https://auth.openai.com/api/accounts/",
"agent-runtime-id"
),
"https://auth.openai.com/api/accounts/v1/agent/agent-runtime-id/task/register"
);
assert_eq!(
agent_task_registration_url("http://localhost:8080", "agent-runtime-id"),
"http://localhost:8080/v1/agent/agent-runtime-id/task/register"
);
}
#[test]
fn retryable_registration_error_accepts_429_and_5xx() {
let too_many_requests = anyhow::Error::new(AgentIdentityRegistrationHttpError::new(
"agent registration",
reqwest::StatusCode::TOO_MANY_REQUESTS,
"rate limited".to_string(),
));
let unavailable = anyhow::Error::new(AgentIdentityRegistrationHttpError::new(
"agent registration",
reqwest::StatusCode::SERVICE_UNAVAILABLE,
"try later".to_string(),
));
assert!(is_retryable_registration_error(&too_many_requests));
assert!(is_retryable_registration_error(&unavailable));
}
#[test]
fn retryable_registration_error_rejects_hard_failures() {
let forbidden = anyhow::Error::new(AgentIdentityRegistrationHttpError::new(
"agent registration",
reqwest::StatusCode::FORBIDDEN,
"not allowed".to_string(),
));
let malformed = anyhow::anyhow!("failed to sign registration request");
assert!(!is_retryable_registration_error(&forbidden));
assert!(!is_retryable_registration_error(&malformed));
}
#[test]
fn agent_identity_jwks_url_uses_agent_identity_jwt_route() {
assert_eq!(
agent_identity_jwks_url("https://chatgpt.com/backend-api"),
"https://chatgpt.com/backend-api/wham/agent-identities/jwks"
@@ -716,7 +956,7 @@ J1bwkqKZTB5dHolX9A58e/xXnfZ5P8f3Z83+Izap3FwqQulk7b1WO1MQcHuVg2NN
}
#[test]
fn agent_identity_jwks_url_uses_codex_api_base_url() {
fn agent_identity_jwks_url_uses_jwt_issuer_base_url() {
assert_eq!(
agent_identity_jwks_url("http://localhost:8080/api/codex"),
"http://localhost:8080/api/codex/agent-identities/jwks"
@@ -9,7 +9,11 @@ use crate::events::CodexCommandExecutionEventParams;
use crate::events::CodexCommandExecutionEventRequest;
use crate::events::CodexCompactionEventRequest;
use crate::events::CodexHookRunEventRequest;
use crate::events::CodexOnboardingExternalAgentImportFailureEventRequest;
use crate::events::CodexOnboardingExternalAgentImportFailureMetadata;
use crate::events::CodexPluginEventRequest;
use crate::events::CodexPluginInstallFailedEventRequest;
use crate::events::CodexPluginInstallFailedMetadata;
use crate::events::CodexPluginUsedEventRequest;
use crate::events::CodexReviewEventParams;
use crate::events::CodexReviewEventRequest;
@@ -51,10 +55,13 @@ use crate::facts::CompactionStatus;
use crate::facts::CompactionStrategy;
use crate::facts::CompactionTrigger;
use crate::facts::CustomAnalyticsFact;
use crate::facts::ExternalAgentConfigImportCompletedInput;
use crate::facts::ExternalAgentConfigImportFailureInput;
use crate::facts::HookRunFact;
use crate::facts::HookRunInput;
use crate::facts::InputError;
use crate::facts::InvocationType;
use crate::facts::PluginInstallFailedInput;
use crate::facts::PluginState;
use crate::facts::PluginStateChangedInput;
use crate::facts::PluginUsedInput;
@@ -178,6 +185,7 @@ fn sample_thread_with_metadata(
model_provider: "openai".to_string(),
created_at: 1,
updated_at: 2,
recency_at: Some(2),
status: AppServerThreadStatus::Idle,
path: None,
cwd: test_path_buf("/tmp").abs(),
@@ -823,7 +831,7 @@ fn sample_command_execution_item_with_id(
ThreadItem::CommandExecution {
id: id.to_string(),
command: "echo hi".to_string(),
cwd: test_path_buf("/tmp").abs(),
cwd: test_path_buf("/tmp").abs().into(),
process_id: Some("pid-1".to_string()),
source: CommandExecutionSource::Agent,
status,
@@ -861,6 +869,7 @@ fn sample_command_approval_request(request_id: i64, approval_id: Option<&str>) -
item_id: "item-1".to_string(),
started_at_ms: 1_000,
approval_id: approval_id.map(str::to_string),
environment_id: None,
reason: None,
network_approval_context: None,
command: Some("echo hi".to_string()),
@@ -3052,6 +3061,36 @@ fn plugin_management_event_serializes_expected_shape() {
);
}
#[test]
fn plugin_install_failed_event_serializes_expected_shape() {
let event = TrackEventRequest::PluginInstallFailed(CodexPluginInstallFailedEventRequest {
event_type: "codex_plugin_install_failed",
event_params: CodexPluginInstallFailedMetadata {
plugin: codex_plugin_metadata(sample_plugin_metadata()),
error_type: "store_io".to_string(),
},
});
let payload = serde_json::to_value(&event).expect("serialize plugin install failed event");
assert_eq!(
payload,
json!({
"event_type": "codex_plugin_install_failed",
"event_params": {
"plugin_id": "sample@test",
"plugin_name": "sample",
"marketplace_name": "test",
"has_skills": true,
"mcp_server_count": 2,
"connector_ids": ["calendar", "drive"],
"product_client_id": originator().value,
"error_type": "store_io"
}
})
);
}
#[test]
fn plugin_management_event_can_use_remote_plugin_id_override() {
let mut plugin = sample_plugin_metadata();
@@ -3419,6 +3458,190 @@ async fn reducer_ingests_plugin_state_changed_fact() {
);
}
#[tokio::test]
async fn reducer_ingests_plugin_install_failed_fact() {
let mut reducer = AnalyticsReducer::default();
let mut events = Vec::new();
reducer
.ingest(
AnalyticsFact::Custom(CustomAnalyticsFact::PluginInstallFailed(
PluginInstallFailedInput {
plugin: sample_plugin_metadata(),
error_type: "invalid_plugin".to_string(),
},
)),
&mut events,
)
.await;
let payload = serde_json::to_value(&events).expect("serialize events");
assert_eq!(
payload,
json!([{
"event_type": "codex_plugin_install_failed",
"event_params": {
"plugin_id": "sample@test",
"plugin_name": "sample",
"marketplace_name": "test",
"has_skills": true,
"mcp_server_count": 2,
"connector_ids": ["calendar", "drive"],
"product_client_id": originator().value,
"error_type": "invalid_plugin"
}
}])
);
}
#[tokio::test]
async fn reducer_ingests_plugin_install_failed_fact_without_detail() {
let mut reducer = AnalyticsReducer::default();
let mut events = Vec::new();
let mut plugin = PluginTelemetryMetadata::from_plugin_id(
&PluginId::parse("unknown@openai-curated-remote").expect("valid plugin id"),
);
plugin.remote_plugin_id = Some("plugins~Plugin_00000000000000000000000000000000".to_string());
reducer
.ingest(
AnalyticsFact::Custom(CustomAnalyticsFact::PluginInstallFailed(
PluginInstallFailedInput {
plugin,
error_type: "remote_catalog_unexpected_status".to_string(),
},
)),
&mut events,
)
.await;
let payload = serde_json::to_value(&events).expect("serialize events");
assert_eq!(
payload,
json!([{
"event_type": "codex_plugin_install_failed",
"event_params": {
"plugin_id": "plugins~Plugin_00000000000000000000000000000000",
"plugin_name": "unknown",
"marketplace_name": "openai-curated-remote",
"has_skills": null,
"mcp_server_count": null,
"connector_ids": null,
"product_client_id": originator().value,
"error_type": "remote_catalog_unexpected_status"
}
}])
);
}
#[tokio::test]
async fn reducer_ingests_external_agent_config_import_completed_fact() {
let mut reducer = AnalyticsReducer::default();
let mut events = Vec::new();
reducer
.ingest(
AnalyticsFact::Custom(CustomAnalyticsFact::ExternalAgentConfigImportCompleted(
ExternalAgentConfigImportCompletedInput {
import_id: "import-1".to_string(),
source: "app_server".to_string(),
item_type: "PLUGINS".to_string(),
success_count: 2,
failed_count: 1,
},
)),
&mut events,
)
.await;
let payload = serde_json::to_value(&events).expect("serialize events");
assert_eq!(
payload,
json!([{
"event_type": "codex_onboarding_external_agent_import_complete",
"event_params": {
"import_id": "import-1",
"source": "app_server",
"type": "PLUGINS",
"success_count": 2,
"failed_count": 1,
"product_client_id": originator().value,
}
}])
);
}
#[test]
fn external_agent_config_import_failure_event_serializes_expected_shape() {
let event = TrackEventRequest::ExternalAgentConfigImportFailure(
CodexOnboardingExternalAgentImportFailureEventRequest {
event_type: "codex_onboarding_external_agent_import_failure",
event_params: CodexOnboardingExternalAgentImportFailureMetadata {
import_id: "import-1".to_string(),
source: "app_server".to_string(),
item_type: "SESSIONS".to_string(),
failure_stage: "session_missing".to_string(),
error_type: "session_missing".to_string(),
product_client_id: Some(originator().value),
},
},
);
let payload = serde_json::to_value(&event).expect("serialize import failure event");
assert_eq!(
payload,
json!({
"event_type": "codex_onboarding_external_agent_import_failure",
"event_params": {
"import_id": "import-1",
"source": "app_server",
"type": "SESSIONS",
"failure_stage": "session_missing",
"error_type": "session_missing",
"product_client_id": originator().value,
}
})
);
}
#[tokio::test]
async fn reducer_ingests_external_agent_config_import_failure_fact() {
let mut reducer = AnalyticsReducer::default();
let mut events = Vec::new();
reducer
.ingest(
AnalyticsFact::Custom(CustomAnalyticsFact::ExternalAgentConfigImportFailure(
ExternalAgentConfigImportFailureInput {
import_id: "import-1".to_string(),
source: "app_server".to_string(),
item_type: "SESSIONS".to_string(),
failure_stage: "session_missing".to_string(),
error_type: "session_missing".to_string(),
},
)),
&mut events,
)
.await;
let payload = serde_json::to_value(&events).expect("serialize events");
assert_eq!(
payload,
json!([{
"event_type": "codex_onboarding_external_agent_import_failure",
"event_params": {
"import_id": "import-1",
"source": "app_server",
"type": "SESSIONS",
"failure_stage": "session_missing",
"error_type": "session_missing",
"product_client_id": originator().value,
}
}])
);
}
#[test]
fn turn_event_serializes_expected_shape() {
let event = TrackEventRequest::TurnEvent(Box::new(CodexTurnEventRequest {
+30
View File
@@ -11,8 +11,11 @@ use crate::facts::AppMentionedInput;
use crate::facts::AppUsedInput;
use crate::facts::CodexGoalEvent;
use crate::facts::CustomAnalyticsFact;
use crate::facts::ExternalAgentConfigImportCompletedInput;
use crate::facts::ExternalAgentConfigImportFailureInput;
use crate::facts::HookRunFact;
use crate::facts::HookRunInput;
use crate::facts::PluginInstallFailedInput;
use crate::facts::PluginState;
use crate::facts::PluginStateChangedInput;
use crate::facts::SkillInvocation;
@@ -343,6 +346,33 @@ impl AnalyticsEventsClient {
));
}
pub fn track_plugin_install_failed(&self, plugin: PluginTelemetryMetadata, error_type: String) {
self.record_fact(AnalyticsFact::Custom(
CustomAnalyticsFact::PluginInstallFailed(PluginInstallFailedInput {
plugin,
error_type,
}),
));
}
pub fn track_external_agent_config_import_completed(
&self,
input: ExternalAgentConfigImportCompletedInput,
) {
self.record_fact(AnalyticsFact::Custom(
CustomAnalyticsFact::ExternalAgentConfigImportCompleted(input),
));
}
pub fn track_external_agent_config_import_failure(
&self,
input: ExternalAgentConfigImportFailureInput,
) {
self.record_fact(AnalyticsFact::Custom(
CustomAnalyticsFact::ExternalAgentConfigImportFailure(input),
));
}
pub fn track_plugin_uninstalled(&self, plugin: PluginTelemetryMetadata) {
self.record_fact(AnalyticsFact::Custom(
CustomAnalyticsFact::PluginStateChanged(PluginStateChangedInput {
+1
View File
@@ -285,6 +285,7 @@ fn sample_thread(thread_id: &str) -> Thread {
model_provider: "openai".to_string(),
created_at: 1,
updated_at: 2,
recency_at: Some(2),
status: AppServerThreadStatus::Idle,
path: None,
cwd: test_path_buf("/tmp").abs(),
+50
View File
@@ -83,6 +83,9 @@ pub(crate) enum TrackEventRequest {
PluginUninstalled(CodexPluginEventRequest),
PluginEnabled(CodexPluginEventRequest),
PluginDisabled(CodexPluginEventRequest),
PluginInstallFailed(CodexPluginInstallFailedEventRequest),
ExternalAgentConfigImportCompleted(CodexOnboardingExternalAgentImportCompleteEventRequest),
ExternalAgentConfigImportFailure(CodexOnboardingExternalAgentImportFailureEventRequest),
}
impl TrackEventRequest {
@@ -954,6 +957,53 @@ pub(crate) struct CodexPluginEventRequest {
pub(crate) event_params: CodexPluginMetadata,
}
#[derive(Serialize)]
pub(crate) struct CodexPluginInstallFailedMetadata {
#[serde(flatten)]
pub(crate) plugin: CodexPluginMetadata,
pub(crate) error_type: String,
}
#[derive(Serialize)]
pub(crate) struct CodexPluginInstallFailedEventRequest {
pub(crate) event_type: &'static str,
pub(crate) event_params: CodexPluginInstallFailedMetadata,
}
#[derive(Serialize)]
pub(crate) struct CodexOnboardingExternalAgentImportCompleteMetadata {
pub(crate) import_id: String,
pub(crate) source: String,
#[serde(rename = "type")]
pub(crate) item_type: String,
pub(crate) success_count: usize,
pub(crate) failed_count: usize,
pub(crate) product_client_id: Option<String>,
}
#[derive(Serialize)]
pub(crate) struct CodexOnboardingExternalAgentImportCompleteEventRequest {
pub(crate) event_type: &'static str,
pub(crate) event_params: CodexOnboardingExternalAgentImportCompleteMetadata,
}
#[derive(Serialize)]
pub(crate) struct CodexOnboardingExternalAgentImportFailureMetadata {
pub(crate) import_id: String,
pub(crate) source: String,
#[serde(rename = "type")]
pub(crate) item_type: String,
pub(crate) failure_stage: String,
pub(crate) error_type: String,
pub(crate) product_client_id: Option<String>,
}
#[derive(Serialize)]
pub(crate) struct CodexOnboardingExternalAgentImportFailureEventRequest {
pub(crate) event_type: &'static str,
pub(crate) event_params: CodexOnboardingExternalAgentImportFailureMetadata,
}
#[derive(Serialize)]
pub(crate) struct CodexPluginUsedEventRequest {
pub(crate) event_type: &'static str,
+24
View File
@@ -504,6 +504,9 @@ pub(crate) enum CustomAnalyticsFact {
HookRun(HookRunInput),
PluginUsed(PluginUsedInput),
PluginStateChanged(PluginStateChangedInput),
PluginInstallFailed(PluginInstallFailedInput),
ExternalAgentConfigImportCompleted(ExternalAgentConfigImportCompletedInput),
ExternalAgentConfigImportFailure(ExternalAgentConfigImportFailureInput),
}
pub(crate) struct SkillInvokedInput {
@@ -542,6 +545,27 @@ pub(crate) struct PluginStateChangedInput {
pub state: PluginState,
}
pub(crate) struct PluginInstallFailedInput {
pub plugin: PluginTelemetryMetadata,
pub error_type: String,
}
pub struct ExternalAgentConfigImportCompletedInput {
pub import_id: String,
pub source: String,
pub item_type: String,
pub success_count: usize,
pub failed_count: usize,
}
pub struct ExternalAgentConfigImportFailureInput {
pub import_id: String,
pub source: String,
pub item_type: String,
pub failure_stage: String,
pub error_type: String,
}
#[derive(Clone, Copy)]
pub(crate) enum PluginState {
Installed,
+2
View File
@@ -36,6 +36,8 @@ pub use facts::CompactionReason;
pub use facts::CompactionStatus;
pub use facts::CompactionStrategy;
pub use facts::CompactionTrigger;
pub use facts::ExternalAgentConfigImportCompletedInput;
pub use facts::ExternalAgentConfigImportFailureInput;
pub use facts::GoalEventKind;
pub use facts::HookRunFact;
pub use facts::InputError;
+75
View File
@@ -21,7 +21,13 @@ use crate::events::CodexImageGenerationEventParams;
use crate::events::CodexImageGenerationEventRequest;
use crate::events::CodexMcpToolCallEventParams;
use crate::events::CodexMcpToolCallEventRequest;
use crate::events::CodexOnboardingExternalAgentImportCompleteEventRequest;
use crate::events::CodexOnboardingExternalAgentImportCompleteMetadata;
use crate::events::CodexOnboardingExternalAgentImportFailureEventRequest;
use crate::events::CodexOnboardingExternalAgentImportFailureMetadata;
use crate::events::CodexPluginEventRequest;
use crate::events::CodexPluginInstallFailedEventRequest;
use crate::events::CodexPluginInstallFailedMetadata;
use crate::events::CodexPluginUsedEventRequest;
use crate::events::CodexReviewEventParams;
use crate::events::CodexReviewEventRequest;
@@ -66,7 +72,10 @@ use crate::facts::AppUsedInput;
use crate::facts::CodexCompactionEvent;
use crate::facts::CodexGoalEvent;
use crate::facts::CustomAnalyticsFact;
use crate::facts::ExternalAgentConfigImportCompletedInput;
use crate::facts::ExternalAgentConfigImportFailureInput;
use crate::facts::HookRunInput;
use crate::facts::PluginInstallFailedInput;
use crate::facts::PluginState;
use crate::facts::PluginStateChangedInput;
use crate::facts::PluginUsedInput;
@@ -511,6 +520,15 @@ impl AnalyticsReducer {
CustomAnalyticsFact::PluginStateChanged(input) => {
self.ingest_plugin_state_changed(input, out);
}
CustomAnalyticsFact::PluginInstallFailed(input) => {
self.ingest_plugin_install_failed(input, out);
}
CustomAnalyticsFact::ExternalAgentConfigImportCompleted(input) => {
self.ingest_external_agent_config_import_completed(input, out);
}
CustomAnalyticsFact::ExternalAgentConfigImportFailure(input) => {
self.ingest_external_agent_config_import_failure(input, out);
}
},
}
}
@@ -775,6 +793,63 @@ impl AnalyticsReducer {
});
}
fn ingest_plugin_install_failed(
&mut self,
input: PluginInstallFailedInput,
out: &mut Vec<TrackEventRequest>,
) {
let PluginInstallFailedInput { plugin, error_type } = input;
out.push(TrackEventRequest::PluginInstallFailed(
CodexPluginInstallFailedEventRequest {
event_type: "codex_plugin_install_failed",
event_params: CodexPluginInstallFailedMetadata {
plugin: codex_plugin_metadata(plugin),
error_type,
},
},
));
}
fn ingest_external_agent_config_import_completed(
&mut self,
input: ExternalAgentConfigImportCompletedInput,
out: &mut Vec<TrackEventRequest>,
) {
out.push(TrackEventRequest::ExternalAgentConfigImportCompleted(
CodexOnboardingExternalAgentImportCompleteEventRequest {
event_type: "codex_onboarding_external_agent_import_complete",
event_params: CodexOnboardingExternalAgentImportCompleteMetadata {
import_id: input.import_id,
source: input.source,
item_type: input.item_type,
success_count: input.success_count,
failed_count: input.failed_count,
product_client_id: Some(originator().value),
},
},
));
}
fn ingest_external_agent_config_import_failure(
&mut self,
input: ExternalAgentConfigImportFailureInput,
out: &mut Vec<TrackEventRequest>,
) {
out.push(TrackEventRequest::ExternalAgentConfigImportFailure(
CodexOnboardingExternalAgentImportFailureEventRequest {
event_type: "codex_onboarding_external_agent_import_failure",
event_params: CodexOnboardingExternalAgentImportFailureMetadata {
import_id: input.import_id,
source: input.source,
item_type: input.item_type,
failure_stage: input.failure_stage,
error_type: input.error_type,
product_client_id: Some(originator().value),
},
},
));
}
async fn ingest_response(
&mut self,
connection_id: u64,
+27 -49
View File
@@ -643,7 +643,8 @@
"ConversationTextRole": {
"enum": [
"user",
"developer"
"developer",
"assistant"
],
"type": "string"
},
@@ -800,7 +801,7 @@
]
},
"includeHome": {
"description": "If true, include detection under the user's home (~/.claude, ~/.codex, etc.).",
"description": "If true, include detection under the user's home directory.",
"type": "boolean"
}
},
@@ -813,6 +814,13 @@
"$ref": "#/definitions/ExternalAgentConfigMigrationItem"
},
"type": "array"
},
"source": {
"description": "Source product that produced the migration items. Missing means unspecified.",
"type": [
"string",
"null"
]
}
},
"required": [
@@ -1303,6 +1311,9 @@
],
"type": "object"
},
"LegacyAppPathString": {
"type": "string"
},
"ListMcpServerStatusParams": {
"properties": {
"cursor": {
@@ -2279,13 +2290,6 @@
},
"type": "array"
},
"id": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
@@ -2427,14 +2431,6 @@
"null"
]
},
"id": {
"description": "Legacy id field retained for compatibility with older payloads.",
"type": [
"string",
"null"
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
@@ -2472,13 +2468,6 @@
"call_id": {
"type": "string"
},
"id": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
@@ -2527,13 +2516,6 @@
"execution": {
"type": "string"
},
"id": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
@@ -2605,13 +2587,6 @@
"call_id": {
"type": "string"
},
"id": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"input": {
"type": "string"
},
@@ -2748,13 +2723,6 @@
}
]
},
"id": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
@@ -2788,7 +2756,11 @@
{
"properties": {
"id": {
"type": "string"
"description": "Existing provider ID retained on serialized history for compatibility.",
"type": [
"string",
"null"
]
},
"metadata": {
"anyOf": [
@@ -2821,7 +2793,6 @@
}
},
"required": [
"id",
"result",
"status",
"type"
@@ -2937,6 +2908,12 @@
},
"ResponseItemMetadata": {
"properties": {
"source_call_id": {
"type": [
"string",
"null"
]
},
"turn_id": {
"type": [
"string",
@@ -4155,7 +4132,8 @@
"ThreadSortKey": {
"enum": [
"created_at",
"updated_at"
"updated_at",
"recency_at"
],
"type": "string"
},
@@ -4331,7 +4309,7 @@
"TurnEnvironmentParams": {
"properties": {
"cwd": {
"$ref": "#/definitions/AbsolutePathBuf"
"$ref": "#/definitions/LegacyAppPathString"
},
"environmentId": {
"type": "string"
@@ -27,7 +27,7 @@
"read": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -37,7 +37,7 @@
"write": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -84,9 +84,6 @@
},
"type": "object"
},
"ApiPathString": {
"type": "string"
},
"CommandAction": {
"oneOf": [
{
@@ -289,7 +286,7 @@
{
"properties": {
"path": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": {
"enum": [
@@ -472,6 +469,9 @@
}
]
},
"LegacyAppPathString": {
"type": "string"
},
"NetworkApprovalContext": {
"properties": {
"host": {
@@ -547,7 +547,7 @@
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
"$ref": "#/definitions/LegacyAppPathString"
},
{
"type": "null"
@@ -555,6 +555,14 @@
],
"description": "The command's working directory."
},
"environmentId": {
"default": null,
"description": "Environment in which the command will run.",
"type": [
"string",
"null"
]
},
"itemId": {
"type": "string"
},
@@ -27,7 +27,7 @@
"read": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -37,7 +37,7 @@
"write": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -58,9 +58,6 @@
},
"type": "object"
},
"ApiPathString": {
"type": "string"
},
"FileSystemAccessMode": {
"enum": [
"read",
@@ -74,7 +71,7 @@
{
"properties": {
"path": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": {
"enum": [
@@ -257,6 +254,9 @@
}
]
},
"LegacyAppPathString": {
"type": "string"
},
"RequestPermissionProfile": {
"additionalProperties": false,
"properties": {
@@ -23,7 +23,7 @@
"read": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -33,7 +33,7 @@
"write": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -54,9 +54,6 @@
},
"type": "object"
},
"ApiPathString": {
"type": "string"
},
"FileSystemAccessMode": {
"enum": [
"read",
@@ -70,7 +67,7 @@
{
"properties": {
"path": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": {
"enum": [
@@ -278,6 +275,9 @@
},
"type": "object"
},
"LegacyAppPathString": {
"type": "string"
},
"PermissionGrantScope": {
"enum": [
"turn",
@@ -107,7 +107,7 @@
"read": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -117,7 +117,7 @@
"write": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -164,9 +164,6 @@
"AgentPath": {
"type": "string"
},
"ApiPathString": {
"type": "string"
},
"AppBranding": {
"description": "EXPERIMENTAL - app metadata returned by app-list APIs.",
"properties": {
@@ -1345,7 +1342,7 @@
{
"properties": {
"path": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": {
"enum": [
@@ -2339,6 +2336,9 @@
],
"type": "object"
},
"LegacyAppPathString": {
"type": "string"
},
"McpServerOauthLoginCompletedNotification": {
"properties": {
"error": {
@@ -3582,6 +3582,14 @@
"description": "Usually the first user message in the thread, if available.",
"type": "string"
},
"recencyAt": {
"description": "Unix timestamp (in seconds) used for thread recency ordering.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"sessionId": {
"description": "Session id shared by threads that belong to the same session tree.",
"type": "string"
@@ -3964,7 +3972,7 @@
"cwd": {
"allOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
"$ref": "#/definitions/LegacyAppPathString"
}
],
"description": "The command's working directory."
+15 -7
View File
@@ -27,7 +27,7 @@
"read": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -37,7 +37,7 @@
"write": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -84,9 +84,6 @@
},
"type": "object"
},
"ApiPathString": {
"type": "string"
},
"ApplyPatchApprovalParams": {
"properties": {
"callId": {
@@ -374,7 +371,7 @@
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
"$ref": "#/definitions/LegacyAppPathString"
},
{
"type": "null"
@@ -382,6 +379,14 @@
],
"description": "The command's working directory."
},
"environmentId": {
"default": null,
"description": "Environment in which the command will run.",
"type": [
"string",
"null"
]
},
"itemId": {
"type": "string"
},
@@ -643,7 +648,7 @@
{
"properties": {
"path": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": {
"enum": [
@@ -826,6 +831,9 @@
}
]
},
"LegacyAppPathString": {
"type": "string"
},
"McpElicitationArrayType": {
"enum": [
"array"
@@ -2378,7 +2378,7 @@
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/v2/AbsolutePathBuf"
"$ref": "#/definitions/v2/LegacyAppPathString"
},
{
"type": "null"
@@ -2386,6 +2386,14 @@
],
"description": "The command's working directory."
},
"environmentId": {
"default": null,
"description": "Environment in which the command will run.",
"type": [
"string",
"null"
]
},
"itemId": {
"type": "string"
},
@@ -6071,7 +6079,7 @@
"read": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/v2/ApiPathString"
"$ref": "#/definitions/v2/LegacyAppPathString"
},
"type": [
"array",
@@ -6081,7 +6089,7 @@
"write": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/v2/ApiPathString"
"$ref": "#/definitions/v2/LegacyAppPathString"
},
"type": [
"array",
@@ -6193,9 +6201,6 @@
},
"type": "object"
},
"ApiPathString": {
"type": "string"
},
"AppBranding": {
"description": "EXPERIMENTAL - app metadata returned by app-list APIs.",
"properties": {
@@ -6699,6 +6704,16 @@
}
]
},
"default_tools_approval_mode": {
"anyOf": [
{
"$ref": "#/definitions/v2/AppToolApproval"
},
{
"type": "null"
}
]
},
"destructive_enabled": {
"default": true,
"type": "boolean"
@@ -8692,7 +8707,8 @@
"ConversationTextRole": {
"enum": [
"user",
"developer"
"developer",
"assistant"
],
"type": "string"
},
@@ -9104,7 +9120,7 @@
]
},
"includeHome": {
"description": "If true, include detection under the user's home (~/.claude, ~/.codex, etc.).",
"description": "If true, include detection under the user's home directory.",
"type": "boolean"
}
},
@@ -9267,6 +9283,13 @@
"$ref": "#/definitions/v2/ExternalAgentConfigMigrationItem"
},
"type": "array"
},
"source": {
"description": "Source product that produced the migration items. Missing means unspecified.",
"type": [
"string",
"null"
]
}
},
"required": [
@@ -9505,7 +9528,7 @@
{
"properties": {
"path": {
"$ref": "#/definitions/v2/ApiPathString"
"$ref": "#/definitions/v2/LegacyAppPathString"
},
"type": {
"enum": [
@@ -11184,6 +11207,9 @@
"title": "ItemStartedNotification",
"type": "object"
},
"LegacyAppPathString": {
"type": "string"
},
"ListMcpServerStatusParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -14766,13 +14792,6 @@
},
"type": "array"
},
"id": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
@@ -14914,14 +14933,6 @@
"null"
]
},
"id": {
"description": "Legacy id field retained for compatibility with older payloads.",
"type": [
"string",
"null"
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
@@ -14959,13 +14970,6 @@
"call_id": {
"type": "string"
},
"id": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
@@ -15014,13 +15018,6 @@
"execution": {
"type": "string"
},
"id": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
@@ -15092,13 +15089,6 @@
"call_id": {
"type": "string"
},
"id": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"input": {
"type": "string"
},
@@ -15235,13 +15225,6 @@
}
]
},
"id": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
@@ -15275,7 +15258,11 @@
{
"properties": {
"id": {
"type": "string"
"description": "Existing provider ID retained on serialized history for compatibility.",
"type": [
"string",
"null"
]
},
"metadata": {
"anyOf": [
@@ -15308,7 +15295,6 @@
}
},
"required": [
"id",
"result",
"status",
"type"
@@ -15424,6 +15410,12 @@
},
"ResponseItemMetadata": {
"properties": {
"source_call_id": {
"type": [
"string",
"null"
]
},
"turn_id": {
"type": [
"string",
@@ -16621,6 +16613,14 @@
"description": "Usually the first user message in the thread, if available.",
"type": "string"
},
"recencyAt": {
"description": "Unix timestamp (in seconds) used for thread recency ordering.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"sessionId": {
"description": "Session id shared by threads that belong to the same session tree.",
"type": "string"
@@ -17392,7 +17392,7 @@
"cwd": {
"allOf": [
{
"$ref": "#/definitions/v2/AbsolutePathBuf"
"$ref": "#/definitions/v2/LegacyAppPathString"
}
],
"description": "The command's working directory."
@@ -18880,7 +18880,8 @@
"ThreadSortKey": {
"enum": [
"created_at",
"updated_at"
"updated_at",
"recency_at"
],
"type": "string"
},
@@ -19517,7 +19518,7 @@
"TurnEnvironmentParams": {
"properties": {
"cwd": {
"$ref": "#/definitions/v2/AbsolutePathBuf"
"$ref": "#/definitions/v2/LegacyAppPathString"
},
"environmentId": {
"type": "string"
@@ -273,7 +273,7 @@
"read": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -283,7 +283,7 @@
"write": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -395,9 +395,6 @@
},
"type": "object"
},
"ApiPathString": {
"type": "string"
},
"AppBranding": {
"description": "EXPERIMENTAL - app metadata returned by app-list APIs.",
"properties": {
@@ -901,6 +898,16 @@
}
]
},
"default_tools_approval_mode": {
"anyOf": [
{
"$ref": "#/definitions/AppToolApproval"
},
{
"type": "null"
}
]
},
"destructive_enabled": {
"default": true,
"type": "boolean"
@@ -4985,7 +4992,8 @@
"ConversationTextRole": {
"enum": [
"user",
"developer"
"developer",
"assistant"
],
"type": "string"
},
@@ -5397,7 +5405,7 @@
]
},
"includeHome": {
"description": "If true, include detection under the user's home (~/.claude, ~/.codex, etc.).",
"description": "If true, include detection under the user's home directory.",
"type": "boolean"
}
},
@@ -5560,6 +5568,13 @@
"$ref": "#/definitions/ExternalAgentConfigMigrationItem"
},
"type": "array"
},
"source": {
"description": "Source product that produced the migration items. Missing means unspecified.",
"type": [
"string",
"null"
]
}
},
"required": [
@@ -5798,7 +5813,7 @@
{
"properties": {
"path": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": {
"enum": [
@@ -7637,6 +7652,9 @@
"title": "ItemStartedNotification",
"type": "object"
},
"LegacyAppPathString": {
"type": "string"
},
"ListMcpServerStatusParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -11219,13 +11237,6 @@
},
"type": "array"
},
"id": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
@@ -11367,14 +11378,6 @@
"null"
]
},
"id": {
"description": "Legacy id field retained for compatibility with older payloads.",
"type": [
"string",
"null"
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
@@ -11412,13 +11415,6 @@
"call_id": {
"type": "string"
},
"id": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
@@ -11467,13 +11463,6 @@
"execution": {
"type": "string"
},
"id": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
@@ -11545,13 +11534,6 @@
"call_id": {
"type": "string"
},
"id": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"input": {
"type": "string"
},
@@ -11688,13 +11670,6 @@
}
]
},
"id": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
@@ -11728,7 +11703,11 @@
{
"properties": {
"id": {
"type": "string"
"description": "Existing provider ID retained on serialized history for compatibility.",
"type": [
"string",
"null"
]
},
"metadata": {
"anyOf": [
@@ -11761,7 +11740,6 @@
}
},
"required": [
"id",
"result",
"status",
"type"
@@ -11877,6 +11855,12 @@
},
"ResponseItemMetadata": {
"properties": {
"source_call_id": {
"type": [
"string",
"null"
]
},
"turn_id": {
"type": [
"string",
@@ -14429,6 +14413,14 @@
"description": "Usually the first user message in the thread, if available.",
"type": "string"
},
"recencyAt": {
"description": "Unix timestamp (in seconds) used for thread recency ordering.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"sessionId": {
"description": "Session id shared by threads that belong to the same session tree.",
"type": "string"
@@ -15200,7 +15192,7 @@
"cwd": {
"allOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
"$ref": "#/definitions/LegacyAppPathString"
}
],
"description": "The command's working directory."
@@ -16688,7 +16680,8 @@
"ThreadSortKey": {
"enum": [
"created_at",
"updated_at"
"updated_at",
"recency_at"
],
"type": "string"
},
@@ -17325,7 +17318,7 @@
"TurnEnvironmentParams": {
"properties": {
"cwd": {
"$ref": "#/definitions/AbsolutePathBuf"
"$ref": "#/definitions/LegacyAppPathString"
},
"environmentId": {
"type": "string"
@@ -143,6 +143,16 @@
}
]
},
"default_tools_approval_mode": {
"anyOf": [
{
"$ref": "#/definitions/AppToolApproval"
},
{
"type": "null"
}
]
},
"destructive_enabled": {
"default": true,
"type": "boolean"
@@ -12,7 +12,7 @@
]
},
"includeHome": {
"description": "If true, include detection under the user's home (~/.claude, ~/.codex, etc.).",
"description": "If true, include detection under the user's home directory.",
"type": "boolean"
}
},
@@ -184,6 +184,13 @@
"$ref": "#/definitions/ExternalAgentConfigMigrationItem"
},
"type": "array"
},
"source": {
"description": "Source product that produced the migration items. Missing means unspecified.",
"type": [
"string",
"null"
]
}
},
"required": [
@@ -294,6 +294,9 @@
],
"type": "string"
},
"LegacyAppPathString": {
"type": "string"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -687,7 +690,7 @@
"cwd": {
"allOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
"$ref": "#/definitions/LegacyAppPathString"
}
],
"description": "The command's working directory."
@@ -27,7 +27,7 @@
"read": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -37,7 +37,7 @@
"write": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -58,9 +58,6 @@
},
"type": "object"
},
"ApiPathString": {
"type": "string"
},
"AutoReviewDecisionSource": {
"description": "[UNSTABLE] Source that produced a terminal approval auto-review decision.",
"enum": [
@@ -81,7 +78,7 @@
{
"properties": {
"path": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": {
"enum": [
@@ -536,6 +533,9 @@
],
"type": "string"
},
"LegacyAppPathString": {
"type": "string"
},
"NetworkApprovalProtocol": {
"enum": [
"http",
@@ -27,7 +27,7 @@
"read": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -37,7 +37,7 @@
"write": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -58,9 +58,6 @@
},
"type": "object"
},
"ApiPathString": {
"type": "string"
},
"FileSystemAccessMode": {
"enum": [
"read",
@@ -74,7 +71,7 @@
{
"properties": {
"path": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": {
"enum": [
@@ -529,6 +526,9 @@
],
"type": "string"
},
"LegacyAppPathString": {
"type": "string"
},
"NetworkApprovalProtocol": {
"enum": [
"http",
@@ -294,6 +294,9 @@
],
"type": "string"
},
"LegacyAppPathString": {
"type": "string"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -687,7 +690,7 @@
"cwd": {
"allOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
"$ref": "#/definitions/LegacyAppPathString"
}
],
"description": "The command's working directory."
@@ -377,13 +377,6 @@
},
"type": "array"
},
"id": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
@@ -525,14 +518,6 @@
"null"
]
},
"id": {
"description": "Legacy id field retained for compatibility with older payloads.",
"type": [
"string",
"null"
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
@@ -570,13 +555,6 @@
"call_id": {
"type": "string"
},
"id": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
@@ -625,13 +603,6 @@
"execution": {
"type": "string"
},
"id": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
@@ -703,13 +674,6 @@
"call_id": {
"type": "string"
},
"id": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"input": {
"type": "string"
},
@@ -846,13 +810,6 @@
}
]
},
"id": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
@@ -886,7 +843,11 @@
{
"properties": {
"id": {
"type": "string"
"description": "Existing provider ID retained on serialized history for compatibility.",
"type": [
"string",
"null"
]
},
"metadata": {
"anyOf": [
@@ -919,7 +880,6 @@
}
},
"required": [
"id",
"result",
"status",
"type"
@@ -1035,6 +995,12 @@
},
"ResponseItemMetadata": {
"properties": {
"source_call_id": {
"type": [
"string",
"null"
]
},
"turn_id": {
"type": [
"string",
@@ -431,6 +431,9 @@
],
"type": "string"
},
"LegacyAppPathString": {
"type": "string"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -831,7 +834,7 @@
"cwd": {
"allOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
"$ref": "#/definitions/LegacyAppPathString"
}
],
"description": "The command's working directory."
@@ -536,6 +536,9 @@
],
"type": "string"
},
"LegacyAppPathString": {
"type": "string"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -1055,6 +1058,14 @@
"description": "Usually the first user message in the thread, if available.",
"type": "string"
},
"recencyAt": {
"description": "Unix timestamp (in seconds) used for thread recency ordering.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"sessionId": {
"description": "Session id shared by threads that belong to the same session tree.",
"type": "string"
@@ -1315,7 +1326,7 @@
"cwd": {
"allOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
"$ref": "#/definitions/LegacyAppPathString"
}
],
"description": "The command's working directory."
@@ -24,7 +24,8 @@
"ThreadSortKey": {
"enum": [
"created_at",
"updated_at"
"updated_at",
"recency_at"
],
"type": "string"
},
@@ -457,6 +457,9 @@
],
"type": "string"
},
"LegacyAppPathString": {
"type": "string"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -870,6 +873,14 @@
"description": "Usually the first user message in the thread, if available.",
"type": "string"
},
"recencyAt": {
"description": "Unix timestamp (in seconds) used for thread recency ordering.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"sessionId": {
"description": "Session id shared by threads that belong to the same session tree.",
"type": "string"
@@ -1130,7 +1141,7 @@
"cwd": {
"allOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
"$ref": "#/definitions/LegacyAppPathString"
}
],
"description": "The command's working directory."
@@ -457,6 +457,9 @@
],
"type": "string"
},
"LegacyAppPathString": {
"type": "string"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -870,6 +873,14 @@
"description": "Usually the first user message in the thread, if available.",
"type": "string"
},
"recencyAt": {
"description": "Unix timestamp (in seconds) used for thread recency ordering.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"sessionId": {
"description": "Session id shared by threads that belong to the same session tree.",
"type": "string"
@@ -1130,7 +1141,7 @@
"cwd": {
"allOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
"$ref": "#/definitions/LegacyAppPathString"
}
],
"description": "The command's working directory."
@@ -457,6 +457,9 @@
],
"type": "string"
},
"LegacyAppPathString": {
"type": "string"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -870,6 +873,14 @@
"description": "Usually the first user message in the thread, if available.",
"type": "string"
},
"recencyAt": {
"description": "Unix timestamp (in seconds) used for thread recency ordering.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"sessionId": {
"description": "Session id shared by threads that belong to the same session tree.",
"type": "string"
@@ -1130,7 +1141,7 @@
"cwd": {
"allOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
"$ref": "#/definitions/LegacyAppPathString"
}
],
"description": "The command's working directory."
@@ -448,13 +448,6 @@
},
"type": "array"
},
"id": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
@@ -596,14 +589,6 @@
"null"
]
},
"id": {
"description": "Legacy id field retained for compatibility with older payloads.",
"type": [
"string",
"null"
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
@@ -641,13 +626,6 @@
"call_id": {
"type": "string"
},
"id": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
@@ -696,13 +674,6 @@
"execution": {
"type": "string"
},
"id": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
@@ -774,13 +745,6 @@
"call_id": {
"type": "string"
},
"id": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"input": {
"type": "string"
},
@@ -917,13 +881,6 @@
}
]
},
"id": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
@@ -957,7 +914,11 @@
{
"properties": {
"id": {
"type": "string"
"description": "Existing provider ID retained on serialized history for compatibility.",
"type": [
"string",
"null"
]
},
"metadata": {
"anyOf": [
@@ -990,7 +951,6 @@
}
},
"required": [
"id",
"result",
"status",
"type"
@@ -1106,6 +1066,12 @@
},
"ResponseItemMetadata": {
"properties": {
"source_call_id": {
"type": [
"string",
"null"
]
},
"turn_id": {
"type": [
"string",
@@ -536,6 +536,9 @@
],
"type": "string"
},
"LegacyAppPathString": {
"type": "string"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -1055,6 +1058,14 @@
"description": "Usually the first user message in the thread, if available.",
"type": "string"
},
"recencyAt": {
"description": "Unix timestamp (in seconds) used for thread recency ordering.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"sessionId": {
"description": "Session id shared by threads that belong to the same session tree.",
"type": "string"
@@ -1315,7 +1326,7 @@
"cwd": {
"allOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
"$ref": "#/definitions/LegacyAppPathString"
}
],
"description": "The command's working directory."
@@ -457,6 +457,9 @@
],
"type": "string"
},
"LegacyAppPathString": {
"type": "string"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -870,6 +873,14 @@
"description": "Usually the first user message in the thread, if available.",
"type": "string"
},
"recencyAt": {
"description": "Unix timestamp (in seconds) used for thread recency ordering.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"sessionId": {
"description": "Session id shared by threads that belong to the same session tree.",
"type": "string"
@@ -1130,7 +1141,7 @@
"cwd": {
"allOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
"$ref": "#/definitions/LegacyAppPathString"
}
],
"description": "The command's working directory."
@@ -191,6 +191,9 @@
}
]
},
"LegacyAppPathString": {
"type": "string"
},
"Personality": {
"enum": [
"none",
@@ -242,7 +245,7 @@
"TurnEnvironmentParams": {
"properties": {
"cwd": {
"$ref": "#/definitions/AbsolutePathBuf"
"$ref": "#/definitions/LegacyAppPathString"
},
"environmentId": {
"type": "string"
@@ -536,6 +536,9 @@
],
"type": "string"
},
"LegacyAppPathString": {
"type": "string"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -1055,6 +1058,14 @@
"description": "Usually the first user message in the thread, if available.",
"type": "string"
},
"recencyAt": {
"description": "Unix timestamp (in seconds) used for thread recency ordering.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"sessionId": {
"description": "Session id shared by threads that belong to the same session tree.",
"type": "string"
@@ -1315,7 +1326,7 @@
"cwd": {
"allOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
"$ref": "#/definitions/LegacyAppPathString"
}
],
"description": "The command's working directory."
@@ -457,6 +457,9 @@
],
"type": "string"
},
"LegacyAppPathString": {
"type": "string"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -870,6 +873,14 @@
"description": "Usually the first user message in the thread, if available.",
"type": "string"
},
"recencyAt": {
"description": "Unix timestamp (in seconds) used for thread recency ordering.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"sessionId": {
"description": "Session id shared by threads that belong to the same session tree.",
"type": "string"
@@ -1130,7 +1141,7 @@
"cwd": {
"allOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
"$ref": "#/definitions/LegacyAppPathString"
}
],
"description": "The command's working directory."
@@ -457,6 +457,9 @@
],
"type": "string"
},
"LegacyAppPathString": {
"type": "string"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -870,6 +873,14 @@
"description": "Usually the first user message in the thread, if available.",
"type": "string"
},
"recencyAt": {
"description": "Unix timestamp (in seconds) used for thread recency ordering.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"sessionId": {
"description": "Session id shared by threads that belong to the same session tree.",
"type": "string"
@@ -1130,7 +1141,7 @@
"cwd": {
"allOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
"$ref": "#/definitions/LegacyAppPathString"
}
],
"description": "The command's working directory."
@@ -431,6 +431,9 @@
],
"type": "string"
},
"LegacyAppPathString": {
"type": "string"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -831,7 +834,7 @@
"cwd": {
"allOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
"$ref": "#/definitions/LegacyAppPathString"
}
],
"description": "The command's working directory."
@@ -130,6 +130,9 @@
],
"type": "string"
},
"LegacyAppPathString": {
"type": "string"
},
"ModeKind": {
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
@@ -331,7 +334,7 @@
"TurnEnvironmentParams": {
"properties": {
"cwd": {
"$ref": "#/definitions/AbsolutePathBuf"
"$ref": "#/definitions/LegacyAppPathString"
},
"environmentId": {
"type": "string"
@@ -431,6 +431,9 @@
],
"type": "string"
},
"LegacyAppPathString": {
"type": "string"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -831,7 +834,7 @@
"cwd": {
"allOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
"$ref": "#/definitions/LegacyAppPathString"
}
],
"description": "The command's working directory."
@@ -431,6 +431,9 @@
],
"type": "string"
},
"LegacyAppPathString": {
"type": "string"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -831,7 +834,7 @@
"cwd": {
"allOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
"$ref": "#/definitions/LegacyAppPathString"
}
],
"description": "The command's working directory."
@@ -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 ConversationTextRole = "user" | "developer";
export type ConversationTextRole = "user" | "developer" | "assistant";
@@ -23,4 +23,4 @@
* [`AbsolutePathBuf`]. Relative path text remains valid until an operation
* such as [`Self::to_path_uri`] requires an absolute path.
*/
export type ApiPathString = string;
export type LegacyAppPathString = string;
@@ -16,4 +16,8 @@ export type ResponseItem = { "type": "message", role: string, content: Array<Con
/**
* Set when using the Responses API.
*/
call_id: string | null, status: LocalShellStatus, action: LocalShellAction, metadata?: ResponseItemMetadata, } | { "type": "function_call", name: string, namespace?: string, arguments: string, call_id: string, metadata?: ResponseItemMetadata, } | { "type": "tool_search_call", call_id: string | null, status?: string, execution: string, arguments: unknown, metadata?: ResponseItemMetadata, } | { "type": "function_call_output", call_id: string, output: FunctionCallOutputBody, metadata?: ResponseItemMetadata, } | { "type": "custom_tool_call", status?: string, call_id: string, name: string, input: string, metadata?: ResponseItemMetadata, } | { "type": "custom_tool_call_output", call_id: string, name?: string, output: FunctionCallOutputBody, metadata?: ResponseItemMetadata, } | { "type": "tool_search_output", call_id: string | null, status: string, execution: string, tools: unknown[], metadata?: ResponseItemMetadata, } | { "type": "web_search_call", status?: string, action?: WebSearchAction, metadata?: ResponseItemMetadata, } | { "type": "image_generation_call", id: string, status: string, revised_prompt?: string, result: string, metadata?: ResponseItemMetadata, } | { "type": "compaction", encrypted_content: string, metadata?: ResponseItemMetadata, } | { "type": "compaction_trigger", metadata?: ResponseItemMetadata, } | { "type": "context_compaction", encrypted_content?: string, metadata?: ResponseItemMetadata, } | { "type": "other" };
call_id: string | null, status: LocalShellStatus, action: LocalShellAction, metadata?: ResponseItemMetadata, } | { "type": "function_call", name: string, namespace?: string, arguments: string, call_id: string, metadata?: ResponseItemMetadata, } | { "type": "tool_search_call", call_id: string | null, status?: string, execution: string, arguments: unknown, metadata?: ResponseItemMetadata, } | { "type": "function_call_output", call_id: string, output: FunctionCallOutputBody, metadata?: ResponseItemMetadata, } | { "type": "custom_tool_call", status?: string, call_id: string, name: string, input: string, metadata?: ResponseItemMetadata, } | { "type": "custom_tool_call_output", call_id: string, name?: string, output: FunctionCallOutputBody, metadata?: ResponseItemMetadata, } | { "type": "tool_search_output", call_id: string | null, status: string, execution: string, tools: unknown[], metadata?: ResponseItemMetadata, } | { "type": "web_search_call", status?: string, action?: WebSearchAction, metadata?: ResponseItemMetadata, } | { "type": "image_generation_call",
/**
* Existing provider ID retained on serialized history for compatibility.
*/
id?: string, status: string, revised_prompt?: string, result: string, metadata?: ResponseItemMetadata, } | { "type": "compaction", encrypted_content: string, metadata?: ResponseItemMetadata, } | { "type": "compaction_trigger", metadata?: ResponseItemMetadata, } | { "type": "context_compaction", encrypted_content?: string, metadata?: ResponseItemMetadata, } | { "type": "other" };
@@ -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 ResponseItemMetadata = { turn_id?: string, };
export type ResponseItemMetadata = { turn_id?: string, source_call_id?: string, };
+1 -1
View File
@@ -4,7 +4,6 @@ export type { AbsolutePathBuf } from "./AbsolutePathBuf";
export type { AgentMessageInputContent } from "./AgentMessageInputContent";
export type { AgentPath } from "./AgentPath";
export type { AmazonBedrockCredentialSource } from "./AmazonBedrockCredentialSource";
export type { ApiPathString } from "./ApiPathString";
export type { ApplyPatchApprovalParams } from "./ApplyPatchApprovalParams";
export type { ApplyPatchApprovalResponse } from "./ApplyPatchApprovalResponse";
export type { AuthMode } from "./AuthMode";
@@ -43,6 +42,7 @@ export type { InitializeParams } from "./InitializeParams";
export type { InitializeResponse } from "./InitializeResponse";
export type { InputModality } from "./InputModality";
export type { InternalSessionSource } from "./InternalSessionSource";
export type { LegacyAppPathString } from "./LegacyAppPathString";
export type { LocalShellAction } from "./LocalShellAction";
export type { LocalShellExecAction } from "./LocalShellExecAction";
export type { LocalShellStatus } from "./LocalShellStatus";
@@ -1,15 +1,15 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ApiPathString } from "../ApiPathString";
import type { LegacyAppPathString } from "../LegacyAppPathString";
import type { FileSystemSandboxEntry } from "./FileSystemSandboxEntry";
export type AdditionalFileSystemPermissions = {
/**
* This will be removed in favor of `entries`.
*/
read: Array<ApiPathString> | null,
read: Array<LegacyAppPathString> | null,
/**
* This will be removed in favor of `entries`.
*/
write: Array<ApiPathString> | null, globScanMaxDepth?: number, entries?: Array<FileSystemSandboxEntry>, };
write: Array<LegacyAppPathString> | null, globScanMaxDepth?: number, entries?: Array<FileSystemSandboxEntry>, };
@@ -1,6 +1,7 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AppToolApproval } from "./AppToolApproval";
import type { ApprovalsReviewer } from "./ApprovalsReviewer";
export type AppsDefaultConfig = { enabled: boolean, approvals_reviewer: ApprovalsReviewer | null, destructive_enabled: boolean, open_world_enabled: boolean, };
export type AppsDefaultConfig = { enabled: boolean, approvals_reviewer: ApprovalsReviewer | null, destructive_enabled: boolean, open_world_enabled: boolean, default_tools_approval_mode: AppToolApproval | null, };
@@ -1,7 +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 { LegacyAppPathString } from "../LegacyAppPathString";
import type { CommandAction } from "./CommandAction";
import type { ExecPolicyAmendment } from "./ExecPolicyAmendment";
import type { NetworkApprovalContext } from "./NetworkApprovalContext";
@@ -20,6 +20,9 @@ startedAtMs: number, /**
* (a UUID) used to disambiguate routing.
*/
approvalId?: string | null, /**
* Environment in which the command will run.
*/
environmentId: string | null, /**
* Optional explanatory reason (e.g. request for network access).
*/
reason?: string | null, /**
@@ -31,7 +34,7 @@ networkApprovalContext?: NetworkApprovalContext | null, /**
command?: string | null, /**
* The command's working directory.
*/
cwd?: AbsolutePathBuf | null, /**
cwd?: LegacyAppPathString | null, /**
* Best-effort parsed command actions for friendly display.
*/
commandActions?: Array<CommandAction> | null, /**
@@ -4,7 +4,7 @@
export type ExternalAgentConfigDetectParams = {
/**
* If true, include detection under the user's home (~/.claude, ~/.codex, etc.).
* If true, include detection under the user's home directory.
*/
includeHome?: boolean,
/**
@@ -3,4 +3,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ExternalAgentConfigMigrationItem } from "./ExternalAgentConfigMigrationItem";
export type ExternalAgentConfigImportParams = { migrationItems: Array<ExternalAgentConfigMigrationItem>, };
export type ExternalAgentConfigImportParams = { migrationItems: Array<ExternalAgentConfigMigrationItem>,
/**
* Source product that produced the migration items. Missing means unspecified.
*/
source?: string | null, };
@@ -1,7 +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 { ApiPathString } from "../ApiPathString";
import type { LegacyAppPathString } from "../LegacyAppPathString";
import type { FileSystemSpecialPath } from "./FileSystemSpecialPath";
export type FileSystemPath = { "type": "path", path: ApiPathString, } | { "type": "glob_pattern", pattern: string, } | { "type": "special", value: FileSystemSpecialPath, };
export type FileSystemPath = { "type": "path", path: LegacyAppPathString, } | { "type": "glob_pattern", pattern: string, } | { "type": "special", value: FileSystemSpecialPath, };
@@ -41,6 +41,10 @@ createdAt: number,
* Unix timestamp (in seconds) when the thread was last updated.
*/
updatedAt: number,
/**
* Unix timestamp (in seconds) used for thread recency ordering.
*/
recencyAt: number | null,
/**
* Current runtime status for the thread.
*/
@@ -2,6 +2,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
import type { LegacyAppPathString } from "../LegacyAppPathString";
import type { MessagePhase } from "../MessagePhase";
import type { ReasoningEffort } from "../ReasoningEffort";
import type { JsonValue } from "../serde_json/JsonValue";
@@ -32,7 +33,7 @@ command: string,
/**
* The command's working directory.
*/
cwd: AbsolutePathBuf,
cwd: LegacyAppPathString,
/**
* Identifier for the underlying PTY process (when available).
*/
@@ -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 ThreadSortKey = "created_at" | "updated_at";
export type ThreadSortKey = "created_at" | "updated_at" | "recency_at";
@@ -1,6 +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 { AbsolutePathBuf } from "../AbsolutePathBuf";
import type { LegacyAppPathString } from "../LegacyAppPathString";
export type TurnEnvironmentParams = { environmentId: string, cwd: AbsolutePathBuf, };
export type TurnEnvironmentParams = { environmentId: string, cwd: LegacyAppPathString, };
@@ -2501,6 +2501,7 @@ mod tests {
model_provider: "openai".to_string(),
created_at: 1,
updated_at: 2,
recency_at: Some(3),
status: v2::ThreadStatus::Idle,
path: None,
cwd: cwd.clone(),
@@ -2544,6 +2545,7 @@ mod tests {
"modelProvider": "openai",
"createdAt": 1,
"updatedAt": 2,
"recencyAt": 3,
"status": {
"type": "idle"
},
@@ -3080,8 +3082,10 @@ mod tests {
request_id: RequestId::Integer(9),
params: v2::ThreadRealtimeStartParams {
architecture: Some(RealtimeConversationArchitecture::Avas),
client_managed_handoffs: Some(true),
codex_responses_as_items: None,
codex_response_item_prefix: None,
codex_response_handoff_prefix: Some("silent context".to_string()),
thread_id: "thr_123".to_string(),
model: Some("realtime-treatment-model".to_string()),
output_modality: RealtimeOutputModality::Audio,
@@ -3100,8 +3104,10 @@ mod tests {
"params": {
"architecture": "avas",
"threadId": "thr_123",
"clientManagedHandoffs": true,
"codexResponsesAsItems": null,
"codexResponseItemPrefix": null,
"codexResponseHandoffPrefix": "silent context",
"model": "realtime-treatment-model",
"outputModality": "audio",
"includeStartupContext": false,
@@ -3123,8 +3129,10 @@ mod tests {
request_id: RequestId::Integer(9),
params: v2::ThreadRealtimeStartParams {
architecture: None,
client_managed_handoffs: None,
codex_responses_as_items: None,
codex_response_item_prefix: None,
codex_response_handoff_prefix: None,
thread_id: "thr_123".to_string(),
model: None,
output_modality: RealtimeOutputModality::Audio,
@@ -3143,8 +3151,10 @@ mod tests {
"params": {
"architecture": null,
"threadId": "thr_123",
"clientManagedHandoffs": null,
"codexResponsesAsItems": null,
"codexResponseItemPrefix": null,
"codexResponseHandoffPrefix": null,
"model": null,
"outputModality": "audio",
"includeStartupContext": null,
@@ -3161,8 +3171,10 @@ mod tests {
request_id: RequestId::Integer(9),
params: v2::ThreadRealtimeStartParams {
architecture: None,
client_managed_handoffs: None,
codex_responses_as_items: None,
codex_response_item_prefix: None,
codex_response_handoff_prefix: None,
thread_id: "thr_123".to_string(),
model: None,
output_modality: RealtimeOutputModality::Audio,
@@ -3181,8 +3193,10 @@ mod tests {
"params": {
"architecture": null,
"threadId": "thr_123",
"clientManagedHandoffs": null,
"codexResponsesAsItems": null,
"codexResponseItemPrefix": null,
"codexResponseHandoffPrefix": null,
"model": null,
"outputModality": "audio",
"includeStartupContext": null,
@@ -3365,8 +3379,10 @@ mod tests {
request_id: RequestId::Integer(1),
params: v2::ThreadRealtimeStartParams {
architecture: None,
client_managed_handoffs: None,
codex_responses_as_items: None,
codex_response_item_prefix: None,
codex_response_handoff_prefix: None,
thread_id: "thr_123".to_string(),
model: None,
output_modality: RealtimeOutputModality::Audio,
@@ -3538,6 +3554,7 @@ mod tests {
item_id: "call_123".to_string(),
started_at_ms: 0,
approval_id: None,
environment_id: None,
reason: None,
network_approval_context: None,
command: Some("cat file".to_string()),
@@ -23,6 +23,7 @@ use crate::protocol::v2::PatchApplyStatus;
use crate::protocol::v2::PatchChangeKind;
use crate::protocol::v2::ThreadItem;
use codex_protocol::ThreadId;
use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
use codex_protocol::protocol::ExecApprovalRequestEvent;
use codex_protocol::protocol::ExecCommandBeginEvent;
@@ -34,8 +35,11 @@ use codex_protocol::protocol::PatchApplyBeginEvent;
use codex_protocol::protocol::PatchApplyEndEvent;
use codex_shell_command::parse_command::parse_command;
use codex_shell_command::parse_command::shlex_join;
use codex_utils_path_uri::PathConvention;
use codex_utils_path_uri::PathUri;
use std::collections::HashMap;
use std::path::PathBuf;
use tracing::warn;
pub fn build_file_change_approval_request_item(
payload: &ApplyPatchApprovalRequestEvent,
@@ -69,7 +73,7 @@ pub fn build_command_execution_approval_request_item(
ThreadItem::CommandExecution {
id: payload.call_id.clone(),
command: shlex_join(&payload.command),
cwd: payload.cwd.clone(),
cwd: payload.cwd.clone().into(),
process_id: None,
source: CommandExecutionSource::Agent,
status: CommandExecutionStatus::InProgress,
@@ -86,19 +90,15 @@ pub fn build_command_execution_approval_request_item(
}
pub fn build_command_execution_begin_item(payload: &ExecCommandBeginEvent) -> ThreadItem {
let command_actions = command_actions_for_path_uri(&payload.parsed_cmd, &payload.cwd);
ThreadItem::CommandExecution {
id: payload.call_id.clone(),
command: shlex_join(&payload.command),
cwd: payload.cwd.clone(),
cwd: payload.cwd.clone().into(),
process_id: payload.process_id.clone(),
source: payload.source.into(),
status: CommandExecutionStatus::InProgress,
command_actions: payload
.parsed_cmd
.iter()
.cloned()
.map(|parsed| CommandAction::from_core_with_cwd(parsed, &payload.cwd))
.collect(),
command_actions,
aggregated_output: None,
exit_code: None,
duration_ms: None,
@@ -112,26 +112,63 @@ pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> Thread
Some(payload.aggregated_output.clone())
};
let duration_ms = i64::try_from(payload.duration.as_millis()).unwrap_or(i64::MAX);
let command_actions = command_actions_for_path_uri(&payload.parsed_cmd, &payload.cwd);
ThreadItem::CommandExecution {
id: payload.call_id.clone(),
command: shlex_join(&payload.command),
cwd: payload.cwd.clone(),
cwd: payload.cwd.clone().into(),
process_id: payload.process_id.clone(),
source: payload.source.into(),
status: (&payload.status).into(),
command_actions: payload
.parsed_cmd
.iter()
.cloned()
.map(|parsed| CommandAction::from_core_with_cwd(parsed, &payload.cwd))
.collect(),
command_actions,
aggregated_output,
exit_code: Some(payload.exit_code),
duration_ms: Some(duration_ms),
}
}
fn command_actions_for_path_uri(parsed_cmd: &[ParsedCommand], cwd: &PathUri) -> Vec<CommandAction> {
// TODO(anp): Carry PathUri into CommandAction so foreign Read actions retain resolved paths.
// Until then, omit those actions rather than project a foreign cwd onto the host.
let native_cwd = if cwd.infer_path_convention() == Some(PathConvention::native()) {
cwd.to_abs_path().ok()
} else {
None
};
parsed_cmd
.iter()
.cloned()
.filter_map(|parsed| match parsed {
ParsedCommand::Read { cmd, name, path } => match native_cwd.as_ref() {
Some(native_cwd) => Some(CommandAction::Read {
command: cmd,
name,
path: native_cwd.join(path),
}),
None => {
warn!(
command = cmd,
%cwd,
"omitting read command action whose path cannot be resolved against a foreign cwd"
);
None
}
},
ParsedCommand::ListFiles { cmd, path } => {
Some(CommandAction::ListFiles { command: cmd, path })
}
ParsedCommand::Search { cmd, query, path } => Some(CommandAction::Search {
command: cmd,
query,
path,
}),
ParsedCommand::Unknown { cmd } => Some(CommandAction::Unknown { command: cmd }),
})
.collect()
}
/// Build a guardian-derived [`ThreadItem`].
///
/// Currently this only synthesizes [`ThreadItem::CommandExecution`] for
@@ -150,7 +187,7 @@ pub fn build_item_from_guardian_event(
Some(ThreadItem::CommandExecution {
id: id.clone(),
command,
cwd: cwd.clone(),
cwd: cwd.clone().into(),
process_id: None,
source: CommandExecutionSource::Agent,
status,
@@ -186,7 +223,7 @@ pub fn build_item_from_guardian_event(
Some(ThreadItem::CommandExecution {
id: id.clone(),
command,
cwd: cwd.clone(),
cwd: cwd.clone().into(),
process_id: None,
source: CommandExecutionSource::Agent,
status,
@@ -315,3 +352,7 @@ fn format_file_change_diff(change: &FileChange) -> String {
}
}
}
#[cfg(test)]
#[path = "item_builders_tests.rs"]
mod tests;
@@ -0,0 +1,41 @@
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn foreign_read_is_omitted_without_dropping_other_command_actions() {
#[cfg(windows)]
let cwd = PathUri::parse("file:///usr/local/src").expect("valid foreign POSIX cwd");
#[cfg(not(windows))]
let cwd = PathUri::parse("file:///C:/src").expect("valid foreign Windows cwd");
let parsed_cmd = vec![
ParsedCommand::Read {
cmd: "cat file.txt".to_string(),
name: "file.txt".to_string(),
path: PathBuf::from("file.txt"),
},
ParsedCommand::ListFiles {
cmd: "ls".to_string(),
path: Some("subdir".to_string()),
},
ParsedCommand::Search {
cmd: "rg needle".to_string(),
query: Some("needle".to_string()),
path: Some("src".to_string()),
},
];
assert_eq!(
command_actions_for_path_uri(&parsed_cmd, &cwd),
vec![
CommandAction::ListFiles {
command: "ls".to_string(),
path: Some("subdir".to_string()),
},
CommandAction::Search {
command: "rg needle".to_string(),
query: Some("needle".to_string()),
path: Some("src".to_string()),
},
]
);
}
@@ -2376,7 +2376,7 @@ mod tests {
turn_id: "turn-1".into(),
completed_at_ms: 0,
command: vec!["echo".into(), "hello world".into()],
cwd: test_path_buf("/tmp").abs(),
cwd: test_path_buf("/tmp").abs().into(),
parsed_cmd: vec![ParsedCommand::Unknown {
cmd: "echo hello world".into(),
}],
@@ -2427,7 +2427,7 @@ mod tests {
ThreadItem::CommandExecution {
id: "exec-1".into(),
command: "echo 'hello world'".into(),
cwd: test_path_buf("/tmp").abs(),
cwd: test_path_buf("/tmp").abs().into(),
process_id: Some("pid-1".into()),
source: CommandExecutionSource::Agent,
status: CommandExecutionStatus::Completed,
@@ -2616,7 +2616,7 @@ mod tests {
turn_id: "turn-1".into(),
completed_at_ms: 0,
command: vec!["ls".into()],
cwd: test_path_buf("/tmp").abs(),
cwd: test_path_buf("/tmp").abs().into(),
parsed_cmd: vec![ParsedCommand::Unknown { cmd: "ls".into() }],
source: ExecCommandSource::Agent,
interaction_input: None,
@@ -2658,7 +2658,7 @@ mod tests {
ThreadItem::CommandExecution {
id: "exec-declined".into(),
command: "ls".into(),
cwd: test_path_buf("/tmp").abs(),
cwd: test_path_buf("/tmp").abs().into(),
process_id: Some("pid-2".into()),
source: CommandExecutionSource::Agent,
status: CommandExecutionStatus::Declined,
@@ -2756,7 +2756,7 @@ mod tests {
ThreadItem::CommandExecution {
id: "guardian-exec".into(),
command: "rm -rf /tmp/guardian".into(),
cwd: test_path_buf("/tmp").abs(),
cwd: test_path_buf("/tmp").abs().into(),
process_id: None,
source: CommandExecutionSource::Agent,
status: CommandExecutionStatus::Declined,
@@ -2822,7 +2822,7 @@ mod tests {
ThreadItem::CommandExecution {
id: "guardian-execve".into(),
command: "/bin/rm -f /tmp/file.sqlite".into(),
cwd: test_path_buf("/tmp").abs(),
cwd: test_path_buf("/tmp").abs().into(),
process_id: None,
source: CommandExecutionSource::Agent,
status: CommandExecutionStatus::InProgress,
@@ -2882,7 +2882,7 @@ mod tests {
turn_id: "turn-a".into(),
completed_at_ms: 0,
command: vec!["echo".into(), "done".into()],
cwd: test_path_buf("/tmp").abs(),
cwd: test_path_buf("/tmp").abs().into(),
parsed_cmd: vec![ParsedCommand::Unknown {
cmd: "echo done".into(),
}],
@@ -2920,7 +2920,7 @@ mod tests {
ThreadItem::CommandExecution {
id: "exec-late".into(),
command: "echo done".into(),
cwd: test_path_buf("/tmp").abs(),
cwd: test_path_buf("/tmp").abs().into(),
process_id: Some("pid-42".into()),
source: CommandExecutionSource::Agent,
status: CommandExecutionStatus::Completed,
@@ -2980,7 +2980,7 @@ mod tests {
turn_id: "turn-missing".into(),
completed_at_ms: 0,
command: vec!["echo".into(), "done".into()],
cwd: test_path_buf("/tmp").abs(),
cwd: test_path_buf("/tmp").abs().into(),
parsed_cmd: vec![ParsedCommand::Unknown {
cmd: "echo done".into(),
}],
@@ -177,6 +177,7 @@ pub struct AppsDefaultConfig {
pub destructive_enabled: bool,
#[serde(default = "default_enabled")]
pub open_world_enabled: bool,
pub default_tools_approval_mode: Option<AppToolApproval>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -650,7 +651,7 @@ pub struct ExternalAgentConfigDetectResponse {
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ExternalAgentConfigDetectParams {
/// If true, include detection under the user's home (~/.claude, ~/.codex, etc.).
/// If true, include detection under the user's home directory.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub include_home: bool,
/// Zero or more working directories to include for repo-scoped detection.
@@ -663,6 +664,9 @@ pub struct ExternalAgentConfigDetectParams {
#[ts(export_to = "v2/")]
pub struct ExternalAgentConfigImportParams {
pub migration_items: Vec<ExternalAgentConfigMigrationItem>,
/// Source product that produced the migration items. Missing means unspecified.
#[ts(optional = nullable)]
pub source: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
@@ -31,6 +31,7 @@ use codex_protocol::protocol::PatchApplyStatus as CorePatchApplyStatus;
use codex_protocol::protocol::ReviewDecision as CoreReviewDecision;
use codex_protocol::protocol::SubAgentActivityKind as CoreSubAgentActivityKind;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::LegacyAppPathString;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
@@ -256,7 +257,7 @@ pub enum ThreadItem {
/// The command to be executed.
command: String,
/// The command's working directory.
cwd: AbsolutePathBuf,
cwd: LegacyAppPathString,
/// Identifier for the underlying PTY process (when available).
process_id: Option<String>,
#[serde(default)]
@@ -1321,6 +1322,9 @@ pub struct CommandExecutionRequestApprovalParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub approval_id: Option<String>,
/// Environment in which the command will run.
#[serde(default)]
pub environment_id: Option<String>,
/// Optional explanatory reason (e.g. request for network access).
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
@@ -1336,7 +1340,7 @@ pub struct CommandExecutionRequestApprovalParams {
/// The command's working directory.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub cwd: Option<AbsolutePathBuf>,
pub cwd: Option<LegacyAppPathString>,
/// Best-effort parsed command actions for friendly display.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
@@ -16,7 +16,7 @@ use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess;
use codex_protocol::request_permissions::PermissionGrantScope as CorePermissionGrantScope;
use codex_protocol::request_permissions::RequestPermissionProfile as CoreRequestPermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::ApiPathString;
use codex_utils_path_uri::LegacyAppPathString;
use codex_utils_path_uri::PathConvention;
use schemars::JsonSchema;
use serde::Deserialize;
@@ -57,9 +57,9 @@ impl From<CoreNetworkApprovalContext> for NetworkApprovalContext {
#[ts(export_to = "v2/")]
pub struct AdditionalFileSystemPermissions {
/// This will be removed in favor of `entries`.
pub read: Option<Vec<ApiPathString>>,
pub read: Option<Vec<LegacyAppPathString>>,
/// This will be removed in favor of `entries`.
pub write: Option<Vec<ApiPathString>>,
pub write: Option<Vec<LegacyAppPathString>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub glob_scan_max_depth: Option<NonZeroUsize>,
@@ -78,7 +78,7 @@ impl From<CoreFileSystemPermissions<AbsolutePathBuf>> for AdditionalFileSystemPe
if let Some(paths) = read.as_ref() {
entries.extend(paths.iter().map(|path| FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: ApiPathString::from_abs_path(path),
path: LegacyAppPathString::from_abs_path(path),
},
access: FileSystemAccessMode::Read,
}));
@@ -86,14 +86,24 @@ impl From<CoreFileSystemPermissions<AbsolutePathBuf>> for AdditionalFileSystemPe
if let Some(paths) = write.as_ref() {
entries.extend(paths.iter().map(|path| FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: ApiPathString::from_abs_path(path),
path: LegacyAppPathString::from_abs_path(path),
},
access: FileSystemAccessMode::Write,
}));
}
Self {
read: read.map(|paths| paths.iter().map(ApiPathString::from_abs_path).collect()),
write: write.map(|paths| paths.iter().map(ApiPathString::from_abs_path).collect()),
read: read.map(|paths| {
paths
.iter()
.map(LegacyAppPathString::from_abs_path)
.collect()
}),
write: write.map(|paths| {
paths
.iter()
.map(LegacyAppPathString::from_abs_path)
.collect()
}),
glob_scan_max_depth: None,
entries: Some(entries),
}
@@ -276,7 +286,7 @@ impl From<FileSystemSpecialPath> for CoreFileSystemSpecialPath {
#[ts(export_to = "v2/")]
// TODO(anp): Rename this type to distinguish it from the generic protocol FileSystemPath.
pub enum FileSystemPath {
Path { path: ApiPathString },
Path { path: LegacyAppPathString },
GlobPattern { pattern: String },
Special { value: FileSystemSpecialPath },
}
@@ -286,7 +296,7 @@ impl From<CoreFileSystemPath<AbsolutePathBuf>> for FileSystemPath {
fn from(value: CoreFileSystemPath<AbsolutePathBuf>) -> Self {
match value {
CoreFileSystemPath::Path { path } => Self::Path {
path: ApiPathString::from_abs_path(&path),
path: LegacyAppPathString::from_abs_path(&path),
},
CoreFileSystemPath::GlobPattern { pattern } => Self::GlobPattern { pattern },
CoreFileSystemPath::Special { value } => Self::Special {
@@ -70,12 +70,21 @@ pub struct ThreadRealtimeStartParams {
/// Overrides the configured realtime architecture for this session only.
#[ts(optional = nullable)]
pub architecture: Option<RealtimeConversationArchitecture>,
/// Leaves Codex response handoffs to the client's explicit append calls instead of forwarding
/// them automatically. Defaults to false.
#[ts(optional = nullable)]
pub client_managed_handoffs: Option<bool>,
/// Sends automatic Codex responses as realtime conversation items instead of handoff appends.
#[ts(optional = nullable)]
pub codex_responses_as_items: Option<bool>,
/// Optional prefix added to automatic Codex response items when `codexResponsesAsItems` is true.
#[ts(optional = nullable)]
pub codex_response_item_prefix: Option<String>,
/// Optional prefix added to automatic V1 Codex commentary sent with
/// `conversation.handoff.append` when `codexResponsesAsItems` is not true. Final answers are
/// sent without the prefix.
#[ts(optional = nullable)]
pub codex_response_handoff_prefix: Option<String>,
/// Overrides the configured realtime model for this session only.
#[ts(optional = nullable)]
pub model: Option<String>,
@@ -35,7 +35,7 @@ use codex_protocol::user_input::UserInput as CoreUserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::test_support::PathBufExt;
use codex_utils_absolute_path::test_support::test_path_buf;
use codex_utils_path_uri::ApiPathString;
use codex_utils_path_uri::LegacyAppPathString;
use pretty_assertions::assert_eq;
use serde_json::Value as JsonValue;
use serde_json::json;
@@ -174,6 +174,7 @@ fn thread_resume_response_round_trips_initial_turns_page() {
model_provider: "openai".to_string(),
created_at: 1,
updated_at: 1,
recency_at: Some(1),
status: ThreadStatus::Idle,
path: None,
cwd: absolute_path("tmp"),
@@ -375,6 +376,7 @@ fn external_agent_config_import_params_accept_legacy_plugin_details() {
..Default::default()
}),
}],
source: None,
}
);
}
@@ -579,8 +581,8 @@ fn additional_file_system_permissions_populates_entries_for_legacy_roots() {
);
let permissions = AdditionalFileSystemPermissions::from(core_permissions.clone());
let read_only_api_path = ApiPathString::from_abs_path(&read_only_path);
let read_write_api_path = ApiPathString::from_abs_path(&read_write_path);
let read_only_api_path = LegacyAppPathString::from_abs_path(&read_only_path);
let read_write_api_path = LegacyAppPathString::from_abs_path(&read_write_path);
assert_eq!(
permissions,
@@ -3600,6 +3602,7 @@ fn thread_lifecycle_responses_default_missing_optional_fields() {
assert_eq!(start.instruction_sources, Vec::<AbsolutePathBuf>::new());
assert_eq!(start.thread.parent_thread_id, None);
assert_eq!(start.thread.recency_at, None);
assert_eq!(resume.instruction_sources, Vec::<AbsolutePathBuf>::new());
assert_eq!(fork.instruction_sources, Vec::<AbsolutePathBuf>::new());
assert_eq!(start.active_permission_profile, None);
@@ -3608,6 +3611,14 @@ fn thread_lifecycle_responses_default_missing_optional_fields() {
assert_eq!(fork.active_permission_profile, None);
}
#[test]
fn thread_recency_sort_key_serializes_as_snake_case() {
assert_eq!(
serde_json::to_value(ThreadSortKey::RecencyAt).expect("sort key should serialize"),
json!("recency_at")
);
}
#[test]
fn turn_start_params_preserve_explicit_null_service_tier() {
let params: TurnStartParams = serde_json::from_value(json!({
@@ -3723,7 +3734,14 @@ fn thread_settings_update_params_preserve_field_level_experimental_gates() {
#[test]
fn turn_start_params_round_trip_environments() {
let cwd = test_absolute_path();
// Use a path foreign to the test host so this exercises syntax preservation instead of the
// host-native conversion performed by test_absolute_path().
#[cfg(windows)]
let raw_cwd = "/workspace";
#[cfg(not(windows))]
let raw_cwd = r"C:\workspace";
let cwd: LegacyAppPathString =
serde_json::from_value(json!(raw_cwd)).expect("API path should deserialize");
let params: TurnStartParams = serde_json::from_value(json!({
"threadId": "thread_123",
"input": [],
@@ -3805,27 +3823,6 @@ fn turn_start_params_treat_null_or_omitted_environments_as_default() {
);
}
#[test]
fn turn_start_params_reject_relative_environment_cwd() {
let err = serde_json::from_value::<TurnStartParams>(json!({
"threadId": "thread_123",
"input": [],
"environments": [
{
"environmentId": "local",
"cwd": "relative"
}
],
}))
.expect_err("relative environment cwd should fail");
assert!(
err.to_string()
.contains("AbsolutePathBuf deserialized without a base path"),
"unexpected error: {err}"
);
}
#[test]
fn realtime_append_text_defaults_role_to_user() {
let params = serde_json::from_value::<ThreadRealtimeAppendTextParams>(json!({
@@ -1086,6 +1086,7 @@ pub enum ThreadSourceKind {
pub enum ThreadSortKey {
CreatedAt,
UpdatedAt,
RecencyAt,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, JsonSchema, TS)]
@@ -152,6 +152,9 @@ pub struct Thread {
/// Unix timestamp (in seconds) when the thread was last updated.
#[ts(type = "number")]
pub updated_at: i64,
/// Unix timestamp (in seconds) used for thread recency ordering.
#[ts(type = "number | null")]
pub recency_at: Option<i64>,
/// Current runtime status for the thread.
pub status: ThreadStatus,
/// [UNSTABLE] Path to the thread on disk.
@@ -14,6 +14,7 @@ use codex_protocol::user_input::ByteRange as CoreByteRange;
use codex_protocol::user_input::TextElement as CoreTextElement;
use codex_protocol::user_input::UserInput as CoreUserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::LegacyAppPathString;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
@@ -38,7 +39,7 @@ pub enum TurnStatus {
#[ts(export_to = "v2/")]
pub struct TurnEnvironmentParams {
pub environment_id: String,
pub cwd: AbsolutePathBuf,
pub cwd: LegacyAppPathString,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
+5 -1
View File
@@ -2058,6 +2058,7 @@ impl CodexClient {
item_id,
started_at_ms: _,
approval_id,
environment_id,
reason,
network_approval_context,
command,
@@ -2075,6 +2076,9 @@ impl CodexClient {
);
self.command_approval_count += 1;
self.command_approval_item_ids.push(item_id.clone());
if let Some(environment_id) = environment_id.as_deref() {
println!("< environment: {environment_id}");
}
if let Some(reason) = reason.as_deref() {
println!("< reason: {reason}");
}
@@ -2088,7 +2092,7 @@ impl CodexClient {
println!("< command: {command}");
}
if let Some(cwd) = cwd.as_ref() {
println!("< cwd: {}", cwd.display());
println!("< cwd: {cwd}");
}
if let Some(command_actions) = command_actions.as_ref()
&& !command_actions.is_empty()
+25 -8
View File
@@ -165,9 +165,9 @@ Example with notification opt-out:
- `thread/inject_items` — append raw Responses API items to a loaded thread’s model-visible history without starting a user turn; returns `{}` on success.
- `turn/steer` — add user input to an already in-flight regular turn without starting a new turn; returns the active `turnId` that accepted the input. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Review and manual compaction turns reject `turn/steer`.
- `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`.
- `thread/realtime/start` — start a thread-scoped realtime session (experimental); pass `outputModality: "text"` or `outputModality: "audio"` to choose model output, optionally pass `model` and `version` to override configured realtime selection for this session only, and pass `includeStartupContext: false` to omit Codex's generated startup context. By default, automatic Codex text follows the protocol's speakable output path. Pass `codexResponsesAsItems: true` to send automatic Codex responses as realtime conversation items instead, and optionally pass `codexResponseItemPrefix` to prepend experiment instructions to those items. Returns `{}` and streams `thread/realtime/*` notifications. Omit `transport` for the websocket transport, or pass `{ "type": "webrtc", "sdp": "..." }` to create a WebRTC session from a browser-generated SDP offer; the remote answer SDP is emitted as `thread/realtime/sdp`.
- `thread/realtime/start` — start a thread-scoped realtime session (experimental); pass `outputModality: "text"` or `outputModality: "audio"` to choose model output, optionally pass `model` and `version` to override configured realtime selection for this session only, and pass `includeStartupContext: false` to omit Codex's generated startup context. By default, automatic Codex text follows the protocol's speakable output path. Pass `clientManagedHandoffs: true` to disable automatic Codex response delivery so only the client's explicit append calls produce handoffs. Pass `codexResponsesAsItems: true` to send automatic Codex responses as realtime conversation items instead, and optionally pass `codexResponseItemPrefix` to prepend experiment instructions to those items. For V1 sessions, pass `codexResponseHandoffPrefix` while item mode is disabled to route automatic Codex commentary through `conversation.handoff.append` with that prefix; final answers remain unprefixed. Returns `{}` and streams `thread/realtime/*` notifications. Omit `transport` for the websocket transport, or pass `{ "type": "webrtc", "sdp": "..." }` to create a WebRTC session from a browser-generated SDP offer; the remote answer SDP is emitted as `thread/realtime/sdp`.
- `thread/realtime/appendAudio` — append an input audio chunk to the active realtime session (experimental); returns `{}`.
- `thread/realtime/appendText` — append text input to the active realtime session with a required `role` of `user` or `developer` (experimental); returns `{}`. Older clients that omit `role` default to `user`.
- `thread/realtime/appendText` — append text input to the active realtime session with a required `role` of `user`, `developer`, or `assistant` (experimental); returns `{}`. Older clients that omit `role` default to `user`.
- `thread/realtime/appendSpeech` — append text that the realtime model should speak to the user (experimental); returns `{}`.
- `thread/realtime/stop` — stop the active realtime session for the thread (experimental); returns `{}`.
- `review/start` — kick off Codex’s automated reviewer for a thread; responds like `turn/start` and emits `item/started`/`item/completed` notifications with `enteredReviewMode` and `exitedReviewMode` items, plus a final assistant `agentMessage` containing the review.
@@ -232,7 +232,7 @@ Example with notification opt-out:
- `feedback/upload` — submit a feedback report (classification + optional reason/logs, conversation_id, and optional `extraLogFiles` attachments array); returns the tracking thread id.
- `config/read` — fetch the effective config on disk after resolving config layering, including opaque `desktop` values stored in `config.toml`.
- `externalAgentConfig/detect` — detect migratable external-agent artifacts with `includeHome` and optional `cwds`; each detected item includes `cwd` (`null` for home), and plugin/session migration items may additionally include structured `details` grouping plugin ids or session metadata.
- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any plugin/session `details` returned by detect. Returns an `importId` used to correlate the completion notification. When a request includes migration items, the server emits `externalAgentConfig/import/completed` once after the full import finishes with type-level `itemResults` containing each migrated type's success count, error count, successes, and raw errors (immediately after the response when everything completed synchronously, or after background imports finish).
- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any plugin/session `details` returned by detect. Callers may pass `source` to identify the product that initiated the import; omitted or `null` means unspecified. The response acknowledges the synchronous import phase with an `importId`. Expected migration failures are reported as per-item failures rather than JSON-RPC errors, so the server still returns that `importId` and emits `externalAgentConfig/import/completed` with the same ID once all synchronous and background work finishes. The completion notification contains type-level `itemTypeResults` with successes and failures, including raw failure messages for the client to report separately.
- `config/value/write` — write a single config key/value to the user's config.toml on disk; dotted paths such as `desktop.someKey` use the same generic write surface.
- `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads, including multiple `desktop.*` edits.
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), remote-control policy (`allowRemoteControl`; `false` force-disables remote control while `true` or `null` preserves existing behavior), computer use policy (`computerUse`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`.
@@ -364,7 +364,8 @@ Like `thread/resume`, experimental clients can pass `excludeTurns: true` to `thr
- `cursor` — opaque string from a prior response; omit for the first page.
- `limit` — server defaults to a reasonable page size if unset.
- `sortKey` — `created_at` (default) or `updated_at`.
- `sortKey` — `created_at` (default), `updated_at`, or `recency_at`.
- `recencyAt` is initialized when the thread is created and advances when a turn starts. Unlike `updatedAt`, background output and other persisted mutations do not advance it.
- `sortDirection` — `desc` (default) or `asc`.
- `modelProviders` — restrict results to specific providers; unset, null, or an empty array will include all providers.
- `sourceKinds` — restrict results to specific sources; omit or pass `[]` for interactive sessions only (`cli`, `vscode`).
@@ -386,8 +387,8 @@ Example:
} }
{ "id": 20, "result": {
"data": [
{ "id": "thr_a", "preview": "Create a TUI", "modelProvider": "openai", "createdAt": 1730831111, "updatedAt": 1730831111, "status": { "type": "notLoaded" }, "agentNickname": "Atlas", "agentRole": "explorer" },
{ "id": "thr_b", "preview": "Fix tests", "modelProvider": "openai", "createdAt": 1730750000, "updatedAt": 1730750000, "status": { "type": "notLoaded" } }
{ "id": "thr_a", "preview": "Create a TUI", "modelProvider": "openai", "createdAt": 1730831111, "updatedAt": 1730831111, "recencyAt": 1730831111, "status": { "type": "notLoaded" }, "agentNickname": "Atlas", "agentRole": "explorer" },
{ "id": "thr_b", "preview": "Fix tests", "modelProvider": "openai", "createdAt": 1730750000, "updatedAt": 1730750000, "recencyAt": 1730750000, "status": { "type": "notLoaded" } }
],
"nextCursor": "opaque-token-or-null",
"backwardsCursor": "opaque-token-or-null"
@@ -881,12 +882,19 @@ Clients may also pass `model` and `version` on `thread/realtime/start` to select
different realtime session configuration without changing thread or user config.
Pass `includeStartupContext: false` to skip Codex's startup context for this
session while still using the selected backend prompt.
Pass `clientManagedHandoffs: true` to suppress automatic Codex response handoffs
and items. The client can then choose which updates to deliver with
`thread/realtime/appendText` or `thread/realtime/appendSpeech`.
Pass `codexResponsesAsItems: true` to inject automatic Codex responses with
`conversation.item.create` instead of the protocol's default speakable output
path. When using that mode, `codexResponseItemPrefix` can prepend short
experiment instructions to each automatic Codex response item. Omit
`codexResponsesAsItems`, or pass `false`, to preserve the default speakable
behavior. Call
behavior. For V1 sessions, `codexResponseHandoffPrefix` instead routes automatic
Codex commentary through `conversation.handoff.append` and prepends the provided
text. Final answers remain unprefixed. Item mode takes precedence when
`codexResponsesAsItems` is true.
Call
`thread/realtime/appendText` to append app-provided realtime text items, or
`thread/realtime/appendSpeech` when the app decides a realtime update should be
spoken.
@@ -1429,7 +1437,7 @@ Certain actions (shell commands or modifying files) may require explicit user ap
Order of messages:
1. `item/started` — shows the pending `commandExecution` item with `command`, `cwd`, and other fields so you can render the proposed action.
2. `item/commandExecution/requestApproval` (request) — carries the same `itemId`, `threadId`, `turnId`, optionally `approvalId` (for subcommand callbacks), and `reason`. For normal command approvals, it also includes `command`, `cwd`, and `commandActions` for friendly display. When `initialize.params.capabilities.experimentalApi = true`, it may also include experimental `additionalPermissions` describing requested per-command sandbox access; any filesystem paths in that payload are absolute on the wire, and network access is represented as `additionalPermissions.network.enabled`. For network-only approvals, those command fields may be omitted and `networkApprovalContext` is provided instead. Optional persistence hints may also be included via `proposedExecpolicyAmendment` and `proposedNetworkPolicyAmendments`. Clients can prefer `availableDecisions` when present to render the exact set of choices the server wants to expose, while still falling back to the older heuristics if it is omitted.
2. `item/commandExecution/requestApproval` (request) — carries the same `itemId`, `threadId`, `turnId`, the nullable `environmentId` where the command will run, optionally `approvalId` (for subcommand callbacks), and `reason`. New shell and unified-exec approvals set `environmentId`; older events that do not provide one are exposed as `null`. For normal command approvals, the request also includes `command`, `cwd`, and `commandActions` for friendly display. When `initialize.params.capabilities.experimentalApi = true`, it may also include experimental `additionalPermissions` describing requested per-command sandbox access; any filesystem paths in that payload are absolute on the wire, and network access is represented as `additionalPermissions.network.enabled`. For network-only approvals, those command fields may be omitted and `networkApprovalContext` is provided instead. Optional persistence hints may also be included via `proposedExecpolicyAmendment` and `proposedNetworkPolicyAmendments`. Clients can prefer `availableDecisions` when present to render the exact set of choices the server wants to expose, while still falling back to the older heuristics if it is omitted.
3. Client response — for example `{ "decision": "accept" }`, `{ "decision": "acceptForSession" }`, `{ "decision": { "acceptWithExecpolicyAmendment": { "execpolicy_amendment": [...] } } }`, `{ "decision": { "applyNetworkPolicyAmendment": { "network_policy_amendment": { "host": "example.com", "action": "allow" } } } }`, `{ "decision": "decline" }`, or `{ "decision": "cancel" }`.
4. `serverRequest/resolved` — `{ threadId, requestId }` confirms the pending request has been resolved or cleared, including lifecycle cleanup on turn start/complete/interrupt.
5. `item/completed` — final `commandExecution` item with `status: "completed" | "failed" | "declined"` and execution output. Render this as the authoritative result.
@@ -1825,15 +1833,24 @@ approvals_reviewer = "auto_review"
[apps._default]
approvals_reviewer = "user"
default_tools_approval_mode = "prompt"
[apps.demo-app]
approvals_reviewer = "auto_review"
default_tools_approval_mode = "approve"
```
Setting the app value to `"user"` routes its approval prompts to the user
instead of Guardian; setting it to `"auto_review"` opts that app into Guardian
review when allowed by configuration requirements.
Use `apps._default.default_tools_approval_mode` to set the approval mode for
tools without a per-app or per-tool override. Supported values are `"auto"`,
`"prompt"`, and `"approve"`. Tool-level `approval_mode` takes precedence over
the per-app `default_tools_approval_mode`, which takes precedence over the
`apps._default` value. Managed tool requirements take precedence over all of
these settings. When none are configured, the mode defaults to `"auto"`.
Invoke an app by inserting `$<app-slug>` in the text input. The slug is derived from the app name and lowercased with non-alphanumeric characters replaced by `-` (for example, "Demo App" becomes `$demo-app`). Add a `mention` input item (recommended) so the server uses the exact `app://<connector-id>` path rather than guessing by name. Plugins use the same `mention` item shape, but with `plugin://<plugin-name>@<marketplace-name>` paths from `plugin/installed` or `plugin/list`.
Example:
@@ -113,6 +113,7 @@ use codex_protocol::request_user_input::RequestUserInputResponse as CoreRequestU
use codex_sandboxing::policy_transforms::intersect_permission_profiles;
use codex_shell_command::parse_command::shlex_join;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::LegacyAppPathString;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::SystemTime;
@@ -129,7 +130,7 @@ enum CommandExecutionApprovalPresentation {
#[derive(Debug, PartialEq)]
struct CommandExecutionCompletionItem {
command: String,
cwd: AbsolutePathBuf,
cwd: LegacyAppPathString,
command_actions: Vec<V2ParsedCommand>,
}
@@ -550,6 +551,7 @@ pub(crate) async fn apply_bespoke_event_handling(
call_id,
approval_id,
turn_id,
environment_id,
started_at_ms,
command,
cwd,
@@ -574,7 +576,7 @@ pub(crate) async fn apply_bespoke_event_handling(
let command_string = shlex_join(&command);
let completion_item = CommandExecutionCompletionItem {
command: command_string,
cwd: cwd.clone(),
cwd: cwd.clone().into(),
command_actions: command_actions.clone(),
};
CommandExecutionApprovalPresentation::Command(completion_item)
@@ -626,6 +628,7 @@ pub(crate) async fn apply_bespoke_event_handling(
item_id: call_id.clone(),
started_at_ms,
approval_id: approval_id.clone(),
environment_id,
reason,
network_approval_context,
command,
@@ -1352,7 +1355,7 @@ async fn start_command_execution_item(
turn_id: String,
item_id: String,
command: String,
cwd: AbsolutePathBuf,
cwd: LegacyAppPathString,
command_actions: Vec<V2ParsedCommand>,
source: CommandExecutionSource,
outgoing: &ThreadScopedOutgoingMessageSender,
@@ -1396,7 +1399,7 @@ async fn complete_command_execution_item(
turn_id: String,
item_id: String,
command: String,
cwd: AbsolutePathBuf,
cwd: LegacyAppPathString,
process_id: Option<String>,
source: CommandExecutionSource,
command_actions: Vec<V2ParsedCommand>,
@@ -2259,6 +2262,7 @@ mod tests {
reasoning_effort: None,
created_at,
updated_at: created_at,
recency_at: created_at,
archived_at: None,
cwd: test_path_buf("/tmp").abs().into(),
cli_version: "0.0.0".to_string(),
@@ -2320,7 +2324,7 @@ mod tests {
fn command_execution_completion_item(command: &str) -> CommandExecutionCompletionItem {
CommandExecutionCompletionItem {
command: command.to_string(),
cwd: test_path_buf("/tmp").abs(),
cwd: test_path_buf("/tmp").abs().into(),
command_actions: vec![V2ParsedCommand::Unknown {
command: command.to_string(),
}],
+7
View File
@@ -237,6 +237,10 @@ impl CommandExecManager {
arg0,
..
} = exec_request;
// TODO(anp): Keep PathUri through the local command launch boundary.
let cwd = cwd
.to_abs_path()
.map_err(|err| invalid_request(format!("invalid command cwd: {err}")))?;
let stream_stdin = tty || stream_stdin;
let stream_stdout_stderr = tty || stream_stdout_stderr;
@@ -700,6 +704,7 @@ mod tests {
cwd.clone(),
HashMap::new(),
/*network*/ None,
/*network_environment_id*/ None,
ExecExpiration::DefaultTimeout,
codex_core::exec::ExecCapturePolicy::ShellTool,
SandboxType::WindowsRestrictedToken,
@@ -817,6 +822,7 @@ mod tests {
cwd.clone(),
HashMap::new(),
/*network*/ None,
/*network_environment_id*/ None,
ExecExpiration::Cancellation(CancellationToken::new()),
codex_core::exec::ExecCapturePolicy::ShellTool,
SandboxType::None,
@@ -904,6 +910,7 @@ mod tests {
cwd.clone(),
HashMap::new(),
/*network*/ None,
/*network_environment_id*/ None,
ExecExpiration::TimeoutOrCancellation {
timeout: Duration::from_secs(30),
cancellation,
@@ -237,7 +237,7 @@ impl ExternalAgentConfigService {
pub(crate) async fn import(
&self,
migration_items: Vec<ExternalAgentConfigMigrationItem>,
) -> io::Result<ExternalAgentConfigImportOutcome> {
) -> ExternalAgentConfigImportOutcome {
let mut outcome = ExternalAgentConfigImportOutcome::default();
for migration_item in migration_items {
let item_type = migration_item.item_type;
@@ -413,17 +413,28 @@ impl ExternalAgentConfigService {
})(),
ExternalAgentConfigMigrationItemType::Sessions => Ok(()),
};
if let Err(err) = import_result {
if item_type == ExternalAgentConfigMigrationItemType::Plugins {
outcome.item_results.push(item_result);
continue;
}
return Err(err);
if let Err(err) = import_result
&& item_type != ExternalAgentConfigMigrationItemType::Plugins
{
let message = err.to_string();
let error_type = if message.contains("invalid existing config.toml") {
"invalid_existing_config"
} else {
"external_agent_config_import_error"
};
item_result.record_error(ExternalAgentConfigImportRawError {
item_type,
error_type: Some(error_type.to_string()),
failure_stage: "import_request_failed".to_string(),
message,
cwd: item_result.cwd.clone(),
source: None,
});
}
outcome.item_results.push(item_result);
}
Ok(outcome)
outcome
}
async fn detect_migrations(
@@ -561,8 +561,7 @@ async fn import_repo_migrates_mcp_hooks_commands_and_subagents() {
details: None,
},
])
.await
.expect("import");
.await;
let config: TomlValue = toml::from_str(
&fs::read_to_string(repo_root.join(".codex").join("config.toml")).expect("read config"),
@@ -724,8 +723,7 @@ url = "https://example.com/mixed-transport"
cwd: Some(repo_root.clone()),
details: None,
}])
.await
.expect("import");
.await;
assert_eq!(
fs::read_to_string(repo_root.join(".codex").join("config.toml")).expect("read config"),
@@ -838,8 +836,7 @@ async fn import_home_migrates_supported_config_fields_skills_and_agents_md() {
details: None,
},
])
.await
.expect("import");
.await;
assert_eq!(
fs::read_to_string(codex_home.join("AGENTS.md")).expect("read agents"),
@@ -894,8 +891,7 @@ async fn import_home_config_uses_local_settings_over_project_settings() {
cwd: None,
details: None,
}])
.await
.expect("import");
.await;
let config: TomlValue =
toml::from_str(&fs::read_to_string(codex_home.join("config.toml")).expect("read config"))
@@ -939,8 +935,7 @@ async fn import_home_config_ignores_invalid_local_settings() {
cwd: None,
details: None,
}])
.await
.expect("import");
.await;
assert_eq!(
fs::read_to_string(codex_home.join("config.toml")).expect("read config"),
@@ -965,8 +960,7 @@ async fn import_home_skips_empty_config_migration() {
cwd: None,
details: None,
}])
.await
.expect("import");
.await;
assert_eq!(
outcome.item_results,
@@ -1043,8 +1037,7 @@ async fn import_local_plugins_returns_completed_status() {
..Default::default()
}),
}])
.await
.expect("import");
.await;
assert_eq!(
outcome.pending_plugin_imports,
@@ -1104,8 +1097,7 @@ async fn import_git_plugins_returns_pending_async_status() {
..Default::default()
}),
}])
.await
.expect("import");
.await;
assert_eq!(
outcome.pending_plugin_imports,
@@ -1235,8 +1227,7 @@ async fn import_repo_agents_md_rewrites_terms_and_skips_non_empty_targets() {
details: None,
},
])
.await
.expect("import");
.await;
assert_eq!(
outcome.item_results,
@@ -1302,8 +1293,7 @@ async fn import_repo_agents_md_overwrites_empty_targets() {
cwd: Some(repo_root.clone()),
details: None,
}])
.await
.expect("import");
.await;
assert_eq!(
outcome.item_results,
@@ -1403,8 +1393,7 @@ async fn import_repo_hooks_preserves_disabled_codex_hooks_feature() {
cwd: Some(repo_root.clone()),
details: None,
}])
.await
.expect("import");
.await;
assert_eq!(
outcome.item_results,
@@ -1481,8 +1470,7 @@ async fn import_repo_mcp_uses_home_settings_toggles_when_repo_settings_missing()
cwd: Some(repo_root.clone()),
details: None,
}])
.await
.expect("import");
.await;
assert_eq!(
outcome.item_results,
@@ -1559,8 +1547,7 @@ async fn import_repo_mcp_uses_local_settings_toggles_over_project_settings() {
cwd: Some(repo_root.clone()),
details: None,
}])
.await
.expect("import");
.await;
let config: TomlValue = toml::from_str(
&fs::read_to_string(repo_root.join(".codex").join("config.toml")).expect("read config"),
@@ -1607,8 +1594,7 @@ async fn import_repo_mcp_ignores_invalid_home_settings_when_repo_settings_missin
cwd: Some(repo_root.clone()),
details: None,
}])
.await
.expect("import");
.await;
let config: TomlValue = toml::from_str(
&fs::read_to_string(repo_root.join(".codex").join("config.toml")).expect("read config"),
@@ -1649,8 +1635,7 @@ async fn import_repo_uses_non_empty_external_agent_agents_source() {
cwd: Some(repo_root.clone()),
details: None,
}])
.await
.expect("import");
.await;
assert_eq!(
fs::read_to_string(repo_root.join("AGENTS.md")).expect("read target"),
@@ -1683,8 +1668,7 @@ async fn import_continues_after_failed_migration_item() {
details: None,
},
])
.await
.expect("import continues");
.await;
assert_eq!(
fs::read_to_string(repo_root.join("AGENTS.md")).expect("read target"),
+1
View File
@@ -101,6 +101,7 @@ pub mod in_process;
mod mcp_refresh;
mod message_processor;
mod models;
mod models_refresh_worker;
mod outgoing_message;
mod request_processors;
mod request_serialization;
+11 -2
View File
@@ -92,6 +92,8 @@ use tokio::time::timeout;
use tokio_util::sync::CancellationToken;
use tracing::Instrument;
use crate::models_refresh_worker::ModelsRefreshWorker;
const EXTERNAL_AUTH_REFRESH_TIMEOUT: Duration = Duration::from_secs(10);
const CONNECTION_RPC_DRAIN_TIMEOUT: Duration = Duration::from_secs(/*secs*/ 30);
@@ -182,6 +184,7 @@ impl ExternalAuth for ExternalAuthRefreshBridge {
pub(crate) struct MessageProcessor {
outgoing: Arc<OutgoingMessageSender>,
models_refresh_worker: ModelsRefreshWorker,
skills_watcher: Arc<SkillsWatcher>,
account_processor: AccountRequestProcessor,
apps_processor: AppsRequestProcessor,
@@ -371,10 +374,12 @@ impl MessageProcessor {
)),
)
});
let models_manager = thread_manager.get_models_manager();
let models_refresh_worker = crate::models_refresh_worker::spawn(&models_manager);
thread_manager
.plugins_manager()
.set_analytics_events_client(analytics_events_client.clone());
let skills_watcher = SkillsWatcher::new(thread_manager.skills_manager(), outgoing.clone());
let skills_watcher = SkillsWatcher::new(thread_manager.skills_service(), outgoing.clone());
let pending_thread_unloads = Arc::new(Mutex::new(HashSet::new()));
let thread_watch_manager =
@@ -510,7 +515,7 @@ impl MessageProcessor {
outgoing.clone(),
config_manager.clone(),
thread_manager.clone(),
analytics_events_client,
analytics_events_client.clone(),
);
let external_agent_config_processor =
ExternalAgentConfigRequestProcessor::new(ExternalAgentConfigRequestProcessorArgs {
@@ -520,6 +525,7 @@ impl MessageProcessor {
config_manager: config_manager.clone(),
config_processor: config_processor.clone(),
state_db,
analytics_events_client,
arg0_paths,
codex_home: config.codex_home.to_path_buf(),
});
@@ -537,6 +543,7 @@ impl MessageProcessor {
Self {
outgoing,
models_refresh_worker,
skills_watcher,
account_processor,
apps_processor,
@@ -566,6 +573,7 @@ impl MessageProcessor {
pub(crate) fn clear_runtime_references(&self) {
self.account_processor.clear_external_auth();
self.apps_processor.shutdown();
self.models_refresh_worker.shutdown();
self.skills_watcher.shutdown();
}
@@ -742,6 +750,7 @@ impl MessageProcessor {
}
pub(crate) async fn drain_background_tasks(&self) {
self.models_refresh_worker.shutdown();
self.thread_processor.drain_background_tasks().await;
}
@@ -0,0 +1,65 @@
use std::sync::Arc;
use std::time::Duration;
use codex_models_manager::manager::RefreshStrategy;
use codex_models_manager::manager::SharedModelsManager;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
const MODELS_REFRESH_INTERVAL: Duration = Duration::from_secs(3 * 60);
#[derive(Debug)]
pub(crate) struct ModelsRefreshWorker {
shutdown: CancellationToken,
_task: JoinHandle<()>,
}
impl ModelsRefreshWorker {
pub(crate) fn shutdown(&self) {
self.shutdown.cancel();
}
}
impl Drop for ModelsRefreshWorker {
fn drop(&mut self) {
self.shutdown();
}
}
pub(crate) fn spawn(models_manager: &SharedModelsManager) -> ModelsRefreshWorker {
spawn_with_interval(models_manager, MODELS_REFRESH_INTERVAL)
}
fn spawn_with_interval(
models_manager: &SharedModelsManager,
refresh_interval: Duration,
) -> ModelsRefreshWorker {
let models_manager = Arc::downgrade(models_manager);
let shutdown = CancellationToken::new();
let worker_shutdown = shutdown.clone();
let task = tokio::spawn(async move {
loop {
if worker_shutdown.is_cancelled() {
break;
}
let Some(models_manager) = models_manager.upgrade() else {
break;
};
models_manager.list_models(RefreshStrategy::Online).await;
drop(models_manager);
tokio::select! {
_ = worker_shutdown.cancelled() => break,
_ = tokio::time::sleep(refresh_interval) => {}
}
}
});
ModelsRefreshWorker {
shutdown,
_task: task,
}
}
#[cfg(test)]
#[path = "models_refresh_worker_tests.rs"]
mod tests;
@@ -0,0 +1,90 @@
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::time::Duration;
use codex_models_manager::manager::ModelsEndpointClient;
use codex_models_manager::manager::ModelsEndpointFuture;
use codex_models_manager::manager::OpenAiModelsManager;
use codex_models_manager::manager::SharedModelsManager;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CoreResult;
use codex_protocol::openai_models::ModelInfo;
use pretty_assertions::assert_eq;
use tempfile::tempdir;
use tokio::sync::Notify;
use super::*;
#[derive(Debug)]
struct TestModelsEndpoint {
fetch_count: AtomicUsize,
fetched: Notify,
release_second_fetch: Notify,
}
impl TestModelsEndpoint {
fn new() -> Arc<Self> {
Arc::new(Self {
fetch_count: AtomicUsize::new(0),
fetched: Notify::new(),
release_second_fetch: Notify::new(),
})
}
async fn wait_for_fetch_count(&self, expected: usize) {
tokio::time::timeout(Duration::from_secs(1), async {
while self.fetch_count.load(Ordering::SeqCst) < expected {
self.fetched.notified().await;
}
})
.await
.unwrap_or_else(|_| panic!("expected {expected} model fetches"));
}
}
impl ModelsEndpointClient for TestModelsEndpoint {
fn has_command_auth(&self) -> bool {
true
}
fn uses_codex_backend(&self) -> ModelsEndpointFuture<'_, bool> {
Box::pin(async { false })
}
fn list_models<'a>(
&'a self,
_client_version: &'a str,
) -> ModelsEndpointFuture<'a, CoreResult<(Vec<ModelInfo>, Option<String>)>> {
Box::pin(async move {
let fetch_index = self.fetch_count.fetch_add(1, Ordering::SeqCst);
self.fetched.notify_one();
if fetch_index == 0 {
return Err(CodexErr::Io(std::io::Error::other("test failure")));
}
if fetch_index == 1 {
self.release_second_fetch.notified().await;
}
Ok((Vec::new(), None))
})
}
}
#[tokio::test]
async fn refreshes_immediately_periodically_and_stops_when_dropped() {
let codex_home = tempdir().expect("temp dir");
let endpoint = TestModelsEndpoint::new();
let models_manager: SharedModelsManager = Arc::new(OpenAiModelsManager::new(
codex_home.path().to_path_buf(),
endpoint.clone(),
/*auth_manager*/ None,
));
let worker = spawn_with_interval(&models_manager, Duration::from_millis(10));
endpoint.wait_for_fetch_count(/*expected*/ 2).await;
drop(worker);
endpoint.release_second_fetch.notify_one();
tokio::time::sleep(Duration::from_millis(30)).await;
assert_eq!(endpoint.fetch_count.load(Ordering::SeqCst), 2);
}
@@ -977,6 +977,7 @@ mod tests {
item_id: "item-1".to_string(),
started_at_ms: 0,
approval_id: None,
environment_id: None,
reason: None,
network_approval_context: None,
command: Some("echo hi".to_string()),
@@ -541,6 +541,36 @@ fn resolve_request_cwd(cwd: Option<PathBuf>) -> Result<Option<AbsolutePathBuf>,
.transpose()
}
fn resolve_turn_environment_selections(
thread_manager: &ThreadManager,
environments: Option<Vec<TurnEnvironmentParams>>,
) -> Result<Option<Vec<TurnEnvironmentSelection>>, JSONRPCErrorError> {
let Some(environments) = environments else {
return Ok(None);
};
let mut selections = Vec::with_capacity(environments.len());
for environment in environments {
let environment_id = environment.environment_id;
let cwd = environment
.cwd
.to_inferred_path_uri()
.ok_or_else(|| {
invalid_request(format!(
"invalid cwd for environment `{environment_id}`: path `{}` does not use absolute POSIX or Windows path syntax",
environment.cwd
))
})?;
selections.push(TurnEnvironmentSelection {
environment_id,
cwd,
});
}
thread_manager
.validate_environment_selections(&selections)
.map_err(environment_selection_error)?;
Ok(Some(selections))
}
fn resolve_runtime_workspace_roots(workspace_roots: Vec<AbsolutePathBuf>) -> Vec<AbsolutePathBuf> {
let mut resolved_roots = Vec::new();
for root in workspace_roots {
@@ -173,7 +173,7 @@ impl AccountRequestProcessor {
}
}
async fn maybe_refresh_remote_installed_plugins_cache_for_current_config(
async fn maybe_refresh_plugin_caches_for_current_config(
config_manager: &ConfigManager,
thread_manager: &Arc<ThreadManager>,
auth: Option<CodexAuth>,
@@ -181,6 +181,9 @@ impl AccountRequestProcessor {
thread_manager
.plugins_manager()
.set_auth_mode(auth.as_ref().map(CodexAuth::api_auth_mode));
thread_manager
.plugins_manager()
.clear_recommended_plugins_cache();
match config_manager
.load_latest_config(/*fallback_cwd*/ None)
@@ -191,7 +194,7 @@ impl AccountRequestProcessor {
let refresh_config_manager = config_manager.clone();
thread_manager
.plugins_manager()
.maybe_start_remote_installed_plugins_cache_refresh(
.maybe_start_remote_plugin_caches_refresh(
&config.plugins_config_input(),
auth,
Some(Arc::new(move || {
@@ -216,7 +219,7 @@ impl AccountRequestProcessor {
) {
tokio::spawn(async move {
thread_manager.plugins_manager().clear_cache();
thread_manager.skills_manager().clear_cache();
thread_manager.skills_service().clear_cache();
if thread_manager.list_thread_ids().await.is_empty() {
return;
}
@@ -618,7 +621,7 @@ impl AccountRequestProcessor {
}
async fn send_login_success_notifications(&self, login_id: Option<Uuid>) {
Self::maybe_refresh_remote_installed_plugins_cache_for_current_config(
Self::maybe_refresh_plugin_caches_for_current_config(
&self.config_manager,
&self.thread_manager,
self.auth_manager.auth_cached(),
@@ -671,7 +674,7 @@ impl AccountRequestProcessor {
.await;
let auth = auth_manager.auth_cached();
Self::maybe_refresh_remote_installed_plugins_cache_for_current_config(
Self::maybe_refresh_plugin_caches_for_current_config(
&config_manager,
&thread_manager,
auth.clone(),
@@ -703,7 +706,7 @@ impl AccountRequestProcessor {
}
}
Self::maybe_refresh_remote_installed_plugins_cache_for_current_config(
Self::maybe_refresh_plugin_caches_for_current_config(
&self.config_manager,
&self.thread_manager,
self.auth_manager.auth_cached(),
@@ -511,7 +511,7 @@ impl CatalogRequestProcessor {
let workspace_codex_plugins_enabled = self
.workspace_codex_plugins_enabled(&config, auth.as_ref())
.await;
let skills_manager = self.thread_manager.skills_manager();
let skills_service = self.thread_manager.skills_service();
let plugins_manager = self.thread_manager.plugins_manager();
let fs = self
.thread_manager
@@ -523,7 +523,7 @@ impl CatalogRequestProcessor {
let config = &config;
let fs = fs.clone();
let plugins_manager = &plugins_manager;
let skills_manager = &skills_manager;
let skills_service = &skills_service;
async move {
let (cwd_abs, config_layer_stack) = match self.resolve_cwd_config(&cwd).await {
Ok(resolved) => resolved,
@@ -559,9 +559,10 @@ impl CatalogRequestProcessor {
config_layer_stack,
config.bundled_skills_enabled(),
);
let outcome = skills_manager
.skills_for_cwd(&skills_input, force_reload, fs)
let snapshot = skills_service
.snapshot_for_cwd(&skills_input, force_reload, fs)
.await;
let outcome = snapshot.outcome();
let errors = errors_to_info(&outcome.errors);
let skills = skills_to_info(&outcome.skills, &outcome.disabled_paths);
(
@@ -590,7 +591,7 @@ impl CatalogRequestProcessor {
self.skills_watcher
.register_runtime_extra_roots(&extra_roots);
self.thread_manager
.skills_manager()
.skills_service()
.set_extra_roots(extra_roots);
self.outgoing
.send_server_notification(ServerNotification::SkillsChanged(
@@ -703,7 +704,7 @@ impl CatalogRequestProcessor {
.await
.map(|()| {
self.thread_manager.plugins_manager().clear_cache();
self.thread_manager.skills_manager().clear_cache();
self.thread_manager.skills_service().clear_cache();
SkillsConfigWriteResponse {
effective_enabled: enabled,
}
@@ -297,6 +297,7 @@ impl CommandExecRequestProcessor {
network: started_network_proxy
.as_ref()
.map(codex_core::config::StartedNetworkProxy::proxy),
network_environment_id: None,
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level,
windows_sandbox_private_desktop: self
@@ -169,7 +169,7 @@ impl ConfigRequestProcessor {
pub(crate) async fn handle_config_mutation(&self) {
self.thread_manager.plugins_manager().clear_cache();
self.thread_manager.skills_manager().clear_cache();
self.thread_manager.skills_service().clear_cache();
}
async fn handle_config_mutation_result<T>(
@@ -15,6 +15,9 @@ use crate::config_manager::ConfigManager;
use crate::error_code::internal_error;
use crate::outgoing_message::ConnectionRequestId;
use crate::outgoing_message::OutgoingMessageSender;
use codex_analytics::AnalyticsEventsClient;
use codex_analytics::ExternalAgentConfigImportCompletedInput;
use codex_analytics::ExternalAgentConfigImportFailureInput;
use codex_app_server_protocol::CommandMigration;
use codex_app_server_protocol::ExternalAgentConfigDetectParams;
use codex_app_server_protocol::ExternalAgentConfigDetectResponse;
@@ -57,6 +60,7 @@ pub(crate) struct ExternalAgentConfigRequestProcessor {
thread_manager: Arc<ThreadManager>,
config_processor: ConfigRequestProcessor,
state_db: Option<StateDbHandle>,
analytics_events_client: AnalyticsEventsClient,
}
pub(crate) struct ExternalAgentConfigRequestProcessorArgs {
@@ -66,6 +70,7 @@ pub(crate) struct ExternalAgentConfigRequestProcessorArgs {
pub(crate) config_manager: ConfigManager,
pub(crate) config_processor: ConfigRequestProcessor,
pub(crate) state_db: Option<StateDbHandle>,
pub(crate) analytics_events_client: AnalyticsEventsClient,
pub(crate) arg0_paths: Arg0DispatchPaths,
pub(crate) codex_home: PathBuf,
}
@@ -79,6 +84,7 @@ impl ExternalAgentConfigRequestProcessor {
config_manager,
config_processor,
state_db,
analytics_events_client,
arg0_paths,
codex_home,
} = args;
@@ -96,6 +102,7 @@ impl ExternalAgentConfigRequestProcessor {
thread_manager,
config_processor,
state_db,
analytics_events_client,
}
}
@@ -199,6 +206,7 @@ impl ExternalAgentConfigRequestProcessor {
params: ExternalAgentConfigImportParams,
) -> Result<(), JSONRPCErrorError> {
let import_id = Uuid::new_v4().to_string();
let analytics_source = params.source.clone().unwrap_or_default();
let needs_runtime_refresh = migration_items_need_runtime_refresh(&params.migration_items);
let has_migration_items = !params.migration_items.is_empty();
let has_plugin_imports = params.migration_items.iter().any(|item| {
@@ -209,7 +217,7 @@ impl ExternalAgentConfigRequestProcessor {
});
let (pending_session_imports, session_validation_result) =
self.validate_pending_session_imports(&params);
let import_outcome = self.import_external_agent_config(params).await?;
let import_outcome = self.import_external_agent_config(params).await;
if needs_runtime_refresh {
self.config_processor.handle_config_mutation().await;
}
@@ -242,7 +250,9 @@ impl ExternalAgentConfigRequestProcessor {
send_completed_import_notification(
&self.outgoing,
self.state_db.as_ref(),
&self.analytics_events_client,
import_id,
analytics_source,
&completed_item_results,
)
.await;
@@ -253,6 +263,7 @@ impl ExternalAgentConfigRequestProcessor {
let plugin_processor = self.clone();
let outgoing = Arc::clone(&self.outgoing);
let state_db = self.state_db.clone();
let analytics_events_client = self.analytics_events_client.clone();
let thread_manager = Arc::clone(&self.thread_manager);
let session_import_result = (!pending_session_imports.is_empty()).then(|| {
CoreImportItemResult::new(
@@ -319,12 +330,14 @@ impl ExternalAgentConfigRequestProcessor {
completed_item_results.extend(background_item_results);
if has_plugin_imports {
thread_manager.plugins_manager().clear_cache();
thread_manager.skills_manager().clear_cache();
thread_manager.skills_service().clear_cache();
}
send_completed_import_notification(
&outgoing,
state_db.as_ref(),
&analytics_events_client,
import_id,
analytics_source,
&completed_item_results,
)
.await;
@@ -421,7 +434,7 @@ impl ExternalAgentConfigRequestProcessor {
async fn import_external_agent_config(
&self,
params: ExternalAgentConfigImportParams,
) -> Result<CoreImportOutcome, JSONRPCErrorError> {
) -> CoreImportOutcome {
self.migration_service
.import(
params
@@ -516,7 +529,6 @@ impl ExternalAgentConfigRequestProcessor {
.collect(),
)
.await
.map_err(|err| internal_error(err.to_string()))
}
async fn complete_pending_plugin_import(
@@ -551,10 +563,14 @@ async fn send_import_progress(
async fn send_completed_import_notification(
outgoing: &OutgoingMessageSender,
state_db: Option<&StateDbHandle>,
analytics_events_client: &AnalyticsEventsClient,
import_id: String,
analytics_source: String,
item_results: &[CoreImportItemResult],
) {
let notification = completed_notification(import_id, item_results);
log_completed_import_failures(&notification);
track_completed_import_notification(analytics_events_client, &analytics_source, &notification);
if let Some(state_db) = state_db
&& let Err(err) = record_completed_import_notification(state_db, &notification).await
{
@@ -571,6 +587,75 @@ async fn send_completed_import_notification(
.await;
}
fn log_completed_import_failures(notification: &ExternalAgentConfigImportCompletedNotification) {
for type_result in &notification.item_type_results {
for failure in &type_result.failures {
let error_type = import_failure_error_type(failure);
tracing::warn!(
import_id = %notification.import_id,
item_type = ?failure.item_type,
error_type = %error_type,
failure_stage = %failure.failure_stage,
cwd = ?failure.cwd,
source = ?failure.source,
error = %failure.message,
"external agent config migration item failed"
);
}
}
}
fn track_completed_import_notification(
analytics_events_client: &AnalyticsEventsClient,
analytics_source: &str,
notification: &ExternalAgentConfigImportCompletedNotification,
) {
for type_result in &notification.item_type_results {
let item_type = analytics_migration_item_type(type_result.item_type).to_string();
analytics_events_client.track_external_agent_config_import_completed(
ExternalAgentConfigImportCompletedInput {
import_id: notification.import_id.clone(),
source: analytics_source.to_string(),
item_type: item_type.clone(),
success_count: type_result.successes.len(),
failed_count: type_result.failures.len(),
},
);
for failure in &type_result.failures {
analytics_events_client.track_external_agent_config_import_failure(
ExternalAgentConfigImportFailureInput {
import_id: notification.import_id.clone(),
source: analytics_source.to_string(),
item_type: item_type.clone(),
failure_stage: failure.failure_stage.clone(),
error_type: import_failure_error_type(failure),
},
);
}
}
}
fn import_failure_error_type(failure: &ProtocolImportFailure) -> String {
failure
.error_type
.clone()
.unwrap_or_else(|| failure.failure_stage.clone())
}
fn analytics_migration_item_type(item_type: ExternalAgentConfigMigrationItemType) -> &'static str {
match item_type {
ExternalAgentConfigMigrationItemType::AgentsMd => "AGENTS_MD",
ExternalAgentConfigMigrationItemType::Config => "CONFIG",
ExternalAgentConfigMigrationItemType::Skills => "SKILLS",
ExternalAgentConfigMigrationItemType::Plugins => "PLUGINS",
ExternalAgentConfigMigrationItemType::McpServerConfig => "MCP_SERVER_CONFIG",
ExternalAgentConfigMigrationItemType::Subagents => "SUBAGENTS",
ExternalAgentConfigMigrationItemType::Hooks => "HOOKS",
ExternalAgentConfigMigrationItemType::Commands => "COMMANDS",
ExternalAgentConfigMigrationItemType::Sessions => "SESSIONS",
}
}
async fn record_completed_import_notification(
state_db: &StateDbHandle,
notification: &ExternalAgentConfigImportCompletedNotification,
@@ -18,9 +18,12 @@ use codex_core_plugins::remote::REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETP
use codex_core_plugins::remote::RemoteAppTemplateUnavailableReason;
use codex_core_plugins::remote::is_valid_remote_plugin_id;
use codex_core_plugins::remote::validate_remote_plugin_id;
use codex_core_plugins::remote_bundle::RemotePluginBundleInstallError;
use codex_mcp::McpOAuthLoginSupport;
use codex_mcp::oauth_login_support;
use codex_mcp::should_retry_without_scopes;
use codex_plugin::PluginId;
use codex_plugin::PluginTelemetryMetadata;
use codex_rmcp_client::perform_oauth_login_silent;
#[derive(Clone)]
@@ -474,7 +477,7 @@ impl PluginRequestProcessor {
) {
tokio::spawn(async move {
thread_manager.plugins_manager().clear_cache();
thread_manager.skills_manager().clear_cache();
thread_manager.skills_service().clear_cache();
if thread_manager.list_thread_ids().await.is_empty() {
return;
}
@@ -484,7 +487,7 @@ impl PluginRequestProcessor {
fn clear_plugin_related_caches(&self) {
self.thread_manager.plugins_manager().clear_cache();
self.thread_manager.skills_manager().clear_cache();
self.thread_manager.skills_service().clear_cache();
}
async fn load_latest_config(
@@ -1437,15 +1440,24 @@ impl PluginRequestProcessor {
}
let plugins_manager = self.thread_manager.plugins_manager();
let marketplace_display = marketplace_path.display().to_string();
let plugin_name_for_log = plugin_name.clone();
let request = PluginInstallRequest {
plugin_name,
marketplace_path,
};
let result = plugins_manager
.install_plugin(request)
.await
.map_err(Self::plugin_install_error)?;
let result = match plugins_manager.install_plugin(request).await {
Ok(result) => result,
Err(err) => {
warn!(
marketplace = %marketplace_display,
plugin_name = %plugin_name_for_log,
"failed to install plugin: {err}"
);
return Err(Self::plugin_install_error(err));
}
};
let config = match self.load_latest_config(config_cwd).await {
Ok(config) => config,
Err(err) => {
@@ -1512,11 +1524,20 @@ impl PluginRequestProcessor {
)
.await
.map_err(|err| {
let error_type = remote_plugin_catalog_error_type(&err);
self.track_plugin_install_failed_for_remote_plugin(
&remote_plugin_id,
&remote_marketplace_name,
error_type,
err.to_string(),
);
remote_plugin_catalog_error_to_jsonrpc(
err,
"read remote plugin details before install",
)
})?;
let actual_remote_marketplace_name = remote_detail.marketplace_name.clone();
let remote_plugin_name = remote_detail.summary.name.clone();
if remote_detail.summary.availability == PluginAvailability::DisabledByAdmin {
return Err(invalid_request(format!(
"remote plugin {remote_plugin_id} is disabled by admin"
@@ -1527,31 +1548,48 @@ impl PluginRequestProcessor {
"remote plugin {remote_plugin_id} is not available for install"
)));
}
let actual_remote_marketplace_name = remote_detail.marketplace_name.clone();
// Direct install writes the same cache tree that installed-plugin sync
// prunes before the backend installed snapshot can include this plugin.
let _remote_plugin_cache_mutation =
codex_core_plugins::remote::mark_remote_plugin_cache_mutation_in_flight(
config.codex_home.as_path(),
&actual_remote_marketplace_name,
&remote_detail.summary.name,
&remote_plugin_name,
);
let validated_bundle = codex_core_plugins::remote_bundle::validate_remote_plugin_bundle(
&remote_plugin_id,
&actual_remote_marketplace_name,
&remote_detail.summary.name,
&remote_plugin_name,
remote_detail.release_version.as_deref(),
remote_detail.bundle_download_url.as_deref(),
remote_detail.app_manifest.clone(),
)
.map_err(remote_plugin_bundle_install_error_to_jsonrpc)?;
.map_err(|err| {
let error_type = remote_plugin_bundle_install_error_type(&err);
self.track_plugin_install_failed_for_remote_plugin(
&remote_plugin_id,
&actual_remote_marketplace_name,
error_type,
err.to_string(),
);
remote_plugin_bundle_install_error_to_jsonrpc(err)
})?;
let result = codex_core_plugins::remote_bundle::download_and_install_remote_plugin_bundle(
config.codex_home.to_path_buf(),
validated_bundle,
)
.await
.map_err(remote_plugin_bundle_install_error_to_jsonrpc)?;
.map_err(|err| {
let error_type = remote_plugin_bundle_install_error_type(&err);
self.track_plugin_install_failed_for_remote_plugin(
&remote_plugin_id,
&actual_remote_marketplace_name,
error_type,
err.to_string(),
);
remote_plugin_bundle_install_error_to_jsonrpc(err)
})?;
// Cache first so a backend install cannot succeed when local materialization fails.
// If this backend call fails, the cache entry is harmless because remote installed state
@@ -1563,7 +1601,16 @@ impl PluginRequestProcessor {
&remote_plugin_id,
)
.await
.map_err(|err| remote_plugin_catalog_error_to_jsonrpc(err, "install remote plugin"))?;
.map_err(|err| {
let error_type = remote_plugin_catalog_error_type(&err);
self.track_plugin_install_failed_for_remote_plugin(
&remote_plugin_id,
&actual_remote_marketplace_name,
error_type,
err.to_string(),
);
remote_plugin_catalog_error_to_jsonrpc(err, "install remote plugin")
})?;
self.thread_manager
.plugins_manager()
@@ -1646,6 +1693,32 @@ impl PluginRequestProcessor {
})
}
fn track_plugin_install_failed_for_remote_plugin(
&self,
remote_plugin_id: &str,
marketplace_name: &str,
error_type: &'static str,
error_message: String,
) {
tracing::warn!(
remote_plugin_id = %remote_plugin_id,
marketplace_name = %marketplace_name,
error_type = %error_type,
error = %error_message,
"remote plugin install failed"
);
// The remote id is reported separately; this local name only satisfies
// PluginId validation before remote details are available.
let Ok(plugin_id) = PluginId::new("unknown".to_string(), marketplace_name.to_string())
else {
return;
};
let mut plugin = PluginTelemetryMetadata::from_plugin_id(&plugin_id);
plugin.remote_plugin_id = Some(remote_plugin_id.to_string());
self.analytics_events_client
.track_plugin_install_failed(plugin, error_type.to_string());
}
async fn plugin_apps_needing_auth_for_install(
&self,
config: &Config,
@@ -2145,6 +2218,75 @@ fn remote_plugin_detail_to_info(
}
}
fn remote_plugin_catalog_error_type(err: &RemotePluginCatalogError) -> &'static str {
match err {
RemotePluginCatalogError::AuthRequired => "remote_catalog_auth_required",
RemotePluginCatalogError::UnsupportedAuthMode => "remote_catalog_unsupported_auth_mode",
RemotePluginCatalogError::AuthToken(_) => "remote_catalog_auth_token",
RemotePluginCatalogError::Request { .. } => "remote_catalog_request",
RemotePluginCatalogError::UnexpectedStatus { .. } => "remote_catalog_unexpected_status",
RemotePluginCatalogError::Decode { .. } => "remote_catalog_decode",
RemotePluginCatalogError::InvalidBaseUrl(_) => "remote_catalog_invalid_base_url",
RemotePluginCatalogError::InvalidBaseUrlPath => "remote_catalog_invalid_base_url_path",
RemotePluginCatalogError::UnknownMarketplace { .. } => "remote_catalog_unknown_marketplace",
RemotePluginCatalogError::UnexpectedPluginId { .. } => {
"remote_catalog_unexpected_plugin_id"
}
RemotePluginCatalogError::UnexpectedSkillName { .. } => {
"remote_catalog_unexpected_skill_name"
}
RemotePluginCatalogError::UnexpectedEnabledState { .. } => {
"remote_catalog_unexpected_enabled_state"
}
RemotePluginCatalogError::InvalidPluginPath { .. } => "remote_catalog_invalid_plugin_path",
RemotePluginCatalogError::PluginShareCheckoutNotAvailable { .. } => {
"remote_catalog_plugin_share_checkout_not_available"
}
RemotePluginCatalogError::Archive { .. } => "remote_catalog_archive",
RemotePluginCatalogError::ArchiveJoin(_) => "remote_catalog_archive_join",
RemotePluginCatalogError::ArchiveTooLarge { .. } => "remote_catalog_archive_too_large",
RemotePluginCatalogError::MissingUploadEtag => "remote_catalog_missing_upload_etag",
RemotePluginCatalogError::UnexpectedResponse(_) => "remote_catalog_unexpected_response",
RemotePluginCatalogError::CacheRemove(_) => "remote_catalog_cache_remove",
}
}
fn remote_plugin_bundle_install_error_type(err: &RemotePluginBundleInstallError) -> &'static str {
match err {
RemotePluginBundleInstallError::MissingReleaseVersion { .. } => {
"remote_bundle_missing_release_version"
}
RemotePluginBundleInstallError::InvalidReleaseVersion { .. } => {
"remote_bundle_invalid_release_version"
}
RemotePluginBundleInstallError::MissingBundleDownloadUrl { .. } => {
"remote_bundle_missing_download_url"
}
RemotePluginBundleInstallError::InvalidBundleDownloadUrl { .. } => {
"remote_bundle_invalid_download_url"
}
RemotePluginBundleInstallError::UnsupportedBundleDownloadUrlScheme { .. } => {
"remote_bundle_unsupported_download_url_scheme"
}
RemotePluginBundleInstallError::InvalidPluginId { .. } => "remote_bundle_invalid_plugin_id",
RemotePluginBundleInstallError::DownloadRequest { .. } => "remote_bundle_download_request",
RemotePluginBundleInstallError::DownloadStatus { .. } => "remote_bundle_download_status",
RemotePluginBundleInstallError::DownloadBody { .. } => "remote_bundle_download_body",
RemotePluginBundleInstallError::DownloadTooLarge { .. } => {
"remote_bundle_download_too_large"
}
RemotePluginBundleInstallError::UnsupportedBundleDownloadFinalUrl { .. } => {
"remote_bundle_unsupported_download_final_url"
}
RemotePluginBundleInstallError::ExtractedBundleTooLarge { .. } => {
"remote_bundle_extracted_too_large"
}
RemotePluginBundleInstallError::Io { .. } => "remote_bundle_io",
RemotePluginBundleInstallError::InvalidBundle(_) => "remote_bundle_invalid_bundle",
RemotePluginBundleInstallError::Store(_) => "remote_bundle_store",
}
}
fn remote_plugin_catalog_error_to_jsonrpc(
err: RemotePluginCatalogError,
context: &str,
@@ -135,6 +135,21 @@ impl ThreadGoalRequestProcessor {
.await
.map_err(goal_service_error)?;
let goal = ThreadGoal::from(outcome.goal.clone());
let persist_result = match self.thread_manager.get_thread(thread_id).await {
Ok(thread) => {
// Live goal-first threads can be listed before any user turn is written.
// Use the live path so JSONL and SQLite preview metadata stay in sync.
thread
.append_rollout_items(&[outcome.thread_goal_updated_item()])
.await
}
Err(_) => Ok(()),
};
if let Err(err) = persist_result {
warn!("failed to persist goal update for live thread {thread_id}: {err}");
}
self.outgoing
.send_response(
request_id.clone(),
@@ -4,7 +4,6 @@ use codex_app_server_protocol::SelectedCapabilityRoot;
use codex_extension_api::ExtensionDataInit;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
use codex_utils_path_uri::PathUri;
const THREAD_LIST_DEFAULT_LIMIT: usize = 25;
const THREAD_LIST_MAX_LIMIT: usize = 100;
@@ -906,7 +905,8 @@ impl ThreadRequestProcessor {
"`permissions` cannot be combined with `sandbox`",
));
}
let environment_selections = self.parse_environment_selections(environments)?;
let environment_selections =
resolve_turn_environment_selections(self.thread_manager.as_ref(), environments)?;
let runtime_workspace_roots = runtime_workspace_roots.map(resolve_runtime_workspace_roots);
let mut typesafe_overrides = self.build_thread_config_overrides(
model,
@@ -1311,27 +1311,6 @@ impl ThreadRequestProcessor {
}
}
fn parse_environment_selections(
&self,
environments: Option<Vec<TurnEnvironmentParams>>,
) -> Result<Option<Vec<TurnEnvironmentSelection>>, JSONRPCErrorError> {
let environment_selections = environments.map(|environments| {
environments
.into_iter()
.map(|environment| TurnEnvironmentSelection {
environment_id: environment.environment_id,
cwd: PathUri::from_abs_path(&environment.cwd),
})
.collect::<Vec<_>>()
});
if let Some(environment_selections) = environment_selections.as_ref() {
self.thread_manager
.validate_environment_selections(environment_selections)
.map_err(environment_selection_error)?;
}
Ok(environment_selections)
}
async fn thread_archive_inner(
&self,
params: ThreadArchiveParams,
@@ -1802,16 +1781,22 @@ impl ThreadRequestProcessor {
.list_background_terminals()
.await
.into_iter()
.map(|terminal| ThreadBackgroundTerminal {
item_id: terminal.item_id,
process_id: terminal.process_id,
command: terminal.command,
cwd: terminal.cwd,
os_pid: None,
cpu_percent: None,
rss_kb: None,
.map(|terminal| {
// TODO(anp): Migrate ThreadBackgroundTerminal to PathUri.
let cwd = terminal.cwd.to_abs_path().map_err(|err| {
internal_error(format!("background terminal has invalid cwd: {err}"))
})?;
Ok(ThreadBackgroundTerminal {
item_id: terminal.item_id,
process_id: terminal.process_id,
command: terminal.command,
cwd,
os_pid: None,
cpu_percent: None,
rss_kb: None,
})
})
.collect::<Vec<_>>();
.collect::<Result<Vec<_>, JSONRPCErrorError>>()?;
let (data, next_cursor) = paginate_background_terminals(&terminals, cursor, limit)?;
@@ -1918,6 +1903,7 @@ impl ThreadRequestProcessor {
let store_sort_key = match sort_key.unwrap_or(ThreadSortKey::CreatedAt) {
ThreadSortKey::CreatedAt => StoreThreadSortKey::CreatedAt,
ThreadSortKey::UpdatedAt => StoreThreadSortKey::UpdatedAt,
ThreadSortKey::RecencyAt => StoreThreadSortKey::RecencyAt,
};
let sort_direction = sort_direction.unwrap_or(SortDirection::Desc);
let (stored_threads, next_cursor) = self
@@ -1999,6 +1985,7 @@ impl ThreadRequestProcessor {
let store_sort_key = match sort_key.unwrap_or(ThreadSortKey::CreatedAt) {
ThreadSortKey::CreatedAt => StoreThreadSortKey::CreatedAt,
ThreadSortKey::UpdatedAt => StoreThreadSortKey::UpdatedAt,
ThreadSortKey::RecencyAt => StoreThreadSortKey::RecencyAt,
};
let store_sort_direction = sort_direction.unwrap_or(SortDirection::Desc);
let (allowed_sources, source_kind_filter) = compute_source_filters(source_kinds);
@@ -3716,6 +3703,7 @@ fn thread_backwards_cursor_for_sort_key(
let timestamp = match sort_key {
StoreThreadSortKey::CreatedAt => thread.created_at,
StoreThreadSortKey::UpdatedAt => thread.updated_at,
StoreThreadSortKey::RecencyAt => thread.recency_at,
};
// The state DB stores unique millisecond timestamps. Offset the reverse cursor by one
// millisecond so the opposite-direction query includes the page anchor.
@@ -4160,6 +4148,7 @@ pub(crate) fn thread_from_stored_thread(
},
created_at: thread.created_at.timestamp(),
updated_at: thread.updated_at.timestamp(),
recency_at: Some(thread.recency_at.timestamp()),
status: ThreadStatus::NotLoaded,
path,
cwd,
@@ -4365,6 +4354,7 @@ fn build_thread_from_snapshot(
model_provider: config_snapshot.model_provider_id.clone(),
created_at: now,
updated_at: now,
recency_at: Some(now),
status: ThreadStatus::NotLoaded,
path,
cwd: config_snapshot.cwd().clone(),
@@ -478,6 +478,7 @@ mod thread_processor_behavior_tests {
reasoning_effort: None,
created_at: created_at.with_timezone(&Utc),
updated_at: updated_at.with_timezone(&Utc),
recency_at: updated_at.with_timezone(&Utc),
archived_at: None,
cwd: PathBuf::from("/tmp"),
cli_version: "0.0.0".to_string(),
@@ -178,6 +178,7 @@ mod tests {
model_provider: "mock_provider".to_string(),
created_at: 0,
updated_at: 0,
recency_at: Some(0),
status: ThreadStatus::Idle,
path: None,
cwd: test_path_buf("/tmp").abs(),
@@ -320,6 +320,7 @@ pub(crate) fn summary_to_thread(
model_provider,
created_at: created_at.map(|dt| dt.timestamp()).unwrap_or(0),
updated_at: updated_at.map(|dt| dt.timestamp()).unwrap_or(0),
recency_at: updated_at.map(|dt| dt.timestamp()),
status: ThreadStatus::NotLoaded,
path: (!path.as_os_str().is_empty()).then_some(path),
cwd,
@@ -4,7 +4,6 @@ use codex_protocol::protocol::AdditionalContextKind as CoreAdditionalContextKind
use codex_protocol::protocol::MultiAgentVersion;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use codex_utils_path_uri::PathUri;
const DIRECT_INPUT_TO_MULTI_AGENT_V2_SUBAGENT_ERROR: &str =
"direct app-server input is not allowed for multi-agent v2 sub-agents";
@@ -341,27 +340,6 @@ impl TurnRequestProcessor {
Ok((review_request, hint))
}
fn parse_environment_selections(
&self,
environments: Option<Vec<TurnEnvironmentParams>>,
) -> Result<Option<Vec<TurnEnvironmentSelection>>, JSONRPCErrorError> {
let environment_selections = environments.map(|environments| {
environments
.into_iter()
.map(|environment| TurnEnvironmentSelection {
environment_id: environment.environment_id,
cwd: PathUri::from_abs_path(&environment.cwd),
})
.collect::<Vec<_>>()
});
if let Some(environment_selections) = environment_selections.as_ref() {
self.thread_manager
.validate_environment_selections(environment_selections)
.map_err(environment_selection_error)?;
}
Ok(environment_selections)
}
async fn request_trace_context(
&self,
request_id: &ConnectionRequestId,
@@ -433,7 +411,8 @@ impl TurnRequestProcessor {
self.track_error_response(&request_id, error, /*error_type*/ None);
})?;
let environment_selections = self.parse_environment_selections(params.environments)?;
let environment_selections =
resolve_turn_environment_selections(self.thread_manager.as_ref(), params.environments)?;
// Map v2 input items to core input items.
let mapped_items: Vec<CoreInputItem> = params
@@ -952,8 +931,10 @@ impl TurnRequestProcessor {
thread.as_ref(),
Op::RealtimeConversationStart(ConversationStartParams {
architecture: params.architecture,
client_managed_handoffs: params.client_managed_handoffs.unwrap_or(false),
codex_responses_as_items: params.codex_responses_as_items.unwrap_or(false),
codex_response_item_prefix: params.codex_response_item_prefix,
codex_response_handoff_prefix: params.codex_response_handoff_prefix,
model: params.model,
output_modality: params.output_modality,
include_startup_context: params.include_startup_context.unwrap_or(true),
+6 -6
View File
@@ -8,7 +8,7 @@ use codex_app_server_protocol::SkillsChangedNotification;
use codex_core::ThreadManager;
use codex_core::config::Config;
use codex_core::skills::SkillsLoadInput;
use codex_core::skills::SkillsManager;
use codex_core::skills::SkillsService;
use codex_file_watcher::FileWatcher;
use codex_file_watcher::FileWatcherSubscriber;
use codex_file_watcher::Receiver;
@@ -35,7 +35,7 @@ pub(crate) struct SkillsWatcher {
impl SkillsWatcher {
pub(crate) fn new(
skills_manager: Arc<SkillsManager>,
skills_service: Arc<SkillsService>,
outgoing: Arc<OutgoingMessageSender>,
) -> Arc<Self> {
let file_watcher = match FileWatcher::new() {
@@ -48,7 +48,7 @@ impl SkillsWatcher {
let (subscriber, rx) = file_watcher.add_subscriber();
let shutdown_token = CancellationToken::new();
let shutdown_drop_guard = shutdown_token.clone().drop_guard();
Self::spawn_event_loop(rx, skills_manager, outgoing, shutdown_token.child_token());
Self::spawn_event_loop(rx, skills_service, outgoing, shutdown_token.child_token());
Arc::new(Self {
subscriber,
runtime_extra_roots_registration: Mutex::new(WatchRegistration::default()),
@@ -110,7 +110,7 @@ impl SkillsWatcher {
config.bundled_skills_enabled(),
);
let roots = thread_manager
.skills_manager()
.skills_service()
.skill_roots_for_config(&skills_input, Some(environment.get_filesystem()))
.await
.into_iter()
@@ -124,7 +124,7 @@ impl SkillsWatcher {
fn spawn_event_loop(
rx: Receiver,
skills_manager: Arc<SkillsManager>,
skills_service: Arc<SkillsService>,
outgoing: Arc<OutgoingMessageSender>,
shutdown_token: CancellationToken,
) {
@@ -142,7 +142,7 @@ impl SkillsWatcher {
if event.is_none() {
break;
}
skills_manager.clear_cache();
skills_service.clear_cache();
outgoing
.send_server_notification(ServerNotification::SkillsChanged(
SkillsChangedNotification {},
+1
View File
@@ -897,6 +897,7 @@ mod tests {
model_provider: "mock-provider".to_string(),
created_at: 0,
updated_at: 0,
recency_at: Some(0),
status: ThreadStatus::NotLoaded,
path: None,
cwd: test_path_buf("/tmp").abs(),
+4 -2
View File
@@ -250,10 +250,11 @@ async fn command_execution_request_approval_strips_additional_permissions_withou
item_id: "call_123".to_string(),
started_at_ms: 0,
approval_id: None,
environment_id: None,
reason: Some("Need extra read access".to_string()),
network_approval_context: None,
command: Some("cat file".to_string()),
cwd: Some(absolute_path("/tmp")),
cwd: Some(absolute_path("/tmp").into()),
command_actions: None,
additional_permissions: Some(
codex_app_server_protocol::AdditionalPermissionProfile {
@@ -315,10 +316,11 @@ async fn command_execution_request_approval_keeps_additional_permissions_with_ca
item_id: "call_123".to_string(),
started_at_ms: 0,
approval_id: None,
environment_id: None,
reason: Some("Need extra read access".to_string()),
network_approval_context: None,
command: Some("cat file".to_string()),
cwd: Some(absolute_path("/tmp")),
cwd: Some(absolute_path("/tmp").into()),
command_actions: None,
additional_permissions: Some(
codex_app_server_protocol::AdditionalPermissionProfile {
@@ -4,6 +4,7 @@ use app_test_support::ChatGptAuthFixture;
use app_test_support::TestAppServer;
use app_test_support::to_response;
use app_test_support::write_chatgpt_auth;
use app_test_support::write_models_cache;
use codex_app_server_protocol::AttestationGenerateResponse;
use codex_app_server_protocol::ClientInfo;
use codex_app_server_protocol::InitializeCapabilities;
@@ -36,36 +37,26 @@ async fn attestation_generate_round_trip_adds_header_to_responses_websocket_hand
{
skip_if_no_network!(Ok(()));
let websocket_server = start_websocket_server_with_headers(vec![
// App-server refreshes `/models` over HTTP during thread startup. It points at the same
// local test base URL, so let that non-websocket probe consume one connection before the
// websocket handshake under test arrives.
WebSocketConnectionConfig {
requests: Vec::new(),
response_headers: Vec::new(),
accept_delay: None,
close_after_requests: true,
},
WebSocketConnectionConfig {
requests: vec![
vec![
responses::ev_response_created("warm-1"),
responses::ev_completed("warm-1"),
],
vec![
responses::ev_response_created("resp-1"),
responses::ev_assistant_message("msg-1", "Done"),
responses::ev_completed("resp-1"),
],
let websocket_server = start_websocket_server_with_headers(vec![WebSocketConnectionConfig {
requests: vec![
vec![
responses::ev_response_created("warm-1"),
responses::ev_completed("warm-1"),
],
response_headers: Vec::new(),
accept_delay: None,
close_after_requests: true,
},
])
vec![
responses::ev_response_created("resp-1"),
responses::ev_assistant_message("msg-1", "Done"),
responses::ev_completed("resp-1"),
],
],
response_headers: Vec::new(),
accept_delay: None,
close_after_requests: true,
}])
.await;
let codex_home = TempDir::new()?;
write_models_cache(codex_home.path())?;
create_chatgpt_websocket_config(
codex_home.path(),
&websocket_server.uri().replacen("ws://", "http://", 1),

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