mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
Merge commit '152e000b1b933fc7285fe3fc7fb204106ac01c6b' into HEAD
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
@@ -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 = [
|
||||
|
||||
Generated
+4
@@ -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",
|
||||
|
||||
@@ -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, ×tamp)?,
|
||||
signature: sign_agent_assertion_payload(key, task_id, ×tamp)?,
|
||||
};
|
||||
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, ×tamp)?,
|
||||
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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
+15
-7
@@ -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"
|
||||
},
|
||||
|
||||
+6
-6
@@ -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": {
|
||||
|
||||
+6
-6
@@ -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
@@ -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"
|
||||
|
||||
+58
-57
@@ -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"
|
||||
|
||||
+49
-56
@@ -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"
|
||||
|
||||
+1
-1
@@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
+7
@@ -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": [
|
||||
|
||||
+4
-1
@@ -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."
|
||||
|
||||
Generated
+6
-6
@@ -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",
|
||||
|
||||
Generated
+6
-6
@@ -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."
|
||||
|
||||
+11
-45
@@ -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."
|
||||
|
||||
+12
-1
@@ -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."
|
||||
|
||||
+11
-45
@@ -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."
|
||||
|
||||
+12
-1
@@ -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."
|
||||
|
||||
+4
-1
@@ -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";
|
||||
|
||||
+1
-1
@@ -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
@@ -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";
|
||||
|
||||
+3
-3
@@ -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, };
|
||||
|
||||
Generated
+5
-2
@@ -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, /**
|
||||
|
||||
+1
-1
@@ -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,
|
||||
/**
|
||||
|
||||
+5
-1
@@ -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";
|
||||
|
||||
+2
-2
@@ -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)]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
}],
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(¶ms.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(¶ms);
|
||||
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(¬ification);
|
||||
track_completed_import_notification(analytics_events_client, &analytics_source, ¬ification);
|
||||
if let Some(state_db) = state_db
|
||||
&& let Err(err) = record_completed_import_notification(state_db, ¬ification).await
|
||||
{
|
||||
@@ -571,6 +587,75 @@ async fn send_completed_import_notification(
|
||||
.await;
|
||||
}
|
||||
|
||||
fn log_completed_import_failures(notification: &ExternalAgentConfigImportCompletedNotification) {
|
||||
for type_result in ¬ification.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 ¬ification.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),
|
||||
|
||||
@@ -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 {},
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user