app-server: use permission ids and runtime workspace roots (#22611)

## Why

This PR builds on [#22610](https://github.com/openai/codex/pull/22610)
and is the app-server side of the migration from mutable per-turn
`SandboxPolicy` replacement toward selecting immutable permission
profiles by id plus mutable runtime workspace roots.

Once permission profiles can carry their own immutable
`workspace_roots`, app-server no longer needs to mutate the selected
`PermissionProfile` just to represent thread-specific filesystem
context. The mutable part now lives on the thread as explicit
`runtimeWorkspaceRoots`, while `:workspace_roots` remains symbolic until
the sandbox is realized for a turn.

## What Changed

- Replaced the v2 permission-selection wrapper surface with plain
profile ids for `thread/start`, `thread/resume`, `thread/fork`, and
`turn/start`.
- Removed the API surface for profile modifications
(`PermissionProfileSelectionParams`,
`PermissionProfileModificationParams`,
`ActivePermissionProfileModification`).
- Added experimental `runtimeWorkspaceRoots` fields to the thread
lifecycle and turn-start APIs.
- Threaded runtime workspace roots through core session/thread
snapshots, turn overrides, app-server request handling, and command
execution permission resolution.
- Kept session permission state symbolic so later runtime root updates
and cwd-only implicit-root retargeting rebind `:workspace_roots`
correctly.
- Updated the embedded clients just enough to send and restore the new
thread state.
- Refreshed the generated schema/TypeScript artifacts and the app-server
README to match the new contract.

## Verification

Targeted coverage for this layer lives in:

- `codex-rs/app-server-protocol/src/protocol/v2/tests.rs`
- `codex-rs/app-server/tests/suite/v2/thread_start.rs`
- `codex-rs/app-server/tests/suite/v2/thread_resume.rs`
- `codex-rs/app-server/tests/suite/v2/turn_start.rs`
- `codex-rs/core/src/session/tests.rs`

The key regression checks exercise that:

- `runtimeWorkspaceRoots` resolve against the effective cwd on thread
start.
- Profile-declared workspace roots are excluded from the runtime
workspace roots returned by app-server.
- A turn-level runtime workspace-root update persists onto the thread
and is returned by `thread/resume`.
- A named permission profile selected on one turn remains symbolic so a
later runtime-root-only turn update changes the actual sandbox writes.
- A cwd-only turn update retargets the implicit runtime cwd root while
preserving additional runtime roots.
- The protocol fixtures and generated client artifacts stay in sync with
the string-based permission selection contract.











---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/22611).
* #22612
* __->__ #22611
This commit is contained in:
Michael Bolin
2026-05-14 23:00:05 -07:00
committed by GitHub
parent e6a7368810
commit 8a5306ff88
58 changed files with 1167 additions and 676 deletions
+9 -5
View File
@@ -130,10 +130,10 @@ Example with notification opt-out:
## API Overview
- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. For permissions, prefer experimental `permissions` profile selection; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd`.
- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; relative paths resolve against the effective thread cwd. For permissions, prefer experimental `permissions` profile selection by id; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd`.
- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`.
- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Experimental clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately. Accepts the same permission override rules as `thread/start`.
- `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. Experimental clients can read response `permissionProfile` for the exact active runtime permissions and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known.
- `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots, `permissionProfile` for the exact active runtime permissions, and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known.
- `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
- `thread/loaded/list` — list the thread ids currently loaded in memory.
- `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
@@ -156,7 +156,7 @@ Example with notification opt-out:
- `thread/shellCommand` — run a user-initiated `!` shell command against a thread; this runs unsandboxed with full access rather than inheriting the thread sandbox policy. Returns `{}` immediately while progress streams through standard turn/item notifications and any active turn receives the formatted output in its message stream.
- `thread/backgroundTerminals/clean` — terminate all running background terminals for a thread (experimental; requires `capabilities.experimentalApi`); returns `{}` when the cleanup request is accepted.
- `thread/rollback` — drop the last N turns from the agents in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success.
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. Prefer experimental `permissions` profile selection for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissions`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode".
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; relative paths resolve against the effective turn cwd. Prefer experimental `permissions` profile selection by id for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissions`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode".
- `thread/inject_items` — append raw Responses API items to a loaded threads model-visible history without starting a user turn; returns `{}` on success.
- `turn/steer` — add user input to an already in-flight regular turn without starting a new turn; returns the active `turnId` that accepted the input. Review and manual compaction turns reject `turn/steer`.
- `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`.
@@ -236,7 +236,9 @@ Start a fresh thread when you need a new Codex conversation.
"approvalPolicy": "never",
"sandbox": "workspaceWrite",
// Prefer experimental profile selection:
// "permissions": { "type": "profile", "id": ":workspace" }
// "permissions": ":workspace"
// Experimental runtime roots for :workspace_roots materialization:
// "runtimeWorkspaceRoots": ["/Users/me/project", "/Users/me/openai"],
// Do not send both "sandbox" and "permissions".
"personality": "friendly",
"serviceName": "my_app_server_client", // optional metrics tag (`service_name`)
@@ -649,7 +651,9 @@ You can optionally specify config overrides on the new turn. If specified, these
"networkAccess": true
},
// Prefer experimental profile selection:
// "permissions": { "type": "profile", "id": ":workspace" }
// "permissions": ":workspace"
// Experimental runtime roots for :workspace_roots materialization:
// "runtimeWorkspaceRoots": ["/Users/me/project", "/Users/me/openai"],
// Do not send both "sandboxPolicy" and "permissions".
"model": "gpt-5.1-codex",
"effort": "medium",