mirror of
https://github.com/openai/codex.git
synced 2026-11-13 22:47:20 +00:00
Honor attachment-owned permissions for MCP servers (#40728)
## Why MCP servers attached to executor environments must retain their owner's permission profile instead of inheriting the thread-wide sandbox authority. ## What changed - Resolve and capture a permission profile for each enabled MCP server when publishing the runtime, and reject calls or elicitations when that authority is unavailable. - Use the captured server profile for tool approval decisions, elicitation, and sandbox metadata, including after runtime refreshes. - Materialize `:workspace_roots` from `PathUri` values so permissions preserve the path convention of remote executor environments. - Give threadless app discovery and resource reads an explicit default permission profile. ## Testing Added coverage for per-server elicitation authority, unresolved attachments, runtime refreshes, restricted tool calls, and foreign-platform workspace roots. GitOrigin-RevId: 6b188d4b08b29e9971ff7aa68a7785a0cdf9e394
This commit is contained in:
1 parent
0b94751cc4
commit
4213b38f3c
25 files changed
+805
-136
No files matched your search
@@ -7,6 +7,7 @@ use std::path::PathBuf;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::canonicalize_preserving_symlinks;
|
||||
use codex_utils_path_uri::LegacyAppPathString;
|
||||
use codex_utils_path_uri::PathConvention;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use globset::GlobBuilder;
|
||||
use globset::GlobMatcher;
|
||||
@@ -1030,6 +1031,65 @@ impl FileSystemSandboxPolicy {
|
||||
self
|
||||
}
|
||||
|
||||
/// Materializes workspace-root entries without projecting executor paths onto the host.
|
||||
pub fn materialize_project_roots_with_path_uris(mut self, workspace_roots: &[PathUri]) -> Self {
|
||||
if let Ok(native_workspace_roots) = workspace_roots
|
||||
.iter()
|
||||
.map(PathUri::to_abs_path)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
{
|
||||
return self.materialize_project_roots_with_workspace_roots(&native_workspace_roots);
|
||||
}
|
||||
|
||||
let mut entries = Vec::with_capacity(self.entries.len());
|
||||
for entry in self.entries {
|
||||
let (subpath, is_glob) = match &entry.path {
|
||||
FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::ProjectRoots { subpath },
|
||||
} => (subpath.as_deref(), false),
|
||||
FileSystemPath::GlobPattern { pattern }
|
||||
if pattern.starts_with(PROJECT_ROOTS_GLOB_PATTERN_PREFIX) =>
|
||||
{
|
||||
(
|
||||
Some(&pattern[PROJECT_ROOTS_GLOB_PATTERN_PREFIX.len()..]),
|
||||
true,
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
entries.push(entry);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
entries.extend(workspace_roots.iter().filter_map(|root| {
|
||||
let path = subpath.map_or_else(
|
||||
|| Some(root.clone()),
|
||||
|subpath| resolve_scoped_workspace_path(root, subpath),
|
||||
);
|
||||
let (path, access) = match path {
|
||||
Some(path) if is_glob => (
|
||||
FileSystemPath::GlobPattern {
|
||||
pattern: path.inferred_native_path_string(),
|
||||
},
|
||||
entry.access,
|
||||
),
|
||||
Some(path) => (FileSystemPath::Path { path }, entry.access),
|
||||
None if !entry.access.can_write() => (
|
||||
FileSystemPath::Path { path: root.clone() },
|
||||
FileSystemAccessMode::Deny,
|
||||
),
|
||||
None => return None,
|
||||
};
|
||||
Some(FileSystemSandboxEntry {
|
||||
path,
|
||||
access,
|
||||
missing_path_behavior: entry.missing_path_behavior,
|
||||
})
|
||||
}));
|
||||
}
|
||||
self.entries = entries;
|
||||
self
|
||||
}
|
||||
|
||||
/// Preserves symbolic `:workspace_roots` entries while also adding concrete
|
||||
/// entries for each provided workspace root.
|
||||
pub fn with_materialized_project_roots_for_workspace_roots(
|
||||
@@ -1600,6 +1660,28 @@ fn resolve_candidate_path(path: &Path, cwd: &Path) -> Option<AbsolutePathBuf> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves a workspace-relative path using the root's own path convention.
|
||||
///
|
||||
/// Rejects absolute paths, traversal, Windows drive changes, and any result
|
||||
/// outside the root so foreign-platform permission rules cannot escape scope.
|
||||
fn resolve_scoped_workspace_path(root: &PathUri, subpath: &str) -> Option<PathUri> {
|
||||
let convention = root.infer_path_convention()?;
|
||||
if subpath.starts_with('/')
|
||||
|| convention == PathConvention::Windows && subpath.starts_with('\\')
|
||||
|| convention
|
||||
.path_segments(subpath)
|
||||
.any(|segment| segment == "." || segment == "..")
|
||||
|| convention == PathConvention::Windows
|
||||
&& convention
|
||||
.path_segments(subpath)
|
||||
.any(|segment| segment.contains(':'))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let path = root.join(subpath).ok()?;
|
||||
path.starts_with(root).then_some(path)
|
||||
}
|
||||
|
||||
/// Returns true when two config paths refer to the same exact target before
|
||||
/// any prefix matching is applied.
|
||||
///
|
||||
|
||||
Reference in new issue
Block a user