Skip to content

app-server: select permission profiles by id#22402

Open
bolinfest wants to merge 1 commit into
pr22401from
pr22402
Open

app-server: select permission profiles by id#22402
bolinfest wants to merge 1 commit into
pr22401from
pr22402

Conversation

@bolinfest
Copy link
Copy Markdown
Collaborator

@bolinfest bolinfest commented May 13, 2026

Why

The app-server should let clients change the thread's named permission profile association and workspace roots without letting them replace the underlying PermissionProfile value outright. The server needs to stay in control of profile definitions while still supporting the operations clients actually need:

  • select a known profile by id
  • preserve the stored effective profile when no selection is provided
  • replace workspaceRoots
  • keep older sandbox-based clients working when their request maps cleanly to a known named profile

Because permissions / workspaceRoots are experimental fields, tightening that contract is preferable to continuing to accept more expressive payloads that the server should not honor.

What Changed

  • thread/start, thread/resume, thread/fork, and turn/start now treat permissions as a profile id (Option<String>) instead of a structured profile payload.
  • App-server request processors resolve that id against the server's known profiles and return a JSON-RPC error when the id is unknown.
  • Omitting permission fields preserves the stored thread PermissionProfile; workspaceRoots can be replaced independently.
  • Add a legacy sandbox compatibility layer so older clients can still send sandbox / sandboxPolicy when it is a no-op or maps to a named profile plus compatible workspace roots.
  • Preserve existing extra workspace roots when older clients repeat sandboxPolicy: workspaceWrite for a cwd that is already a workspace root, while still rebinding roots for a changed cwd and honoring non-empty legacy writableRoots.
  • Validate legacy sandbox compatibility against the exact sandbox projection of the resolved profile after workspace roots are materialized, rather than checking only enforcement and network mode.
  • Preserve rollout-resume permission state without leaking stale active profile names when the latest persisted turn has an unnamed effective profile.
  • Preserve older rollout fileSystemSandboxPolicy data when a TurnContextItem predates the serialized permissionProfile field, so resume keeps deny entries and internal writable-root grants instead of falling back to the coarser legacy sandbox projection.
  • Update the app-server protocol handling and README to document the narrowed lifecycle and turn-level rules.

Compatibility

  • Older app-server clients such as slow-moving IDE integrations continue to work through the legacy sandbox mapping path when their request exactly matches a known named profile.
  • Repeated legacy workspace-write updates no longer drop server-side extra workspace roots, such as roots seeded from --add-dir, when the current cwd is already among the thread roots.
  • Clients can still change the profile name/selection by id and update workspaceRoots, but they cannot submit arbitrary PermissionProfile values through the app-server API.
  • Persisted rollouts keep their latest effective permission profile and workspace roots on resume; if a later turn moved to an unnamed profile, the resumed app-server response no longer reports the earlier active profile name.
  • Rollouts written before permissionProfile was serialized still resume from their serialized filesystem policy when present, which protects split-policy details such as deny rules and Codex-internal writable roots used by memories.
  • The compatibility path intentionally stops at requests that would require inventing a new unnamed profile on the server, or whose legacy sandbox flags do not match the named profile's derived projection.

Verification

The targeted regression coverage here focused on the app-server call paths where older clients or persisted threads could drift from the server's effective permissions:

  • thread_start_selects_permission_profile_by_id
  • thread_resume_running_applies_workspace_roots_and_active_profile_name
  • turn_start_updates_cwd_without_replacing_workspace_roots_v2
  • turn_start_legacy_workspace_sandbox_updates_workspace_roots_for_cwd
  • turn_start_legacy_workspace_sandbox_preserves_extra_roots
  • turn_start_rejects_unknown_permission_selection_before_starting_turn
  • legacy_workspace_sandbox_preserves_current_extra_roots_when_cwd_is_still_a_root
  • legacy_workspace_sandbox_honors_non_empty_legacy_writable_roots
  • legacy_workspace_selection_validation_requires_exact_projection
  • persisted_thread_permission_state_clears_stale_active_profile
  • persisted_thread_permission_state_uses_legacy_turn_context_file_system_policy
  • focused app-server persisted-permission-state tests for active profile and empty workspace-root reconstruction

Stack created with Sapling. Best reviewed with ReviewStack.

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: dc0f61af1f

ℹ️ 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 thread codex-rs/tui/src/app_server_session.rs Outdated
@bolinfest bolinfest requested a review from a team as a code owner May 13, 2026 02:26
@bolinfest bolinfest force-pushed the pr22402 branch 2 times, most recently from 03e72f1 to 24f01e1 Compare May 13, 2026 03:23
@bolinfest bolinfest force-pushed the pr22401 branch 2 times, most recently from 7336736 to bb76782 Compare May 13, 2026 03:44
@bolinfest bolinfest force-pushed the pr22402 branch 2 times, most recently from f965330 to 1bb4cb8 Compare May 13, 2026 03:52
@bolinfest bolinfest force-pushed the pr22401 branch 2 times, most recently from b9f3284 to 7aa7447 Compare May 13, 2026 05:14
@bolinfest bolinfest force-pushed the pr22402 branch 2 times, most recently from 802324c to 8ce7db8 Compare May 13, 2026 05:31
@bolinfest bolinfest force-pushed the pr22402 branch 3 times, most recently from aa3919e to 188df1b Compare May 13, 2026 06:19
@bolinfest bolinfest force-pushed the pr22402 branch 2 times, most recently from 8eb3d14 to 93aef9b Compare May 13, 2026 15:44
@bolinfest bolinfest force-pushed the pr22401 branch 2 times, most recently from 0f90ce4 to e6b6753 Compare May 13, 2026 18:30
@bolinfest bolinfest force-pushed the pr22402 branch 2 times, most recently from 629d74d to 11db556 Compare May 13, 2026 19:25
@bolinfest bolinfest force-pushed the pr22401 branch 2 times, most recently from 5e82c0d to f65d5b5 Compare May 13, 2026 19:59
bolinfest added a commit that referenced this pull request May 14, 2026
#22624)

## Why

This is a small precursor to the larger permissions-migration work. Both
the comparison stack in
[#22401](#22401) /
[#22402](#22402) and the alternate
stack in [#22610](#22610) /
[#22611](#22611) /
[#22612](#22612) are easier to
review if the terminology is already settled underneath them.

Because `:project_roots` and `:danger-no-sandbox` have not shipped as
stable user-facing surface area, carrying them forward as aliases would
just add more migration logic to the later stacks. This PR removes that
ambiguity now so the follow-on work can rely on one spelling for each
built-in concept.

## What Changed

- renamed the config-facing special filesystem key from `:project_roots`
to `:workspace_roots`
- dropped unpublished `:project_roots` parsing support in
`core/src/config/permissions.rs`, so new config only recognizes
`:workspace_roots`
- renamed the built-in full-access permission profile id from
`:danger-no-sandbox` to `:danger-full-access`
- dropped unpublished `:danger-no-sandbox` support entirely, including
the old active-profile canonicalization path, and added explicit
rejection coverage for the legacy id
- introduced shared built-in permission-profile id constants in
`codex-rs/protocol/src/models.rs`
- updated `core`, `app-server`, and `tui` call sites that special-case
built-in profiles to use the shared constants and canonical ids
- updated tests and the Linux sandbox README to use `:workspace_roots` /
`:danger-full-access`

## Verification

I focused verification on the three places this rename can regress:
config parsing, active-profile identity surfaced back out of `core`, and
user/server call sites that special-case built-in profiles.

Targeted checks:

-
`config::tests::default_permissions_can_select_builtin_profile_without_permissions_table`
-
`config::tests::default_permissions_read_only_applies_additional_writable_roots_as_modifications`
-
`config::tests::default_permissions_can_select_builtin_full_access_profile`
- `config::tests::legacy_danger_no_sandbox_is_rejected`
- `workspace_root` filtered `codex-core` tests
-
`request_processors::thread_processor::thread_processor_tests::thread_processor_behavior_tests::requested_permissions_trust_project_uses_permission_profile_intent`
-
`suite::v2::turn_start::turn_start_rejects_invalid_permission_selection_before_starting_turn`
- `status::tests::status_snapshot_shows_auto_review_permissions`
-
`status::tests::status_permissions_full_disk_managed_with_network_is_danger_full_access`
-
`app_server_session::tests::embedded_turn_permissions_use_active_profile_selection`
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant