Skip to content

sandboxing: intersect permission profiles semantically#18275

Merged
bolinfest merged 1 commit intomainfrom
pr18275
Apr 21, 2026
Merged

sandboxing: intersect permission profiles semantically#18275
bolinfest merged 1 commit intomainfrom
pr18275

Conversation

@bolinfest
Copy link
Copy Markdown
Collaborator

@bolinfest bolinfest commented Apr 17, 2026

Why

Permission approval responses must not be able to grant more access than the tool requested. Moving this flow to PermissionProfile means the comparison must be profile-shaped instead of SandboxPolicy-shaped, and cwd-relative special paths such as :cwd and :project_roots must stay anchored to the turn that produced the request.

What changed

This implements semantic PermissionProfile intersection in codex-sandboxing for file-system and network permissions. The intersection accepts narrower path grants, rejects broader grants, preserves deny-read carve-outs and glob scan depth, and materializes cwd-dependent special-path grants to absolute paths before they can be recorded for reuse.

The request-permissions response paths now use that intersection consistently. App-server captures the request turn cwd before waiting for the client response, includes that cwd in the v2 approval params, and core stores the requested profile plus cwd for direct TUI/client responses and Guardian decisions before recording turn- or session-scoped grants. The TUI app-server bridge now preserves the app-server request cwd when converting permission approval params into core events.

Verification

  • cargo test -p codex-sandboxing intersect_permission_profiles -- --nocapture
  • cargo test -p codex-app-server request_permissions_response -- --nocapture
  • cargo test -p codex-core request_permissions_response_materializes_session_cwd_grants_before_recording -- --nocapture
  • cargo check -p codex-tui --tests
  • cargo check --tests
  • cargo test -p codex-tui app_server_request_permissions_preserves_file_system_permissions

Copy link
Copy Markdown
Contributor

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 627b9d3f0f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +182 to +184
if !granted_entry.access.can_read() {
return false;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve deny entries when intersecting permissions

The new intersection logic unconditionally drops granted filesystem entries with access: none (can_read() is false), which means deny carve-outs are never preserved even when both requested and granted profiles include them. In request_permissions_response_from_client_result, this can widen the stored grant: a request like "write cwd except cwd/secret" intersected with the same client response becomes plain write cwd after this filter, re-enabling paths that were explicitly denied.

Useful? React with 👍 / 👎.

@bolinfest bolinfest force-pushed the pr18713 branch 2 times, most recently from cb3ad47 to 6b4eb40 Compare April 20, 2026 20:11
@bolinfest bolinfest force-pushed the pr18713 branch 2 times, most recently from b612649 to f6b0616 Compare April 20, 2026 22:21
@bolinfest bolinfest force-pushed the pr18275 branch 2 times, most recently from e6518e0 to f9163e8 Compare April 21, 2026 01:59
Base automatically changed from pr18713 to main April 21, 2026 02:42
bolinfest added a commit that referenced this pull request Apr 21, 2026
## Why

#18274 made `PermissionProfile` the canonical file-system permissions
shape, but the round-trip from `FileSystemSandboxPolicy` to
`PermissionProfile` still dropped one piece of policy metadata:
`glob_scan_max_depth`.

That field is security-relevant for deny-read globs such as `**/*.env`.
On Linux, bubblewrap sandbox construction uses it to bound unreadable
glob expansion. If a profile copied from active runtime permissions
loses this value and is submitted back as an override, the resulting
`FileSystemSandboxPolicy` can behave differently even though the visible
permission entries look equivalent.

## What changed

- Add `glob_scan_max_depth` to protocol `FileSystemPermissions` and
preserve it when converting to/from `FileSystemSandboxPolicy`.
- Keep legacy `read`/`write` JSON for simple path-only permissions, but
force canonical JSON when glob scan depth is present so the metadata is
not silently dropped.
- Carry `globScanMaxDepth` through app-server
`AdditionalFileSystemPermissions`, generated JSON/TypeScript schemas,
and app-server/TUI conversion call sites.
- Preserve the metadata through sandboxing permission normalization,
merging, and intersection.
- Carry the merged scan depth into the effective
`FileSystemSandboxPolicy` used for command execution, so bounded
deny-read globs reach Linux bubblewrap materialization.

## Verification

- `cargo test -p codex-sandboxing glob_scan -- --nocapture`
- `cargo test -p codex-sandboxing policy_transforms -- --nocapture`
- `just fix -p codex-sandboxing`





---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/18713).
* #18288
* #18287
* #18286
* #18285
* #18284
* #18283
* #18282
* #18281
* #18280
* #18279
* #18278
* #18277
* #18276
* #18275
* __->__ #18713
@bolinfest bolinfest force-pushed the pr18275 branch 4 times, most recently from 9a0c246 to f26a648 Compare April 21, 2026 05:31
@bolinfest bolinfest force-pushed the pr18275 branch 4 times, most recently from 5bc0ac8 to cf9f1ea Compare April 21, 2026 06:17
@bolinfest bolinfest requested a review from viyatb-oai April 21, 2026 06:18
{
let entry = materialize_cwd_dependent_entry(entry, cwd);
if !entries.contains(&entry) {
entries.push(entry);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Do not persist standalone deny entries from partial grants

The intersection now appends every FileSystemAccessMode::None entry from both the request and the response after filtering accepted read/write grants. That preserves carveouts for broad filesystem grants, but it also records denies when no filesystem grant survived. For example, if the model requests network plus :cwd write with /repo/secrets denied, and the client approves only network, the returned session grant still includes the /repo/secrets deny. Later commands then run with network enabled but unexpectedly lose read access to /repo/secrets. Deny entries should only be retained when attached to an accepted filesystem grant they constrain.

}

if let Some(path) = resolve_permission_path(&granted_entry.path, cwd) {
return access_covers(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Honor deny globs when accepting concrete grants

For concrete granted paths, this check asks resolve_access_with_cwd() whether the requested policy covers the path, but that resolver only considers resolved path/special entries and ignores GlobPattern deny entries. A request like :cwd write plus **/*.env deny can therefore accept a client response granting /repo/.env write because the broad :cwd entry covers it during intersection. The returned profile also carries the deny glob, so enforcement either later rejects the approved path or, on paths that consult can_write_path_with_cwd(), treats it as writable despite the requested carveout. The semantic subset check should reject concrete grants matched by requested deny globs before accepting the broad grant.

let request_cwd = match request.cwd.clone() {
Some(cwd) => cwd,
None => conversation.config_snapshot().await.cwd,
};
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Send request cwd to permission reviewers

The server computes request_cwd and uses it later to normalize the response, but the approval request sent to app-server clients still omits that cwd. Permission profiles can contain :cwd, :project_roots, and cwd-relative deny globs; in delegated flows the request cwd can differ from the visible thread cwd. A client can therefore approve write :cwd without knowing which concrete directory the server will apply. Include the request cwd in the approval params, or materialize cwd-dependent entries before sending the request to clients.

Copy link
Copy Markdown
Collaborator

@viyatb-oai viyatb-oai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non blocking - there are some edge cases codex found that are not covered by subsequent prs in the stack

@bolinfest bolinfest force-pushed the pr18275 branch 2 times, most recently from 87b4c7d to 9a98ccb Compare April 21, 2026 16:30
@bolinfest bolinfest merged commit f8562bd into main Apr 21, 2026
39 checks passed
@bolinfest bolinfest deleted the pr18275 branch April 21, 2026 17:23
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 21, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants