diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index b76e079dce..1382c61b40 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1375,6 +1375,7 @@ dependencies = [ "codex-login", "codex-plugin", "codex-protocol", + "codex-utils-absolute-path", "os_info", "pretty_assertions", "serde", @@ -2236,6 +2237,7 @@ dependencies = [ "chrono", "codex-config", "codex-protocol", + "codex-utils-absolute-path", "futures", "pretty_assertions", "regex", @@ -2385,6 +2387,7 @@ dependencies = [ "codex-models-manager", "codex-protocol", "codex-shell-command", + "codex-utils-absolute-path", "codex-utils-cli", "codex-utils-json-to-toml", "core_test_support", diff --git a/codex-rs/analytics/Cargo.toml b/codex-rs/analytics/Cargo.toml index 0f36373145..f706814d41 100644 --- a/codex-rs/analytics/Cargo.toml +++ b/codex-rs/analytics/Cargo.toml @@ -29,4 +29,5 @@ tokio = { workspace = true, features = [ tracing = { workspace = true, features = ["log"] } [dev-dependencies] +codex-utils-absolute-path = { workspace = true } pretty_assertions = { workspace = true } diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 0c672f6eec..eb46674156 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -85,6 +85,8 @@ use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; use codex_protocol::protocol::TokenUsage; +use codex_utils_absolute_path::test_support::PathBufExt; +use codex_utils_absolute_path::test_support::test_path_buf; use pretty_assertions::assert_eq; use serde_json::json; use std::collections::HashSet; @@ -112,7 +114,7 @@ fn sample_thread_with_source( updated_at: 2, status: AppServerThreadStatus::Idle, path: None, - cwd: PathBuf::from("/tmp"), + cwd: test_path_buf("/tmp").abs(), cli_version: "0.0.0".to_string(), source, agent_nickname: None, @@ -131,7 +133,7 @@ fn sample_thread_start_response(thread_id: &str, ephemeral: bool, model: &str) - model: model.to_string(), model_provider: "openai".to_string(), service_tier: None, - cwd: PathBuf::from("/tmp"), + cwd: test_path_buf("/tmp").abs(), instruction_sources: Vec::new(), approval_policy: AppServerAskForApproval::OnFailure, approvals_reviewer: AppServerApprovalsReviewer::User, @@ -182,7 +184,7 @@ fn sample_thread_resume_response_with_source( model: model.to_string(), model_provider: "openai".to_string(), service_tier: None, - cwd: PathBuf::from("/tmp"), + cwd: test_path_buf("/tmp").abs(), instruction_sources: Vec::new(), approval_policy: AppServerAskForApproval::OnFailure, approvals_reviewer: AppServerApprovalsReviewer::User, diff --git a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json index 617fa1f3cb..e5287e1c65 100644 --- a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json +++ b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json @@ -75,7 +75,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -326,11 +326,15 @@ ] }, "cwd": { - "description": "The command's working directory.", - "type": [ - "string", - "null" - ] + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ], + "description": "The command's working directory." }, "itemId": { "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 4edff15748..a613590e3f 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -608,7 +608,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -1180,7 +1180,7 @@ "type": "string" }, "cwd": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "source": { "$ref": "#/definitions/GuardianCommandSource" @@ -1211,7 +1211,7 @@ "type": "array" }, "cwd": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "program": { "type": "string" @@ -1240,11 +1240,11 @@ { "properties": { "cwd": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "files": { "items": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": "array" }, @@ -1518,7 +1518,7 @@ "$ref": "#/definitions/HookScope" }, "sourcePath": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "startedAt": { "format": "int64", @@ -2461,8 +2461,12 @@ "type": "integer" }, "cwd": { - "description": "Working directory captured for the thread.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Working directory captured for the thread." }, "ephemeral": { "description": "Whether the thread is ephemeral and should not be materialized on disk.", @@ -2769,8 +2773,12 @@ "type": "array" }, "cwd": { - "description": "The command's working directory.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "The command's working directory." }, "durationMs": { "description": "The duration of the command execution in milliseconds.", @@ -3099,7 +3107,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -3132,9 +3140,13 @@ ] }, "savedPath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "status": { diff --git a/codex-rs/app-server-protocol/schema/json/ServerRequest.json b/codex-rs/app-server-protocol/schema/json/ServerRequest.json index 7c11a4c02b..b31e69f203 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ServerRequest.json @@ -141,7 +141,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -346,11 +346,15 @@ ] }, "cwd": { - "description": "The command's working directory.", - "type": [ - "string", - "null" - ] + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ], + "description": "The command's working directory." }, "itemId": { "type": "string" 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 ec3db74945..49b0c19a37 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 @@ -1841,11 +1841,15 @@ ] }, "cwd": { - "description": "The command's working directory.", - "type": [ - "string", - "null" - ] + "anyOf": [ + { + "$ref": "#/definitions/v2/AbsolutePathBuf" + }, + { + "type": "null" + } + ], + "description": "The command's working directory." }, "itemId": { "type": "string" @@ -5950,7 +5954,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/v2/AbsolutePathBuf" }, "type": { "enum": [ @@ -8243,7 +8247,7 @@ "type": "string" }, "cwd": { - "type": "string" + "$ref": "#/definitions/v2/AbsolutePathBuf" }, "source": { "$ref": "#/definitions/v2/GuardianCommandSource" @@ -8274,7 +8278,7 @@ "type": "array" }, "cwd": { - "type": "string" + "$ref": "#/definitions/v2/AbsolutePathBuf" }, "program": { "type": "string" @@ -8303,11 +8307,11 @@ { "properties": { "cwd": { - "type": "string" + "$ref": "#/definitions/v2/AbsolutePathBuf" }, "files": { "items": { - "type": "string" + "$ref": "#/definitions/v2/AbsolutePathBuf" }, "type": "array" }, @@ -8583,7 +8587,7 @@ "$ref": "#/definitions/v2/HookScope" }, "sourcePath": { - "type": "string" + "$ref": "#/definitions/v2/AbsolutePathBuf" }, "startedAt": { "format": "int64", @@ -12158,15 +12162,23 @@ ] }, "iconLarge": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/v2/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "iconSmall": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/v2/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "shortDescription": { @@ -12647,8 +12659,12 @@ "type": "integer" }, "cwd": { - "description": "Working directory captured for the thread.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/v2/AbsolutePathBuf" + } + ], + "description": "Working directory captured for the thread." }, "ephemeral": { "description": "Whether the thread is ephemeral and should not be materialized on disk.", @@ -12928,13 +12944,13 @@ "description": "Reviewer currently used for approval requests on this thread." }, "cwd": { - "type": "string" + "$ref": "#/definitions/v2/AbsolutePathBuf" }, "instructionSources": { "default": [], "description": "Instruction source files currently loaded for this thread.", "items": { - "type": "string" + "$ref": "#/definitions/v2/AbsolutePathBuf" }, "type": "array" }, @@ -13192,8 +13208,12 @@ "type": "array" }, "cwd": { - "description": "The command's working directory.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/v2/AbsolutePathBuf" + } + ], + "description": "The command's working directory." }, "durationMs": { "description": "The duration of the command execution in milliseconds.", @@ -13522,7 +13542,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/v2/AbsolutePathBuf" }, "type": { "enum": [ @@ -13555,9 +13575,13 @@ ] }, "savedPath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/v2/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "status": { @@ -14280,13 +14304,13 @@ "description": "Reviewer currently used for approval requests on this thread." }, "cwd": { - "type": "string" + "$ref": "#/definitions/v2/AbsolutePathBuf" }, "instructionSources": { "default": [], "description": "Instruction source files currently loaded for this thread.", "items": { - "type": "string" + "$ref": "#/definitions/v2/AbsolutePathBuf" }, "type": "array" }, @@ -14579,13 +14603,13 @@ "description": "Reviewer currently used for approval requests on this thread." }, "cwd": { - "type": "string" + "$ref": "#/definitions/v2/AbsolutePathBuf" }, "instructionSources": { "default": [], "description": "Instruction source files currently loaded for this thread.", "items": { - "type": "string" + "$ref": "#/definitions/v2/AbsolutePathBuf" }, "type": "array" }, 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 9294e533f3..7c1844e6ef 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 @@ -2571,7 +2571,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -4975,7 +4975,7 @@ "type": "string" }, "cwd": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "source": { "$ref": "#/definitions/GuardianCommandSource" @@ -5006,7 +5006,7 @@ "type": "array" }, "cwd": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "program": { "type": "string" @@ -5035,11 +5035,11 @@ { "properties": { "cwd": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "files": { "items": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": "array" }, @@ -5315,7 +5315,7 @@ "$ref": "#/definitions/HookScope" }, "sourcePath": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "startedAt": { "format": "int64", @@ -10006,15 +10006,23 @@ ] }, "iconLarge": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "iconSmall": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "shortDescription": { @@ -10495,8 +10503,12 @@ "type": "integer" }, "cwd": { - "description": "Working directory captured for the thread.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Working directory captured for the thread." }, "ephemeral": { "description": "Whether the thread is ephemeral and should not be materialized on disk.", @@ -10776,13 +10788,13 @@ "description": "Reviewer currently used for approval requests on this thread." }, "cwd": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "instructionSources": { "default": [], "description": "Instruction source files currently loaded for this thread.", "items": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": "array" }, @@ -11040,8 +11052,12 @@ "type": "array" }, "cwd": { - "description": "The command's working directory.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "The command's working directory." }, "durationMs": { "description": "The duration of the command execution in milliseconds.", @@ -11370,7 +11386,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -11403,9 +11419,13 @@ ] }, "savedPath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "status": { @@ -12128,13 +12148,13 @@ "description": "Reviewer currently used for approval requests on this thread." }, "cwd": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "instructionSources": { "default": [], "description": "Instruction source files currently loaded for this thread.", "items": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": "array" }, @@ -12427,13 +12447,13 @@ "description": "Reviewer currently used for approval requests on this thread." }, "cwd": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "instructionSources": { "default": [], "description": "Instruction source files currently loaded for this thread.", "items": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": "array" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/HookCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/HookCompletedNotification.json index bce797086c..2faa347f8e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/HookCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/HookCompletedNotification.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" + }, "HookEventName": { "enum": [ "preToolUse", @@ -103,7 +107,7 @@ "$ref": "#/definitions/HookScope" }, "sourcePath": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "startedAt": { "format": "int64", diff --git a/codex-rs/app-server-protocol/schema/json/v2/HookStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/HookStartedNotification.json index 72f32d0d9d..496175d01b 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/HookStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/HookStartedNotification.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" + }, "HookEventName": { "enum": [ "preToolUse", @@ -103,7 +107,7 @@ "$ref": "#/definitions/HookScope" }, "sourcePath": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "startedAt": { "format": "int64", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json index 2883670c88..4e8cfcb621 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.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" + }, "ByteRange": { "properties": { "end": { @@ -78,7 +82,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -665,8 +669,12 @@ "type": "array" }, "cwd": { - "description": "The command's working directory.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "The command's working directory." }, "durationMs": { "description": "The duration of the command execution in milliseconds.", @@ -995,7 +1003,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -1028,9 +1036,13 @@ ] }, "savedPath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "status": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewCompletedNotification.json index 590a7a5d65..2b223c8bb1 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewCompletedNotification.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" + }, "AutoReviewDecisionSource": { "description": "[UNSTABLE] Source that produced a terminal guardian approval review decision.", "enum": [ @@ -54,7 +58,7 @@ "type": "string" }, "cwd": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "source": { "$ref": "#/definitions/GuardianCommandSource" @@ -85,7 +89,7 @@ "type": "array" }, "cwd": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "program": { "type": "string" @@ -114,11 +118,11 @@ { "properties": { "cwd": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "files": { "items": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": "array" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewStartedNotification.json index fdb01f27e5..e505f13320 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewStartedNotification.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" + }, "GuardianApprovalReview": { "description": "[UNSTABLE] Temporary guardian approval review payload used by `item/autoApprovalReview/*` notifications. This shape is expected to change soon.", "properties": { @@ -47,7 +51,7 @@ "type": "string" }, "cwd": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "source": { "$ref": "#/definitions/GuardianCommandSource" @@ -78,7 +82,7 @@ "type": "array" }, "cwd": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "program": { "type": "string" @@ -107,11 +111,11 @@ { "properties": { "cwd": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "files": { "items": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": "array" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json index c2e71ccba9..5defb06618 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.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" + }, "ByteRange": { "properties": { "end": { @@ -78,7 +82,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -665,8 +669,12 @@ "type": "array" }, "cwd": { - "description": "The command's working directory.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "The command's working directory." }, "durationMs": { "description": "The duration of the command execution in milliseconds.", @@ -995,7 +1003,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -1028,9 +1036,13 @@ ] }, "savedPath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "status": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json index 1194587224..abe36390c9 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json @@ -293,15 +293,23 @@ ] }, "iconLarge": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "iconSmall": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "shortDescription": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json index a7fe2e8d60..b7490d5e12 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.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" + }, "ByteRange": { "properties": { "end": { @@ -214,7 +218,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -808,8 +812,12 @@ "type": "array" }, "cwd": { - "description": "The command's working directory.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "The command's working directory." }, "durationMs": { "description": "The duration of the command execution in milliseconds.", @@ -1138,7 +1146,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -1171,9 +1179,13 @@ ] }, "savedPath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "status": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/SkillsListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/SkillsListResponse.json index 59efc850d4..6c72bfbb68 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/SkillsListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/SkillsListResponse.json @@ -55,15 +55,23 @@ ] }, "iconLarge": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "iconSmall": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "shortDescription": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 5af02d344e..6db7d990f0 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -279,7 +279,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -1036,8 +1036,12 @@ "type": "integer" }, "cwd": { - "description": "Working directory captured for the thread.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Working directory captured for the thread." }, "ephemeral": { "description": "Whether the thread is ephemeral and should not be materialized on disk.", @@ -1322,8 +1326,12 @@ "type": "array" }, "cwd": { - "description": "The command's working directory.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "The command's working directory." }, "durationMs": { "description": "The duration of the command execution in milliseconds.", @@ -1652,7 +1660,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -1685,9 +1693,13 @@ ] }, "savedPath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "status": { @@ -2185,13 +2197,13 @@ "description": "Reviewer currently used for approval requests on this thread." }, "cwd": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "instructionSources": { "default": [], "description": "Instruction source files currently loaded for this thread.", "items": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": "array" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json index 426f34ce35..7435fbeff7 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.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" + }, "AgentPath": { "type": "string" }, @@ -217,7 +221,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -794,8 +798,12 @@ "type": "integer" }, "cwd": { - "description": "Working directory captured for the thread.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Working directory captured for the thread." }, "ephemeral": { "description": "Whether the thread is ephemeral and should not be materialized on disk.", @@ -1080,8 +1088,12 @@ "type": "array" }, "cwd": { - "description": "The command's working directory.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "The command's working directory." }, "durationMs": { "description": "The duration of the command execution in milliseconds.", @@ -1410,7 +1422,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -1443,9 +1455,13 @@ ] }, "savedPath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "status": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json index c869a79749..c6abbab166 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.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" + }, "AgentPath": { "type": "string" }, @@ -217,7 +221,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -794,8 +798,12 @@ "type": "integer" }, "cwd": { - "description": "Working directory captured for the thread.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Working directory captured for the thread." }, "ephemeral": { "description": "Whether the thread is ephemeral and should not be materialized on disk.", @@ -1080,8 +1088,12 @@ "type": "array" }, "cwd": { - "description": "The command's working directory.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "The command's working directory." }, "durationMs": { "description": "The duration of the command execution in milliseconds.", @@ -1410,7 +1422,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -1443,9 +1455,13 @@ ] }, "savedPath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "status": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json index 9569860c38..fb658030f4 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.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" + }, "AgentPath": { "type": "string" }, @@ -217,7 +221,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -794,8 +798,12 @@ "type": "integer" }, "cwd": { - "description": "Working directory captured for the thread.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Working directory captured for the thread." }, "ephemeral": { "description": "Whether the thread is ephemeral and should not be materialized on disk.", @@ -1080,8 +1088,12 @@ "type": "array" }, "cwd": { - "description": "The command's working directory.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "The command's working directory." }, "durationMs": { "description": "The duration of the command execution in milliseconds.", @@ -1410,7 +1422,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -1443,9 +1455,13 @@ ] }, "savedPath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "status": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index d888485d15..9879db55f6 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -279,7 +279,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -1036,8 +1036,12 @@ "type": "integer" }, "cwd": { - "description": "Working directory captured for the thread.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Working directory captured for the thread." }, "ephemeral": { "description": "Whether the thread is ephemeral and should not be materialized on disk.", @@ -1322,8 +1326,12 @@ "type": "array" }, "cwd": { - "description": "The command's working directory.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "The command's working directory." }, "durationMs": { "description": "The duration of the command execution in milliseconds.", @@ -1652,7 +1660,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -1685,9 +1693,13 @@ ] }, "savedPath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "status": { @@ -2185,13 +2197,13 @@ "description": "Reviewer currently used for approval requests on this thread." }, "cwd": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "instructionSources": { "default": [], "description": "Instruction source files currently loaded for this thread.", "items": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": "array" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json index 502dd3961f..84d2768855 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.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" + }, "AgentPath": { "type": "string" }, @@ -217,7 +221,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -794,8 +798,12 @@ "type": "integer" }, "cwd": { - "description": "Working directory captured for the thread.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Working directory captured for the thread." }, "ephemeral": { "description": "Whether the thread is ephemeral and should not be materialized on disk.", @@ -1080,8 +1088,12 @@ "type": "array" }, "cwd": { - "description": "The command's working directory.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "The command's working directory." }, "durationMs": { "description": "The duration of the command execution in milliseconds.", @@ -1410,7 +1422,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -1443,9 +1455,13 @@ ] }, "savedPath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "status": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index 7a0e083093..42d76d6818 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -279,7 +279,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -1036,8 +1036,12 @@ "type": "integer" }, "cwd": { - "description": "Working directory captured for the thread.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Working directory captured for the thread." }, "ephemeral": { "description": "Whether the thread is ephemeral and should not be materialized on disk.", @@ -1322,8 +1326,12 @@ "type": "array" }, "cwd": { - "description": "The command's working directory.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "The command's working directory." }, "durationMs": { "description": "The duration of the command execution in milliseconds.", @@ -1652,7 +1660,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -1685,9 +1693,13 @@ ] }, "savedPath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "status": { @@ -2185,13 +2197,13 @@ "description": "Reviewer currently used for approval requests on this thread." }, "cwd": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "instructionSources": { "default": [], "description": "Instruction source files currently loaded for this thread.", "items": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": "array" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json index ff87af2069..4b24a129bb 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.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" + }, "AgentPath": { "type": "string" }, @@ -217,7 +221,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -794,8 +798,12 @@ "type": "integer" }, "cwd": { - "description": "Working directory captured for the thread.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Working directory captured for the thread." }, "ephemeral": { "description": "Whether the thread is ephemeral and should not be materialized on disk.", @@ -1080,8 +1088,12 @@ "type": "array" }, "cwd": { - "description": "The command's working directory.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "The command's working directory." }, "durationMs": { "description": "The duration of the command execution in milliseconds.", @@ -1410,7 +1422,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -1443,9 +1455,13 @@ ] }, "savedPath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "status": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json index daf821c374..bebc8a6bbf 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.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" + }, "AgentPath": { "type": "string" }, @@ -217,7 +221,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -794,8 +798,12 @@ "type": "integer" }, "cwd": { - "description": "Working directory captured for the thread.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Working directory captured for the thread." }, "ephemeral": { "description": "Whether the thread is ephemeral and should not be materialized on disk.", @@ -1080,8 +1088,12 @@ "type": "array" }, "cwd": { - "description": "The command's working directory.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "The command's working directory." }, "durationMs": { "description": "The duration of the command execution in milliseconds.", @@ -1410,7 +1422,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -1443,9 +1455,13 @@ ] }, "savedPath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "status": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json index 82c2b3c76c..381b78ab71 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.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" + }, "ByteRange": { "properties": { "end": { @@ -214,7 +218,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -808,8 +812,12 @@ "type": "array" }, "cwd": { - "description": "The command's working directory.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "The command's working directory." }, "durationMs": { "description": "The duration of the command execution in milliseconds.", @@ -1138,7 +1146,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -1171,9 +1179,13 @@ ] }, "savedPath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "status": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json index ebb2065cb8..02727fbadd 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.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" + }, "ByteRange": { "properties": { "end": { @@ -214,7 +218,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -808,8 +812,12 @@ "type": "array" }, "cwd": { - "description": "The command's working directory.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "The command's working directory." }, "durationMs": { "description": "The duration of the command execution in milliseconds.", @@ -1138,7 +1146,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -1171,9 +1179,13 @@ ] }, "savedPath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "status": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json index 8b7c2bc410..25c15d78dc 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.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" + }, "ByteRange": { "properties": { "end": { @@ -214,7 +218,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -808,8 +812,12 @@ "type": "array" }, "cwd": { - "description": "The command's working directory.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "The command's working directory." }, "durationMs": { "description": "The duration of the command execution in milliseconds.", @@ -1138,7 +1146,7 @@ "type": "string" }, "path": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, "type": { "enum": [ @@ -1171,9 +1179,13 @@ ] }, "savedPath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } ] }, "status": { diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CommandAction.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CommandAction.ts index ac1314c89b..a17fb06a0c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/CommandAction.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CommandAction.ts @@ -1,5 +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"; -export type CommandAction = { "type": "read", command: string, name: string, path: string, } | { "type": "listFiles", command: string, path: string | null, } | { "type": "search", command: string, query: string | null, path: string | null, } | { "type": "unknown", command: string, }; +export type CommandAction = { "type": "read", command: string, name: string, path: AbsolutePathBuf, } | { "type": "listFiles", command: string, path: string | null, } | { "type": "search", command: string, query: string | null, path: string | null, } | { "type": "unknown", command: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalParams.ts index e1330e2591..59da1de945 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalParams.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 { AdditionalPermissionProfile } from "./AdditionalPermissionProfile"; import type { CommandAction } from "./CommandAction"; import type { CommandExecutionApprovalDecision } from "./CommandExecutionApprovalDecision"; @@ -34,7 +35,7 @@ command?: string | null, /** * The command's working directory. */ -cwd?: string | null, +cwd?: AbsolutePathBuf | null, /** * Best-effort parsed command actions for friendly display. */ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/GuardianApprovalReviewAction.ts b/codex-rs/app-server-protocol/schema/typescript/v2/GuardianApprovalReviewAction.ts index 101fe3f3ef..4bbfe24190 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/GuardianApprovalReviewAction.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/GuardianApprovalReviewAction.ts @@ -1,7 +1,8 @@ // 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 { GuardianCommandSource } from "./GuardianCommandSource"; import type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol"; -export type GuardianApprovalReviewAction = { "type": "command", source: GuardianCommandSource, command: string, cwd: string, } | { "type": "execve", source: GuardianCommandSource, program: string, argv: Array, cwd: string, } | { "type": "applyPatch", cwd: string, files: Array, } | { "type": "networkAccess", target: string, host: string, protocol: NetworkApprovalProtocol, port: number, } | { "type": "mcpToolCall", server: string, toolName: string, connectorId: string | null, connectorName: string | null, toolTitle: string | null, }; +export type GuardianApprovalReviewAction = { "type": "command", source: GuardianCommandSource, command: string, cwd: AbsolutePathBuf, } | { "type": "execve", source: GuardianCommandSource, program: string, argv: Array, cwd: AbsolutePathBuf, } | { "type": "applyPatch", cwd: AbsolutePathBuf, files: Array, } | { "type": "networkAccess", target: string, host: string, protocol: NetworkApprovalProtocol, port: number, } | { "type": "mcpToolCall", server: string, toolName: string, connectorId: string | null, connectorName: string | null, toolTitle: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/HookRunSummary.ts b/codex-rs/app-server-protocol/schema/typescript/v2/HookRunSummary.ts index 68fb4e10af..f6d4b75378 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/HookRunSummary.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/HookRunSummary.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 { HookEventName } from "./HookEventName"; import type { HookExecutionMode } from "./HookExecutionMode"; import type { HookHandlerType } from "./HookHandlerType"; @@ -8,4 +9,4 @@ import type { HookOutputEntry } from "./HookOutputEntry"; import type { HookRunStatus } from "./HookRunStatus"; import type { HookScope } from "./HookScope"; -export type HookRunSummary = { id: string, eventName: HookEventName, handlerType: HookHandlerType, executionMode: HookExecutionMode, scope: HookScope, sourcePath: string, displayOrder: bigint, status: HookRunStatus, statusMessage: string | null, startedAt: bigint, completedAt: bigint | null, durationMs: bigint | null, entries: Array, }; +export type HookRunSummary = { id: string, eventName: HookEventName, handlerType: HookHandlerType, executionMode: HookExecutionMode, scope: HookScope, sourcePath: AbsolutePathBuf, displayOrder: bigint, status: HookRunStatus, statusMessage: string | null, startedAt: bigint, completedAt: bigint | null, durationMs: bigint | null, entries: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SkillInterface.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SkillInterface.ts index 86c37a0bd7..2361afcf0f 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/SkillInterface.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SkillInterface.ts @@ -1,5 +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"; -export type SkillInterface = { displayName?: string, shortDescription?: string, iconSmall?: string, iconLarge?: string, brandColor?: string, defaultPrompt?: string, }; +export type SkillInterface = { displayName?: string, shortDescription?: string, iconSmall?: AbsolutePathBuf, iconLarge?: AbsolutePathBuf, brandColor?: string, defaultPrompt?: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/Thread.ts b/codex-rs/app-server-protocol/schema/typescript/v2/Thread.ts index 57ef3c1075..8c4c9394bf 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/Thread.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/Thread.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 { GitInfo } from "./GitInfo"; import type { SessionSource } from "./SessionSource"; import type { ThreadStatus } from "./ThreadStatus"; @@ -42,7 +43,7 @@ path: string | null, /** * Working directory captured for the thread. */ -cwd: string, +cwd: AbsolutePathBuf, /** * Version of the CLI that created the thread. */ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts index df6c50227d..470e98c9b8 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.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 { ReasoningEffort } from "../ReasoningEffort"; import type { ServiceTier } from "../ServiceTier"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; @@ -8,11 +9,11 @@ import type { AskForApproval } from "./AskForApproval"; import type { SandboxPolicy } from "./SandboxPolicy"; import type { Thread } from "./Thread"; -export type ThreadForkResponse = { thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: string, +export type ThreadForkResponse = { thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: AbsolutePathBuf, /** * Instruction source files currently loaded for this thread. */ -instructionSources: Array, approvalPolicy: AskForApproval, +instructionSources: Array, approvalPolicy: AskForApproval, /** * Reviewer currently used for approval requests on this thread. */ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts index 54d3eaaa8b..e7fe940aa7 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.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 { MessagePhase } from "../MessagePhase"; import type { ReasoningEffort } from "../ReasoningEffort"; import type { JsonValue } from "../serde_json/JsonValue"; @@ -30,7 +31,7 @@ command: string, /** * The command's working directory. */ -cwd: string, +cwd: AbsolutePathBuf, /** * Identifier for the underlying PTY process (when available). */ @@ -97,4 +98,4 @@ reasoningEffort: ReasoningEffort | null, /** * Last known status of the target agents, when available. */ -agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "webSearch", id: string, query: string, action: WebSearchAction | null, } | { "type": "imageView", id: string, path: string, } | { "type": "imageGeneration", id: string, status: string, revisedPrompt: string | null, result: string, savedPath?: string, } | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, }; +agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "webSearch", id: string, query: string, action: WebSearchAction | null, } | { "type": "imageView", id: string, path: AbsolutePathBuf, } | { "type": "imageGeneration", id: string, status: string, revisedPrompt: string | null, result: string, savedPath?: AbsolutePathBuf, } | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts index 3234e8b4b3..177add8350 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.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 { ReasoningEffort } from "../ReasoningEffort"; import type { ServiceTier } from "../ServiceTier"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; @@ -8,11 +9,11 @@ import type { AskForApproval } from "./AskForApproval"; import type { SandboxPolicy } from "./SandboxPolicy"; import type { Thread } from "./Thread"; -export type ThreadResumeResponse = { thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: string, +export type ThreadResumeResponse = { thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: AbsolutePathBuf, /** * Instruction source files currently loaded for this thread. */ -instructionSources: Array, approvalPolicy: AskForApproval, +instructionSources: Array, approvalPolicy: AskForApproval, /** * Reviewer currently used for approval requests on this thread. */ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts index e3355b9108..fd84a41ae8 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.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 { ReasoningEffort } from "../ReasoningEffort"; import type { ServiceTier } from "../ServiceTier"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; @@ -8,11 +9,11 @@ import type { AskForApproval } from "./AskForApproval"; import type { SandboxPolicy } from "./SandboxPolicy"; import type { Thread } from "./Thread"; -export type ThreadStartResponse = { thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: string, +export type ThreadStartResponse = { thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: AbsolutePathBuf, /** * Instruction source files currently loaded for this thread. */ -instructionSources: Array, approvalPolicy: AskForApproval, +instructionSources: Array, approvalPolicy: AskForApproval, /** * Reviewer currently used for approval requests on this thread. */ diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 6853703042..9efc6571f2 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -1065,21 +1065,20 @@ mod tests { use codex_protocol::protocol::RealtimeOutputModality; use codex_protocol::protocol::RealtimeVoice; 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 pretty_assertions::assert_eq; use serde_json::json; use std::path::PathBuf; fn absolute_path_string(path: &str) -> String { - let trimmed = path.trim_start_matches('/'); - if cfg!(windows) { - format!(r"C:\{}", trimmed.replace('/', "\\")) - } else { - format!("/{trimmed}") - } + let path = format!("/{}", path.trim_start_matches('/')); + test_path_buf(&path).display().to_string() } fn absolute_path(path: &str) -> AbsolutePathBuf { - AbsolutePathBuf::from_absolute_path(absolute_path_string(path)).expect("absolute path") + let path = format!("/{}", path.trim_start_matches('/')); + test_path_buf(&path).abs() } #[test] @@ -1410,7 +1409,7 @@ mod tests { updated_at: 2, status: v2::ThreadStatus::Idle, path: None, - cwd: PathBuf::from("/tmp"), + cwd: absolute_path("/tmp"), cli_version: "0.0.0".to_string(), source: v2::SessionSource::Exec, agent_nickname: None, @@ -1422,8 +1421,8 @@ mod tests { model: "gpt-5".to_string(), model_provider: "openai".to_string(), service_tier: None, - cwd: PathBuf::from("/tmp"), - instruction_sources: vec![PathBuf::from("/tmp/AGENTS.md")], + cwd: absolute_path("/tmp"), + instruction_sources: vec![absolute_path("/tmp/AGENTS.md")], approval_policy: v2::AskForApproval::OnFailure, approvals_reviewer: v2::ApprovalsReviewer::User, sandbox: v2::SandboxPolicy::DangerFullAccess, @@ -1450,7 +1449,7 @@ mod tests { "type": "idle" }, "path": null, - "cwd": "/tmp", + "cwd": absolute_path_string("tmp"), "cliVersion": "0.0.0", "source": "exec", "agentNickname": null, @@ -1462,8 +1461,8 @@ mod tests { "model": "gpt-5", "modelProvider": "openai", "serviceTier": null, - "cwd": "/tmp", - "instructionSources": ["/tmp/AGENTS.md"], + "cwd": absolute_path_string("tmp"), + "instructionSources": [absolute_path_string("tmp/AGENTS.md")], "approvalPolicy": "on-failure", "approvalsReviewer": "user", "sandbox": { diff --git a/codex-rs/app-server-protocol/src/protocol/item_builders.rs b/codex-rs/app-server-protocol/src/protocol/item_builders.rs index e6d9588f1c..f69c414b02 100644 --- a/codex-rs/app-server-protocol/src/protocol/item_builders.rs +++ b/codex-rs/app-server-protocol/src/protocol/item_builders.rs @@ -78,7 +78,7 @@ pub fn build_command_execution_approval_request_item( .parsed_cmd .iter() .cloned() - .map(CommandAction::from) + .map(|parsed| CommandAction::from_core_with_cwd(parsed, &payload.cwd)) .collect(), aggregated_output: None, exit_code: None, @@ -98,7 +98,7 @@ pub fn build_command_execution_begin_item(payload: &ExecCommandBeginEvent) -> Th .parsed_cmd .iter() .cloned() - .map(CommandAction::from) + .map(|parsed| CommandAction::from_core_with_cwd(parsed, &payload.cwd)) .collect(), aggregated_output: None, exit_code: None, @@ -125,7 +125,7 @@ pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> Thread .parsed_cmd .iter() .cloned() - .map(CommandAction::from) + .map(|parsed| CommandAction::from_core_with_cwd(parsed, &payload.cwd)) .collect(), aggregated_output, exit_code: Some(payload.exit_code), @@ -179,7 +179,10 @@ pub fn build_item_from_guardian_event( command: command.clone(), }] } else { - parsed_cmd.into_iter().map(CommandAction::from).collect() + parsed_cmd + .into_iter() + .map(|parsed| CommandAction::from_core_with_cwd(parsed, cwd)) + .collect() }; Some(ThreadItem::CommandExecution { id: id.clone(), diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history.rs b/codex-rs/app-server-protocol/src/protocol/thread_history.rs index d2296c75c5..9e94515dd9 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -552,7 +552,7 @@ impl ThreadHistoryBuilder { fn handle_view_image_tool_call(&mut self, payload: &ViewImageToolCallEvent) { let item = ThreadItem::ImageView { id: payload.call_id.clone(), - path: payload.path.to_string_lossy().into_owned(), + path: payload.path.clone(), }; self.upsert_item_in_current_turn(item); } @@ -1193,6 +1193,8 @@ mod tests { use codex_protocol::protocol::TurnStartedEvent; use codex_protocol::protocol::UserMessageEvent; use codex_protocol::protocol::WebSearchEndEvent; + use codex_utils_absolute_path::test_support::PathBufExt; + use codex_utils_absolute_path::test_support::test_path_buf; use pretty_assertions::assert_eq; use std::path::PathBuf; use std::time::Duration; @@ -1397,7 +1399,7 @@ mod tests { status: "completed".into(), revised_prompt: Some("final prompt".into()), result: "Zm9v".into(), - saved_path: Some("/tmp/ig_123.png".into()), + saved_path: Some(test_path_buf("/tmp/ig_123.png").abs()), })), RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-image".into(), @@ -1431,7 +1433,7 @@ mod tests { status: "completed".into(), revised_prompt: Some("final prompt".into()), result: "Zm9v".into(), - saved_path: Some("/tmp/ig_123.png".into()), + saved_path: Some(test_path_buf("/tmp/ig_123.png").abs()), }, ], } @@ -1786,7 +1788,7 @@ mod tests { process_id: Some("pid-1".into()), turn_id: "turn-1".into(), command: vec!["echo".into(), "hello world".into()], - cwd: PathBuf::from("/tmp"), + cwd: test_path_buf("/tmp").abs(), parsed_cmd: vec![ParsedCommand::Unknown { cmd: "echo hello world".into(), }], @@ -1835,7 +1837,7 @@ mod tests { ThreadItem::CommandExecution { id: "exec-1".into(), command: "echo 'hello world'".into(), - cwd: PathBuf::from("/tmp"), + cwd: test_path_buf("/tmp").abs(), process_id: Some("pid-1".into()), source: CommandExecutionSource::Agent, status: CommandExecutionStatus::Completed, @@ -2005,7 +2007,7 @@ mod tests { process_id: Some("pid-2".into()), turn_id: "turn-1".into(), command: vec!["ls".into()], - cwd: PathBuf::from("/tmp"), + cwd: test_path_buf("/tmp").abs(), parsed_cmd: vec![ParsedCommand::Unknown { cmd: "ls".into() }], source: ExecCommandSource::Agent, interaction_input: None, @@ -2047,7 +2049,7 @@ mod tests { ThreadItem::CommandExecution { id: "exec-declined".into(), command: "ls".into(), - cwd: PathBuf::from("/tmp"), + cwd: test_path_buf("/tmp").abs(), process_id: Some("pid-2".into()), source: CommandExecutionSource::Agent, status: CommandExecutionStatus::Declined, @@ -2101,7 +2103,7 @@ mod tests { "type": "command", "source": "shell", "command": "rm -rf /tmp/guardian", - "cwd": "/tmp", + "cwd": test_path_buf("/tmp"), })) .expect("guardian action"), }), @@ -2120,7 +2122,7 @@ mod tests { "type": "command", "source": "shell", "command": "rm -rf /tmp/guardian", - "cwd": "/tmp", + "cwd": test_path_buf("/tmp"), })) .expect("guardian action"), }), @@ -2138,7 +2140,7 @@ mod tests { ThreadItem::CommandExecution { id: "guardian-exec".into(), command: "rm -rf /tmp/guardian".into(), - cwd: PathBuf::from("/tmp"), + cwd: test_path_buf("/tmp").abs(), process_id: None, source: CommandExecutionSource::Agent, status: CommandExecutionStatus::Declined, @@ -2181,7 +2183,7 @@ mod tests { "source": "shell", "program": "/bin/rm", "argv": ["/usr/bin/rm", "-f", "/tmp/file.sqlite"], - "cwd": "/tmp", + "cwd": test_path_buf("/tmp"), })) .expect("guardian action"), }), @@ -2199,7 +2201,7 @@ mod tests { ThreadItem::CommandExecution { id: "guardian-execve".into(), command: "/bin/rm -f /tmp/file.sqlite".into(), - cwd: PathBuf::from("/tmp"), + cwd: test_path_buf("/tmp").abs(), process_id: None, source: CommandExecutionSource::Agent, status: CommandExecutionStatus::InProgress, @@ -2251,7 +2253,7 @@ mod tests { process_id: Some("pid-42".into()), turn_id: "turn-a".into(), command: vec!["echo".into(), "done".into()], - cwd: PathBuf::from("/tmp"), + cwd: test_path_buf("/tmp").abs(), parsed_cmd: vec![ParsedCommand::Unknown { cmd: "echo done".into(), }], @@ -2288,7 +2290,7 @@ mod tests { ThreadItem::CommandExecution { id: "exec-late".into(), command: "echo done".into(), - cwd: PathBuf::from("/tmp"), + cwd: test_path_buf("/tmp").abs(), process_id: Some("pid-42".into()), source: CommandExecutionSource::Agent, status: CommandExecutionStatus::Completed, @@ -2340,7 +2342,7 @@ mod tests { process_id: Some("pid-42".into()), turn_id: "turn-missing".into(), command: vec!["echo".into(), "done".into()], - cwd: PathBuf::from("/tmp"), + cwd: test_path_buf("/tmp").abs(), parsed_cmd: vec![ParsedCommand::Unknown { cmd: "echo done".into(), }], diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 6144ac290f..8ea3f96236 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -80,7 +80,6 @@ use codex_protocol::protocol::RealtimeVoicesList; use codex_protocol::protocol::ReviewDecision as CoreReviewDecision; use codex_protocol::protocol::SessionSource as CoreSessionSource; use codex_protocol::protocol::SkillDependencies as CoreSkillDependencies; -use codex_protocol::protocol::SkillErrorInfo as CoreSkillErrorInfo; use codex_protocol::protocol::SkillInterface as CoreSkillInterface; use codex_protocol::protocol::SkillMetadata as CoreSkillMetadata; use codex_protocol::protocol::SkillScope as CoreSkillScope; @@ -449,7 +448,7 @@ pub struct HookRunSummary { pub handler_type: HookHandlerType, pub execution_mode: HookExecutionMode, pub scope: HookScope, - pub source_path: PathBuf, + pub source_path: AbsolutePathBuf, pub display_order: i64, pub status: HookRunStatus, pub status_message: Option, @@ -1467,7 +1466,7 @@ pub enum CommandAction { Read { command: String, name: String, - path: PathBuf, + path: AbsolutePathBuf, }, ListFiles { command: String, @@ -1545,7 +1544,11 @@ impl CommandAction { command: cmd, name, path, - } => CoreParsedCommand::Read { cmd, name, path }, + } => CoreParsedCommand::Read { + cmd, + name, + path: path.into_path_buf(), + }, CommandAction::ListFiles { command: cmd, path } => { CoreParsedCommand::ListFiles { cmd, path } } @@ -1559,13 +1562,13 @@ impl CommandAction { } } -impl From for CommandAction { - fn from(value: CoreParsedCommand) -> Self { +impl CommandAction { + pub fn from_core_with_cwd(value: CoreParsedCommand, cwd: &AbsolutePathBuf) -> Self { match value { CoreParsedCommand::Read { cmd, name, path } => CommandAction::Read { command: cmd, name, - path, + path: cwd.join(path), }, CoreParsedCommand::ListFiles { cmd, path } => { CommandAction::ListFiles { command: cmd, path } @@ -2720,10 +2723,10 @@ pub struct ThreadStartResponse { pub model: String, pub model_provider: String, pub service_tier: Option, - pub cwd: PathBuf, + pub cwd: AbsolutePathBuf, /// Instruction source files currently loaded for this thread. #[serde(default)] - pub instruction_sources: Vec, + pub instruction_sources: Vec, #[experimental(nested)] pub approval_policy: AskForApproval, /// Reviewer currently used for approval requests on this thread. @@ -2809,10 +2812,10 @@ pub struct ThreadResumeResponse { pub model: String, pub model_provider: String, pub service_tier: Option, - pub cwd: PathBuf, + pub cwd: AbsolutePathBuf, /// Instruction source files currently loaded for this thread. #[serde(default)] - pub instruction_sources: Vec, + pub instruction_sources: Vec, #[experimental(nested)] pub approval_policy: AskForApproval, /// Reviewer currently used for approval requests on this thread. @@ -2889,10 +2892,10 @@ pub struct ThreadForkResponse { pub model: String, pub model_provider: String, pub service_tier: Option, - pub cwd: PathBuf, + pub cwd: AbsolutePathBuf, /// Instruction source files currently loaded for this thread. #[serde(default)] - pub instruction_sources: Vec, + pub instruction_sources: Vec, #[experimental(nested)] pub approval_policy: AskForApproval, /// Reviewer currently used for approval requests on this thread. @@ -3436,9 +3439,9 @@ pub struct SkillInterface { #[ts(optional)] pub short_description: Option, #[ts(optional)] - pub icon_small: Option, + pub icon_small: Option, #[ts(optional)] - pub icon_large: Option, + pub icon_large: Option, #[ts(optional)] pub brand_color: Option, #[ts(optional)] @@ -3722,15 +3725,6 @@ impl From for SkillScope { } } -impl From for SkillErrorInfo { - fn from(value: CoreSkillErrorInfo) -> Self { - Self { - path: value.path, - message: value.message, - } - } -} - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -3755,7 +3749,7 @@ pub struct Thread { /// [UNSTABLE] Path to the thread on disk. pub path: Option, /// Working directory captured for the thread. - pub cwd: PathBuf, + pub cwd: AbsolutePathBuf, /// Version of the CLI that created the thread. pub cli_version: String, /// Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.). @@ -4530,7 +4524,7 @@ pub enum ThreadItem { /// The command to be executed. command: String, /// The command's working directory. - cwd: PathBuf, + cwd: AbsolutePathBuf, /// Identifier for the underlying PTY process (when available). process_id: Option, #[serde(default)] @@ -4614,7 +4608,7 @@ pub enum ThreadItem { }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] - ImageView { id: String, path: String }, + ImageView { id: String, path: AbsolutePathBuf }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] ImageGeneration { @@ -4624,7 +4618,7 @@ pub enum ThreadItem { result: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] - saved_path: Option, + saved_path: Option, }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] @@ -4786,7 +4780,7 @@ impl From for CoreGuardianCommandSource { pub struct GuardianCommandReviewAction { pub source: GuardianCommandSource, pub command: String, - pub cwd: PathBuf, + pub cwd: AbsolutePathBuf, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] @@ -4796,15 +4790,15 @@ pub struct GuardianExecveReviewAction { pub source: GuardianCommandSource, pub program: String, pub argv: Vec, - pub cwd: PathBuf, + pub cwd: AbsolutePathBuf, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct GuardianApplyPatchReviewAction { - pub cwd: PathBuf, - pub files: Vec, + pub cwd: AbsolutePathBuf, + pub files: Vec, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] @@ -4838,7 +4832,7 @@ pub enum GuardianApprovalReviewAction { Command { source: GuardianCommandSource, command: String, - cwd: PathBuf, + cwd: AbsolutePathBuf, }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] @@ -4846,11 +4840,14 @@ pub enum GuardianApprovalReviewAction { source: GuardianCommandSource, program: String, argv: Vec, - cwd: PathBuf, + cwd: AbsolutePathBuf, }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] - ApplyPatch { cwd: PathBuf, files: Vec }, + ApplyPatch { + cwd: AbsolutePathBuf, + files: Vec, + }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] NetworkAccess { @@ -5759,7 +5756,7 @@ pub struct CommandExecutionRequestApprovalParams { /// The command's working directory. #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional = nullable)] - pub cwd: Option, + pub cwd: Option, /// Best-effort parsed command actions for friendly display. #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional = nullable)] @@ -6564,22 +6561,20 @@ mod tests { use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess; use codex_protocol::protocol::ReadOnlyAccess as CoreReadOnlyAccess; use codex_protocol::user_input::UserInput as CoreUserInput; + use codex_utils_absolute_path::test_support::PathBufExt; + use codex_utils_absolute_path::test_support::test_path_buf; use pretty_assertions::assert_eq; use serde_json::json; use std::path::PathBuf; fn absolute_path_string(path: &str) -> String { - let trimmed = path.trim_start_matches('/'); - if cfg!(windows) { - format!(r"C:\{}", trimmed.replace('/', "\\")) - } else { - format!("/{trimmed}") - } + let path = format!("/{}", path.trim_start_matches('/')); + test_path_buf(&path).display().to_string() } fn absolute_path(path: &str) -> AbsolutePathBuf { - AbsolutePathBuf::from_absolute_path(absolute_path_string(path)) - .expect("path must be absolute") + let path = format!("/{}", path.trim_start_matches('/')); + test_path_buf(&path).abs() } fn test_absolute_path() -> AbsolutePathBuf { @@ -6604,7 +6599,7 @@ mod tests { "turnId": "turn_123", "itemId": "call_123", "command": "cat file", - "cwd": "/tmp", + "cwd": absolute_path_string("tmp"), "commandActions": null, "reason": null, "networkApprovalContext": null, @@ -8063,7 +8058,7 @@ mod tests { "type": "command", "source": "shell", "command": "rm -rf /tmp/example.sqlite", - "cwd": "/tmp", + "cwd": absolute_path_string("tmp"), }); let action: GuardianApprovalReviewAction = serde_json::from_value(value.clone()).expect("guardian review action"); @@ -8073,7 +8068,7 @@ mod tests { GuardianApprovalReviewAction::Command { source: GuardianCommandSource::Shell, command: "rm -rf /tmp/example.sqlite".to_string(), - cwd: "/tmp".into(), + cwd: absolute_path("tmp"), } ); assert_eq!( @@ -8647,7 +8642,7 @@ mod tests { "updatedAt": 1, "status": { "type": "idle" }, "path": null, - "cwd": "/tmp", + "cwd": absolute_path_string("tmp"), "cliVersion": "0.0.0", "source": "exec", "agentNickname": null, @@ -8659,7 +8654,7 @@ mod tests { "model": "gpt-5", "modelProvider": "openai", "serviceTier": null, - "cwd": "/tmp", + "cwd": absolute_path_string("tmp"), "approvalPolicy": "on-failure", "approvalsReviewer": "user", "sandbox": { "type": "dangerFullAccess" }, @@ -8673,9 +8668,9 @@ mod tests { let fork: ThreadForkResponse = serde_json::from_value(response).expect("thread/fork response"); - assert_eq!(start.instruction_sources, Vec::::new()); - assert_eq!(resume.instruction_sources, Vec::::new()); - assert_eq!(fork.instruction_sources, Vec::::new()); + assert_eq!(start.instruction_sources, Vec::::new()); + assert_eq!(resume.instruction_sources, Vec::::new()); + assert_eq!(fork.instruction_sources, Vec::::new()); } #[test] diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 0eec5ad0d4..4fddee4a11 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -140,9 +140,9 @@ use codex_protocol::request_user_input::RequestUserInputAnswer as CoreRequestUse use codex_protocol::request_user_input::RequestUserInputResponse as CoreRequestUserInputResponse; use codex_sandboxing::policy_transforms::intersect_permission_profiles; use codex_shell_command::parse_command::shlex_join; +use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashMap; use std::path::Path; -use std::path::PathBuf; use std::sync::Arc; use tokio::sync::Mutex; use tokio::sync::oneshot; @@ -159,7 +159,7 @@ enum CommandExecutionApprovalPresentation { #[derive(Debug, PartialEq)] struct CommandExecutionCompletionItem { command: String, - cwd: PathBuf, + cwd: AbsolutePathBuf, command_actions: Vec, } @@ -644,7 +644,7 @@ pub(crate) async fn apply_bespoke_event_handling( call_id: call_id.clone(), approval_id, command, - cwd, + cwd: cwd.to_path_buf(), reason, parsed_cmd, }; @@ -666,7 +666,7 @@ pub(crate) async fn apply_bespoke_event_handling( let command_actions = parsed_cmd .iter() .cloned() - .map(V2ParsedCommand::from) + .map(|parsed| V2ParsedCommand::from_core_with_cwd(parsed, &cwd)) .collect::>(); let presentation = if let Some(network_approval_context) = network_approval_context.map(V2NetworkApprovalContext::from) @@ -1463,7 +1463,7 @@ pub(crate) async fn apply_bespoke_event_handling( EventMsg::ViewImageToolCall(view_image_event) => { let item = ThreadItem::ImageView { id: view_image_event.call_id.clone(), - path: view_image_event.path.to_string_lossy().into_owned(), + path: view_image_event.path.clone(), }; let started = ItemStartedNotification { thread_id: conversation_id.to_string(), @@ -1648,13 +1648,13 @@ pub(crate) async fn apply_bespoke_event_handling( return; } let item_id = exec_command_begin_event.call_id.clone(); + let cwd = exec_command_begin_event.cwd.clone(); let command_actions = exec_command_begin_event .parsed_cmd .into_iter() - .map(V2ParsedCommand::from) + .map(|parsed| V2ParsedCommand::from_core_with_cwd(parsed, &cwd)) .collect::>(); let command = shlex_join(&exec_command_begin_event.command); - let cwd = exec_command_begin_event.cwd; let process_id = exec_command_begin_event.process_id; let first_start = { let mut state = thread_state.lock().await; @@ -1834,7 +1834,8 @@ pub(crate) async fn apply_bespoke_event_handling( .await { Ok(summary) => { - let mut thread = summary_to_thread(summary); + let fallback_cwd = conversation.config_snapshot().await.cwd; + let mut thread = summary_to_thread(summary, &fallback_cwd); match read_rollout_items_from_rollout(rollout_path.as_path()).await { Ok(items) => { thread.turns = build_turns_from_rollout_items(&items); @@ -2035,7 +2036,7 @@ async fn start_command_execution_item( turn_id: String, item_id: String, command: String, - cwd: PathBuf, + cwd: AbsolutePathBuf, command_actions: Vec, source: CommandExecutionSource, outgoing: &ThreadScopedOutgoingMessageSender, @@ -2078,7 +2079,7 @@ async fn complete_command_execution_item( turn_id: String, item_id: String, command: String, - cwd: PathBuf, + cwd: AbsolutePathBuf, process_id: Option, source: CommandExecutionSource, command_actions: Vec, @@ -3002,6 +3003,8 @@ mod tests { use codex_protocol::protocol::TokenUsage; use codex_protocol::protocol::TokenUsageInfo; 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 core_test_support::load_default_config_for_test; use pretty_assertions::assert_eq; use rmcp::model::Content; @@ -3054,7 +3057,7 @@ mod tests { fn command_execution_completion_item(command: &str) -> CommandExecutionCompletionItem { CommandExecutionCompletionItem { command: command.to_string(), - cwd: PathBuf::from("/tmp"), + cwd: test_path_buf("/tmp").abs(), command_actions: vec![V2ParsedCommand::Unknown { command: command.to_string(), }], @@ -3100,7 +3103,7 @@ mod tests { "type": "command", "source": "shell", "command": format!("rm -f /tmp/{id}.sqlite"), - "cwd": "/tmp", + "cwd": test_path_buf("/tmp"), })) .expect("guardian action"), } @@ -3146,7 +3149,7 @@ mod tests { let action = codex_protocol::protocol::GuardianAssessmentAction::Command { source: codex_protocol::protocol::GuardianCommandSource::Shell, command: "rm -rf /tmp/example.sqlite".to_string(), - cwd: "/tmp".into(), + cwd: test_path_buf("/tmp").abs(), }; let notification = guardian_auto_approval_review_notification( &conversation_id, @@ -3189,7 +3192,7 @@ mod tests { let action = codex_protocol::protocol::GuardianAssessmentAction::Command { source: codex_protocol::protocol::GuardianCommandSource::Shell, command: "rm -rf /tmp/example.sqlite".to_string(), - cwd: "/tmp".into(), + cwd: test_path_buf("/tmp").abs(), }; let notification = guardian_auto_approval_review_notification( &conversation_id, diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index b381317d70..1864509e8d 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -611,15 +611,12 @@ pub(crate) struct CodexMessageProcessorArgs { } impl CodexMessageProcessor { - async fn instruction_sources_from_config(config: &Config) -> Vec { - let mut paths: Vec = config.user_instructions_path.iter().cloned().collect(); + async fn instruction_sources_from_config(config: &Config) -> Vec { + let mut paths: Vec = + config.user_instructions_path.iter().cloned().collect(); match codex_core::discover_project_doc_paths(config, LOCAL_FS.as_ref()).await { Ok(project_doc_paths) => { - paths.extend( - project_doc_paths - .into_iter() - .map(|path| path.as_path().to_path_buf()), - ); + paths.extend(project_doc_paths); } Err(err) => { tracing::warn!(error = %err, "failed to discover project docs for thread response"); @@ -2162,7 +2159,7 @@ impl CodexMessageProcessor { &effective_policy, &effective_file_system_sandbox_policy, effective_network_sandbox_policy, - sandbox_cwd.as_path(), + &sandbox_cwd, &codex_linux_sandbox_exe, use_legacy_landlock, ) { @@ -3201,7 +3198,7 @@ impl CodexMessageProcessor { return; }; - let mut thread = summary_to_thread(summary); + let mut thread = summary_to_thread(summary, &self.config.cwd); self.attach_thread_name(thread_uuid, &mut thread).await; thread.status = resolve_thread_status( self.thread_watch_manager @@ -3284,7 +3281,7 @@ impl CodexMessageProcessor { config_snapshot.session_source.clone(), ); builder.model_provider = Some(model_provider.clone()); - builder.cwd = config_snapshot.cwd.clone(); + builder.cwd = config_snapshot.cwd.to_path_buf(); builder.cli_version = Some(env!("CARGO_PKG_VERSION").to_string()); builder.sandbox_policy = config_snapshot.sandbox_policy.clone(); builder.approval_mode = config_snapshot.approval_policy; @@ -3509,7 +3506,7 @@ impl CodexMessageProcessor { message: format!("failed to read unarchived thread: {err}"), data: None, })?; - Ok(summary_to_thread(summary)) + Ok(summary_to_thread(summary, &self.config.cwd)) } .await; @@ -3770,7 +3767,7 @@ impl CodexMessageProcessor { let conversation_id = summary.conversation_id; thread_ids.insert(conversation_id); - let thread = summary_to_thread(summary); + let thread = summary_to_thread(summary, &self.config.cwd); status_ids.push(thread.id.clone()); threads.push((conversation_id, thread)); } @@ -3914,11 +3911,11 @@ impl CodexMessageProcessor { } let mut thread = if let Some(summary) = db_summary { - summary_to_thread(summary) + summary_to_thread(summary, &self.config.cwd) } else if let Some(rollout_path) = rollout_path.as_ref() { let fallback_provider = self.config.model_provider_id.as_str(); match read_summary_from_rollout(rollout_path, fallback_provider).await { - Ok(summary) => summary_to_thread(summary), + Ok(summary) => summary_to_thread(summary, &self.config.cwd), Err(err) => { self.send_internal_error( request_id, @@ -4424,9 +4421,7 @@ impl CodexMessageProcessor { ); } let mut config_for_instruction_sources = self.config.as_ref().clone(); - if let Ok(cwd) = AbsolutePathBuf::try_from(config_snapshot.cwd.clone()) { - config_for_instruction_sources.cwd = cwd; - } + config_for_instruction_sources.cwd = config_snapshot.cwd.clone(); let instruction_sources = Self::instruction_sources_from_config(&config_for_instruction_sources).await; let thread_summary = match load_thread_summary_for_rollout( @@ -4809,7 +4804,7 @@ impl CodexMessageProcessor { .await { Ok(summary) => { - let mut thread = summary_to_thread(summary); + let mut thread = summary_to_thread(summary, &self.config.cwd); thread.forked_from_id = forked_from_id_from_rollout(fork_rollout_path.as_path()).await; thread @@ -6348,7 +6343,7 @@ impl CodexMessageProcessor { ) .await; let skills_input = codex_core::skills::SkillsLoadInput::new( - cwd_abs, + cwd_abs.clone(), effective_skill_roots, config_layer_stack, config.bundled_skills_enabled(), @@ -7615,7 +7610,7 @@ impl CodexMessageProcessor { if let Some(rollout_path) = review_thread.rollout_path() { match read_summary_from_rollout(rollout_path.as_path(), fallback_provider).await { Ok(summary) => { - let mut thread = summary_to_thread(summary); + let mut thread = summary_to_thread(summary, &self.config.cwd); self.thread_watch_manager .upsert_thread_silently(thread.clone()) .await; @@ -8817,7 +8812,7 @@ fn collect_resume_override_mismatches( } if let Some(requested_cwd) = request.cwd.as_deref() { let requested_cwd_path = std::path::PathBuf::from(requested_cwd); - if requested_cwd_path != config_snapshot.cwd { + if requested_cwd_path != config_snapshot.cwd.as_path() { mismatch_details.push(format!( "cwd requested={} active={}", requested_cwd_path.display(), @@ -9601,7 +9596,7 @@ async fn load_thread_summary_for_rollout( ) -> std::result::Result { let mut thread = read_summary_from_rollout(rollout_path, fallback_provider) .await - .map(summary_to_thread) + .map(|summary| summary_to_thread(summary, &config.cwd)) .map_err(|err| { format!( "failed to load rollout `{}` for thread {thread_id}: {err}", @@ -9612,10 +9607,13 @@ async fn load_thread_summary_for_rollout( if let Some(persisted_metadata) = persisted_metadata { merge_mutable_thread_metadata( &mut thread, - summary_to_thread(summary_from_thread_metadata(persisted_metadata)), + summary_to_thread( + summary_from_thread_metadata(persisted_metadata), + &config.cwd, + ), ); } else if let Some(summary) = read_summary_from_state_db_by_thread_id(config, thread_id).await { - merge_mutable_thread_metadata(&mut thread, summary_to_thread(summary)); + merge_mutable_thread_metadata(&mut thread, summary_to_thread(summary, &config.cwd)); } let title = if let Some(metadata) = persisted_metadata { non_empty_title(metadata) @@ -9735,7 +9733,10 @@ fn build_thread_from_snapshot( } } -pub(crate) fn summary_to_thread(summary: ConversationSummary) -> Thread { +pub(crate) fn summary_to_thread( + summary: ConversationSummary, + fallback_cwd: &AbsolutePathBuf, +) -> Thread { let ConversationSummary { conversation_id, path, @@ -9756,6 +9757,15 @@ pub(crate) fn summary_to_thread(summary: ConversationSummary) -> Thread { branch: info.branch, origin_url: info.origin_url, }); + let cwd = + AbsolutePathBuf::relative_to_current_dir(path_utils::normalize_for_native_workdir(cwd)) + .unwrap_or_else(|err| { + warn!( + path = %path.display(), + "failed to normalize thread cwd while summarizing thread: {err}" + ); + fallback_cwd.clone() + }); Thread { id: conversation_id.to_string(), @@ -9789,6 +9799,8 @@ mod tests { use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; + use codex_utils_absolute_path::test_support::PathBufExt; + use codex_utils_absolute_path::test_support::test_path_buf; use pretty_assertions::assert_eq; use serde_json::json; use std::path::PathBuf; @@ -9923,7 +9935,7 @@ mod tests { approval_policy: codex_protocol::protocol::AskForApproval::OnRequest, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User, sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess, - cwd: PathBuf::from("/tmp"), + cwd: test_path_buf("/tmp").abs(), ephemeral: false, reasoning_effort: None, personality: None, @@ -10283,7 +10295,8 @@ mod tests { fs::write(&path, format!("{}\n", serde_json::to_string(&line)?))?; let summary = read_summary_from_rollout(path.as_path(), "fallback").await?; - let thread = summary_to_thread(summary); + let fallback_cwd = AbsolutePathBuf::from_absolute_path("/")?; + let thread = summary_to_thread(summary, &fallback_cwd); assert_eq!(thread.agent_nickname, Some("atlas".to_string())); assert_eq!(thread.agent_role, Some("explorer".to_string())); @@ -10417,7 +10430,8 @@ mod tests { /*git_origin_url*/ None, ); - let thread = summary_to_thread(summary); + let fallback_cwd = AbsolutePathBuf::from_absolute_path("/")?; + let thread = summary_to_thread(summary, &fallback_cwd); assert_eq!(thread.agent_nickname, Some("atlas".to_string())); assert_eq!(thread.agent_role, Some("explorer".to_string())); diff --git a/codex-rs/app-server/src/fs_watch.rs b/codex-rs/app-server/src/fs_watch.rs index 3a5b226248..ff00051472 100644 --- a/codex-rs/app-server/src/fs_watch.rs +++ b/codex-rs/app-server/src/fs_watch.rs @@ -14,7 +14,6 @@ use codex_core::file_watcher::FileWatcherSubscriber; use codex_core::file_watcher::Receiver; use codex_core::file_watcher::WatchPath; use codex_core::file_watcher::WatchRegistration; -use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashMap; use std::collections::HashSet; use std::collections::hash_map::Entry; @@ -128,7 +127,7 @@ impl FsWatchManager { }; let outgoing = self.outgoing.clone(); let (subscriber, rx) = self.file_watcher.add_subscriber(); - let watch_root = params.path.to_path_buf().clone(); + let watch_root = params.path.clone(); let registration = subscriber.register_paths(vec![WatchPath { path: params.path.to_path_buf(), recursive: false, @@ -166,7 +165,7 @@ impl FsWatchManager { let mut changed_paths = event .paths .into_iter() - .map(|path| AbsolutePathBuf::resolve_path_against_base(&path, &watch_root)) + .map(|path| watch_root.join(path)) .collect::>(); changed_paths.sort_by(|left, right| left.as_path().cmp(right.as_path())); if !changed_paths.is_empty() { diff --git a/codex-rs/app-server/src/thread_state.rs b/codex-rs/app-server/src/thread_state.rs index 80116a695e..504e59468b 100644 --- a/codex-rs/app-server/src/thread_state.rs +++ b/codex-rs/app-server/src/thread_state.rs @@ -8,6 +8,7 @@ use codex_core::CodexThread; use codex_core::ThreadConfigSnapshot; use codex_protocol::ThreadId; use codex_protocol::protocol::EventMsg; +use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashMap; use std::collections::HashSet; use std::path::PathBuf; @@ -28,7 +29,7 @@ pub(crate) struct PendingThreadResumeRequest { pub(crate) request_id: ConnectionRequestId, pub(crate) rollout_path: PathBuf, pub(crate) config_snapshot: ThreadConfigSnapshot, - pub(crate) instruction_sources: Vec, + pub(crate) instruction_sources: Vec, pub(crate) thread_summary: codex_app_server_protocol::Thread, } diff --git a/codex-rs/app-server/src/thread_status.rs b/codex-rs/app-server/src/thread_status.rs index 74bafc146f..f78b8753a9 100644 --- a/codex-rs/app-server/src/thread_status.rs +++ b/codex-rs/app-server/src/thread_status.rs @@ -10,8 +10,6 @@ use codex_app_server_protocol::ThreadStatus; use codex_app_server_protocol::ThreadStatusChangedNotification; use codex_protocol::ThreadId; use std::collections::HashMap; -#[cfg(test)] -use std::path::PathBuf; use std::sync::Arc; use tokio::sync::Mutex; #[cfg(test)] @@ -455,6 +453,8 @@ fn loaded_thread_status(runtime: &RuntimeFacts) -> ThreadStatus { #[cfg(test)] mod tests { use super::*; + use codex_utils_absolute_path::test_support::PathBufExt; + use codex_utils_absolute_path::test_support::test_path_buf; use pretty_assertions::assert_eq; use tokio::time::Duration; use tokio::time::timeout; @@ -895,7 +895,7 @@ mod tests { updated_at: 0, status: ThreadStatus::NotLoaded, path: None, - cwd: PathBuf::from("/tmp"), + cwd: test_path_buf("/tmp").abs(), cli_version: "test".to_string(), agent_nickname: None, agent_role: None, diff --git a/codex-rs/app-server/src/transport/mod.rs b/codex-rs/app-server/src/transport/mod.rs index 44e2cb0f92..75a4971905 100644 --- a/codex-rs/app-server/src/transport/mod.rs +++ b/codex-rs/app-server/src/transport/mod.rs @@ -402,7 +402,6 @@ mod tests { use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use serde_json::json; - use std::path::PathBuf; use tokio::time::Duration; use tokio::time::timeout; @@ -772,7 +771,7 @@ mod tests { reason: Some("Need extra read access".to_string()), network_approval_context: None, command: Some("cat file".to_string()), - cwd: Some(PathBuf::from("/tmp")), + cwd: Some(absolute_path("/tmp")), command_actions: None, additional_permissions: Some( codex_app_server_protocol::AdditionalPermissionProfile { @@ -834,7 +833,7 @@ mod tests { reason: Some("Need extra read access".to_string()), network_approval_context: None, command: Some("cat file".to_string()), - cwd: Some(PathBuf::from("/tmp")), + cwd: Some(absolute_path("/tmp")), command_actions: None, additional_permissions: Some( codex_app_server_protocol::AdditionalPermissionProfile { diff --git a/codex-rs/app-server/tests/common/lib.rs b/codex-rs/app-server/tests/common/lib.rs index 90553760d9..6bb7372162 100644 --- a/codex-rs/app-server/tests/common/lib.rs +++ b/codex-rs/app-server/tests/common/lib.rs @@ -15,10 +15,12 @@ pub use auth_fixtures::write_chatgpt_auth; use codex_app_server_protocol::JSONRPCResponse; pub use config::write_mock_responses_config_toml; pub use config::write_mock_responses_config_toml_with_chatgpt_base_url; +pub use core_test_support::PathBufExt; pub use core_test_support::format_with_current_shell; pub use core_test_support::format_with_current_shell_display; pub use core_test_support::format_with_current_shell_display_non_login; pub use core_test_support::format_with_current_shell_non_login; +pub use core_test_support::test_absolute_path; pub use core_test_support::test_path_buf_with_windows; pub use core_test_support::test_tmp_path; pub use core_test_support::test_tmp_path_buf; diff --git a/codex-rs/app-server/tests/suite/v2/skills_list.rs b/codex-rs/app-server/tests/suite/v2/skills_list.rs index b5fb55f7a8..51dae76b08 100644 --- a/codex-rs/app-server/tests/suite/v2/skills_list.rs +++ b/codex-rs/app-server/tests/suite/v2/skills_list.rs @@ -54,7 +54,7 @@ async fn skills_list_includes_skills_from_per_cwd_extra_user_roots() -> Result<( .await??; let SkillsListResponse { data } = to_response(response)?; assert_eq!(data.len(), 1); - assert_eq!(data[0].cwd, cwd.path().to_path_buf()); + assert_eq!(data[0].cwd.as_path(), cwd.path()); assert!( data[0] .skills @@ -156,7 +156,7 @@ async fn skills_list_ignores_per_cwd_extra_roots_for_unknown_cwd() -> Result<()> .await??; let SkillsListResponse { data } = to_response(response)?; assert_eq!(data.len(), 1); - assert_eq!(data[0].cwd, requested_cwd.path().to_path_buf()); + assert_eq!(data[0].cwd.as_path(), requested_cwd.path()); assert!( data[0] .skills diff --git a/codex-rs/app-server/tests/suite/v2/thread_fork.rs b/codex-rs/app-server/tests/suite/v2/thread_fork.rs index 19bf00f64a..576a46d643 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_fork.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_fork.rs @@ -117,9 +117,9 @@ async fn thread_fork_creates_new_thread_and_emits_started() -> Result<()> { assert_eq!(thread.model_provider, "mock_provider"); assert_eq!(thread.status, ThreadStatus::Idle); let thread_path = thread.path.clone().expect("thread path"); - assert!(thread_path.is_absolute()); - assert_ne!(thread_path, original_path); - assert!(thread.cwd.is_absolute()); + assert!(thread_path.as_path().is_absolute()); + assert_ne!(thread_path.as_path(), original_path); + assert!(thread.cwd.as_path().is_absolute()); assert_eq!(thread.source, SessionSource::VsCode); assert_eq!(thread.name, None); diff --git a/codex-rs/app-server/tests/suite/v2/thread_list.rs b/codex-rs/app-server/tests/suite/v2/thread_list.rs index 62faba9c17..cf1ac4ff11 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_list.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_list.rs @@ -5,6 +5,7 @@ use app_test_support::create_fake_rollout_with_source; use app_test_support::create_final_assistant_message_sse_response; use app_test_support::create_mock_responses_server_sequence; use app_test_support::rollout_path; +use app_test_support::test_absolute_path; use app_test_support::to_response; use chrono::DateTime; use chrono::Utc; @@ -37,7 +38,6 @@ use std::fs; use std::fs::FileTimes; use std::fs::OpenOptions; use std::path::Path; -use std::path::PathBuf; use tempfile::TempDir; use tokio::time::timeout; use uuid::Uuid; @@ -372,7 +372,7 @@ async fn thread_list_pagination_next_cursor_none_on_last_page() -> Result<()> { assert_eq!(thread.model_provider, "mock_provider"); assert!(thread.created_at > 0); assert_eq!(thread.updated_at, thread.created_at); - assert_eq!(thread.cwd, PathBuf::from("/")); + assert_eq!(thread.cwd, test_absolute_path("/")); assert_eq!(thread.cli_version, "0.0.0"); assert_eq!(thread.source, SessionSource::Cli); assert_eq!(thread.git_info, None); @@ -399,7 +399,7 @@ async fn thread_list_pagination_next_cursor_none_on_last_page() -> Result<()> { assert_eq!(thread.model_provider, "mock_provider"); assert!(thread.created_at > 0); assert_eq!(thread.updated_at, thread.created_at); - assert_eq!(thread.cwd, PathBuf::from("/")); + assert_eq!(thread.cwd, test_absolute_path("/")); assert_eq!(thread.cli_version, "0.0.0"); assert_eq!(thread.source, SessionSource::Cli); assert_eq!(thread.git_info, None); @@ -455,7 +455,7 @@ async fn thread_list_respects_provider_filter() -> Result<()> { let expected_ts = chrono::DateTime::parse_from_rfc3339("2025-01-02T11:00:00Z")?.timestamp(); assert_eq!(thread.created_at, expected_ts); assert_eq!(thread.updated_at, expected_ts); - assert_eq!(thread.cwd, PathBuf::from("/")); + assert_eq!(thread.cwd, test_absolute_path("/")); assert_eq!(thread.cli_version, "0.0.0"); assert_eq!(thread.source, SessionSource::Cli); assert_eq!(thread.git_info, None); @@ -518,7 +518,7 @@ async fn thread_list_respects_cwd_filter() -> Result<()> { assert_eq!(data.len(), 1); assert_eq!(data[0].id, filtered_id); assert_ne!(data[0].id, unfiltered_id); - assert_eq!(data[0].cwd, target_cwd); + assert_eq!(data[0].cwd.as_path(), target_cwd.as_path()); Ok(()) } @@ -1032,7 +1032,7 @@ async fn thread_list_includes_git_info() -> Result<()> { }; assert_eq!(thread.git_info, Some(expected_git)); assert_eq!(thread.source, SessionSource::Cli); - assert_eq!(thread.cwd, PathBuf::from("/")); + assert_eq!(thread.cwd, test_absolute_path("/")); assert_eq!(thread.cli_version, "0.0.0"); Ok(()) diff --git a/codex-rs/app-server/tests/suite/v2/thread_read.rs b/codex-rs/app-server/tests/suite/v2/thread_read.rs index c5fd699855..e4ff900150 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_read.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_read.rs @@ -2,6 +2,7 @@ use anyhow::Result; use app_test_support::McpProcess; use app_test_support::create_fake_rollout_with_text_elements; use app_test_support::create_mock_responses_server_repeating_assistant; +use app_test_support::test_absolute_path; use app_test_support::to_response; use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCResponse; @@ -32,7 +33,6 @@ use core_test_support::responses; use pretty_assertions::assert_eq; use serde_json::Value; use std::path::Path; -use std::path::PathBuf; use tempfile::TempDir; use tokio::time::timeout; @@ -83,7 +83,7 @@ async fn thread_read_returns_summary_without_turns() -> Result<()> { assert_eq!(thread.model_provider, "mock_provider"); assert!(!thread.ephemeral, "stored rollouts should not be ephemeral"); assert!(thread.path.as_ref().expect("thread path").is_absolute()); - assert_eq!(thread.cwd, PathBuf::from("/")); + assert_eq!(thread.cwd, test_absolute_path("/")); assert_eq!(thread.cli_version, "0.0.0"); assert_eq!(thread.source, SessionSource::Cli); assert_eq!(thread.git_info, None); diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index 0a302c2380..db3a0897fc 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -8,6 +8,7 @@ use app_test_support::create_mock_responses_server_repeating_assistant; use app_test_support::create_mock_responses_server_sequence_unchecked; use app_test_support::create_shell_command_sse_response; use app_test_support::rollout_path; +use app_test_support::test_absolute_path; use app_test_support::to_response; use app_test_support::write_chatgpt_auth; use chrono::Utc; @@ -244,7 +245,7 @@ async fn thread_resume_returns_rollout_history() -> Result<()> { assert_eq!(thread.preview, preview); assert_eq!(thread.model_provider, "mock_provider"); assert!(thread.path.as_ref().expect("thread path").is_absolute()); - assert_eq!(thread.cwd, PathBuf::from("/")); + assert_eq!(thread.cwd, test_absolute_path("/")); assert_eq!(thread.cli_version, "0.0.0"); assert_eq!(thread.source, SessionSource::Cli); assert_eq!(thread.git_info, None); @@ -1613,7 +1614,7 @@ async fn thread_resume_prefers_path_over_thread_id() -> Result<()> { let resume_id = mcp .send_thread_resume_request(ThreadResumeParams { thread_id: "not-a-valid-thread-id".to_string(), - path: Some(thread_path), + path: Some(thread_path.to_path_buf()), ..Default::default() }) .await?; @@ -1742,7 +1743,7 @@ async fn start_materialized_thread_and_restart( Ok(RestartedThreadFixture { mcp: second_mcp, thread_id, - rollout_file_path, + rollout_file_path: rollout_file_path.to_path_buf(), }) } diff --git a/codex-rs/app-server/tests/suite/v2/thread_start.rs b/codex-rs/app-server/tests/suite/v2/thread_start.rs index f7d3147414..b8d0db9a01 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_start.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_start.rs @@ -211,14 +211,15 @@ async fn thread_start_response_includes_loaded_instruction_sources() -> Result<( } #[cfg(windows)] -fn normalize_path_for_comparison(path: PathBuf) -> PathBuf { +fn normalize_path_for_comparison(path: impl AsRef) -> PathBuf { + let path = path.as_ref(); let path = path.display().to_string(); PathBuf::from(path.strip_prefix(r"\\?\").unwrap_or(&path)) } #[cfg(not(windows))] -fn normalize_path_for_comparison(path: PathBuf) -> PathBuf { - path +fn normalize_path_for_comparison(path: impl AsRef) -> PathBuf { + path.as_ref().to_path_buf() } #[tokio::test] diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 694d0a6eef..e8682d7325 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -1716,7 +1716,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> { else { unreachable!("loop ensures we break on command execution items"); }; - assert_eq!(cwd, second_cwd); + assert_eq!(cwd.as_path(), second_cwd.as_path()); let expected_command = format_with_current_shell_display("echo second turn"); assert_eq!(command, expected_command); assert_eq!(status, CommandExecutionStatus::InProgress); diff --git a/codex-rs/app-server/tests/suite/v2/turn_start_zsh_fork.rs b/codex-rs/app-server/tests/suite/v2/turn_start_zsh_fork.rs index 105ae54542..eda24358ce 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start_zsh_fork.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start_zsh_fork.rs @@ -166,7 +166,7 @@ async fn turn_start_shell_zsh_fork_executes_command_v2() -> Result<()> { assert!(command.contains("/bin/sh -c")); assert!(command.contains("sleep 0.01")); assert!(command.contains(&release_marker.display().to_string())); - assert_eq!(cwd, workspace); + assert_eq!(cwd.as_path(), workspace.as_path()); mcp.interrupt_turn_and_wait_for_aborted(thread.id, turn.id, DEFAULT_READ_TIMEOUT) .await?; diff --git a/codex-rs/core-skills/src/loader.rs b/codex-rs/core-skills/src/loader.rs index 498ae14244..b8303df1a3 100644 --- a/codex-rs/core-skills/src/loader.rs +++ b/codex-rs/core-skills/src/loader.rs @@ -511,8 +511,11 @@ fn discover_skills_under_root( } } -fn parse_skill_file(path: &Path, scope: SkillScope) -> Result { - let contents = fs::read_to_string(path).map_err(SkillParseError::Read)?; +fn parse_skill_file( + path: &AbsolutePathBuf, + scope: SkillScope, +) -> Result { + let contents = fs::read_to_string(path.as_path()).map_err(SkillParseError::Read)?; let frontmatter = extract_frontmatter(&contents).ok_or(SkillParseError::MissingFrontmatter)?; @@ -524,8 +527,8 @@ fn parse_skill_file(path: &Path, scope: SkillScope) -> Result Result String { .unwrap_or_else(|| base_name.to_string()) } -fn load_skill_metadata(skill_path: &Path) -> LoadedSkillMetadata { +fn load_skill_metadata(skill_path: &AbsolutePathBuf) -> LoadedSkillMetadata { // Fail open: optional metadata should not block loading SKILL.md. let Some(skill_dir) = skill_path.parent() else { return LoadedSkillMetadata::default(); @@ -592,11 +593,11 @@ fn load_skill_metadata(skill_path: &Path) -> LoadedSkillMetadata { let metadata_path = skill_dir .join(SKILLS_METADATA_DIR) .join(SKILLS_METADATA_FILENAME); - if !metadata_path.exists() { + if !metadata_path.as_path().exists() { return LoadedSkillMetadata::default(); } - let contents = match fs::read_to_string(&metadata_path) { + let contents = match fs::read_to_string(metadata_path.as_path()) { Ok(contents) => contents, Err(error) => { tracing::warn!( @@ -609,7 +610,7 @@ fn load_skill_metadata(skill_path: &Path) -> LoadedSkillMetadata { }; let parsed: SkillMetadataFile = { - let _guard = AbsolutePathBufGuard::new(skill_dir); + let _guard = AbsolutePathBufGuard::new(skill_dir.as_path()); match serde_yaml::from_str(&contents) { Ok(parsed) => parsed, Err(error) => { @@ -629,13 +630,16 @@ fn load_skill_metadata(skill_path: &Path) -> LoadedSkillMetadata { policy, } = parsed; LoadedSkillMetadata { - interface: resolve_interface(interface, skill_dir), + interface: resolve_interface(interface, &skill_dir), dependencies: resolve_dependencies(dependencies), policy: resolve_policy(policy), } } -fn resolve_interface(interface: Option, skill_dir: &Path) -> Option { +fn resolve_interface( + interface: Option, + skill_dir: &AbsolutePathBuf, +) -> Option { let interface = interface?; let interface = SkillInterface { display_name: resolve_str( @@ -726,10 +730,10 @@ fn resolve_dependency_tool(tool: DependencyTool) -> Option } fn resolve_asset_path( - skill_dir: &Path, + skill_dir: &AbsolutePathBuf, field: &'static str, path: Option, -) -> Option { +) -> Option { // Icons must be relative paths under the skill's assets/ directory; otherwise return None. let path = path?; if path.as_os_str().is_empty() { diff --git a/codex-rs/core-skills/src/loader_tests.rs b/codex-rs/core-skills/src/loader_tests.rs index a54e9fcce2..9c7561a77f 100644 --- a/codex-rs/core-skills/src/loader_tests.rs +++ b/codex-rs/core-skills/src/loader_tests.rs @@ -495,16 +495,8 @@ interface: interface: Some(SkillInterface { display_name: Some("UI Skill".to_string()), short_description: Some("short desc".to_string()), - icon_small: Some( - normalized_skill_dir - .join("assets/small-400px.png") - .to_path_buf() - ), - icon_large: Some( - normalized_skill_dir - .join("assets/large-logo.svg") - .to_path_buf() - ), + icon_small: Some(normalized_skill_dir.join("assets/small-400px.png")), + icon_large: Some(normalized_skill_dir.join("assets/large-logo.svg")), brand_color: Some("#3B82F6".to_string()), default_prompt: Some("default prompt".to_string()), }), @@ -656,8 +648,8 @@ async fn accepts_icon_paths_under_assets_dir() { interface: Some(SkillInterface { display_name: Some("UI Skill".to_string()), short_description: None, - icon_small: Some(normalized_skill_dir.join("assets/icon.png").to_path_buf()), - icon_large: Some(normalized_skill_dir.join("assets/logo.svg").to_path_buf()), + icon_small: Some(normalized_skill_dir.join("assets/icon.png")), + icon_large: Some(normalized_skill_dir.join("assets/logo.svg")), brand_color: None, default_prompt: None, }), @@ -749,11 +741,7 @@ async fn ignores_default_prompt_over_max_length() { interface: Some(SkillInterface { display_name: Some("UI Skill".to_string()), short_description: None, - icon_small: Some( - normalized_skill_dir - .join("assets/small-400px.png") - .to_path_buf() - ), + icon_small: Some(normalized_skill_dir.join("assets/small-400px.png")), icon_large: None, brand_color: None, default_prompt: None, diff --git a/codex-rs/core-skills/src/model.rs b/codex-rs/core-skills/src/model.rs index fed6d766eb..a3523d2f43 100644 --- a/codex-rs/core-skills/src/model.rs +++ b/codex-rs/core-skills/src/model.rs @@ -1,6 +1,5 @@ use std::collections::HashMap; use std::collections::HashSet; -use std::path::PathBuf; use std::sync::Arc; use codex_protocol::protocol::Product; @@ -56,8 +55,8 @@ pub struct SkillPolicy { pub struct SkillInterface { pub display_name: Option, pub short_description: Option, - pub icon_small: Option, - pub icon_large: Option, + pub icon_small: Option, + pub icon_large: Option, pub brand_color: Option, pub default_prompt: Option, } diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 21725cd334..f16f76dad1 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1,7 +1,6 @@ use std::collections::HashMap; use std::collections::HashSet; use std::fmt::Debug; -use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicU64; @@ -650,7 +649,7 @@ impl Codex { network_sandbox_policy: config.permissions.network_sandbox_policy, windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), - codex_home: config.codex_home.to_path_buf(), + codex_home: config.codex_home.clone(), thread_name: None, original_config_do_not_use: Arc::clone(&config), metrics_service_name, @@ -1132,7 +1131,7 @@ fn local_time_context() -> (String, String) { async fn thread_title_from_state_db( state_db: Option<&state_db::StateDbHandle>, - codex_home: &Path, + codex_home: &AbsolutePathBuf, conversation_id: ThreadId, ) -> Option { if let Some(metadata) = state_db @@ -1189,7 +1188,7 @@ pub(crate) struct SessionConfiguration { /// the process-wide current working directory. cwd: AbsolutePathBuf, /// Directory containing all Codex state for this session. - codex_home: PathBuf, + codex_home: AbsolutePathBuf, /// Optional user-facing name for the thread, updated during the session. thread_name: Option, @@ -1208,7 +1207,7 @@ pub(crate) struct SessionConfiguration { } impl SessionConfiguration { - pub(crate) fn codex_home(&self) -> &PathBuf { + pub(crate) fn codex_home(&self) -> &AbsolutePathBuf { &self.codex_home } @@ -1220,7 +1219,7 @@ impl SessionConfiguration { approval_policy: self.approval_policy.value(), approvals_reviewer: self.approvals_reviewer, sandbox_policy: self.sandbox_policy.get().clone(), - cwd: self.cwd.to_path_buf(), + cwd: self.cwd.clone(), ephemeral: self.original_config_do_not_use.ephemeral, reasoning_effort: self.collaboration_mode.reasoning_effort(), personality: self.personality, @@ -1471,7 +1470,7 @@ impl Session { per_turn_config } - pub(crate) async fn codex_home(&self) -> PathBuf { + pub(crate) async fn codex_home(&self) -> AbsolutePathBuf { let state = self.state.lock().await; state.session_configuration.codex_home().clone() } @@ -1572,7 +1571,7 @@ impl Session { conversation_id.to_string(), &session_source, sub_id.clone(), - cwd.to_path_buf(), + cwd.clone(), session_configuration.sandbox_policy.get(), session_configuration.windows_sandbox_level, )); @@ -1927,9 +1926,9 @@ impl Session { tx } else { ShellSnapshot::start_snapshotting( - config.codex_home.to_path_buf(), + config.codex_home.clone(), conversation_id, - session_configuration.cwd.to_path_buf(), + session_configuration.cwd.clone(), &mut default_shell, session_telemetry.clone(), ) @@ -2133,7 +2132,7 @@ impl Session { approval_policy: session_configuration.approval_policy.value(), approvals_reviewer: session_configuration.approvals_reviewer, sandbox_policy: session_configuration.sandbox_policy.get().clone(), - cwd: session_configuration.cwd.to_path_buf(), + cwd: session_configuration.cwd.clone(), reasoning_effort: session_configuration.collaboration_mode.reasoning_effort(), history_log_id, history_entry_count, @@ -2492,9 +2491,9 @@ impl Session { fn maybe_refresh_shell_snapshot_for_cwd( &self, - previous_cwd: &Path, - next_cwd: &Path, - codex_home: &Path, + previous_cwd: &AbsolutePathBuf, + next_cwd: &AbsolutePathBuf, + codex_home: &AbsolutePathBuf, session_source: &SessionSource, ) { if previous_cwd == next_cwd { @@ -2513,9 +2512,9 @@ impl Session { } ShellSnapshot::refresh_snapshot( - codex_home.to_path_buf(), + codex_home.clone(), self.conversation_id, - next_cwd.to_path_buf(), + next_cwd.clone(), self.services.user_shell.as_ref().clone(), self.services.shell_snapshot_tx.clone(), self.services.session_telemetry.clone(), @@ -2779,14 +2778,6 @@ impl Session { } }; - let config_toml_path = match AbsolutePathBuf::try_from(config_toml_path) { - Ok(path) => path, - Err(err) => { - warn!("failed to resolve user config path while reloading layer: {err}"); - return; - } - }; - let mut state = self.state.lock().await; let mut config = (*state.session_configuration.original_config_do_not_use).clone(); config.config_layer_stack = config @@ -3184,7 +3175,7 @@ impl Session { call_id: String, approval_id: Option, command: Vec, - cwd: PathBuf, + cwd: AbsolutePathBuf, reason: Option, network_approval_context: Option, proposed_execpolicy_amendment: Option, @@ -5395,9 +5386,12 @@ mod handlers { cwds: Vec, force_reload: bool, ) { - let cwds = if cwds.is_empty() { + let default_cwd = { let state = sess.state.lock().await; - vec![state.session_configuration.cwd.to_path_buf()] + state.session_configuration.cwd.to_path_buf() + }; + let cwds = if cwds.is_empty() { + vec![default_cwd] } else { cwds }; @@ -5412,14 +5406,13 @@ mod handlers { let cwd_abs = match AbsolutePathBuf::relative_to_current_dir(cwd.as_path()) { Ok(path) => path, Err(err) => { - let message = err.to_string(); - let cwd_for_entry = cwd.clone(); + let error_path = cwd.clone(); skills.push(SkillsListEntry { - cwd: cwd_for_entry.clone(), + cwd, skills: Vec::new(), errors: vec![SkillErrorInfo { - path: cwd_for_entry, - message, + path: error_path, + message: err.to_string(), }], }); continue; @@ -5436,14 +5429,13 @@ mod handlers { { Ok(config_layer_stack) => config_layer_stack, Err(err) => { - let message = err.to_string(); - let cwd_for_entry = cwd.clone(); + let error_path = cwd.clone(); skills.push(SkillsListEntry { - cwd: cwd_for_entry.clone(), + cwd, skills: Vec::new(), errors: vec![SkillErrorInfo { - path: cwd_for_entry, - message, + path: error_path, + message: err.to_string(), }], }); continue; @@ -5456,7 +5448,7 @@ mod handlers { ) .await; let skills_input = crate::SkillsLoadInput::new( - cwd_abs, + cwd_abs.clone(), effective_skill_roots, config_layer_stack, config.bundled_skills_enabled(), @@ -5870,7 +5862,7 @@ mod handlers { sess.maybe_emit_unknown_model_warning_for_turn(turn_context.as_ref()) .await; sess.refresh_mcp_servers_if_requested(&turn_context).await; - match resolve_review_request(review_request, turn_context.cwd.as_path()) { + match resolve_review_request(review_request, &turn_context.cwd) { Ok(resolved) => { spawn_review_thread( Arc::clone(sess), @@ -5986,7 +5978,7 @@ async fn spawn_review_thread( sess.conversation_id.to_string(), &session_source, review_turn_id.clone(), - parent_turn_context.cwd.to_path_buf(), + parent_turn_context.cwd.clone(), parent_turn_context.sandbox_policy.get(), parent_turn_context.windows_sandbox_level, )); @@ -6501,7 +6493,7 @@ pub(crate) async fn run_turn( let stop_request = codex_hooks::StopRequest { session_id: sess.conversation_id, turn_id: turn_context.sub_id.clone(), - cwd: turn_context.cwd.to_path_buf(), + cwd: turn_context.cwd.clone(), transcript_path: sess.hook_transcript_path().await, model: turn_context.model_info.slug.clone(), permission_mode: stop_hook_permission_mode, @@ -6551,7 +6543,7 @@ pub(crate) async fn run_turn( .hooks() .dispatch(HookPayload { session_id: sess.conversation_id, - cwd: turn_context.cwd.to_path_buf(), + cwd: turn_context.cwd.clone(), client: turn_context.app_server_client_name.clone(), triggered_at: chrono::Utc::now(), hook_event: HookEvent::AfterAgent { diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index 78ab8c163c..d5af9304af 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -571,7 +571,7 @@ async fn handle_patch_approval( new_guardian_review_id(), GuardianApprovalRequest::ApplyPatch { id: approval_id.clone(), - cwd: parent_ctx.cwd.to_path_buf(), + cwd: parent_ctx.cwd.clone(), files, patch, }, diff --git a/codex-rs/core/src/codex_delegate_tests.rs b/codex-rs/core/src/codex_delegate_tests.rs index 62ee884815..beee114a96 100644 --- a/codex-rs/core/src/codex_delegate_tests.rs +++ b/codex-rs/core/src/codex_delegate_tests.rs @@ -23,9 +23,10 @@ use codex_protocol::request_permissions::RequestPermissionsResponse; use codex_protocol::request_user_input::RequestUserInputAnswer; use codex_protocol::request_user_input::RequestUserInputEvent; use codex_protocol::request_user_input::RequestUserInputQuestion; +use core_test_support::PathBufExt; +use core_test_support::test_path_buf; use pretty_assertions::assert_eq; use std::collections::HashMap; -use std::path::PathBuf; use std::sync::Arc; use tokio::sync::Mutex; use tokio::sync::watch; @@ -282,7 +283,7 @@ async fn handle_exec_approval_uses_call_id_for_guardian_review_and_approval_id_f approval_id: Some("callback-approval-1".to_string()), turn_id: "child-turn-1".to_string(), command: vec!["rm".to_string(), "-rf".to_string(), "tmp".to_string()], - cwd: PathBuf::from("/tmp"), + cwd: test_path_buf("/tmp").abs(), reason: Some("unsafe subcommand".to_string()), network_approval_context: None, proposed_execpolicy_amendment: None, @@ -313,7 +314,7 @@ async fn handle_exec_approval_uses_call_id_for_guardian_review_and_approval_id_f let expected_action = GuardianAssessmentAction::Command { source: GuardianCommandSource::Shell, command: "rm -rf tmp".to_string(), - cwd: "/tmp".into(), + cwd: test_path_buf("/tmp").abs(), }; assert!(!assessment_event.id.is_empty()); assert_eq!( diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 25a72d3bb7..bf213a6457 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -1919,7 +1919,7 @@ async fn set_rate_limits_retains_previous_credits() { network_sandbox_policy: config.permissions.network_sandbox_policy, windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), - codex_home: config.codex_home.to_path_buf(), + codex_home: config.codex_home.clone(), thread_name: None, original_config_do_not_use: Arc::clone(&config), metrics_service_name: None, @@ -2021,7 +2021,7 @@ async fn set_rate_limits_updates_plan_type_when_present() { network_sandbox_policy: config.permissions.network_sandbox_policy, windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), - codex_home: config.codex_home.to_path_buf(), + codex_home: config.codex_home.clone(), thread_name: None, original_config_do_not_use: Arc::clone(&config), metrics_service_name: None, @@ -2373,7 +2373,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati network_sandbox_policy: config.permissions.network_sandbox_policy, windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), - codex_home: config.codex_home.to_path_buf(), + codex_home: config.codex_home.clone(), thread_name: None, original_config_do_not_use: Arc::clone(&config), metrics_service_name: None, @@ -2636,7 +2636,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() { network_sandbox_policy: config.permissions.network_sandbox_policy, windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), - codex_home: config.codex_home.to_path_buf(), + codex_home: config.codex_home.clone(), thread_name: None, original_config_do_not_use: Arc::clone(&config), metrics_service_name: None, @@ -2740,7 +2740,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { network_sandbox_policy: config.permissions.network_sandbox_policy, windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), - codex_home: config.codex_home.to_path_buf(), + codex_home: config.codex_home.clone(), thread_name: None, original_config_do_not_use: Arc::clone(&config), metrics_service_name: None, @@ -3586,7 +3586,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( network_sandbox_policy: config.permissions.network_sandbox_policy, windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), - codex_home: config.codex_home.to_path_buf(), + codex_home: config.codex_home.clone(), thread_name: None, original_config_do_not_use: Arc::clone(&config), metrics_service_name: None, @@ -4121,7 +4121,7 @@ async fn handle_output_item_done_records_image_save_history_message() { let turn_context = Arc::new(turn_context); let call_id = "ig_history_records_message"; let expected_saved_path = crate::stream_events_utils::image_generation_artifact_path( - turn_context.config.codex_home.as_path(), + &turn_context.config.codex_home, &session.conversation_id.to_string(), call_id, ); @@ -4145,7 +4145,7 @@ async fn handle_output_item_done_records_image_save_history_message() { let history = session.clone_history().await; let image_output_path = crate::stream_events_utils::image_generation_artifact_path( - turn_context.config.codex_home.as_path(), + &turn_context.config.codex_home, &session.conversation_id.to_string(), "", ); @@ -4173,7 +4173,7 @@ async fn handle_output_item_done_skips_image_save_message_when_save_fails() { let turn_context = Arc::new(turn_context); let call_id = "ig_history_no_message"; let expected_saved_path = crate::stream_events_utils::image_generation_artifact_path( - turn_context.config.codex_home.as_path(), + &turn_context.config.codex_home, &session.conversation_id.to_string(), call_id, ); diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 2a68b1e4f9..fa9f835181 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -24,6 +24,7 @@ use codex_protocol::protocol::ThreadMemoryMode; use codex_protocol::protocol::TokenUsage; use codex_protocol::protocol::W3cTraceContext; use codex_protocol::user_input::UserInput; +use codex_utils_absolute_path::AbsolutePathBuf; use rmcp::model::ReadResourceRequestParams; use std::collections::HashMap; use std::path::PathBuf; @@ -40,7 +41,7 @@ pub struct ThreadConfigSnapshot { pub approval_policy: AskForApproval, pub approvals_reviewer: ApprovalsReviewer, pub sandbox_policy: SandboxPolicy, - pub cwd: PathBuf, + pub cwd: AbsolutePathBuf, pub ephemeral: bool, pub reasoning_effort: Option, pub personality: Option, diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 88987e4166..b58acbf651 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -272,7 +272,7 @@ pub struct Config { pub user_instructions: Option, /// Path to the global AGENTS file loaded into `user_instructions`. - pub user_instructions_path: Option, + pub user_instructions_path: Option, /// Base instructions override. pub base_instructions: Option, @@ -1578,7 +1578,6 @@ impl Config { }; let memories_root = memory_root(&codex_home); std::fs::create_dir_all(&memories_root)?; - let memories_root = AbsolutePathBuf::from_absolute_path(&memories_root)?; if !additional_writable_roots .iter() .any(|existing| existing == &memories_root) @@ -2210,11 +2209,10 @@ impl Config { Ok(config) } - fn load_instructions(codex_dir: Option<&Path>) -> Option { + fn load_instructions(codex_dir: Option<&AbsolutePathBuf>) -> Option { let base = codex_dir?; for candidate in [LOCAL_PROJECT_DOC_FILENAME, DEFAULT_PROJECT_DOC_FILENAME] { - let mut path = base.to_path_buf(); - path.push(candidate); + let path = base.join(candidate); if let Ok(contents) = std::fs::read_to_string(&path) { let trimmed = contents.trim(); if !trimmed.is_empty() { @@ -2297,7 +2295,7 @@ impl Config { struct LoadedUserInstructions { contents: String, - path: PathBuf, + path: AbsolutePathBuf, } pub(crate) fn uses_deprecated_instructions_file(config_layer_stack: &ConfigLayerStack) -> bool { diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index fd1395d7b7..d5b96902dc 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -221,7 +221,7 @@ pub async fn process_exec_tool_call( sandbox_policy: &SandboxPolicy, file_system_sandbox_policy: &FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, - sandbox_cwd: &Path, + sandbox_cwd: &AbsolutePathBuf, codex_linux_sandbox_exe: &Option, use_legacy_landlock: bool, stdout_stream: Option, @@ -247,7 +247,7 @@ pub fn build_exec_request( sandbox_policy: &SandboxPolicy, file_system_sandbox_policy: &FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, - sandbox_cwd: &Path, + sandbox_cwd: &AbsolutePathBuf, codex_linux_sandbox_exe: &Option, use_legacy_landlock: bool, ) -> Result { @@ -845,7 +845,7 @@ async fn exec( program: PathBuf::from(program), args: args.into(), arg0: arg0_ref, - cwd: cwd.to_path_buf(), + cwd, network_sandbox_policy, // The environment already has attempt-scoped proxy settings from // apply_to_env_for_attempt above. Passing network here would reapply @@ -881,7 +881,7 @@ pub(crate) fn unsupported_windows_restricted_token_sandbox_reason( sandbox_policy: &SandboxPolicy, file_system_sandbox_policy: &FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, - sandbox_policy_cwd: &Path, + sandbox_policy_cwd: &AbsolutePathBuf, windows_sandbox_level: WindowsSandboxLevel, ) -> Option { if windows_sandbox_level == WindowsSandboxLevel::Elevated { @@ -912,7 +912,7 @@ pub(crate) fn resolve_windows_restricted_token_filesystem_overrides( sandbox_policy: &SandboxPolicy, file_system_sandbox_policy: &FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, - sandbox_policy_cwd: &Path, + sandbox_policy_cwd: &AbsolutePathBuf, windows_sandbox_level: WindowsSandboxLevel, ) -> std::result::Result, String> { if sandbox != SandboxType::WindowsRestrictedToken @@ -1048,7 +1048,7 @@ pub(crate) fn resolve_windows_elevated_filesystem_overrides( sandbox_policy: &SandboxPolicy, file_system_sandbox_policy: &FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, - sandbox_policy_cwd: &Path, + sandbox_policy_cwd: &AbsolutePathBuf, use_windows_elevated_backend: bool, ) -> std::result::Result, String> { if sandbox != SandboxType::WindowsRestrictedToken || !use_windows_elevated_backend { diff --git a/codex-rs/core/src/exec_tests.rs b/codex-rs/core/src/exec_tests.rs index 937a7d6f80..796062b3fc 100644 --- a/codex-rs/core/src/exec_tests.rs +++ b/codex-rs/core/src/exec_tests.rs @@ -1,6 +1,8 @@ use super::*; use codex_protocol::config_types::WindowsSandboxLevel; use codex_sandboxing::SandboxType; +use core_test_support::PathBufExt; +use core_test_support::PathExt; use pretty_assertions::assert_eq; use std::collections::HashMap; use std::time::Duration; @@ -369,7 +371,7 @@ async fn process_exec_tool_call_preserves_full_buffer_capture_policy() -> Result &sandbox_policy, &FileSystemSandboxPolicy::from(&sandbox_policy), NetworkSandboxPolicy::Enabled, - cwd.as_path(), + &cwd, &None, /*use_legacy_landlock*/ false, /*stdout_stream*/ None, @@ -436,7 +438,7 @@ fn windows_restricted_token_rejects_network_only_restrictions() { network_access: codex_protocol::protocol::NetworkAccess::Restricted, }; let file_system_policy = FileSystemSandboxPolicy::unrestricted(); - let sandbox_policy_cwd = std::env::current_dir().expect("cwd"); + let sandbox_policy_cwd = AbsolutePathBuf::current_dir().expect("cwd"); assert_eq!( unsupported_windows_restricted_token_sandbox_reason( @@ -457,7 +459,7 @@ fn windows_restricted_token_rejects_network_only_restrictions() { fn windows_restricted_token_allows_legacy_restricted_policies() { let policy = SandboxPolicy::new_read_only_policy(); let file_system_policy = FileSystemSandboxPolicy::from(&policy); - let sandbox_policy_cwd = std::env::current_dir().expect("cwd"); + let sandbox_policy_cwd = AbsolutePathBuf::current_dir().expect("cwd"); assert_eq!( unsupported_windows_restricted_token_sandbox_reason( @@ -482,7 +484,7 @@ fn windows_restricted_token_allows_legacy_workspace_write_policies() { exclude_slash_tmp: true, }; let file_system_policy = FileSystemSandboxPolicy::from(&policy); - let sandbox_policy_cwd = std::env::current_dir().expect("cwd"); + let sandbox_policy_cwd = AbsolutePathBuf::current_dir().expect("cwd"); assert_eq!( unsupported_windows_restricted_token_sandbox_reason( @@ -520,7 +522,7 @@ fn windows_elevated_allows_legacy_restricted_read_policies() { &policy, &file_system_policy, NetworkSandboxPolicy::Restricted, - temp_dir.path(), + &temp_dir.path().abs(), WindowsSandboxLevel::Elevated, ), None @@ -561,7 +563,7 @@ fn windows_restricted_token_rejects_split_only_filesystem_policies() { &policy, &file_system_policy, NetworkSandboxPolicy::Restricted, - temp_dir.path(), + &temp_dir.path().abs(), WindowsSandboxLevel::RestrictedToken, ), Some( @@ -605,7 +607,7 @@ fn windows_restricted_token_rejects_root_write_read_only_carveouts() { &policy, &file_system_policy, NetworkSandboxPolicy::Restricted, - temp_dir.path(), + &temp_dir.path().abs(), WindowsSandboxLevel::RestrictedToken, ), Some( @@ -618,9 +620,11 @@ fn windows_restricted_token_rejects_root_write_read_only_carveouts() { #[test] fn windows_restricted_token_supports_full_read_split_write_read_carveouts() { let temp_dir = tempfile::TempDir::new().expect("tempdir"); - let cwd = dunce::canonicalize(temp_dir.path()).expect("canonicalize temp dir"); + let cwd = dunce::canonicalize(temp_dir.path()) + .expect("canonicalize temp dir") + .abs(); let docs = cwd.join("docs"); - std::fs::create_dir_all(&docs).expect("create docs"); + std::fs::create_dir_all(docs.as_path()).expect("create docs"); let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], read_only_access: codex_protocol::protocol::ReadOnlyAccess::FullAccess, @@ -642,20 +646,14 @@ fn windows_restricted_token_supports_full_read_split_write_read_carveouts() { access: codex_protocol::permissions::FileSystemAccessMode::Write, }, codex_protocol::permissions::FileSystemSandboxEntry { - path: codex_protocol::permissions::FileSystemPath::Path { - path: codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(&docs) - .expect("absolute docs"), - }, + path: codex_protocol::permissions::FileSystemPath::Path { path: docs.clone() }, access: codex_protocol::permissions::FileSystemAccessMode::Read, }, ]); // The legacy workspace-write root already protects top-level `.codex`, so // the restricted-token overlay only needs the extra read-only docs carveout. - let expected_deny_write_paths = vec![ - codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(&docs) - .expect("absolute docs"), - ]; + let expected_deny_write_paths = vec![docs]; assert_eq!( resolve_windows_restricted_token_filesystem_overrides( @@ -700,7 +698,7 @@ fn windows_elevated_supports_split_restricted_read_roots() { &policy, &file_system_policy, NetworkSandboxPolicy::Restricted, - temp_dir.path(), + &temp_dir.path().abs(), /*use_windows_elevated_backend*/ true, ), Ok(Some(WindowsSandboxFilesystemOverrides { @@ -752,7 +750,7 @@ fn windows_elevated_supports_split_write_read_carveouts() { &policy, &file_system_policy, NetworkSandboxPolicy::Restricted, - temp_dir.path(), + &temp_dir.path().abs(), /*use_windows_elevated_backend*/ true, ), Ok(Some(WindowsSandboxFilesystemOverrides { @@ -806,7 +804,7 @@ fn windows_elevated_rejects_unreadable_split_carveouts() { &policy, &file_system_policy, NetworkSandboxPolicy::Restricted, - temp_dir.path(), + &temp_dir.path().abs(), WindowsSandboxLevel::Elevated, ), Some( @@ -864,7 +862,7 @@ fn windows_elevated_rejects_reopened_writable_descendants() { &policy, &file_system_policy, NetworkSandboxPolicy::Restricted, - temp_dir.path(), + &temp_dir.path().abs(), WindowsSandboxLevel::Elevated, ), Some( @@ -998,7 +996,7 @@ async fn process_exec_tool_call_respects_cancellation_token() -> Result<()> { &SandboxPolicy::DangerFullAccess, &FileSystemSandboxPolicy::from(&SandboxPolicy::DangerFullAccess), NetworkSandboxPolicy::Enabled, - cwd.as_path(), + &cwd, &None, /*use_legacy_landlock*/ false, /*stdout_stream*/ None, diff --git a/codex-rs/core/src/guardian/approval_request.rs b/codex-rs/core/src/guardian/approval_request.rs index c9cc9d9fa4..6d1d3f76af 100644 --- a/codex-rs/core/src/guardian/approval_request.rs +++ b/codex-rs/core/src/guardian/approval_request.rs @@ -1,5 +1,4 @@ use std::path::Path; -use std::path::PathBuf; use codex_protocol::approvals::GuardianAssessmentAction; use codex_protocol::approvals::GuardianCommandSource; @@ -17,7 +16,7 @@ pub(crate) enum GuardianApprovalRequest { Shell { id: String, command: Vec, - cwd: PathBuf, + cwd: AbsolutePathBuf, sandbox_permissions: crate::sandboxing::SandboxPermissions, additional_permissions: Option, justification: Option, @@ -25,7 +24,7 @@ pub(crate) enum GuardianApprovalRequest { ExecCommand { id: String, command: Vec, - cwd: PathBuf, + cwd: AbsolutePathBuf, sandbox_permissions: crate::sandboxing::SandboxPermissions, additional_permissions: Option, justification: Option, @@ -37,12 +36,12 @@ pub(crate) enum GuardianApprovalRequest { source: GuardianCommandSource, program: String, argv: Vec, - cwd: PathBuf, + cwd: AbsolutePathBuf, additional_permissions: Option, }, ApplyPatch { id: String, - cwd: PathBuf, + cwd: AbsolutePathBuf, files: Vec, patch: String, }, @@ -151,12 +150,12 @@ fn serialize_command_guardian_action( fn command_assessment_action( source: GuardianCommandSource, command: &[String], - cwd: &Path, + cwd: &AbsolutePathBuf, ) -> GuardianAssessmentAction { GuardianAssessmentAction::Command { source, command: codex_shell_command::parse_command::shlex_join(command), - cwd: cwd.to_path_buf(), + cwd: cwd.clone(), } } @@ -323,10 +322,7 @@ pub(crate) fn guardian_assessment_action( GuardianApprovalRequest::ApplyPatch { cwd, files, .. } => { GuardianAssessmentAction::ApplyPatch { cwd: cwd.clone(), - files: files - .iter() - .map(codex_utils_absolute_path::AbsolutePathBuf::to_path_buf) - .collect(), + files: files.clone(), } } GuardianApprovalRequest::NetworkAccess { diff --git a/codex-rs/core/src/guardian/review_session.rs b/codex-rs/core/src/guardian/review_session.rs index f6cd4d02b2..08825cfde2 100644 --- a/codex-rs/core/src/guardian/review_session.rs +++ b/codex-rs/core/src/guardian/review_session.rs @@ -35,6 +35,7 @@ use crate::rollout::recorder::RolloutRecorder; use codex_config::types::McpServerConfig; use codex_features::Feature; use codex_model_provider_info::ModelProviderInfo; +use codex_utils_absolute_path::AbsolutePathBuf; use super::GUARDIAN_REVIEW_TIMEOUT; use super::GUARDIAN_REVIEWER_NAME; @@ -129,7 +130,7 @@ struct GuardianReviewSessionReuseKey { base_instructions: Option, user_instructions: Option, compact_prompt: Option, - cwd: PathBuf, + cwd: AbsolutePathBuf, mcp_servers: Constrained>, codex_linux_sandbox_exe: Option, main_execve_wrapper_exe: Option, @@ -156,7 +157,7 @@ impl GuardianReviewSessionReuseKey { base_instructions: spawn_config.base_instructions.clone(), user_instructions: spawn_config.user_instructions.clone(), compact_prompt: spawn_config.compact_prompt.clone(), - cwd: spawn_config.cwd.to_path_buf(), + cwd: spawn_config.cwd.clone(), mcp_servers: spawn_config.mcp_servers.clone(), codex_linux_sandbox_exe: spawn_config.codex_linux_sandbox_exe.clone(), main_execve_wrapper_exe: spawn_config.main_execve_wrapper_exe.clone(), diff --git a/codex-rs/core/src/guardian/tests.rs b/codex-rs/core/src/guardian/tests.rs index b6ba6349f4..5e44aa972a 100644 --- a/codex-rs/core/src/guardian/tests.rs +++ b/codex-rs/core/src/guardian/tests.rs @@ -50,7 +50,6 @@ use insta::Settings; use insta::assert_snapshot; use pretty_assertions::assert_eq; use std::collections::BTreeMap; -use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; use tempfile::TempDir; @@ -155,6 +154,20 @@ fn guardian_snapshot_options() -> ContextSnapshotOptions { .strip_agents_md_user_context() } +fn normalize_guardian_snapshot_paths(text: String) -> String { + let platform_path = test_path_buf("/repo/codex-rs/core").display().to_string(); + if platform_path == "/repo/codex-rs/core" { + return text; + } + + let escaped_platform_path = serde_json::to_string(&platform_path) + .expect("test path should serialize") + .trim_matches('"') + .to_string(); + text.replace(&escaped_platform_path, "/repo/codex-rs/core") + .replace(&platform_path, "/repo/codex-rs/core") +} + fn guardian_prompt_text(items: &[codex_protocol::user_input::UserInput]) -> String { items .iter() @@ -220,7 +233,7 @@ async fn build_guardian_prompt_full_mode_preserves_initial_review_format() -> an GuardianApprovalRequest::Shell { id: "shell-1".to_string(), command: vec!["git".to_string(), "push".to_string()], - cwd: PathBuf::from("/repo/codex-rs/core"), + cwd: test_path_buf("/repo/codex-rs/core").abs(), sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault, additional_permissions: None, justification: Some("Need to push the reviewed docs fix.".to_string()), @@ -276,7 +289,7 @@ async fn build_guardian_prompt_delta_mode_preserves_original_numbering() -> anyh GuardianApprovalRequest::Shell { id: "shell-2".to_string(), command: vec!["git".to_string(), "push".to_string()], - cwd: PathBuf::from("/repo/codex-rs/core"), + cwd: test_path_buf("/repo/codex-rs/core").abs(), sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault, additional_permissions: None, justification: Some("Need to push the second docs fix.".to_string()), @@ -314,7 +327,7 @@ async fn build_guardian_prompt_delta_mode_handles_empty_delta() -> anyhow::Resul GuardianApprovalRequest::Shell { id: "shell-2".to_string(), command: vec!["git".to_string(), "push".to_string()], - cwd: PathBuf::from("/repo/codex-rs/core"), + cwd: test_path_buf("/repo/codex-rs/core").abs(), sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault, additional_permissions: None, justification: Some("Need to push the second docs fix.".to_string()), @@ -349,7 +362,7 @@ async fn build_guardian_prompt_stale_delta_cursor_falls_back_to_full_prompt() -> GuardianApprovalRequest::Shell { id: "shell-3".to_string(), command: vec!["git".to_string(), "push".to_string()], - cwd: PathBuf::from("/repo/codex-rs/core"), + cwd: test_path_buf("/repo/codex-rs/core").abs(), sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault, additional_permissions: None, justification: Some("Need to push the docs fix.".to_string()), @@ -434,7 +447,7 @@ async fn build_guardian_prompt_stale_delta_version_falls_back_to_full_prompt() - GuardianApprovalRequest::Shell { id: "shell-4".to_string(), command: vec!["git".to_string(), "push".to_string()], - cwd: PathBuf::from("/repo/codex-rs/core"), + cwd: test_path_buf("/repo/codex-rs/core").abs(), sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault, additional_permissions: None, justification: Some("Need to push after the compaction.".to_string()), @@ -566,7 +579,7 @@ fn format_guardian_action_pretty_truncates_large_string_fields() -> serde_json:: let patch = "line\n".repeat(100_000); let action = GuardianApprovalRequest::ApplyPatch { id: "patch-1".to_string(), - cwd: PathBuf::from("/tmp"), + cwd: test_path_buf("/tmp").abs(), files: Vec::new(), patch: patch.clone(), }; @@ -622,7 +635,7 @@ fn guardian_approval_request_to_json_renders_mcp_tool_call_shape() -> serde_json #[test] fn guardian_assessment_action_redacts_apply_patch_patch_text() { - let cwd = test_path_buf("/tmp"); + let cwd = test_path_buf("/tmp").abs(); let file = test_path_buf("/tmp/guardian.txt").abs(); let action = GuardianApprovalRequest::ApplyPatch { id: "patch-1".to_string(), @@ -654,7 +667,7 @@ fn guardian_request_turn_id_prefers_network_access_owner_turn() { }; let apply_patch = GuardianApprovalRequest::ApplyPatch { id: "patch-1".to_string(), - cwd: test_path_buf("/tmp"), + cwd: test_path_buf("/tmp").abs(), files: vec![test_path_buf("/tmp/guardian.txt").abs()], patch: "*** Begin Patch\n*** Update File: guardian.txt\n@@\n+hello\n*** End Patch" .to_string(), @@ -682,7 +695,7 @@ async fn cancelled_guardian_review_emits_terminal_abort_without_warning() { "review-cancelled-guardian".to_string(), GuardianApprovalRequest::ApplyPatch { id: "patch-1".to_string(), - cwd: test_path_buf("/tmp"), + cwd: test_path_buf("/tmp").abs(), files: vec![test_path_buf("/tmp/guardian.txt").abs()], patch: "*** Begin Patch\n*** Update File: guardian.txt\n@@\n+hello\n*** End Patch" .to_string(), @@ -888,7 +901,7 @@ async fn guardian_review_request_layout_matches_model_visible_request_snapshot() "origin".to_string(), "guardian-approval-mvp".to_string(), ], - cwd: PathBuf::from("/repo/codex-rs/core"), + cwd: test_path_buf("/repo/codex-rs/core").abs(), sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault, additional_permissions: None, justification: Some("Need to push the reviewed docs fix to the repo remote.".to_string()), @@ -915,11 +928,11 @@ async fn guardian_review_request_layout_matches_model_visible_request_snapshot() settings.bind(|| { assert_snapshot!( "codex_core__guardian__tests__guardian_review_request_layout", - context_snapshot::format_labeled_requests_snapshot( + normalize_guardian_snapshot_paths(context_snapshot::format_labeled_requests_snapshot( "Guardian review request layout", &[("Guardian Review Request", &request)], &guardian_snapshot_options(), - ) + )) ); }); @@ -935,7 +948,7 @@ async fn build_guardian_prompt_items_includes_parent_session_id() -> anyhow::Res GuardianApprovalRequest::Shell { id: "shell-1".to_string(), command: vec!["git".to_string(), "status".to_string()], - cwd: PathBuf::from("/repo"), + cwd: test_path_buf("/repo").abs(), sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault, additional_permissions: None, justification: None, @@ -1009,7 +1022,7 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow: let first_request = GuardianApprovalRequest::Shell { id: "shell-1".to_string(), command: vec!["git".to_string(), "push".to_string()], - cwd: PathBuf::from("/repo/codex-rs/core"), + cwd: test_path_buf("/repo/codex-rs/core").abs(), sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault, additional_permissions: None, justification: Some("Need to push the first docs fix.".to_string()), @@ -1055,7 +1068,7 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow: "push".to_string(), "--force-with-lease".to_string(), ], - cwd: PathBuf::from("/repo/codex-rs/core"), + cwd: test_path_buf("/repo/codex-rs/core").abs(), sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault, additional_permissions: None, justification: Some("Need to push the second docs fix.".to_string()), @@ -1097,7 +1110,7 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow: let third_request = GuardianApprovalRequest::Shell { id: "shell-3".to_string(), command: vec!["git".to_string(), "push".to_string()], - cwd: PathBuf::from("/repo/codex-rs/core"), + cwd: test_path_buf("/repo/codex-rs/core").abs(), sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault, additional_permissions: None, justification: Some("Need to push the third docs fix.".to_string()), @@ -1193,13 +1206,15 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow: "codex_core__guardian__tests__guardian_followup_review_request_layout", format!( "{}\n\nshared_prompt_cache_key: {}\nfollowup_contains_first_rationale: {}", - context_snapshot::format_labeled_requests_snapshot( - "Guardian follow-up review request layout", - &[ - ("Initial Guardian Review Request", &requests[0]), - ("Follow-up Guardian Review Request", &requests[1]), - ], - &guardian_snapshot_options(), + normalize_guardian_snapshot_paths( + context_snapshot::format_labeled_requests_snapshot( + "Guardian follow-up review request layout", + &[ + ("Initial Guardian Review Request", &requests[0]), + ("Follow-up Guardian Review Request", &requests[1]), + ], + &guardian_snapshot_options(), + ) ), first_body["prompt_cache_key"] == second_body["prompt_cache_key"], second_body.to_string().contains(first_rationale), @@ -1257,7 +1272,7 @@ async fn guardian_review_surfaces_responses_api_errors_in_rejection_reason() -> GuardianApprovalRequest::Shell { id: "shell-guardian-error".to_string(), command: vec!["git".to_string(), "push".to_string()], - cwd: PathBuf::from("/repo/codex-rs/core"), + cwd: test_path_buf("/repo/codex-rs/core").abs(), sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault, additional_permissions: None, justification: Some("Need to push the reviewed docs fix.".to_string()), @@ -1380,7 +1395,7 @@ async fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> a let initial_request = GuardianApprovalRequest::Shell { id: "shell-guardian-1".to_string(), command: vec!["git".to_string(), "status".to_string()], - cwd: PathBuf::from("/repo/codex-rs/core"), + cwd: test_path_buf("/repo/codex-rs/core").abs(), sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault, additional_permissions: None, justification: Some("Inspect repo state before proceeding.".to_string()), @@ -1425,7 +1440,7 @@ async fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> a let second_request = GuardianApprovalRequest::Shell { id: "shell-guardian-2".to_string(), command: vec!["git".to_string(), "diff".to_string()], - cwd: PathBuf::from("/repo/codex-rs/core"), + cwd: test_path_buf("/repo/codex-rs/core").abs(), sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault, additional_permissions: None, justification: Some("Inspect pending changes before proceeding.".to_string()), @@ -1433,7 +1448,7 @@ async fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> a let third_request = GuardianApprovalRequest::Shell { id: "shell-guardian-3".to_string(), command: vec!["git".to_string(), "push".to_string()], - cwd: PathBuf::from("/repo/codex-rs/core"), + cwd: test_path_buf("/repo/codex-rs/core").abs(), sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault, additional_permissions: None, justification: Some("Inspect whether pushing is safe before proceeding.".to_string()), diff --git a/codex-rs/core/src/hook_runtime.rs b/codex-rs/core/src/hook_runtime.rs index 0d620119f7..d2d672fcd4 100644 --- a/codex-rs/core/src/hook_runtime.rs +++ b/codex-rs/core/src/hook_runtime.rs @@ -96,7 +96,7 @@ pub(crate) async fn run_pending_session_start_hooks( let request = codex_hooks::SessionStartRequest { session_id: sess.conversation_id, - cwd: turn_context.cwd.to_path_buf(), + cwd: turn_context.cwd.clone(), transcript_path: sess.hook_transcript_path().await, model: turn_context.model_info.slug.clone(), permission_mode: hook_permission_mode(turn_context), @@ -124,7 +124,7 @@ pub(crate) async fn run_pre_tool_use_hooks( let request = PreToolUseRequest { session_id: sess.conversation_id, turn_id: turn_context.sub_id.clone(), - cwd: turn_context.cwd.to_path_buf(), + cwd: turn_context.cwd.clone(), transcript_path: sess.hook_transcript_path().await, model: turn_context.model_info.slug.clone(), permission_mode: hook_permission_mode(turn_context), @@ -155,7 +155,7 @@ pub(crate) async fn run_post_tool_use_hooks( let request = PostToolUseRequest { session_id: sess.conversation_id, turn_id: turn_context.sub_id.clone(), - cwd: turn_context.cwd.to_path_buf(), + cwd: turn_context.cwd.clone(), transcript_path: sess.hook_transcript_path().await, model: turn_context.model_info.slug.clone(), permission_mode: hook_permission_mode(turn_context), @@ -180,7 +180,7 @@ pub(crate) async fn run_user_prompt_submit_hooks( let request = UserPromptSubmitRequest { session_id: sess.conversation_id, turn_id: turn_context.sub_id.clone(), - cwd: turn_context.cwd.to_path_buf(), + cwd: turn_context.cwd.clone(), transcript_path: sess.hook_transcript_path().await, model: turn_context.model_info.slug.clone(), permission_mode: hook_permission_mode(turn_context), diff --git a/codex-rs/core/src/installation_id.rs b/codex-rs/core/src/installation_id.rs index 940e17eb01..e9e5445c8c 100644 --- a/codex-rs/core/src/installation_id.rs +++ b/codex-rs/core/src/installation_id.rs @@ -4,19 +4,19 @@ use std::io::Result; use std::io::Seek; use std::io::SeekFrom; use std::io::Write; -use std::path::Path; #[cfg(unix)] use std::os::unix::fs::OpenOptionsExt; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; +use codex_utils_absolute_path::AbsolutePathBuf; use tokio::fs; use uuid::Uuid; pub(crate) const INSTALLATION_ID_FILENAME: &str = "installation_id"; -pub(crate) async fn resolve_installation_id(codex_home: &Path) -> Result { +pub(crate) async fn resolve_installation_id(codex_home: &AbsolutePathBuf) -> Result { let path = codex_home.join(INSTALLATION_ID_FILENAME); fs::create_dir_all(codex_home).await?; tokio::task::spawn_blocking(move || { @@ -67,6 +67,7 @@ pub(crate) async fn resolve_installation_id(codex_home: &Path) -> Result mod tests { use super::INSTALLATION_ID_FILENAME; use super::resolve_installation_id; + use core_test_support::PathExt; use pretty_assertions::assert_eq; use tempfile::TempDir; use uuid::Uuid; @@ -77,9 +78,10 @@ mod tests { #[tokio::test] async fn resolve_installation_id_generates_and_persists_uuid() { let codex_home = TempDir::new().expect("create temp dir"); + let codex_home_abs = codex_home.path().abs(); let persisted_path = codex_home.path().join(INSTALLATION_ID_FILENAME); - let installation_id = resolve_installation_id(codex_home.path()) + let installation_id = resolve_installation_id(&codex_home_abs) .await .expect("resolve installation id"); @@ -103,6 +105,7 @@ mod tests { #[tokio::test] async fn resolve_installation_id_reuses_existing_uuid() { let codex_home = TempDir::new().expect("create temp dir"); + let codex_home_abs = codex_home.path().abs(); let existing = Uuid::new_v4().to_string().to_uppercase(); std::fs::write( codex_home.path().join(INSTALLATION_ID_FILENAME), @@ -110,7 +113,7 @@ mod tests { ) .expect("write installation id"); - let resolved = resolve_installation_id(codex_home.path()) + let resolved = resolve_installation_id(&codex_home_abs) .await .expect("resolve installation id"); @@ -125,13 +128,14 @@ mod tests { #[tokio::test] async fn resolve_installation_id_rewrites_invalid_file_contents() { let codex_home = TempDir::new().expect("create temp dir"); + let codex_home_abs = codex_home.path().abs(); std::fs::write( codex_home.path().join(INSTALLATION_ID_FILENAME), "not-a-uuid", ) .expect("write invalid installation id"); - let resolved = resolve_installation_id(codex_home.path()) + let resolved = resolve_installation_id(&codex_home_abs) .await .expect("resolve installation id"); diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index 95f107be9c..0884642008 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -8,9 +8,9 @@ use codex_protocol::protocol::SandboxPolicy; use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0; use codex_sandboxing::landlock::allow_network_for_proxy; use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_policies; +use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashMap; use std::path::Path; -use std::path::PathBuf; use tokio::process::Child; /// Spawn a shell tool command under the Linux sandbox helper @@ -25,9 +25,9 @@ use tokio::process::Child; pub async fn spawn_command_under_linux_sandbox

( codex_linux_sandbox_exe: P, command: Vec, - command_cwd: PathBuf, + command_cwd: AbsolutePathBuf, sandbox_policy: &SandboxPolicy, - sandbox_policy_cwd: &Path, + sandbox_policy_cwd: &AbsolutePathBuf, use_legacy_landlock: bool, stdio_policy: StdioPolicy, network: Option<&NetworkProxy>, diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index 996f997c06..9aea9b87c8 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -1,6 +1,5 @@ use std::collections::BTreeMap; use std::collections::HashMap; -use std::path::PathBuf; use std::time::Duration; use std::time::Instant; @@ -55,10 +54,10 @@ use codex_protocol::request_user_input::RequestUserInputResponse; use codex_rmcp_client::ElicitationAction; use codex_rmcp_client::ElicitationResponse; use codex_rollout::state_db; +use codex_utils_absolute_path::AbsolutePathBuf; use rmcp::model::ToolAnnotations; use serde::Deserialize; use serde::Serialize; -use std::path::Path; use std::sync::Arc; use toml_edit::value; use tracing::Instrument; @@ -1512,7 +1511,7 @@ async fn maybe_persist_mcp_tool_approval( } async fn persist_codex_app_tool_approval( - codex_home: &Path, + codex_home: &AbsolutePathBuf, connector_id: &str, tool_name: &str, ) -> anyhow::Result<()> { @@ -1545,7 +1544,7 @@ async fn persist_custom_mcp_tool_approval( if !servers.contains_key(server) { anyhow::bail!("MCP server `{server}` is not configured in config.toml"); } - config.codex_home.to_path_buf() + config.codex_home.clone() }; ConfigEditsBuilder::new(&config_folder) @@ -1563,7 +1562,10 @@ async fn persist_custom_mcp_tool_approval( .await } -fn project_mcp_tool_approval_config_folder(config: &Config, server: &str) -> Option { +fn project_mcp_tool_approval_config_folder( + config: &Config, + server: &str, +) -> Option { config .config_layer_stack .layers_high_to_low() @@ -1582,9 +1584,7 @@ fn project_mcp_tool_approval_config_folder(config: &Config, server: &str) -> Opt HashMap::::deserialize(value).ok() })?; if servers.contains_key(server) { - layer - .config_folder() - .map(|folder| folder.as_path().to_path_buf()) + layer.config_folder() } else { None } diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index 48a19c2a68..201a162ef7 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -14,6 +14,7 @@ use codex_config::types::McpServerConfig; use codex_config::types::McpServerToolConfig; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::SandboxPolicy; +use core_test_support::PathExt; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; use core_test_support::responses::ev_response_created; @@ -1043,7 +1044,7 @@ fn accepted_elicitation_without_content_defaults_to_accept() { async fn persist_codex_app_tool_approval_writes_tool_override() { let tmp = tempdir().expect("tempdir"); - persist_codex_app_tool_approval(tmp.path(), "calendar", "calendar/list_events") + persist_codex_app_tool_approval(&tmp.path().abs(), "calendar", "calendar/list_events") .await .expect("persist approval"); @@ -1216,7 +1217,7 @@ async fn maybe_persist_mcp_tool_approval_writes_project_config_for_project_serve .await .expect("trust project"); let config = ConfigBuilder::default() - .codex_home(codex_home) + .codex_home(codex_home.to_path_buf()) .fallback_cwd(Some(project_dir.path().to_path_buf())) .build() .await diff --git a/codex-rs/core/src/memories/mod.rs b/codex-rs/core/src/memories/mod.rs index ecb6c05df2..fb033a32c1 100644 --- a/codex-rs/core/src/memories/mod.rs +++ b/codex-rs/core/src/memories/mod.rs @@ -96,10 +96,11 @@ mod metrics { pub(super) const MEMORY_PHASE_TWO_TOKEN_USAGE: &str = "codex.memory.phase2.token_usage"; } +use codex_utils_absolute_path::AbsolutePathBuf; use std::path::Path; use std::path::PathBuf; -pub fn memory_root(codex_home: &Path) -> PathBuf { +pub fn memory_root(codex_home: &AbsolutePathBuf) -> AbsolutePathBuf { codex_home.join("memories") } diff --git a/codex-rs/core/src/memories/phase2.rs b/codex-rs/core/src/memories/phase2.rs index 203a19075c..2abc435ca7 100644 --- a/codex-rs/core/src/memories/phase2.rs +++ b/codex-rs/core/src/memories/phase2.rs @@ -21,7 +21,6 @@ use codex_protocol::protocol::TokenUsage; use codex_protocol::user_input::UserInput; use codex_state::Stage1Output; use codex_state::StateRuntime; -use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashSet; use std::sync::Arc; use std::time::Duration; @@ -288,16 +287,7 @@ mod agent { let root = memory_root(&config.codex_home); let mut agent_config = config.as_ref().clone(); - match AbsolutePathBuf::from_absolute_path(root) { - Ok(root) => agent_config.cwd = root, - Err(err) => { - warn!( - "memory phase-2 consolidation could not set cwd from codex_home {}: {err}", - agent_config.codex_home.display() - ); - return None; - } - } + agent_config.cwd = root; // Consolidation threads must never feed back into phase-1 memory generation. agent_config.memories.generate_memories = false; // Approval policy @@ -308,14 +298,7 @@ mod agent { let _ = agent_config.features.disable(Feature::MemoryTool); // Sandbox policy - let mut writable_roots = Vec::new(); - match AbsolutePathBuf::from_absolute_path(agent_config.codex_home.clone()) { - Ok(codex_home) => writable_roots.push(codex_home), - Err(err) => warn!( - "memory phase-2 consolidation could not add codex_home writable root {}: {err}", - agent_config.codex_home.display() - ), - } + let writable_roots = vec![agent_config.codex_home.clone()]; // The consolidation agent only needs local codex_home write access and no network. let consolidation_sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots, diff --git a/codex-rs/core/src/memories/prompts.rs b/codex-rs/core/src/memories/prompts.rs index 079ccd5c6a..b6e18b4057 100644 --- a/codex-rs/core/src/memories/prompts.rs +++ b/codex-rs/core/src/memories/prompts.rs @@ -6,6 +6,7 @@ use codex_protocol::openai_models::ModelInfo; use codex_state::Phase2InputSelection; use codex_state::Stage1Output; use codex_state::Stage1OutputRef; +use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_output_truncation::TruncationPolicy; use codex_utils_output_truncation::truncate_text; use codex_utils_template::Template; @@ -231,7 +232,9 @@ pub(super) fn build_stage_one_input_message( /// Build prompt used for read path. This prompt must be added to the developer instructions. In /// case of large memory files, the `memory_summary.md` is truncated at /// [phase_one::MEMORY_TOOL_DEVELOPER_INSTRUCTIONS_SUMMARY_TOKEN_LIMIT]. -pub(crate) async fn build_memory_tool_developer_instructions(codex_home: &Path) -> Option { +pub(crate) async fn build_memory_tool_developer_instructions( + codex_home: &AbsolutePathBuf, +) -> Option { let base_path = memory_root(codex_home); let memory_summary_path = base_path.join("memory_summary.md"); let memory_summary = fs::read_to_string(&memory_summary_path) diff --git a/codex-rs/core/src/memories/prompts_tests.rs b/codex-rs/core/src/memories/prompts_tests.rs index 488e18fcd6..325d5e9234 100644 --- a/codex-rs/core/src/memories/prompts_tests.rs +++ b/codex-rs/core/src/memories/prompts_tests.rs @@ -1,5 +1,6 @@ use super::*; use codex_models_manager::model_info::model_info_from_slug; +use core_test_support::PathExt; use pretty_assertions::assert_eq; use tempfile::tempdir; use tokio::fs as tokio_fs; @@ -56,7 +57,7 @@ fn build_stage_one_input_message_uses_default_limit_when_model_context_window_mi #[tokio::test] async fn build_memory_tool_developer_instructions_renders_embedded_template() { let temp = tempdir().unwrap(); - let codex_home = temp.path(); + let codex_home = temp.path().abs(); let memories_dir = codex_home.join("memories"); tokio_fs::create_dir_all(&memories_dir).await.unwrap(); tokio_fs::write( @@ -66,7 +67,7 @@ async fn build_memory_tool_developer_instructions_renders_embedded_template() { .await .unwrap(); - let instructions = build_memory_tool_developer_instructions(codex_home) + let instructions = build_memory_tool_developer_instructions(&codex_home) .await .unwrap(); diff --git a/codex-rs/core/src/memories/tests.rs b/codex-rs/core/src/memories/tests.rs index 57c1534c77..a0ed29dbec 100644 --- a/codex-rs/core/src/memories/tests.rs +++ b/codex-rs/core/src/memories/tests.rs @@ -10,6 +10,7 @@ use chrono::Utc; use codex_config::types::DEFAULT_MEMORIES_MAX_RAW_MEMORIES_FOR_CONSOLIDATION; use codex_protocol::ThreadId; use codex_state::Stage1Output; +use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use serde_json::Value; use std::path::PathBuf; @@ -17,8 +18,7 @@ use tempfile::tempdir; #[test] fn memory_root_uses_shared_global_path() { - let dir = tempdir().expect("tempdir"); - let codex_home = dir.path().join("codex"); + let codex_home = AbsolutePathBuf::current_dir().expect("cwd").join("codex"); assert_eq!(memory_root(&codex_home), codex_home.join("memories")); } @@ -678,7 +678,10 @@ mod phase2 { .expect("get consolidation thread"); let config_snapshot = subagent.config_snapshot().await; pretty_assertions::assert_eq!(config_snapshot.approval_policy, AskForApproval::Never); - pretty_assertions::assert_eq!(config_snapshot.cwd, memory_root(&harness.config.codex_home)); + pretty_assertions::assert_eq!( + config_snapshot.cwd.as_path(), + memory_root(&harness.config.codex_home).as_path() + ); match config_snapshot.sandbox_policy { SandboxPolicy::WorkspaceWrite { writable_roots, .. } => { assert!( diff --git a/codex-rs/core/src/message_history.rs b/codex-rs/core/src/message_history.rs index 9a46ca820c..3458ec7306 100644 --- a/codex-rs/core/src/message_history.rs +++ b/codex-rs/core/src/message_history.rs @@ -26,7 +26,6 @@ use std::io::Seek; use std::io::SeekFrom; use std::io::Write; use std::path::Path; -use std::path::PathBuf; use serde::Deserialize; use serde::Serialize; @@ -37,6 +36,7 @@ use tokio::io::AsyncReadExt; use crate::config::Config; use codex_config::types::HistoryPersistence; +use codex_utils_absolute_path::AbsolutePathBuf; use codex_protocol::ThreadId; #[cfg(unix)] @@ -60,8 +60,8 @@ pub struct HistoryEntry { pub text: String, } -fn history_filepath(config: &Config) -> PathBuf { - config.codex_home.join(HISTORY_FILENAME).to_path_buf() +fn history_filepath(config: &Config) -> AbsolutePathBuf { + config.codex_home.join(HISTORY_FILENAME) } /// Append a `text` entry associated with `conversation_id` to the history file. diff --git a/codex-rs/core/src/network_proxy_loader.rs b/codex-rs/core/src/network_proxy_loader.rs index 8d387ca968..b5eaf26b7b 100644 --- a/codex-rs/core/src/network_proxy_loader.rs +++ b/codex-rs/core/src/network_proxy_loader.rs @@ -25,8 +25,8 @@ use codex_network_proxy::NetworkProxyState; use codex_network_proxy::build_config_state; use codex_network_proxy::normalize_host; use codex_network_proxy::validate_policy_against_constraints; +use codex_utils_absolute_path::AbsolutePathBuf; use serde::Deserialize; -use std::path::PathBuf; use std::sync::Arc; use tokio::sync::RwLock; @@ -86,17 +86,12 @@ fn collect_layer_mtimes(stack: &ConfigLayerStack) -> Vec { .iter() .filter_map(|layer| { let path = match &layer.name { - ConfigLayerSource::System { file } => Some(file.as_path().to_path_buf()), - ConfigLayerSource::User { file } => Some(file.as_path().to_path_buf()), - ConfigLayerSource::Project { dot_codex_folder } => Some( - dot_codex_folder - .join(CONFIG_TOML_FILE) - .as_path() - .to_path_buf(), - ), - ConfigLayerSource::LegacyManagedConfigTomlFromFile { file } => { - Some(file.as_path().to_path_buf()) + ConfigLayerSource::System { file } => Some(file.clone()), + ConfigLayerSource::User { file } => Some(file.clone()), + ConfigLayerSource::Project { dot_codex_folder } => { + Some(dot_codex_folder.join(CONFIG_TOML_FILE)) } + ConfigLayerSource::LegacyManagedConfigTomlFromFile { file } => Some(file.clone()), _ => None, }; path.map(LayerMtime::new) @@ -265,12 +260,12 @@ fn is_user_controlled_layer(layer: &ConfigLayerSource) -> bool { #[derive(Clone)] struct LayerMtime { - path: PathBuf, + path: AbsolutePathBuf, mtime: Option, } impl LayerMtime { - fn new(path: PathBuf) -> Self { + fn new(path: AbsolutePathBuf) -> Self { let mtime = path.metadata().and_then(|m| m.modified()).ok(); Self { path, mtime } } diff --git a/codex-rs/core/src/plugins/discoverable_tests.rs b/codex-rs/core/src/plugins/discoverable_tests.rs index a0fc3fb289..f330418b0b 100644 --- a/codex-rs/core/src/plugins/discoverable_tests.rs +++ b/codex-rs/core/src/plugins/discoverable_tests.rs @@ -199,7 +199,9 @@ async fn list_tool_suggest_discoverable_plugins_does_not_reload_marketplace_per_ assert_eq!(discoverable_plugins.len(), 1); assert_eq!(discoverable_plugins[0].id, "slack@openai-curated"); - let logs = String::from_utf8(buffer.lock().expect("buffer lock").clone()).expect("utf8 logs"); + let logs = String::from_utf8(buffer.lock().expect("buffer lock").clone()) + .expect("utf8 logs") + .replace('\\', "/"); assert_eq!(logs.matches("ignoring interface.defaultPrompt").count(), 2); let normalized_logs = logs.replace('\\', "/"); assert_eq!( diff --git a/codex-rs/core/src/review_prompts.rs b/codex-rs/core/src/review_prompts.rs index 988ceff821..12a5eb4a52 100644 --- a/codex-rs/core/src/review_prompts.rs +++ b/codex-rs/core/src/review_prompts.rs @@ -1,8 +1,8 @@ use codex_git_utils::merge_base_with_head; use codex_protocol::protocol::ReviewRequest; use codex_protocol::protocol::ReviewTarget; +use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_template::Template; -use std::path::Path; use std::sync::LazyLock; #[derive(Clone, Debug, PartialEq)] @@ -38,7 +38,7 @@ static COMMIT_PROMPT_TEMPLATE: LazyLock