Spread AbsolutePathBuf (#17792)

Mechanical change to promote absolute paths through code.
This commit is contained in:
pakrym-oai
2026-04-14 14:26:10 -07:00
committed by GitHub
parent dae56994da
commit dd1321d11b
166 changed files with 1638 additions and 1214 deletions
+3
View File
@@ -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",
+1
View File
@@ -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 }
@@ -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,
@@ -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"
@@ -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": {
@@ -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"
@@ -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"
},
@@ -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"
},
@@ -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",
@@ -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",
@@ -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": {
@@ -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"
},
@@ -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"
},
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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"
},
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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"
},
@@ -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": {
@@ -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"
},
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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, };
@@ -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.
*/
@@ -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<string>, cwd: string, } | { "type": "applyPatch", cwd: string, files: Array<string>, } | { "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<string>, cwd: AbsolutePathBuf, } | { "type": "applyPatch", cwd: AbsolutePathBuf, files: Array<AbsolutePathBuf>, } | { "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, };
@@ -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<HookOutputEntry>, };
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<HookOutputEntry>, };
@@ -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, };
@@ -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.
*/
@@ -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<string>, approvalPolicy: AskForApproval,
instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval,
/**
* Reviewer currently used for approval requests on this thread.
*/
@@ -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, };
@@ -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<string>, approvalPolicy: AskForApproval,
instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval,
/**
* Reviewer currently used for approval requests on this thread.
*/
@@ -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<string>, approvalPolicy: AskForApproval,
instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval,
/**
* Reviewer currently used for approval requests on this thread.
*/
@@ -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": {
@@ -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(),
@@ -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(),
}],
+47 -52
View File
@@ -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<String>,
@@ -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<CoreParsedCommand> 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<ServiceTier>,
pub cwd: PathBuf,
pub cwd: AbsolutePathBuf,
/// Instruction source files currently loaded for this thread.
#[serde(default)]
pub instruction_sources: Vec<PathBuf>,
pub instruction_sources: Vec<AbsolutePathBuf>,
#[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<ServiceTier>,
pub cwd: PathBuf,
pub cwd: AbsolutePathBuf,
/// Instruction source files currently loaded for this thread.
#[serde(default)]
pub instruction_sources: Vec<PathBuf>,
pub instruction_sources: Vec<AbsolutePathBuf>,
#[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<ServiceTier>,
pub cwd: PathBuf,
pub cwd: AbsolutePathBuf,
/// Instruction source files currently loaded for this thread.
#[serde(default)]
pub instruction_sources: Vec<PathBuf>,
pub instruction_sources: Vec<AbsolutePathBuf>,
#[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<String>,
#[ts(optional)]
pub icon_small: Option<PathBuf>,
pub icon_small: Option<AbsolutePathBuf>,
#[ts(optional)]
pub icon_large: Option<PathBuf>,
pub icon_large: Option<AbsolutePathBuf>,
#[ts(optional)]
pub brand_color: Option<String>,
#[ts(optional)]
@@ -3722,15 +3725,6 @@ impl From<CoreSkillScope> for SkillScope {
}
}
impl From<CoreSkillErrorInfo> 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<PathBuf>,
/// 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<String>,
#[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<String>,
saved_path: Option<AbsolutePathBuf>,
},
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
@@ -4786,7 +4780,7 @@ impl From<GuardianCommandSource> 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<String>,
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<PathBuf>,
pub cwd: AbsolutePathBuf,
pub files: Vec<AbsolutePathBuf>,
}
#[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<String>,
cwd: PathBuf,
cwd: AbsolutePathBuf,
},
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
ApplyPatch { cwd: PathBuf, files: Vec<PathBuf> },
ApplyPatch {
cwd: AbsolutePathBuf,
files: Vec<AbsolutePathBuf>,
},
#[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<PathBuf>,
pub cwd: Option<AbsolutePathBuf>,
/// 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::<PathBuf>::new());
assert_eq!(resume.instruction_sources, Vec::<PathBuf>::new());
assert_eq!(fork.instruction_sources, Vec::<PathBuf>::new());
assert_eq!(start.instruction_sources, Vec::<AbsolutePathBuf>::new());
assert_eq!(resume.instruction_sources, Vec::<AbsolutePathBuf>::new());
assert_eq!(fork.instruction_sources, Vec::<AbsolutePathBuf>::new());
}
#[test]
@@ -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<V2ParsedCommand>,
}
@@ -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::<Vec<_>>();
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::<Vec<_>>();
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<V2ParsedCommand>,
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<String>,
source: CommandExecutionSource,
command_actions: Vec<V2ParsedCommand>,
@@ -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,
@@ -611,15 +611,12 @@ pub(crate) struct CodexMessageProcessorArgs {
}
impl CodexMessageProcessor {
async fn instruction_sources_from_config(config: &Config) -> Vec<PathBuf> {
let mut paths: Vec<PathBuf> = config.user_instructions_path.iter().cloned().collect();
async fn instruction_sources_from_config(config: &Config) -> Vec<AbsolutePathBuf> {
let mut paths: Vec<AbsolutePathBuf> =
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<Thread, String> {
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()));
+2 -3
View File
@@ -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::<Vec<_>>();
changed_paths.sort_by(|left, right| left.as_path().cmp(right.as_path()));
if !changed_paths.is_empty() {
+2 -1
View File
@@ -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<PathBuf>,
pub(crate) instruction_sources: Vec<AbsolutePathBuf>,
pub(crate) thread_summary: codex_app_server_protocol::Thread,
}
+3 -3
View File
@@ -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,
+2 -3
View File
@@ -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 {
+2
View File
@@ -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;
@@ -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
@@ -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);
@@ -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(())
@@ -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);
@@ -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(),
})
}
@@ -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<Path>) -> 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<Path>) -> PathBuf {
path.as_ref().to_path_buf()
}
#[tokio::test]
@@ -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);
@@ -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?;
+19 -15
View File
@@ -511,8 +511,11 @@ fn discover_skills_under_root(
}
}
fn parse_skill_file(path: &Path, scope: SkillScope) -> Result<SkillMetadata, SkillParseError> {
let contents = fs::read_to_string(path).map_err(SkillParseError::Read)?;
fn parse_skill_file(
path: &AbsolutePathBuf,
scope: SkillScope,
) -> Result<SkillMetadata, SkillParseError> {
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<SkillMetadata, Ski
.as_deref()
.map(sanitize_single_line)
.filter(|value| !value.is_empty())
.unwrap_or_else(|| default_skill_name(path));
let name = namespaced_skill_name(path, &base_name);
.unwrap_or_else(|| default_skill_name(path.as_path()));
let name = namespaced_skill_name(path.as_path(), &base_name);
let description = parsed
.description
.as_deref()
@@ -553,9 +556,7 @@ fn parse_skill_file(path: &Path, scope: SkillScope) -> Result<SkillMetadata, Ski
)?;
}
let resolved_path = AbsolutePathBuf::from_absolute_path_checked(path)
.and_then(|path| path.canonicalize())
.map_err(SkillParseError::Read)?;
let resolved_path = path.canonicalize().map_err(SkillParseError::Read)?;
Ok(SkillMetadata {
name,
@@ -584,7 +585,7 @@ fn namespaced_skill_name(path: &Path, base_name: &str) -> 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<Interface>, skill_dir: &Path) -> Option<SkillInterface> {
fn resolve_interface(
interface: Option<Interface>,
skill_dir: &AbsolutePathBuf,
) -> Option<SkillInterface> {
let interface = interface?;
let interface = SkillInterface {
display_name: resolve_str(
@@ -726,10 +730,10 @@ fn resolve_dependency_tool(tool: DependencyTool) -> Option<SkillToolDependency>
}
fn resolve_asset_path(
skill_dir: &Path,
skill_dir: &AbsolutePathBuf,
field: &'static str,
path: Option<PathBuf>,
) -> Option<PathBuf> {
) -> Option<AbsolutePathBuf> {
// Icons must be relative paths under the skill's assets/ directory; otherwise return None.
let path = path?;
if path.as_os_str().is_empty() {
+5 -17
View File
@@ -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,
+2 -3
View File
@@ -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<String>,
pub short_description: Option<String>,
pub icon_small: Option<PathBuf>,
pub icon_large: Option<PathBuf>,
pub icon_small: Option<AbsolutePathBuf>,
pub icon_large: Option<AbsolutePathBuf>,
pub brand_color: Option<String>,
pub default_prompt: Option<String>,
}
+34 -42
View File
@@ -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<String> {
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<String>,
@@ -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<String>,
command: Vec<String>,
cwd: PathBuf,
cwd: AbsolutePathBuf,
reason: Option<String>,
network_approval_context: Option<NetworkApprovalContext>,
proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
@@ -5395,9 +5386,12 @@ mod handlers {
cwds: Vec<PathBuf>,
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 {
+1 -1
View File
@@ -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,
},
+4 -3
View File
@@ -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!(
+9 -9
View File
@@ -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(),
"<image_id>",
);
@@ -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,
);
+2 -1
View File
@@ -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<ReasoningEffort>,
pub personality: Option<Personality>,
+4 -6
View File
@@ -272,7 +272,7 @@ pub struct Config {
pub user_instructions: Option<String>,
/// Path to the global AGENTS file loaded into `user_instructions`.
pub user_instructions_path: Option<PathBuf>,
pub user_instructions_path: Option<AbsolutePathBuf>,
/// Base instructions override.
pub base_instructions: Option<String>,
@@ -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<LoadedUserInstructions> {
fn load_instructions(codex_dir: Option<&AbsolutePathBuf>) -> Option<LoadedUserInstructions> {
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 {
+6 -6
View File
@@ -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<PathBuf>,
use_legacy_landlock: bool,
stdout_stream: Option<StdoutStream>,
@@ -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<PathBuf>,
use_legacy_landlock: bool,
) -> Result<ExecRequest> {
@@ -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<String> {
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<Option<WindowsSandboxFilesystemOverrides>, 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<Option<WindowsSandboxFilesystemOverrides>, String> {
if sandbox != SandboxType::WindowsRestrictedToken || !use_windows_elevated_backend {
+20 -22
View File
@@ -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,
+7 -11
View File
@@ -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<String>,
cwd: PathBuf,
cwd: AbsolutePathBuf,
sandbox_permissions: crate::sandboxing::SandboxPermissions,
additional_permissions: Option<PermissionProfile>,
justification: Option<String>,
@@ -25,7 +24,7 @@ pub(crate) enum GuardianApprovalRequest {
ExecCommand {
id: String,
command: Vec<String>,
cwd: PathBuf,
cwd: AbsolutePathBuf,
sandbox_permissions: crate::sandboxing::SandboxPermissions,
additional_permissions: Option<PermissionProfile>,
justification: Option<String>,
@@ -37,12 +36,12 @@ pub(crate) enum GuardianApprovalRequest {
source: GuardianCommandSource,
program: String,
argv: Vec<String>,
cwd: PathBuf,
cwd: AbsolutePathBuf,
additional_permissions: Option<PermissionProfile>,
},
ApplyPatch {
id: String,
cwd: PathBuf,
cwd: AbsolutePathBuf,
files: Vec<AbsolutePathBuf>,
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 {
+3 -2
View File
@@ -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<String>,
user_instructions: Option<String>,
compact_prompt: Option<String>,
cwd: PathBuf,
cwd: AbsolutePathBuf,
mcp_servers: Constrained<HashMap<String, McpServerConfig>>,
codex_linux_sandbox_exe: Option<PathBuf>,
main_execve_wrapper_exe: Option<PathBuf>,
@@ -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(),
+43 -28
View File
@@ -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()),
+4 -4
View File
@@ -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),
+9 -5
View File
@@ -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<String> {
pub(crate) async fn resolve_installation_id(codex_home: &AbsolutePathBuf) -> Result<String> {
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<String>
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");
+3 -3
View File
@@ -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<P>(
codex_linux_sandbox_exe: P,
command: Vec<String>,
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>,
+8 -8
View File
@@ -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<PathBuf> {
fn project_mcp_tool_approval_config_folder(
config: &Config,
server: &str,
) -> Option<AbsolutePathBuf> {
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::<String, codex_config::types::McpServerConfig>::deserialize(value).ok()
})?;
if servers.contains_key(server) {
layer
.config_folder()
.map(|folder| folder.as_path().to_path_buf())
layer.config_folder()
} else {
None
}
+3 -2
View File
@@ -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
+2 -1
View File
@@ -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")
}
+2 -19
View File
@@ -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,
+4 -1
View File
@@ -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<String> {
pub(crate) async fn build_memory_tool_developer_instructions(
codex_home: &AbsolutePathBuf,
) -> Option<String> {
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)
+3 -2
View File
@@ -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();
+6 -3
View File
@@ -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!(
+3 -3
View File
@@ -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.
+8 -13
View File
@@ -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<LayerMtime> {
.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<std::time::SystemTime>,
}
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 }
}
@@ -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!(
+5 -5
View File
@@ -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<Template> = LazyLock::new(|| {
pub fn resolve_review_request(
request: ReviewRequest,
cwd: &Path,
cwd: &AbsolutePathBuf,
) -> anyhow::Result<ResolvedReviewRequest> {
let target = request.target;
let prompt = review_prompt(&target, cwd)?;
@@ -53,7 +53,7 @@ pub fn resolve_review_request(
})
}
pub fn review_prompt(target: &ReviewTarget, cwd: &Path) -> anyhow::Result<String> {
pub fn review_prompt(target: &ReviewTarget, cwd: &AbsolutePathBuf) -> anyhow::Result<String> {
match target {
ReviewTarget::UncommittedChanges => Ok(UNCOMMITTED_PROMPT.to_string()),
ReviewTarget::BaseBranch { branch } => {
@@ -161,7 +161,7 @@ mod tests {
sha: "deadbeef".to_string(),
title: None,
},
Path::new("."),
&AbsolutePathBuf::current_dir().expect("cwd"),
)
.expect("commit prompt should render"),
"Review the code changes introduced by commit deadbeef. Provide prioritized, actionable findings."
@@ -176,7 +176,7 @@ mod tests {
sha: "deadbeef".to_string(),
title: Some("Fix bug".to_string()),
},
Path::new("."),
&AbsolutePathBuf::current_dir().expect("cwd"),
)
.expect("commit prompt should render"),
"Review the code changes introduced by commit deadbeef (\"Fix bug\"). Provide prioritized, actionable findings."
+3 -2
View File
@@ -11,6 +11,7 @@ use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy;
use codex_sandboxing::SandboxType;
use codex_sandboxing::get_platform_sandbox;
use codex_utils_absolute_path::AbsolutePathBuf;
const PATCH_REJECTED_OUTSIDE_PROJECT_REASON: &str =
"writing outside of the project; rejected by user approval settings";
@@ -34,7 +35,7 @@ pub fn assess_patch_safety(
policy: AskForApproval,
sandbox_policy: &SandboxPolicy,
file_system_sandbox_policy: &FileSystemSandboxPolicy,
cwd: &Path,
cwd: &AbsolutePathBuf,
windows_sandbox_level: WindowsSandboxLevel,
) -> SafetyCheck {
if action.is_empty() {
@@ -119,7 +120,7 @@ fn patch_rejection_reason(sandbox_policy: &SandboxPolicy) -> &'static str {
fn is_write_patch_constrained_to_writable_paths(
action: &ApplyPatchAction,
file_system_sandbox_policy: &FileSystemSandboxPolicy,
cwd: &Path,
cwd: &AbsolutePathBuf,
) -> bool {
// Normalize a path by removing `.` and resolving `..` without touching the
// filesystem (works even if the file does not exist).
+22 -24
View File
@@ -5,7 +5,7 @@ use codex_protocol::protocol::FileSystemSandboxEntry;
use codex_protocol::protocol::FileSystemSpecialPath;
use codex_protocol::protocol::GranularApprovalConfig;
use codex_utils_absolute_path::AbsolutePathBuf;
use core_test_support::PathBufExt;
use core_test_support::PathExt;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
@@ -14,14 +14,12 @@ fn test_writable_roots_constraint() {
// Use a temporary directory as our workspace to avoid touching
// the real current working directory.
let tmp = TempDir::new().unwrap();
let cwd = tmp.path().to_path_buf();
let parent = cwd.parent().unwrap().to_path_buf();
let cwd = tmp.path().abs();
let parent = cwd.parent().unwrap();
// Helper to build a singleentry patch that adds a file at `p`.
let make_add_change = |p: PathBuf| {
let p = p.abs();
ApplyPatchAction::new_add_for_test(&p, "".to_string())
};
let make_add_change =
|p: AbsolutePathBuf| ApplyPatchAction::new_add_for_test(&p, "".to_string());
let add_inside = make_add_change(cwd.join("inner.txt"));
let add_outside = make_add_change(parent.join("outside.txt"));
@@ -51,7 +49,7 @@ fn test_writable_roots_constraint() {
// With the parent dir explicitly added as a writable root, the
// outside write should be permitted.
let policy_with_parent = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![AbsolutePathBuf::try_from(parent).unwrap()],
writable_roots: vec![parent],
read_only_access: Default::default(),
network_access: false,
exclude_tmpdir_env_var: true,
@@ -67,8 +65,8 @@ fn test_writable_roots_constraint() {
#[test]
fn external_sandbox_auto_approves_in_on_request() {
let tmp = TempDir::new().unwrap();
let cwd = tmp.path().to_path_buf();
let add_inside_path = cwd.join("inner.txt").abs();
let cwd = tmp.path().abs();
let add_inside_path = cwd.join("inner.txt");
let add_inside = ApplyPatchAction::new_add_for_test(&add_inside_path, "".to_string());
let policy = SandboxPolicy::ExternalSandbox {
@@ -94,9 +92,9 @@ fn external_sandbox_auto_approves_in_on_request() {
#[test]
fn granular_with_all_flags_true_matches_on_request_for_out_of_root_patch() {
let tmp = TempDir::new().unwrap();
let cwd = tmp.path().to_path_buf();
let parent = cwd.parent().unwrap().to_path_buf();
let outside_path = parent.join("outside.txt").abs();
let cwd = tmp.path().abs();
let parent = cwd.parent().unwrap();
let outside_path = parent.join("outside.txt");
let add_outside = ApplyPatchAction::new_add_for_test(&outside_path, "".to_string());
let policy_workspace_only = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
@@ -139,9 +137,9 @@ fn granular_with_all_flags_true_matches_on_request_for_out_of_root_patch() {
#[test]
fn granular_sandbox_approval_false_rejects_out_of_root_patch() {
let tmp = TempDir::new().unwrap();
let cwd = tmp.path().to_path_buf();
let parent = cwd.parent().unwrap().to_path_buf();
let outside_path = parent.join("outside.txt").abs();
let cwd = tmp.path().abs();
let parent = cwd.parent().unwrap();
let outside_path = parent.join("outside.txt");
let add_outside = ApplyPatchAction::new_add_for_test(&outside_path, "".to_string());
let policy_workspace_only = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
@@ -175,8 +173,8 @@ fn granular_sandbox_approval_false_rejects_out_of_root_patch() {
#[test]
fn read_only_policy_rejects_patch_with_read_only_reason() {
let tmp = TempDir::new().unwrap();
let cwd = tmp.path().to_path_buf();
let inside_path = cwd.join("inside.txt").abs();
let cwd = tmp.path().abs();
let inside_path = cwd.join("inside.txt");
let action = ApplyPatchAction::new_add_for_test(&inside_path, "".to_string());
let sandbox_policy = SandboxPolicy::new_read_only_policy();
let file_system_sandbox_policy =
@@ -204,9 +202,9 @@ fn read_only_policy_rejects_patch_with_read_only_reason() {
#[test]
fn explicit_unreadable_paths_prevent_auto_approval_for_external_sandbox() {
let tmp = TempDir::new().unwrap();
let cwd = tmp.path().to_path_buf();
let cwd = tmp.path().abs();
let blocked_path = cwd.join("blocked.txt");
let blocked_absolute = blocked_path.abs();
let blocked_absolute = blocked_path;
let action = ApplyPatchAction::new_add_for_test(&blocked_absolute, "".to_string());
let sandbox_policy = SandboxPolicy::ExternalSandbox {
network_access: codex_protocol::protocol::NetworkAccess::Restricted,
@@ -247,9 +245,9 @@ fn explicit_unreadable_paths_prevent_auto_approval_for_external_sandbox() {
#[test]
fn explicit_read_only_subpaths_prevent_auto_approval_for_external_sandbox() {
let tmp = TempDir::new().unwrap();
let cwd = tmp.path().to_path_buf();
let cwd = tmp.path().abs();
let blocked_path = cwd.join("docs").join("blocked.txt");
let blocked_absolute = blocked_path.abs();
let blocked_absolute = blocked_path;
let docs_absolute = AbsolutePathBuf::resolve_path_against_base("docs", &cwd);
let action = ApplyPatchAction::new_add_for_test(&blocked_absolute, "".to_string());
let sandbox_policy = SandboxPolicy::ExternalSandbox {
@@ -291,8 +289,8 @@ fn explicit_read_only_subpaths_prevent_auto_approval_for_external_sandbox() {
#[test]
fn missing_project_dot_codex_config_requires_approval() {
let tmp = TempDir::new().unwrap();
let cwd = tmp.path().to_path_buf();
let config_path = cwd.join(".codex").join("config.toml").abs();
let cwd = tmp.path().abs();
let config_path = cwd.join(".codex").join("config.toml");
let action = ApplyPatchAction::new_add_for_test(&config_path, "".to_string());
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
+3 -3
View File
@@ -10,16 +10,16 @@ use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::SandboxPolicy;
use codex_sandboxing::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
use codex_sandboxing::seatbelt::create_seatbelt_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;
pub async fn spawn_command_under_seatbelt(
command: Vec<String>,
command_cwd: PathBuf,
command_cwd: AbsolutePathBuf,
sandbox_policy: &SandboxPolicy,
sandbox_policy_cwd: &Path,
sandbox_policy_cwd: &AbsolutePathBuf,
stdio_policy: StdioPolicy,
network: Option<&NetworkProxy>,
mut env: HashMap<String, String>,
+48 -46
View File
@@ -1,6 +1,5 @@
use std::io::ErrorKind;
use std::path::Path;
use std::path::PathBuf;
use std::process::Stdio;
use std::sync::Arc;
use std::time::Duration;
@@ -16,6 +15,7 @@ use anyhow::anyhow;
use anyhow::bail;
use codex_otel::SessionTelemetry;
use codex_protocol::ThreadId;
use codex_utils_absolute_path::AbsolutePathBuf;
use tokio::fs;
use tokio::process::Command;
use tokio::sync::watch;
@@ -25,8 +25,8 @@ use tracing::info_span;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ShellSnapshot {
pub path: PathBuf,
pub cwd: PathBuf,
pub path: AbsolutePathBuf,
pub cwd: AbsolutePathBuf,
}
const SNAPSHOT_TIMEOUT: Duration = Duration::from_secs(10);
@@ -36,9 +36,9 @@ const EXCLUDED_EXPORT_VARS: &[&str] = &["PWD", "OLDPWD"];
impl ShellSnapshot {
pub fn start_snapshotting(
codex_home: PathBuf,
codex_home: AbsolutePathBuf,
session_id: ThreadId,
session_cwd: PathBuf,
session_cwd: AbsolutePathBuf,
shell: &mut Shell,
session_telemetry: SessionTelemetry,
) -> watch::Sender<Option<Arc<ShellSnapshot>>> {
@@ -58,9 +58,9 @@ impl ShellSnapshot {
}
pub fn refresh_snapshot(
codex_home: PathBuf,
codex_home: AbsolutePathBuf,
session_id: ThreadId,
session_cwd: PathBuf,
session_cwd: AbsolutePathBuf,
shell: Shell,
shell_snapshot_tx: watch::Sender<Option<Arc<ShellSnapshot>>>,
session_telemetry: SessionTelemetry,
@@ -76,9 +76,9 @@ impl ShellSnapshot {
}
fn spawn_snapshot_task(
codex_home: PathBuf,
codex_home: AbsolutePathBuf,
session_id: ThreadId,
session_cwd: PathBuf,
session_cwd: AbsolutePathBuf,
snapshot_shell: Shell,
shell_snapshot_tx: watch::Sender<Option<Arc<ShellSnapshot>>>,
session_telemetry: SessionTelemetry,
@@ -87,14 +87,10 @@ impl ShellSnapshot {
tokio::spawn(
async move {
let timer = session_telemetry.start_timer("codex.shell_snapshot.duration_ms", &[]);
let snapshot = ShellSnapshot::try_new(
&codex_home,
session_id,
session_cwd.as_path(),
&snapshot_shell,
)
.await
.map(Arc::new);
let snapshot =
ShellSnapshot::try_new(&codex_home, session_id, &session_cwd, &snapshot_shell)
.await
.map(Arc::new);
let success = snapshot.is_ok();
let success_tag = if success { "true" } else { "false" };
let _ = timer.map(|timer| timer.record(&[("success", success_tag)]));
@@ -110,9 +106,9 @@ impl ShellSnapshot {
}
async fn try_new(
codex_home: &Path,
codex_home: &AbsolutePathBuf,
session_id: ThreadId,
session_cwd: &Path,
session_cwd: &AbsolutePathBuf,
shell: &Shell,
) -> std::result::Result<Self, &'static str> {
// File to store the snapshot
@@ -132,7 +128,7 @@ impl ShellSnapshot {
.join(format!("{session_id}.tmp-{nonce}"));
// Clean the (unlikely) leaked snapshot files.
let codex_home = codex_home.to_path_buf();
let codex_home = codex_home.clone();
let cleanup_session_id = session_id;
tokio::spawn(async move {
if let Err(err) = cleanup_stale_snapshots(&codex_home, cleanup_session_id).await {
@@ -141,24 +137,23 @@ impl ShellSnapshot {
});
// Make the new snapshot.
let temp_path =
match write_shell_snapshot(shell.shell_type.clone(), &temp_path, session_cwd).await {
Ok(path) => {
tracing::info!("Shell snapshot successfully created: {}", path.display());
path
}
Err(err) => {
tracing::warn!(
"Failed to create shell snapshot for {}: {err:?}",
shell.name()
);
return Err("write_failed");
}
};
if let Err(err) =
write_shell_snapshot(shell.shell_type.clone(), &temp_path, session_cwd).await
{
tracing::warn!(
"Failed to create shell snapshot for {}: {err:?}",
shell.name()
);
return Err("write_failed");
}
tracing::info!(
"Shell snapshot successfully created: {}",
temp_path.display()
);
let temp_snapshot = Self {
path: temp_path.clone(),
cwd: session_cwd.to_path_buf(),
cwd: session_cwd.clone(),
};
if let Err(err) = validate_snapshot(shell, &temp_snapshot.path, session_cwd).await {
@@ -175,7 +170,7 @@ impl ShellSnapshot {
Ok(Self {
path,
cwd: session_cwd.to_path_buf(),
cwd: session_cwd.clone(),
})
}
}
@@ -193,9 +188,9 @@ impl Drop for ShellSnapshot {
async fn write_shell_snapshot(
shell_type: ShellType,
output_path: &Path,
cwd: &Path,
) -> Result<PathBuf> {
output_path: &AbsolutePathBuf,
cwd: &AbsolutePathBuf,
) -> Result<()> {
if shell_type == ShellType::PowerShell || shell_type == ShellType::Cmd {
bail!("Shell snapshot not supported yet for {shell_type:?}");
}
@@ -207,7 +202,7 @@ async fn write_shell_snapshot(
if let Some(parent) = output_path.parent() {
let parent_display = parent.display();
fs::create_dir_all(parent)
fs::create_dir_all(&parent)
.await
.with_context(|| format!("Failed to create snapshot parent {parent_display}"))?;
}
@@ -217,10 +212,10 @@ async fn write_shell_snapshot(
.await
.with_context(|| format!("Failed to write snapshot to {snapshot_path}"))?;
Ok(output_path.to_path_buf())
Ok(())
}
async fn capture_snapshot(shell: &Shell, cwd: &Path) -> Result<String> {
async fn capture_snapshot(shell: &Shell, cwd: &AbsolutePathBuf) -> Result<String> {
let shell_type = shell.shell_type.clone();
match shell_type {
ShellType::Zsh => run_shell_script(shell, &zsh_snapshot_script(), cwd).await,
@@ -240,7 +235,11 @@ fn strip_snapshot_preamble(snapshot: &str) -> Result<String> {
Ok(snapshot[start..].to_string())
}
async fn validate_snapshot(shell: &Shell, snapshot_path: &Path, cwd: &Path) -> Result<()> {
async fn validate_snapshot(
shell: &Shell,
snapshot_path: &AbsolutePathBuf,
cwd: &AbsolutePathBuf,
) -> Result<()> {
let snapshot_path_display = snapshot_path.display();
let script = format!("set -e; . \"{snapshot_path_display}\"");
run_script_with_timeout(
@@ -254,7 +253,7 @@ async fn validate_snapshot(shell: &Shell, snapshot_path: &Path, cwd: &Path) -> R
.map(|_| ())
}
async fn run_shell_script(shell: &Shell, script: &str, cwd: &Path) -> Result<String> {
async fn run_shell_script(shell: &Shell, script: &str, cwd: &AbsolutePathBuf) -> Result<String> {
run_script_with_timeout(
shell,
script,
@@ -270,7 +269,7 @@ async fn run_script_with_timeout(
script: &str,
snapshot_timeout: Duration,
use_login_shell: bool,
cwd: &Path,
cwd: &AbsolutePathBuf,
) -> Result<String> {
let args = shell.derive_exec_args(script, use_login_shell);
let shell_name = shell.name();
@@ -489,7 +488,10 @@ $envVars | ForEach-Object {
/// Removes shell snapshots that either lack a matching session rollout file or
/// whose rollouts have not been updated within the retention window.
/// The active session id is exempt from cleanup.
pub async fn cleanup_stale_snapshots(codex_home: &Path, active_session_id: ThreadId) -> Result<()> {
pub async fn cleanup_stale_snapshots(
codex_home: &AbsolutePathBuf,
active_session_id: ThreadId,
) -> Result<()> {
let snapshot_dir = codex_home.join(SNAPSHOT_DIR);
let mut entries = match fs::read_dir(&snapshot_dir).await {
+33 -23
View File
@@ -1,7 +1,10 @@
use super::*;
use core_test_support::PathBufExt;
use core_test_support::PathExt;
use pretty_assertions::assert_eq;
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
use std::path::PathBuf;
#[cfg(unix)]
use std::process::Command;
#[cfg(target_os = "linux")]
@@ -80,7 +83,7 @@ fn assert_posix_snapshot_sections(snapshot: &str) {
async fn get_snapshot(shell_type: ShellType) -> Result<String> {
let dir = tempdir()?;
let path = dir.path().join("snapshot.sh");
write_shell_snapshot(shell_type, &path, dir.path()).await?;
write_shell_snapshot(shell_type, &path.abs(), &dir.path().abs()).await?;
let content = fs::read_to_string(&path).await?;
Ok(content)
}
@@ -194,12 +197,17 @@ async fn try_new_creates_and_deletes_snapshot_file() -> Result<()> {
shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
};
let snapshot = ShellSnapshot::try_new(dir.path(), ThreadId::new(), dir.path(), &shell)
.await
.expect("snapshot should be created");
let snapshot = ShellSnapshot::try_new(
&dir.path().abs(),
ThreadId::new(),
&dir.path().abs(),
&shell,
)
.await
.expect("snapshot should be created");
let path = snapshot.path.clone();
assert!(path.exists());
assert_eq!(snapshot.cwd, dir.path().to_path_buf());
assert_eq!(snapshot.cwd, dir.path().abs());
drop(snapshot);
@@ -219,12 +227,14 @@ async fn try_new_uses_distinct_generation_paths() -> Result<()> {
shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
};
let initial_snapshot = ShellSnapshot::try_new(dir.path(), session_id, dir.path(), &shell)
.await
.expect("initial snapshot should be created");
let refreshed_snapshot = ShellSnapshot::try_new(dir.path(), session_id, dir.path(), &shell)
.await
.expect("refreshed snapshot should be created");
let initial_snapshot =
ShellSnapshot::try_new(&dir.path().abs(), session_id, &dir.path().abs(), &shell)
.await
.expect("initial snapshot should be created");
let refreshed_snapshot =
ShellSnapshot::try_new(&dir.path().abs(), session_id, &dir.path().abs(), &shell)
.await
.expect("refreshed snapshot should be created");
let initial_path = initial_snapshot.path.clone();
let refreshed_path = refreshed_snapshot.path.clone();
@@ -250,7 +260,7 @@ async fn snapshot_shell_does_not_inherit_stdin() -> Result<()> {
let _stdin_guard = BlockingStdinPipe::install()?;
let dir = tempdir()?;
let home = dir.path();
let home = dir.path().abs();
let read_status_path = home.join("stdin-read-status");
let read_status_display = read_status_path.display();
// Persist the startup `read` exit status so the test can assert whether
@@ -274,7 +284,7 @@ async fn snapshot_shell_does_not_inherit_stdin() -> Result<()> {
&script,
Duration::from_secs(2),
/*use_login_shell*/ true,
home,
&home,
)
.await
.context("run snapshot command")?;
@@ -318,7 +328,7 @@ async fn timed_out_snapshot_shell_is_terminated() -> Result<()> {
&script,
Duration::from_secs(1),
/*use_login_shell*/ true,
dir.path(),
&dir.path().abs(),
)
.await
.expect_err("snapshot shell should time out");
@@ -403,7 +413,7 @@ async fn write_rollout_stub(codex_home: &Path, session_id: ThreadId) -> Result<P
#[tokio::test]
async fn cleanup_stale_snapshots_removes_orphans_and_keeps_live() -> Result<()> {
let dir = tempdir()?;
let codex_home = dir.path();
let codex_home = dir.path().abs();
let snapshot_dir = codex_home.join(SNAPSHOT_DIR);
fs::create_dir_all(&snapshot_dir).await?;
@@ -413,12 +423,12 @@ async fn cleanup_stale_snapshots_removes_orphans_and_keeps_live() -> Result<()>
let orphan_snapshot = snapshot_dir.join(format!("{orphan_session}.456.sh"));
let invalid_snapshot = snapshot_dir.join("not-a-snapshot.txt");
write_rollout_stub(codex_home, live_session).await?;
write_rollout_stub(&codex_home, live_session).await?;
fs::write(&live_snapshot, "live").await?;
fs::write(&orphan_snapshot, "orphan").await?;
fs::write(&invalid_snapshot, "invalid").await?;
cleanup_stale_snapshots(codex_home, ThreadId::new()).await?;
cleanup_stale_snapshots(&codex_home, ThreadId::new()).await?;
assert_eq!(live_snapshot.exists(), true);
assert_eq!(orphan_snapshot.exists(), false);
@@ -430,18 +440,18 @@ async fn cleanup_stale_snapshots_removes_orphans_and_keeps_live() -> Result<()>
#[tokio::test]
async fn cleanup_stale_snapshots_removes_stale_rollouts() -> Result<()> {
let dir = tempdir()?;
let codex_home = dir.path();
let codex_home = dir.path().abs();
let snapshot_dir = codex_home.join(SNAPSHOT_DIR);
fs::create_dir_all(&snapshot_dir).await?;
let stale_session = ThreadId::new();
let stale_snapshot = snapshot_dir.join(format!("{stale_session}.123.sh"));
let rollout_path = write_rollout_stub(codex_home, stale_session).await?;
let rollout_path = write_rollout_stub(&codex_home, stale_session).await?;
fs::write(&stale_snapshot, "stale").await?;
set_file_mtime(&rollout_path, SNAPSHOT_RETENTION + Duration::from_secs(60))?;
cleanup_stale_snapshots(codex_home, ThreadId::new()).await?;
cleanup_stale_snapshots(&codex_home, ThreadId::new()).await?;
assert_eq!(stale_snapshot.exists(), false);
Ok(())
@@ -451,18 +461,18 @@ async fn cleanup_stale_snapshots_removes_stale_rollouts() -> Result<()> {
#[tokio::test]
async fn cleanup_stale_snapshots_skips_active_session() -> Result<()> {
let dir = tempdir()?;
let codex_home = dir.path();
let codex_home = dir.path().abs();
let snapshot_dir = codex_home.join(SNAPSHOT_DIR);
fs::create_dir_all(&snapshot_dir).await?;
let active_session = ThreadId::new();
let active_snapshot = snapshot_dir.join(format!("{active_session}.123.sh"));
let rollout_path = write_rollout_stub(codex_home, active_session).await?;
let rollout_path = write_rollout_stub(&codex_home, active_session).await?;
fs::write(&active_snapshot, "active").await?;
set_file_mtime(&rollout_path, SNAPSHOT_RETENTION + Duration::from_secs(60))?;
cleanup_stale_snapshots(codex_home, active_session).await?;
cleanup_stale_snapshots(&codex_home, active_session).await?;
assert_eq!(active_snapshot.exists(), true);
Ok(())
+2 -1
View File
@@ -1,4 +1,5 @@
use codex_network_proxy::NetworkProxy;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;
use std::path::PathBuf;
use std::process::Stdio;
@@ -40,7 +41,7 @@ pub(crate) struct SpawnChildRequest<'a> {
pub program: PathBuf,
pub args: Vec<String>,
pub arg0: Option<&'a str>,
pub cwd: PathBuf,
pub cwd: AbsolutePathBuf,
pub network_sandbox_policy: NetworkSandboxPolicy,
pub network: Option<&'a NetworkProxy>,
pub stdio_policy: StdioPolicy,
+11 -12
View File
@@ -1,5 +1,3 @@
use std::path::Path;
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::Arc;
@@ -27,6 +25,7 @@ use codex_protocol::models::MessagePhase;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use codex_rollout::state_db;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_stream_parser::strip_proposed_plan_blocks;
use futures::Future;
use tracing::debug;
@@ -35,10 +34,10 @@ use tracing::instrument;
const GENERATED_IMAGE_ARTIFACTS_DIR: &str = "generated_images";
pub(crate) fn image_generation_artifact_path(
codex_home: &Path,
codex_home: &AbsolutePathBuf,
session_id: &str,
call_id: &str,
) -> PathBuf {
) -> AbsolutePathBuf {
let sanitize = |value: &str| {
let mut sanitized: String = value
.chars()
@@ -104,11 +103,11 @@ pub(crate) fn raw_assistant_output_text_from_item(item: &ResponseItem) -> Option
}
async fn save_image_generation_result(
codex_home: &std::path::Path,
codex_home: &AbsolutePathBuf,
session_id: &str,
call_id: &str,
result: &str,
) -> Result<PathBuf> {
) -> Result<AbsolutePathBuf> {
let bytes = BASE64_STANDARD
.decode(result.trim().as_bytes())
.map_err(|err| {
@@ -361,7 +360,7 @@ pub(crate) async fn handle_non_tool_response_item(
if let TurnItem::ImageGeneration(image_item) = &mut turn_item {
let session_id = sess.conversation_id.to_string();
match save_image_generation_result(
turn_context.config.codex_home.as_path(),
&turn_context.config.codex_home,
&session_id,
&image_item.id,
&image_item.result,
@@ -369,15 +368,15 @@ pub(crate) async fn handle_non_tool_response_item(
.await
{
Ok(path) => {
image_item.saved_path = Some(path.to_string_lossy().into_owned());
image_item.saved_path = Some(path);
let image_output_path = image_generation_artifact_path(
turn_context.config.codex_home.as_path(),
&turn_context.config.codex_home,
&session_id,
"<image_id>",
);
let image_output_dir = image_output_path
.parent()
.unwrap_or(turn_context.config.codex_home.as_path());
.unwrap_or_else(|| turn_context.config.codex_home.clone());
let message: ResponseItem = DeveloperInstructions::new(format!(
"Generated images are saved to {} as {} by default.\nIf you need to use a generated image at another path, copy it and leave the original in place unless the user explicitly asks you to delete it.",
image_output_dir.display(),
@@ -389,13 +388,13 @@ pub(crate) async fn handle_non_tool_response_item(
}
Err(err) => {
let output_path = image_generation_artifact_path(
turn_context.config.codex_home.as_path(),
&turn_context.config.codex_home,
&session_id,
&image_item.id,
);
let output_dir = output_path
.parent()
.unwrap_or(turn_context.config.codex_home.as_path());
.unwrap_or_else(|| turn_context.config.codex_home.clone());
tracing::warn!(
call_id = %image_item.id,
output_dir = %output_dir.display(),
+20 -17
View File
@@ -9,6 +9,7 @@ use codex_protocol::items::TurnItem;
use codex_protocol::models::ContentItem;
use codex_protocol::models::MessagePhase;
use codex_protocol::models::ResponseItem;
use codex_utils_absolute_path::test_support::PathExt;
use pretty_assertions::assert_eq;
fn assistant_output_text(text: &str) -> ResponseItem {
@@ -128,12 +129,12 @@ fn completed_item_defers_mailbox_delivery_for_image_generation_calls() {
#[tokio::test]
async fn save_image_generation_result_saves_base64_to_png_in_codex_home() {
let codex_home = tempfile::tempdir().expect("create codex home");
let expected_path =
image_generation_artifact_path(codex_home.path(), "session-1", "ig_save_base64");
let codex_home = codex_home.path().abs();
let expected_path = image_generation_artifact_path(&codex_home, "session-1", "ig_save_base64");
let _ = std::fs::remove_file(&expected_path);
let saved_path =
save_image_generation_result(codex_home.path(), "session-1", "ig_save_base64", "Zm9v")
save_image_generation_result(&codex_home, "session-1", "ig_save_base64", "Zm9v")
.await
.expect("image should be saved");
@@ -146,8 +147,9 @@ async fn save_image_generation_result_saves_base64_to_png_in_codex_home() {
async fn save_image_generation_result_rejects_data_url_payload() {
let result = "data:image/jpeg;base64,Zm9v";
let codex_home = tempfile::tempdir().expect("create codex home");
let codex_home = codex_home.path().abs();
let err = save_image_generation_result(codex_home.path(), "session-1", "ig_456", result)
let err = save_image_generation_result(&codex_home, "session-1", "ig_456", result)
.await
.expect_err("data url payload should error");
assert!(matches!(err, CodexErr::InvalidRequest(_)));
@@ -156,8 +158,8 @@ async fn save_image_generation_result_rejects_data_url_payload() {
#[tokio::test]
async fn save_image_generation_result_overwrites_existing_file() {
let codex_home = tempfile::tempdir().expect("create codex home");
let existing_path =
image_generation_artifact_path(codex_home.path(), "session-1", "ig_overwrite");
let codex_home = codex_home.path().abs();
let existing_path = image_generation_artifact_path(&codex_home, "session-1", "ig_overwrite");
std::fs::create_dir_all(
existing_path
.parent()
@@ -166,10 +168,9 @@ async fn save_image_generation_result_overwrites_existing_file() {
.expect("create image output dir");
std::fs::write(&existing_path, b"existing").expect("seed existing image");
let saved_path =
save_image_generation_result(codex_home.path(), "session-1", "ig_overwrite", "Zm9v")
.await
.expect("image should be saved");
let saved_path = save_image_generation_result(&codex_home, "session-1", "ig_overwrite", "Zm9v")
.await
.expect("image should be saved");
assert_eq!(saved_path, existing_path);
assert_eq!(std::fs::read(&saved_path).expect("saved file"), b"foo");
@@ -179,13 +180,13 @@ async fn save_image_generation_result_overwrites_existing_file() {
#[tokio::test]
async fn save_image_generation_result_sanitizes_call_id_for_codex_home_output_path() {
let codex_home = tempfile::tempdir().expect("create codex home");
let expected_path = image_generation_artifact_path(codex_home.path(), "session-1", "../ig/..");
let codex_home = codex_home.path().abs();
let expected_path = image_generation_artifact_path(&codex_home, "session-1", "../ig/..");
let _ = std::fs::remove_file(&expected_path);
let saved_path =
save_image_generation_result(codex_home.path(), "session-1", "../ig/..", "Zm9v")
.await
.expect("image should be saved");
let saved_path = save_image_generation_result(&codex_home, "session-1", "../ig/..", "Zm9v")
.await
.expect("image should be saved");
assert_eq!(saved_path, expected_path);
assert_eq!(std::fs::read(&saved_path).expect("saved file"), b"foo");
@@ -195,7 +196,8 @@ async fn save_image_generation_result_sanitizes_call_id_for_codex_home_output_pa
#[tokio::test]
async fn save_image_generation_result_rejects_non_standard_base64() {
let codex_home = tempfile::tempdir().expect("create codex home");
let err = save_image_generation_result(codex_home.path(), "session-1", "ig_urlsafe", "_-8")
let codex_home = codex_home.path().abs();
let err = save_image_generation_result(&codex_home, "session-1", "ig_urlsafe", "_-8")
.await
.expect_err("non-standard base64 should error");
assert!(matches!(err, CodexErr::InvalidRequest(_)));
@@ -204,8 +206,9 @@ async fn save_image_generation_result_rejects_non_standard_base64() {
#[tokio::test]
async fn save_image_generation_result_rejects_non_base64_data_urls() {
let codex_home = tempfile::tempdir().expect("create codex home");
let codex_home = codex_home.path().abs();
let err = save_image_generation_result(
codex_home.path(),
&codex_home,
"session-1",
"ig_svg",
"data:image/svg+xml,<svg/>",
+5 -5
View File
@@ -131,7 +131,7 @@ pub(crate) async fn execute_user_shell_command(
let exec_command = maybe_wrap_shell_lc_with_snapshot(
&display_command,
session_shell.as_ref(),
turn_context.cwd.as_path(),
&turn_context.cwd,
&turn_context.shell_environment_policy.r#set,
&exec_env_map,
);
@@ -149,7 +149,7 @@ pub(crate) async fn execute_user_shell_command(
process_id: None,
turn_id: turn_context.sub_id.clone(),
command: display_command.clone(),
cwd: cwd.to_path_buf(),
cwd: cwd.clone(),
parsed_cmd: parsed_cmd.clone(),
source: ExecCommandSource::UserShell,
interaction_input: None,
@@ -218,7 +218,7 @@ pub(crate) async fn execute_user_shell_command(
process_id: None,
turn_id: turn_context.sub_id.clone(),
command: display_command.clone(),
cwd: cwd.to_path_buf(),
cwd: cwd.clone(),
parsed_cmd: parsed_cmd.clone(),
source: ExecCommandSource::UserShell,
interaction_input: None,
@@ -242,7 +242,7 @@ pub(crate) async fn execute_user_shell_command(
process_id: None,
turn_id: turn_context.sub_id.clone(),
command: display_command.clone(),
cwd: cwd.to_path_buf(),
cwd: cwd.clone(),
parsed_cmd: parsed_cmd.clone(),
source: ExecCommandSource::UserShell,
interaction_input: None,
@@ -286,7 +286,7 @@ pub(crate) async fn execute_user_shell_command(
process_id: None,
turn_id: turn_context.sub_id.clone(),
command: display_command,
cwd: cwd.to_path_buf(),
cwd,
parsed_cmd,
source: ExecCommandSource::UserShell,
interaction_input: None,
+12 -16
View File
@@ -18,8 +18,8 @@ use codex_protocol::protocol::PatchApplyEndEvent;
use codex_protocol::protocol::PatchApplyStatus;
use codex_protocol::protocol::TurnDiffEvent;
use codex_shell_command::parse_command::parse_command;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use std::time::Duration;
@@ -64,7 +64,7 @@ pub(crate) enum ToolEventFailure {
pub(crate) async fn emit_exec_command_begin(
ctx: ToolEventCtx<'_>,
command: &[String],
cwd: &Path,
cwd: &AbsolutePathBuf,
parsed_cmd: &[ParsedCommand],
source: ExecCommandSource,
interaction_input: Option<String>,
@@ -78,7 +78,7 @@ pub(crate) async fn emit_exec_command_begin(
process_id: process_id.map(str::to_owned),
turn_id: ctx.turn.sub_id.clone(),
command: command.to_vec(),
cwd: cwd.to_path_buf(),
cwd: cwd.clone(),
parsed_cmd: parsed_cmd.to_vec(),
source,
interaction_input,
@@ -90,7 +90,7 @@ pub(crate) async fn emit_exec_command_begin(
pub(crate) enum ToolEmitter {
Shell {
command: Vec<String>,
cwd: PathBuf,
cwd: AbsolutePathBuf,
source: ExecCommandSource,
parsed_cmd: Vec<ParsedCommand>,
freeform: bool,
@@ -101,7 +101,7 @@ pub(crate) enum ToolEmitter {
},
UnifiedExec {
command: Vec<String>,
cwd: PathBuf,
cwd: AbsolutePathBuf,
source: ExecCommandSource,
parsed_cmd: Vec<ParsedCommand>,
process_id: Option<String>,
@@ -111,7 +111,7 @@ pub(crate) enum ToolEmitter {
impl ToolEmitter {
pub fn shell(
command: Vec<String>,
cwd: PathBuf,
cwd: AbsolutePathBuf,
source: ExecCommandSource,
freeform: bool,
) -> Self {
@@ -134,7 +134,7 @@ impl ToolEmitter {
pub fn unified_exec(
command: &[String],
cwd: PathBuf,
cwd: AbsolutePathBuf,
source: ExecCommandSource,
process_id: Option<String>,
) -> Self {
@@ -163,11 +163,7 @@ impl ToolEmitter {
emit_exec_stage(
ctx,
ExecCommandInput::new(
command,
cwd.as_path(),
parsed_cmd,
*source,
/*interaction_input*/ None,
command, cwd, parsed_cmd, *source, /*interaction_input*/ None,
/*process_id*/ None,
),
stage,
@@ -273,7 +269,7 @@ impl ToolEmitter {
ctx,
ExecCommandInput::new(
command,
cwd.as_path(),
cwd,
parsed_cmd,
*source,
/*interaction_input*/ None,
@@ -365,7 +361,7 @@ impl ToolEmitter {
struct ExecCommandInput<'a> {
command: &'a [String],
cwd: &'a Path,
cwd: &'a AbsolutePathBuf,
parsed_cmd: &'a [ParsedCommand],
source: ExecCommandSource,
interaction_input: Option<&'a str>,
@@ -375,7 +371,7 @@ struct ExecCommandInput<'a> {
impl<'a> ExecCommandInput<'a> {
fn new(
command: &'a [String],
cwd: &'a Path,
cwd: &'a AbsolutePathBuf,
parsed_cmd: &'a [ParsedCommand],
source: ExecCommandSource,
interaction_input: Option<&'a str>,
@@ -479,7 +475,7 @@ async fn emit_exec_end(
process_id: exec_input.process_id.map(str::to_owned),
turn_id: ctx.turn.sub_id.clone(),
command: exec_input.command.to_vec(),
cwd: exec_input.cwd.to_path_buf(),
cwd: exec_input.cwd.clone(),
parsed_cmd: exec_input.parsed_cmd.to_vec(),
source: exec_input.source,
interaction_input: exec_input.interaction_input.map(str::to_owned),
+2 -2
View File
@@ -60,7 +60,7 @@ async fn emit_js_repl_exec_begin(
) {
let emitter = ToolEmitter::shell(
vec!["js_repl".to_string()],
turn.cwd.to_path_buf(),
turn.cwd.clone(),
ExecCommandSource::Agent,
/*freeform*/ false,
);
@@ -79,7 +79,7 @@ async fn emit_js_repl_exec_end(
let exec_output = build_js_repl_exec_output(output, error, duration);
let emitter = ToolEmitter::shell(
vec!["js_repl".to_string()],
turn.cwd.to_path_buf(),
turn.cwd.clone(),
ExecCommandSource::Agent,
/*freeform*/ false,
);
@@ -77,7 +77,7 @@ async fn emit_js_repl_exec_end_sends_event() {
assert_eq!(event.call_id, "call-1");
assert_eq!(event.turn_id, turn.sub_id);
assert_eq!(event.command, vec!["js_repl".to_string()]);
assert_eq!(event.cwd, turn.cwd.to_path_buf());
assert_eq!(event.cwd, turn.cwd);
assert_eq!(event.source, ExecCommandSource::Agent);
assert_eq!(event.interaction_input, None);
assert_eq!(event.stdout, "hello");
+1 -1
View File
@@ -483,7 +483,7 @@ impl ShellHandler {
let source = ExecCommandSource::Agent;
let emitter = ToolEmitter::shell(
exec_params.command.clone(),
exec_params.cwd.to_path_buf(),
exec_params.cwd.clone(),
source,
freeform,
);
@@ -2,6 +2,8 @@ use std::path::PathBuf;
use std::sync::Arc;
use codex_protocol::models::ShellCommandToolCallParams;
use core_test_support::PathBufExt;
use core_test_support::test_path_buf;
use pretty_assertions::assert_eq;
use crate::codex::make_session_and_context;
@@ -125,8 +127,8 @@ async fn shell_command_handler_to_exec_params_uses_session_shell_and_turn_contex
#[test]
fn shell_command_handler_respects_explicit_login_flag() {
let (_tx, shell_snapshot) = watch::channel(Some(Arc::new(ShellSnapshot {
path: PathBuf::from("/tmp/snapshot.sh"),
cwd: PathBuf::from("/tmp"),
path: test_path_buf("/tmp/snapshot.sh").abs(),
cwd: test_path_buf("/tmp").abs(),
})));
let shell = Shell {
shell_type: ShellType::Bash,

Some files were not shown because too many files have changed in this diff Show More