thread-store: store permission profiles (#23165)

## Why

`SandboxPolicy` is the legacy compatibility shape, but
`codex-thread-store` still exposed it through `StoredThread`,
`ThreadMetadataPatch`, and live metadata sync. That kept thread-store
consumers tied to the legacy representation and meant richer permission
profile data could not round-trip through thread metadata or cold
rollout reconciliation.

## What Changed

- Replaced thread-store `sandbox_policy` API fields with canonical
`PermissionProfile` fields.
- Persist new permission-profile metadata as canonical JSON in the
existing SQLite metadata slot while continuing to read older legacy
sandbox policy values.
- Updated local, in-memory, live metadata sync, and rollout extraction
paths to propagate `TurnContextItem::permission_profile()`.
- Re-materialize legacy permission metadata against the final rollout
cwd when rollout-derived metadata replaces stale SQLite summaries.
- Updated affected app-server and core test constructors to build
`PermissionProfile` values directly.

## Test Plan

- `cargo test -p codex-state`
- `cargo test -p codex-thread-store`
- `cargo test -p codex-app-server
summary_from_stored_thread_preserves_millisecond_precision --lib`
- `cargo test -p codex-core realtime_context --lib`
This commit is contained in:
Michael Bolin
2026-05-29 11:55:31 -07:00
committed by GitHub
parent c9dc0f6338
commit a1ecf0cf1c
10 changed files with 245 additions and 32 deletions
+39 -2
View File
@@ -76,7 +76,8 @@ fn apply_turn_context(metadata: &mut ThreadMetadata, turn_ctx: &TurnContextItem)
}
metadata.model = Some(turn_ctx.model.clone());
metadata.reasoning_effort = turn_ctx.effort;
metadata.sandbox_policy = enum_to_string(&turn_ctx.sandbox_policy);
metadata.sandbox_policy =
serde_json::to_string(&turn_ctx.permission_profile()).unwrap_or_default();
metadata.approval_mode = enum_to_string(&turn_ctx.approval_policy);
}
@@ -157,6 +158,7 @@ mod tests {
use chrono::Utc;
use codex_protocol::ThreadId;
use codex_protocol::models::ContentItem;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AskForApproval;
@@ -364,11 +366,46 @@ mod tests {
assert_eq!(metadata.cwd, PathBuf::from("/child/worktree"));
assert_eq!(
metadata.sandbox_policy,
super::enum_to_string(&SandboxPolicy::DangerFullAccess)
serde_json::to_string(&PermissionProfile::Disabled)
.expect("serialize permission profile")
);
assert_eq!(metadata.approval_mode, "never");
}
#[test]
fn turn_context_sets_permission_profile_metadata() {
let mut metadata = metadata_for_test();
let permission_profile = PermissionProfile::workspace_write();
apply_rollout_item(
&mut metadata,
&RolloutItem::TurnContext(TurnContextItem {
turn_id: Some("turn-1".to_string()),
cwd: PathBuf::from("/workspace"),
workspace_roots: None,
current_date: None,
timezone: None,
approval_policy: AskForApproval::OnRequest,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: Some(permission_profile.clone()),
network: None,
file_system_sandbox_policy: None,
model: "gpt-5".to_string(),
personality: None,
collaboration_mode: None,
realtime_active: None,
effort: None,
summary: codex_protocol::config_types::ReasoningSummary::Auto,
}),
"test-provider",
);
assert_eq!(
metadata.sandbox_policy,
serde_json::to_string(&permission_profile).expect("serialize permission profile")
);
}
#[test]
fn turn_context_sets_cwd_when_session_cwd_missing() {
let mut metadata = metadata_for_test();