Keep strict app-server path APIs

This commit is contained in:
pakrym-oai
2026-04-07 18:07:11 -07:00
parent 9c04f54e52
commit 25b70c707a
12 changed files with 75 additions and 117 deletions
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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",
@@ -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": {
@@ -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<string> | null,
cwds?: Array<AbsolutePathBuf> | null,
/**
* When true, reconcile the official curated marketplace against the remote plugin state
* before listing marketplaces.
@@ -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, };
@@ -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<Vec<PathBuf>>,
pub cwds: Option<Vec<AbsolutePathBuf>>,
/// 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<PathBuf>,
pub cwd: Option<AbsolutePathBuf>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
+1 -1
View File
@@ -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).
@@ -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::<Result<Vec<_>, _>>()
{
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(),
@@ -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?;
@@ -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(())
}