Skip to content

permissions: make profiles represent enforcement#19231

Merged
bolinfest merged 1 commit intomainfrom
pr19231
Apr 24, 2026
Merged

permissions: make profiles represent enforcement#19231
bolinfest merged 1 commit intomainfrom
pr19231

Conversation

@bolinfest
Copy link
Copy Markdown
Collaborator

@bolinfest bolinfest commented Apr 23, 2026

Why

PermissionProfile is becoming the canonical permissions abstraction, but the old shape only carried optional filesystem and network fields. It could describe allowed access, but not who is responsible for enforcing it. That made DangerFullAccess and ExternalSandbox lossy when profiles were exported, cached, or round-tripped through app-server APIs.

The important model change is that active permissions are now a disjoint union over the enforcement mode. Conceptually:

pub enum PermissionProfile {
    Managed {
        file_system: FileSystemSandboxPolicy,
        network: NetworkSandboxPolicy,
    },
    Disabled,
    External {
        network: NetworkSandboxPolicy,
    },
}

This distinction matters because Disabled means Codex should apply no outer sandbox at all, while External means filesystem isolation is owned by an outside caller. Those are not equivalent to a broad managed sandbox. For example, macOS cannot nest Seatbelt inside Seatbelt, so an inner sandbox may require the outer Codex layer to use no sandbox rather than a permissive one.

How Existing Modeling Maps

Legacy SandboxPolicy remains a boundary projection, but it now maps into the higher-fidelity profile model:

  • ReadOnly and WorkspaceWrite map to PermissionProfile::Managed with restricted filesystem entries plus the corresponding network policy.
  • DangerFullAccess maps to PermissionProfile::Disabled, preserving the “no outer sandbox” intent instead of treating it as a lax managed sandbox.
  • ExternalSandbox { network_access } maps to PermissionProfile::External { network }, preserving external filesystem enforcement while still carrying the active network policy.
  • Split runtime policies that legacy SandboxPolicy cannot faithfully express, such as managed unrestricted filesystem plus restricted network, stay Managed instead of being collapsed into ExternalSandbox.
  • Per-command/session/turn grants remain partial overlays via AdditionalPermissionProfile; full PermissionProfile is reserved for complete active runtime permissions.

What Changed

  • Change active PermissionProfile into a tagged union: managed, disabled, and external.
  • Keep partial permission grants separate with AdditionalPermissionProfile for command/session/turn overlays.
  • Represent managed filesystem permissions as either restricted entries or unrestricted; glob_scan_max_depth is non-zero when present.
  • Preserve old rollout compatibility by accepting the pre-tagged { network, file_system } profile shape during deserialization.
  • Preserve fidelity for important edge cases: DangerFullAccess round-trips as disabled, ExternalSandbox round-trips as external, and managed unrestricted filesystem + restricted network stays managed instead of being mistaken for external enforcement.
  • Preserve configured deny-read entries and bounded glob scan depth when full profiles are projected back into runtime policies, including unrestricted replacements that now become :root = write plus deny entries.
  • Regenerate the experimental app-server v2 JSON/TypeScript schema and update the command/exec README example for the tagged permissionProfile shape.

Compatibility

Legacy SandboxPolicy remains available at config/API boundaries as the compatibility projection. Existing rollout lines with the old PermissionProfile shape continue to load. The app-server permissionProfile field is experimental, so its v2 wire shape is intentionally updated to match the higher-fidelity model.

Verification

  • just write-app-server-schema
  • cargo check --tests
  • cargo test -p codex-protocol permission_profile
  • cargo test -p codex-protocol preserving_deny_entries_keeps_unrestricted_policy_enforceable
  • cargo test -p codex-app-server-protocol permission_profile_file_system_permissions
  • cargo test -p codex-app-server-protocol serialize_client_response
  • cargo test -p codex-core session_configured_reports_permission_profile_for_external_sandbox
  • just fix
  • just fix -p codex-protocol
  • just fix -p codex-app-server-protocol
  • just fix -p codex-core
  • just fix -p codex-app-server

@bolinfest bolinfest requested a review from a team as a code owner April 23, 2026 23:32
@bolinfest bolinfest force-pushed the pr19231 branch 2 times, most recently from aa0a3a7 to 4af9fac Compare April 23, 2026 23:41
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: 766bbc471a

ℹ️ 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 +353 to +355
#[serde(tag = "type", rename_all = "snake_case")]
#[ts(tag = "type")]
pub enum PermissionProfile {
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 Keep PermissionProfile deserialization backward compatible

Switching PermissionProfile to a tagged enum requires a type discriminator, so previously persisted rollout lines that serialized permission_profile as { network, file_system } will fail to deserialize after upgrade. RolloutRecorder::load_rollout_items drops any line that fails serde_json::from_value::<RolloutLine>, so older TurnContext entries get silently skipped, which can corrupt resume/fork reconstruction by losing turn context metadata from existing threads. Please add a legacy-compatible deserialization path (e.g., untagged fallback) so old rollout files continue to load.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in the current head. PermissionProfile now uses a custom untagged deserialize path that accepts both the tagged enum shape and the previous rollout { network, file_system } shape, with permission_profile_deserializes_legacy_rollout_shape covering the old format including glob_scan_max_depth.

@bolinfest bolinfest force-pushed the pr19231 branch 5 times, most recently from 1e4cc1c to fa7ac07 Compare April 24, 2026 00:07
@bolinfest bolinfest merged commit 4816b89 into main Apr 24, 2026
39 checks passed
@bolinfest bolinfest deleted the pr19231 branch April 24, 2026 06:02
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 24, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants