diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index ab7ea7b42e..5039a0bfe7 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1378,7 +1378,7 @@ "cwds": { "description": "Optional working directories used to discover repo marketplaces. When omitted, only home-scoped marketplaces and the official curated marketplace are considered.", "items": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": [ "array", @@ -3457,9 +3457,13 @@ "WindowsSandboxSetupStartParams": { "properties": { "cwd": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "mode": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index d6124b6802..da21699875 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -9994,7 +9994,7 @@ "cwds": { "description": "Optional working directories used to discover repo marketplaces. When omitted, only home-scoped marketplaces and the official curated marketplace are considered.", "items": { - "type": "string" + "$ref": "#/definitions/v2/AbsolutePathBuf" }, "type": [ "array", @@ -15207,9 +15207,13 @@ "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "cwd": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/v2/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "mode": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 6eee862a38..a66c34e14d 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -6817,7 +6817,7 @@ "cwds": { "description": "Optional working directories used to discover repo marketplaces. When omitted, only home-scoped marketplaces and the official curated marketplace are considered.", "items": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": [ "array", @@ -13062,9 +13062,13 @@ "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "cwd": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "mode": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginListParams.json b/codex-rs/app-server-protocol/schema/json/v2/PluginListParams.json index 8d7940b0b5..669ff92b9e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginListParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginListParams.json @@ -1,10 +1,16 @@ { "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "AbsolutePathBuf": { + "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", + "type": "string" + } + }, "properties": { "cwds": { "description": "Optional working directories used to discover repo marketplaces. When omitted, only home-scoped marketplaces and the official curated marketplace are considered.", "items": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": [ "array", diff --git a/codex-rs/app-server-protocol/schema/json/v2/WindowsSandboxSetupStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/WindowsSandboxSetupStartParams.json index 00edc8b02e..ed93913cb3 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/WindowsSandboxSetupStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/WindowsSandboxSetupStartParams.json @@ -1,6 +1,10 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { + "AbsolutePathBuf": { + "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", + "type": "string" + }, "WindowsSandboxSetupMode": { "enum": [ "elevated", @@ -11,9 +15,13 @@ }, "properties": { "cwd": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "mode": { diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PluginListParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PluginListParams.ts index 2dd7db55f6..07ecee5e5f 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PluginListParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PluginListParams.ts @@ -1,13 +1,14 @@ // 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"; export type PluginListParams = { /** * Optional working directories used to discover repo marketplaces. When omitted, * only home-scoped marketplaces and the official curated marketplace are considered. */ -cwds?: Array | null, +cwds?: Array | null, /** * When true, reconcile the official curated marketplace against the remote plugin state * before listing marketplaces. diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/WindowsSandboxSetupStartParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/WindowsSandboxSetupStartParams.ts index cb342c24cd..596c9f5b8c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/WindowsSandboxSetupStartParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/WindowsSandboxSetupStartParams.ts @@ -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 { AbsolutePathBuf } from "../AbsolutePathBuf"; import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode"; -export type WindowsSandboxSetupStartParams = { mode: WindowsSandboxSetupMode, cwd?: string | null, }; +export type WindowsSandboxSetupStartParams = { mode: WindowsSandboxSetupMode, cwd?: AbsolutePathBuf | null, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 60d2665bbe..e0d22bfa24 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -3259,7 +3259,7 @@ pub struct PluginListParams { /// Optional working directories used to discover repo marketplaces. When omitted, /// only home-scoped marketplaces and the official curated marketplace are considered. #[ts(optional = nullable)] - pub cwds: Option>, + pub cwds: Option>, /// When true, reconcile the official curated marketplace against the remote plugin state /// before listing marketplaces. #[serde(default, skip_serializing_if = "std::ops::Not::not")] @@ -5459,7 +5459,7 @@ pub enum WindowsSandboxSetupMode { pub struct WindowsSandboxSetupStartParams { pub mode: WindowsSandboxSetupMode, #[ts(optional = nullable)] - pub cwd: Option, + pub cwd: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index b1652e5cc7..3aff647766 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -189,7 +189,7 @@ Example with notification opt-out: - `config/mcpServer/reload` — reload MCP server config from disk and queue a refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` without restarting the server. - `mcpServerStatus/list` — enumerate configured MCP servers with their tools and auth status, plus resources/resource templates for `full` detail; supports cursor+limit pagination. If `detail` is omitted, the server defaults to `full`. - `mcpServer/resource/read` — read a resource from a thread's configured MCP server by `threadId`, `server`, and `uri`, returning text/blob resource `contents`. -- `windowsSandbox/setupStart` — start Windows sandbox setup for the selected mode (`elevated` or `unelevated`); accepts an optional `cwd` to target setup for a specific workspace, returns `{ started: true }` immediately, and later emits `windowsSandbox/setupCompleted`. +- `windowsSandbox/setupStart` — start Windows sandbox setup for the selected mode (`elevated` or `unelevated`); accepts an optional absolute `cwd` to target setup for a specific workspace, returns `{ started: true }` immediately, and later emits `windowsSandbox/setupCompleted`. - `feedback/upload` — submit a feedback report (classification + optional reason/logs, conversation_id, and optional `extraLogFiles` attachments array); returns the tracking thread id. - `config/read` — fetch the effective config on disk after resolving config layering. - `externalAgentConfig/detect` — detect migratable external-agent artifacts with `includeHome` and optional `cwds`; each detected item includes `cwd` (`null` for home). diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index ecc6f1c22f..5d4d983b11 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -6080,27 +6080,7 @@ impl CodexMessageProcessor { cwds, force_remote_sync, } = params; - let roots = match cwds - .unwrap_or_default() - .into_iter() - .map(AbsolutePathBuf::relative_to_current_dir) - .collect::, _>>() - { - Ok(roots) => roots, - Err(err) => { - self.outgoing - .send_error( - request_id, - JSONRPCErrorError { - code: INTERNAL_ERROR_CODE, - message: format!("invalid plugin cwd: {err}"), - data: None, - }, - ) - .await; - return; - } - }; + let roots = cwds.unwrap_or_default(); plugins_manager.maybe_start_non_curated_plugin_cache_refresh_for_roots(&roots); let mut config = match self.load_latest_config(/*fallback_cwd*/ None).await { @@ -7886,25 +7866,7 @@ impl CodexMessageProcessor { request_id: ConnectionRequestId, params: WindowsSandboxSetupStartParams, ) { - let command_cwd = match params.cwd { - Some(cwd) => match AbsolutePathBuf::relative_to_current_dir(cwd) { - Ok(cwd) => cwd, - Err(err) => { - self.outgoing - .send_error( - request_id, - JSONRPCErrorError { - code: INTERNAL_ERROR_CODE, - message: format!("invalid windows sandbox setup cwd: {err}"), - data: None, - }, - ) - .await; - return; - } - }, - None => self.config.cwd.clone(), - }; + let command_cwd = params.cwd.unwrap_or_else(|| self.config.cwd.clone()); self.outgoing .send_response( request_id.clone(), diff --git a/codex-rs/app-server/tests/suite/v2/plugin_list.rs b/codex-rs/app-server/tests/suite/v2/plugin_list.rs index 6ac33c59d5..8bc4a8598e 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_list.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_list.rs @@ -1,4 +1,3 @@ -use std::path::PathBuf; use std::time::Duration; use anyhow::Result; @@ -68,7 +67,7 @@ async fn plugin_list_skips_invalid_marketplace_file_and_reports_error() -> Resul let request_id = mcp .send_plugin_list_request(PluginListParams { - cwds: Some(vec![repo_root.path().to_path_buf()]), + cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]), force_remote_sync: false, }) .await?; @@ -103,58 +102,28 @@ async fn plugin_list_skips_invalid_marketplace_file_and_reports_error() -> Resul } #[tokio::test] -async fn plugin_list_resolves_relative_cwds() -> Result<()> { +async fn plugin_list_rejects_relative_cwds() -> Result<()> { let codex_home = TempDir::new()?; - write_plugins_enabled_config(codex_home.path())?; - let repo_root = codex_home.path().join("relative-root"); - std::fs::create_dir_all(repo_root.join(".git"))?; - std::fs::create_dir_all(repo_root.join(".agents/plugins"))?; - std::fs::create_dir_all(repo_root.join("plugins/relative-plugin/.codex-plugin"))?; - std::fs::write( - repo_root.join(".agents/plugins/marketplace.json"), - r#"{ - "name": "relative-marketplace", - "plugins": [ - { - "name": "relative-plugin", - "source": { - "source": "local", - "path": "./plugins/relative-plugin" - } - } - ] -}"#, - )?; - std::fs::write( - repo_root.join("plugins/relative-plugin/.codex-plugin/plugin.json"), - r#"{"name":"relative-plugin"}"#, - )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp - .send_plugin_list_request(PluginListParams { - cwds: Some(vec![PathBuf::from("relative-root")]), - force_remote_sync: false, - }) + .send_raw_request( + "plugin/list", + Some(serde_json::json!({ + "cwds": ["relative-root"], + })), + ) .await?; - let response: JSONRPCResponse = timeout( + let err = timeout( DEFAULT_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), ) .await??; - let response: PluginListResponse = to_response(response)?; - assert!( - response - .marketplaces - .iter() - .any(|marketplace| marketplace.name == "relative-marketplace"), - "relative cwd marketplace should be discovered: {:?}", - response.marketplaces - ); + assert_eq!(err.error.code, -32600); + assert!(err.error.message.contains("Invalid request")); Ok(()) } @@ -225,8 +194,8 @@ async fn plugin_list_keeps_valid_marketplaces_when_another_marketplace_fails_to_ let request_id = mcp .send_plugin_list_request(PluginListParams { cwds: Some(vec![ - valid_repo_root.path().to_path_buf(), - invalid_repo_root.path().to_path_buf(), + AbsolutePathBuf::try_from(valid_repo_root.path())?, + AbsolutePathBuf::try_from(invalid_repo_root.path())?, ]), force_remote_sync: false, }) @@ -381,7 +350,7 @@ enabled = false let request_id = mcp .send_plugin_list_request(PluginListParams { - cwds: Some(vec![repo_root.path().to_path_buf()]), + cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]), force_remote_sync: false, }) .await?; @@ -534,8 +503,8 @@ enabled = false let request_id = mcp .send_plugin_list_request(PluginListParams { cwds: Some(vec![ - workspace_enabled.path().to_path_buf(), - workspace_default.path().to_path_buf(), + AbsolutePathBuf::try_from(workspace_enabled.path())?, + AbsolutePathBuf::try_from(workspace_default.path())?, ]), force_remote_sync: false, }) @@ -620,7 +589,7 @@ async fn plugin_list_returns_plugin_interface_with_absolute_asset_paths() -> Res let request_id = mcp .send_plugin_list_request(PluginListParams { - cwds: Some(vec![repo_root.path().to_path_buf()]), + cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]), force_remote_sync: false, }) .await?; @@ -733,7 +702,7 @@ async fn plugin_list_accepts_legacy_string_default_prompt() -> Result<()> { let request_id = mcp .send_plugin_list_request(PluginListParams { - cwds: Some(vec![repo_root.path().to_path_buf()]), + cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]), force_remote_sync: false, }) .await?; diff --git a/codex-rs/app-server/tests/suite/v2/windows_sandbox_setup.rs b/codex-rs/app-server/tests/suite/v2/windows_sandbox_setup.rs index b8b72b5165..a0466a459b 100644 --- a/codex-rs/app-server/tests/suite/v2/windows_sandbox_setup.rs +++ b/codex-rs/app-server/tests/suite/v2/windows_sandbox_setup.rs @@ -64,9 +64,8 @@ async fn windows_sandbox_setup_start_emits_completion_notification() -> Result<( } #[tokio::test] -async fn windows_sandbox_setup_start_accepts_relative_cwd() -> Result<()> { +async fn windows_sandbox_setup_start_rejects_relative_cwd() -> Result<()> { let codex_home = TempDir::new()?; - std::fs::create_dir_all(codex_home.path().join("relative-root"))?; let mut mcp = McpProcess::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; @@ -80,13 +79,13 @@ async fn windows_sandbox_setup_start_accepts_relative_cwd() -> Result<()> { ) .await?; - let response: JSONRPCResponse = timeout( + let err = timeout( DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), ) .await??; - let start_payload: WindowsSandboxSetupStartResponse = to_response(response)?; - assert!(start_payload.started); + assert_eq!(err.error.code, -32600); + assert!(err.error.message.contains("Invalid request")); Ok(()) }