fix: use AbsolutePathBuf for permission profile file roots (#12970)

## Why
`PermissionProfile` should describe filesystem roots as absolute paths
at the type level. Using `PathBuf` in `FileSystemPermissions` made the
shared type too permissive and blurred together three different
deserialization cases:

- skill metadata in `agents/openai.yaml`, where relative paths should
resolve against the skill directory
- app-server API payloads, where callers should have to send absolute
paths
- local tool-call payloads for commands like `shell_command` and
`exec_command`, where `additional_permissions.file_system` may
legitimately be relative to the command `workdir`

This change tightens the shared model without regressing the existing
local command flow.

## What Changed
- changed `protocol::models::FileSystemPermissions` and the app-server
`AdditionalFileSystemPermissions` mirror to use `AbsolutePathBuf`
- wrapped skill metadata deserialization in `AbsolutePathBufGuard`, so
relative permission roots in `agents/openai.yaml` resolve against the
containing skill directory
- kept app-server/API deserialization strict, so relative
`additionalPermissions.fileSystem.*` paths are rejected at the boundary
- restored cwd/workdir-relative deserialization for local tool-call
payloads by parsing `shell`, `shell_command`, and `exec_command`
arguments under an `AbsolutePathBufGuard` rooted at the resolved command
working directory
- simplified runtime additional-permission normalization so it only
canonicalizes and deduplicates absolute roots instead of trying to
recover relative ones later
- updated the app-server schema fixtures, `app-server/README.md`, and
the affected transport/TUI tests to match the final behavior
This commit is contained in:
Michael Bolin
2026-02-27 09:42:52 -08:00
committed by GitHub
parent 8cf5b00aef
commit d09a7535ed
22 changed files with 384 additions and 191 deletions

View File

@@ -16,9 +16,12 @@ mod test_sync;
pub(crate) mod unified_exec;
mod view_image;
use codex_utils_absolute_path::AbsolutePathBufGuard;
pub use plan::PLAN_TOOL;
use serde::Deserialize;
use serde_json::Value;
use std::path::Path;
use std::path::PathBuf;
use crate::function_tool::FunctionCallError;
use crate::sandboxing::SandboxPermissions;
@@ -56,6 +59,33 @@ where
})
}
fn parse_arguments_with_base_path<T>(
arguments: &str,
base_path: &Path,
) -> Result<T, FunctionCallError>
where
T: for<'de> Deserialize<'de>,
{
let _guard = AbsolutePathBufGuard::new(base_path);
parse_arguments(arguments)
}
fn resolve_workdir_base_path(
arguments: &str,
default_cwd: &Path,
) -> Result<PathBuf, FunctionCallError> {
let arguments: Value = parse_arguments(arguments)?;
Ok(arguments
.get("workdir")
.and_then(Value::as_str)
.filter(|workdir| !workdir.is_empty())
.map(PathBuf::from)
.map_or_else(
|| default_cwd.to_path_buf(),
|workdir| crate::util::resolve_path(default_cwd, &workdir),
))
}
/// Validates feature/policy constraints for `with_additional_permissions` and
/// returns normalized absolute paths. Errors if paths are invalid.
pub(super) fn normalize_and_validate_additional_permissions(
@@ -63,7 +93,7 @@ pub(super) fn normalize_and_validate_additional_permissions(
approval_policy: AskForApproval,
sandbox_permissions: SandboxPermissions,
additional_permissions: Option<PermissionProfile>,
cwd: &Path,
_cwd: &Path,
) -> Result<Option<PermissionProfile>, String> {
let uses_additional_permissions = matches!(
sandbox_permissions,
@@ -91,7 +121,7 @@ pub(super) fn normalize_and_validate_additional_permissions(
.to_string(),
);
};
let normalized = normalize_additional_permissions(additional_permissions, cwd)?;
let normalized = normalize_additional_permissions(additional_permissions)?;
if normalized.is_empty() {
return Err(
"`additional_permissions` must include at least one path in `file_system.read` or `file_system.write`"