Skip to content

permissions: resolve profile identity with constraints#22683

Merged
bolinfest merged 1 commit into
mainfrom
pr22683
May 15, 2026
Merged

permissions: resolve profile identity with constraints#22683
bolinfest merged 1 commit into
mainfrom
pr22683

Conversation

@bolinfest
Copy link
Copy Markdown
Collaborator

@bolinfest bolinfest commented May 14, 2026

Why

This PR is the invariant-cleanup layer that follows the workspace-roots base merged in #22610.

#22610 adds [permissions.<id>.workspace_roots] and keeps runtime workspace roots separate from the raw permission profile, but its in-memory representation is intentionally transitional: Permissions still carries the selected profile identity next to a constrained PermissionProfile. That makes APIs such as set_constrained_permission_profile_with_active_profile() fragile because the id and value only mean the right thing when every caller keeps them in sync.

This PR introduces a single resolved profile state so profile identity, extends, the profile value, and profile-declared workspace roots travel together. The next PR, #22611, builds on this by changing the app-server turn API to select permission profiles by id plus runtime workspace roots.

Stack Context

Keeping this separate from #22611 is deliberate: reviewers can validate the internal state invariant before reviewing the app-server protocol migration.

What Changed

  • Added ResolvedPermissionProfile::{Legacy, BuiltIn, Named} and PermissionProfileState.
  • Typed built-in profile ids with BuiltInPermissionProfileId.
  • Moved selected profile identity and profile-declared workspace roots into the resolved state.
  • Replaced Permissions parallel profile fields with one permission_profile_state.
  • Removed set_constrained_permission_profile_with_active_profile() from session sync paths.
  • Kept trusted session replay/SessionConfigured compatibility through explicit session snapshot helpers.
  • Updated session configuration, MCP initialization, app-server, exec, TUI, and guardian call sites to consume &PermissionProfile directly.

Review Guide

Start with codex-rs/core/src/config/resolved_permission_profile.rs; it is the new invariant boundary. Then review codex-rs/core/src/config/mod.rs to see how config loading records active profile identity and profile workspace roots. The remaining call-site changes are mostly mechanical fallout from Permissions::permission_profile() returning &PermissionProfile instead of &Constrained<PermissionProfile>.

Verification

The existing config/session coverage now constructs and asserts through PermissionProfileState. The workspace-root config test also asserts that profile-declared roots are preserved in the resolved state, which is the behavior #22611 relies on when runtime roots become mutable through the app-server API.


Stack created with Sapling. Best reviewed with ReviewStack.

@bolinfest bolinfest requested a review from a team as a code owner May 14, 2026 19:45
@bolinfest bolinfest changed the base branch from main to pr22610 May 14, 2026 19:45
@bolinfest bolinfest force-pushed the pr22610 branch 4 times, most recently from 28c51e6 to 797dbb4 Compare May 14, 2026 22:45
@bolinfest bolinfest requested a review from viyatb-oai May 15, 2026 00:46
@bolinfest bolinfest force-pushed the pr22683 branch 2 times, most recently from 396a7e8 to cccf53e Compare May 15, 2026 00:52
@bolinfest bolinfest force-pushed the pr22683 branch 3 times, most recently from 033e494 to bb493c2 Compare May 15, 2026 01:25
bolinfest added a commit that referenced this pull request May 15, 2026
## Why

This is the configuration/model half of the alternative permissions
migration we discussed as a comparison point for
[#22401](#22401) and
[#22402](#22402).

The old `workspace-write` model mixes three concerns that we want to
keep separate:
- reusable profile rules that should stay immutable once selected
- user/runtime workspace roots from `cwd`, `--add-dir`, and legacy
workspace-write config
- internal Codex writable roots such as memories, which should not be
shown as user workspace roots

This PR gives permission profiles first-class `workspace_roots` so users
can opt multiple repositories into the same `:workspace_roots` rules
without using broad absolute-path write grants. It also starts
separating the raw selected profile from the effective runtime profile
by making `Permissions` expose explicit accessors instead of public
mutable fields.

A representative `config.toml` looks like this:

```toml
default_permissions = "dev"

[permissions.dev.workspace_roots]
"~/code/openai" = true
"~/code/developers-website" = true

[permissions.dev.filesystem.":workspace_roots"]
"." = "write"
".codex" = "read"
".git" = "read"
".vscode" = "read"
```

If Codex starts in `~/code/codex` with that profile selected, the
effective workspace-root set becomes:
- `~/code/codex` from the runtime `cwd`
- `~/code/openai` from the profile
- `~/code/developers-website` from the profile

The `:workspace_roots` rules are materialized across each root, so
`.git`, `.codex`, and `.vscode` stay scoped the same way everywhere.
Runtime additions such as `--add-dir` can still layer on later stack
entries without mutating the selected profile.

## Stack Shape

This PR intentionally stops before the profile-identity cleanup in
[#22683](#22683) so the base review
stays focused on config loading, workspace-root materialization, and
compatibility with legacy `workspace-write`.

The representation in this PR is therefore transitional: `Permissions`
carries enough state to distinguish the raw constrained profile from the
effective runtime profile, and there are still call sites that must keep
the active profile identity and constrained profile value in sync. The
follow-up PR replaces that with a single resolved profile state
(`ResolvedPermissionProfile` / `PermissionProfileState`) that keeps the
profile id, immutable `PermissionProfile`, and profile-declared
workspace roots together. That follow-up removes APIs such as
`set_constrained_permission_profile_with_active_profile()` where
separate arguments could drift out of sync.

Downstream PRs then build on this base to switch app-server turn updates
to profile ids plus runtime workspace roots and to finish the
user-visible summary behavior. Reviewers should judge this PR as the
workspace-roots foundation, not as the final in-memory shape of selected
permission profiles.

## Review Guide

Suggested review order:

1. Start with `codex-rs/core/src/config/mod.rs`.
This is the main shape change in the base slice. `Permissions` now
stores a private raw `Constrained<PermissionProfile>` plus runtime
`workspace_roots`. Callers use `permission_profile()` when they need the
raw constrained value and `effective_permission_profile()` when they
need a materialized runtime profile. As noted above,
[#22683](#22683) replaces this
transitional shape with a resolved profile state that keeps identity and
profile data together.

2. Review `codex-rs/config/src/permissions_toml.rs` and
`codex-rs/core/src/config/permissions.rs`.
These add `[permissions.<id>.workspace_roots]`, resolve enabled entries
relative to the policy cwd, and keep `:workspace_roots` deny-read glob
patterns symbolic until the actual roots are known.

3. Review `codex-rs/protocol/src/permissions.rs` and
`codex-rs/protocol/src/models.rs`.
These add the policy/profile materialization helpers that expand exact
`:workspace_roots` entries and scoped deny-read globs over every
workspace root. This is also where `ActivePermissionProfileModification`
is removed from the core model.

4. Review the legacy bridge in
`Config::load_from_base_config_with_overrides` and
`Config::set_legacy_sandbox_policy`.
This is where legacy `workspace-write` roots become runtime workspace
roots, while Codex internal writable roots stay internal and do not
appear as user-facing workspace roots.

5. Then skim downstream call sites.
The interesting pattern is raw-vs-effective access: state/proxy/bwrap
paths keep the raw constrained profile, while execution, summaries, and
user-visible status use the effective profile and workspace-root list.

## What Changed

- added `[permissions.<id>.workspace_roots]` to the config model and
schema
- added runtime `workspace_roots` state to `Config`/`Permissions` and
`ConfigOverrides`
- made `Permissions` profile fields private and replaced direct mutation
with accessors/setters
- added `PermissionProfile` and `FileSystemSandboxPolicy` helpers for
materializing `:workspace_roots` exact paths and deny-read globs across
all roots
- moved legacy additional writable roots into runtime workspace-root
state instead of active profile modifications
- removed `ActivePermissionProfileModification` and its app-server
protocol/schema export
- updated sandbox/status summary paths so internal writable roots are
not reported as user workspace roots

## Verification Strategy

The targeted tests cover the behavior at the layers where regressions
are most likely:
- `codex-rs/core/src/config/config_tests.rs` verifies config loading,
legacy workspace-root seeding, effective profile materialization, and
memory-root handling.
- `codex-rs/core/src/config/permissions_tests.rs` verifies profile
`workspace_roots` parsing and `:workspace_roots` scoped/glob
compilation.
- `codex-rs/protocol/src/permissions.rs` unit tests verify exact and
glob materialization over multiple workspace roots.
- `codex-rs/tui/src/status/tests.rs` and
`codex-rs/utils/sandbox-summary/src/sandbox_summary.rs` verify the
user-facing summaries show effective workspace roots and hide internal
writes.

I also ran `cargo check --tests` locally after the latest stack refresh
to catch cross-crate API breakage from the private-field/accessor
changes.







---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/22610).
* #22612
* #22611
* #22683
* __->__ #22610
Base automatically changed from pr22610 to main May 15, 2026 01:25
)?;
next_configuration
.permission_profile_state
.set_legacy_permission_profile(
Copy link
Copy Markdown
Collaborator

@viyatb-oai viyatb-oai May 15, 2026

Choose a reason for hiding this comment

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

[P2] Preserve the active profile on cwd-only rebinding.

This branch is the cwd-only rederivation path. Before this refactor it updated the constrained permission profile in place and left active_permission_profile intact. Routing it through set_legacy_permission_profile(...) now drops the selected profile identity (and any bound profile metadata) whenever a workspace-style session changes cwd, so later snapshots/status surfaces stop reporting the named or built-in profile even though the user did not change permissions.

Suggested change
.set_legacy_permission_profile(
next_configuration.permission_profile_state = next_configuration
.permission_profile_state
.clone_with_permission_profile(
PermissionProfile::from_runtime_permissions_with_enforcement(
SandboxEnforcement::from_legacy_sandbox_policy(&current_sandbox_policy),
&file_system_sandbox_policy,
current_network_sandbox_policy,
),
)?;
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.

Approving to unblock. I left one P2 regression note inline: the cwd-only rebind path now clears the active permission-profile identity by converting the state to Legacy; preserving the resolved state while replacing only the rebound profile value should fix it.

@bolinfest bolinfest merged commit 3c6d727 into main May 15, 2026
46 checks passed
@bolinfest bolinfest deleted the pr22683 branch May 15, 2026 01:47
@github-actions github-actions Bot locked and limited conversation to collaborators May 15, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

2 participants