diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 8e4fda1034..65f3a64328 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2911,26 +2911,19 @@ dependencies = [ name = "codex-core-skills" version = "0.0.0" dependencies = [ - "anyhow", "codex-analytics", "codex-config", "codex-context-fragments", "codex-exec-server", - "codex-login", - "codex-model-provider", "codex-otel", "codex-skills", "codex-utils-absolute-path", "codex-utils-path-uri", "codex-utils-string", "pretty_assertions", - "serde", - "serde_json", "tempfile", - "tokio", "toml 0.9.11+spec-1.1.0", "tracing", - "zip", ] [[package]] diff --git a/codex-rs/core-skills/Cargo.toml b/codex-rs/core-skills/Cargo.toml index c0d27559aa..16e98853f1 100644 --- a/codex-rs/core-skills/Cargo.toml +++ b/codex-rs/core-skills/Cargo.toml @@ -13,23 +13,16 @@ path = "src/lib.rs" workspace = true [dependencies] -anyhow = { workspace = true } codex-analytics = { workspace = true } codex-config = { workspace = true } codex-context-fragments = { workspace = true } codex-exec-server = { workspace = true } -codex-login = { workspace = true } -codex-model-provider = { workspace = true } codex-otel = { workspace = true } codex-skills = { workspace = true } codex-utils-absolute-path = { workspace = true } codex-utils-path-uri = { workspace = true } codex-utils-string = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -tokio = { workspace = true, features = ["fs", "macros", "rt"] } tracing = { workspace = true } -zip = { workspace = true } [dev-dependencies] pretty_assertions = { workspace = true } diff --git a/codex-rs/core-skills/src/lib.rs b/codex-rs/core-skills/src/lib.rs index f7e5ad130a..b373c28d51 100644 --- a/codex-rs/core-skills/src/lib.rs +++ b/codex-rs/core-skills/src/lib.rs @@ -2,7 +2,6 @@ pub mod config_rules; pub mod injection; pub(crate) mod invocation_utils; pub mod model; -pub mod remote; mod skill_instructions; /// Hard byte limit for one model-visible skill instruction body. diff --git a/codex-rs/core-skills/src/remote.rs b/codex-rs/core-skills/src/remote.rs deleted file mode 100644 index b7e27e34f7..0000000000 --- a/codex-rs/core-skills/src/remote.rs +++ /dev/null @@ -1,259 +0,0 @@ -use anyhow::Context; -use anyhow::Result; -use serde::Deserialize; -use std::path::Component; -use std::path::Path; -use std::path::PathBuf; -use std::time::Duration; - -use codex_login::CodexAuth; -use codex_login::default_client::create_client_without_request_logging; - -const REMOTE_SKILLS_API_TIMEOUT: Duration = Duration::from_secs(30); - -// Low-level client for the remote skill API. This is intentionally kept around for -// future wiring, but it is not used yet by any active product surface. - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RemoteSkillScope { - WorkspaceShared, - AllShared, - Personal, - Example, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RemoteSkillProductSurface { - Chatgpt, - Codex, - Api, - Atlas, -} - -fn as_query_scope(scope: RemoteSkillScope) -> Option<&'static str> { - match scope { - RemoteSkillScope::WorkspaceShared => Some("workspace-shared"), - RemoteSkillScope::AllShared => Some("all-shared"), - RemoteSkillScope::Personal => Some("personal"), - RemoteSkillScope::Example => Some("example"), - } -} - -fn as_query_product_surface(product_surface: RemoteSkillProductSurface) -> &'static str { - match product_surface { - RemoteSkillProductSurface::Chatgpt => "chatgpt", - RemoteSkillProductSurface::Codex => "codex", - RemoteSkillProductSurface::Api => "api", - RemoteSkillProductSurface::Atlas => "atlas", - } -} - -fn ensure_codex_backend_auth(auth: Option<&CodexAuth>) -> Result<&CodexAuth> { - let Some(auth) = auth else { - anyhow::bail!("chatgpt authentication required for remote skill scopes"); - }; - if !auth.uses_codex_backend() { - anyhow::bail!( - "chatgpt authentication required for remote skill scopes; api key auth is not supported" - ); - } - Ok(auth) -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RemoteSkillSummary { - pub id: String, - pub name: String, - pub description: String, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RemoteSkillDownloadResult { - pub id: String, - pub path: PathBuf, -} - -#[derive(Debug, Deserialize)] -struct RemoteSkillsResponse { - #[serde(rename = "hazelnuts")] - skills: Vec, -} - -#[derive(Debug, Deserialize)] -struct RemoteSkill { - id: String, - name: String, - description: String, -} - -pub async fn list_remote_skills( - chatgpt_base_url: String, - auth: Option<&CodexAuth>, - scope: RemoteSkillScope, - product_surface: RemoteSkillProductSurface, - enabled: Option, -) -> Result> { - let base_url = chatgpt_base_url.trim_end_matches('/'); - let auth = ensure_codex_backend_auth(auth)?; - - let url = format!("{base_url}/hazelnuts"); - let product_surface = as_query_product_surface(product_surface); - let mut query_params = vec![("product_surface", product_surface)]; - if let Some(scope) = as_query_scope(scope) { - query_params.push(("scope", scope)); - } - if let Some(enabled) = enabled { - let enabled = if enabled { "true" } else { "false" }; - query_params.push(("enabled", enabled)); - } - - let client = create_client_without_request_logging(); - let request = client - .get(&url) - .timeout(REMOTE_SKILLS_API_TIMEOUT) - .query(&query_params) - .headers(codex_model_provider::auth_provider_from_auth(auth).to_auth_headers()); - let response = request - .send() - .await - .with_context(|| format!("Failed to send request to {url}"))?; - - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - if !status.is_success() { - anyhow::bail!("Request failed with status {status} from {url}: {body}"); - } - - let parsed: RemoteSkillsResponse = - serde_json::from_str(&body).context("Failed to parse skills response")?; - - Ok(parsed - .skills - .into_iter() - .map(|skill| RemoteSkillSummary { - id: skill.id, - name: skill.name, - description: skill.description, - }) - .collect()) -} - -pub async fn export_remote_skill( - chatgpt_base_url: String, - codex_home: PathBuf, - auth: Option<&CodexAuth>, - skill_id: &str, -) -> Result { - let auth = ensure_codex_backend_auth(auth)?; - - let client = create_client_without_request_logging(); - let base_url = chatgpt_base_url.trim_end_matches('/'); - let url = format!("{base_url}/hazelnuts/{skill_id}/export"); - let request = client - .get(&url) - .timeout(REMOTE_SKILLS_API_TIMEOUT) - .headers(codex_model_provider::auth_provider_from_auth(auth).to_auth_headers()); - - let response = request - .send() - .await - .with_context(|| format!("Failed to send download request to {url}"))?; - - let status = response.status(); - let body = response.bytes().await.context("Failed to read download")?; - if !status.is_success() { - let body_text = String::from_utf8_lossy(&body); - anyhow::bail!("Download failed with status {status} from {url}: {body_text}"); - } - - if !is_zip_payload(&body) { - anyhow::bail!("Downloaded remote skill payload is not a zip archive"); - } - - let output_dir = codex_home.join("skills").join(skill_id); - tokio::fs::create_dir_all(&output_dir) - .await - .context("Failed to create downloaded skills directory")?; - - let zip_bytes = body.to_vec(); - let output_dir_clone = output_dir.clone(); - let prefix_candidates = vec![skill_id.to_string()]; - tokio::task::spawn_blocking(move || { - extract_zip_to_dir(zip_bytes, &output_dir_clone, &prefix_candidates) - }) - .await - .context("Zip extraction task failed")??; - - Ok(RemoteSkillDownloadResult { - id: skill_id.to_string(), - path: output_dir, - }) -} - -fn safe_join(base: &Path, name: &str) -> Result { - let path = Path::new(name); - for component in path.components() { - match component { - Component::Normal(_) => {} - _ => { - anyhow::bail!("Invalid file path in remote skill payload: {name}"); - } - } - } - Ok(base.join(path)) -} - -fn is_zip_payload(bytes: &[u8]) -> bool { - bytes.starts_with(b"PK\x03\x04") - || bytes.starts_with(b"PK\x05\x06") - || bytes.starts_with(b"PK\x07\x08") -} - -fn extract_zip_to_dir( - bytes: Vec, - output_dir: &Path, - prefix_candidates: &[String], -) -> Result<()> { - let cursor = std::io::Cursor::new(bytes); - let mut archive = zip::ZipArchive::new(cursor).context("Failed to open zip archive")?; - for i in 0..archive.len() { - let mut file = archive.by_index(i).context("Failed to read zip entry")?; - if file.is_dir() { - continue; - } - let raw_name = file.name().to_string(); - let normalized = normalize_zip_name(&raw_name, prefix_candidates); - let Some(normalized) = normalized else { - continue; - }; - let file_path = safe_join(output_dir, &normalized)?; - if let Some(parent) = file_path.parent() { - std::fs::create_dir_all(parent) - .with_context(|| format!("Failed to create parent dir for {normalized}"))?; - } - let mut out = std::fs::File::create(&file_path) - .with_context(|| format!("Failed to create file {normalized}"))?; - std::io::copy(&mut file, &mut out) - .with_context(|| format!("Failed to write skill file {normalized}"))?; - } - Ok(()) -} - -fn normalize_zip_name(name: &str, prefix_candidates: &[String]) -> Option { - let mut trimmed = name.trim_start_matches("./"); - for prefix in prefix_candidates { - if prefix.is_empty() { - continue; - } - let prefix = format!("{prefix}/"); - if let Some(rest) = trimmed.strip_prefix(&prefix) { - trimmed = rest; - break; - } - } - if trimmed.is_empty() { - None - } else { - Some(trimmed.to_string()) - } -} diff --git a/codex-rs/core/src/skills.rs b/codex-rs/core/src/skills.rs index 8fef509fd8..726bf93e89 100644 --- a/codex-rs/core/src/skills.rs +++ b/codex-rs/core/src/skills.rs @@ -22,7 +22,6 @@ pub use codex_core_skills::injection; pub use codex_core_skills::injection::SkillInjections; pub use codex_core_skills::injection::build_skill_injections; pub use codex_core_skills::model; -pub use codex_core_skills::remote; pub use codex_skills::SkillMetadata; pub use codex_skills::SkillPolicy; pub use codex_skills::collect_explicit_skill_mentions;