mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
c9dc0f6338
commit
a1ecf0cf1c
@@ -2095,6 +2095,7 @@ mod tests {
|
||||
use codex_protocol::items::build_hook_prompt_message;
|
||||
use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions;
|
||||
use codex_protocol::models::NetworkPermissions as CoreNetworkPermissions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
@@ -2110,7 +2111,6 @@ mod tests {
|
||||
use codex_protocol::protocol::RateLimitSnapshot;
|
||||
use codex_protocol::protocol::RateLimitWindow;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
use codex_protocol::protocol::TokenUsageInfo;
|
||||
@@ -2187,7 +2187,7 @@ mod tests {
|
||||
agent_path: None,
|
||||
git_info: None,
|
||||
approval_mode: AskForApproval::OnRequest,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
token_usage: None,
|
||||
first_user_message: Some("before rollback".to_string()),
|
||||
history: Some(StoredThreadHistory {
|
||||
|
||||
@@ -68,13 +68,13 @@ mod thread_processor_behavior_tests {
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_state::ThreadMetadataBuilder;
|
||||
@@ -412,7 +412,7 @@ mod thread_processor_behavior_tests {
|
||||
agent_path: None,
|
||||
git_info: None,
|
||||
approval_mode: AskForApproval::OnRequest,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
token_usage: None,
|
||||
first_user_message: Some("first user message".to_string()),
|
||||
history: None,
|
||||
|
||||
@@ -13,10 +13,10 @@ use chrono::Utc;
|
||||
use codex_git_utils::GitSha;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::GitInfo;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_thread_store::StoredThread;
|
||||
use core_test_support::PathBufExt;
|
||||
@@ -59,7 +59,7 @@ fn stored_thread(cwd: &str, title: &str, first_user_message: &str) -> StoredThre
|
||||
repository_url: None,
|
||||
}),
|
||||
approval_mode: AskForApproval::Never,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
token_usage: None,
|
||||
first_user_message: Some(first_user_message.to_string()),
|
||||
history: None,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -8,9 +8,9 @@ use std::sync::OnceLock;
|
||||
use async_trait::async_trait;
|
||||
use chrono::Utc;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
|
||||
use crate::AppendThreadItemsParams;
|
||||
use crate::ArchiveThreadParams;
|
||||
@@ -363,9 +363,9 @@ fn stored_thread_from_state(
|
||||
approval_mode: metadata
|
||||
.and_then(|metadata| metadata.approval_mode)
|
||||
.unwrap_or(AskForApproval::Never),
|
||||
sandbox_policy: metadata
|
||||
.and_then(|metadata| metadata.sandbox_policy.clone())
|
||||
.unwrap_or_else(SandboxPolicy::new_read_only_policy),
|
||||
permission_profile: metadata
|
||||
.and_then(|metadata| metadata.permission_profile.clone())
|
||||
.unwrap_or_else(PermissionProfile::read_only),
|
||||
token_usage: metadata.and_then(|metadata| metadata.token_usage.clone()),
|
||||
first_user_message: metadata.and_then(|metadata| metadata.first_user_message.clone()),
|
||||
history,
|
||||
|
||||
@@ -9,8 +9,10 @@ use chrono::DateTime;
|
||||
use chrono::Utc;
|
||||
use codex_git_utils::GitSha;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::GitInfo;
|
||||
use codex_protocol::protocol::NetworkAccess;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_rollout::ARCHIVED_SESSIONS_SUBDIR;
|
||||
@@ -140,13 +142,34 @@ pub(super) fn stored_thread_from_rollout_item(
|
||||
agent_path: None,
|
||||
git_info,
|
||||
approval_mode: AskForApproval::OnRequest,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
token_usage: None,
|
||||
first_user_message: item.first_user_message,
|
||||
history: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn permission_profile_from_metadata_value(value: &str, cwd: &Path) -> PermissionProfile {
|
||||
serde_json::from_str::<PermissionProfile>(value)
|
||||
.or_else(|_| {
|
||||
parse_legacy_sandbox_policy(value)
|
||||
.map(|policy| PermissionProfile::from_legacy_sandbox_policy_for_cwd(&policy, cwd))
|
||||
})
|
||||
.unwrap_or_else(|_| PermissionProfile::read_only())
|
||||
}
|
||||
|
||||
pub(super) fn permission_profile_to_metadata_value(
|
||||
permission_profile: &PermissionProfile,
|
||||
) -> String {
|
||||
match serde_json::to_string(permission_profile) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
tracing::warn!("failed to serialize permission profile metadata: {err}");
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn distinct_thread_metadata_title(metadata: &ThreadMetadata) -> Option<String> {
|
||||
let title = metadata.title.trim();
|
||||
if title.is_empty() || metadata.first_user_message.as_deref().map(str::trim) == Some(title) {
|
||||
@@ -169,6 +192,20 @@ fn parse_rfc3339(value: Option<&str>) -> Option<DateTime<Utc>> {
|
||||
.map(|dt| dt.with_timezone(&Utc))
|
||||
}
|
||||
|
||||
fn parse_legacy_sandbox_policy(value: &str) -> serde_json::Result<SandboxPolicy> {
|
||||
serde_json::from_str(value)
|
||||
.or_else(|_| serde_json::from_value(serde_json::Value::String(value.to_string())))
|
||||
.or_else(|_| match value {
|
||||
"danger-full-access" => Ok(SandboxPolicy::DangerFullAccess),
|
||||
"read-only" => Ok(SandboxPolicy::new_read_only_policy()),
|
||||
"workspace-write" => Ok(SandboxPolicy::new_workspace_write_policy()),
|
||||
"external-sandbox" => Ok(SandboxPolicy::ExternalSandbox {
|
||||
network_access: NetworkAccess::Restricted,
|
||||
}),
|
||||
_ => serde_json::from_value(serde_json::Value::String(value.to_string())),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn git_info_from_parts(
|
||||
sha: Option<String>,
|
||||
branch: Option<String>,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use chrono::DateTime;
|
||||
use chrono::Utc;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SessionMetaLine;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_rollout::RolloutRecorder;
|
||||
@@ -15,6 +15,7 @@ use codex_state::ThreadMetadata;
|
||||
use super::LocalThreadStore;
|
||||
use super::helpers::distinct_thread_metadata_title;
|
||||
use super::helpers::git_info_from_parts;
|
||||
use super::helpers::permission_profile_from_metadata_value;
|
||||
use super::helpers::rollout_path_is_archived;
|
||||
use super::helpers::set_thread_name_from_title;
|
||||
use super::helpers::stored_thread_from_rollout_item;
|
||||
@@ -45,6 +46,7 @@ pub(super) async fn read_thread(
|
||||
)
|
||||
.await)
|
||||
{
|
||||
let metadata_sandbox_policy = metadata.sandbox_policy.clone();
|
||||
let mut thread = stored_thread_from_sqlite_metadata(store, metadata).await;
|
||||
if !params.include_history
|
||||
&& let Some(rollout_path) = thread.rollout_path.clone()
|
||||
@@ -57,6 +59,10 @@ pub(super) async fn read_thread(
|
||||
rollout_thread.name = thread.name;
|
||||
}
|
||||
rollout_thread.git_info = thread.git_info;
|
||||
rollout_thread.permission_profile = permission_profile_from_metadata_value(
|
||||
&metadata_sandbox_policy,
|
||||
rollout_thread.cwd.as_path(),
|
||||
);
|
||||
thread = rollout_thread;
|
||||
}
|
||||
attach_history_if_requested(&mut thread, params.include_history).await?;
|
||||
@@ -286,6 +292,8 @@ async fn stored_thread_from_sqlite_metadata(
|
||||
.clone()
|
||||
.or_else(|| metadata.first_user_message.clone())
|
||||
.unwrap_or_default();
|
||||
let permission_profile =
|
||||
permission_profile_from_metadata_value(&metadata.sandbox_policy, metadata.cwd.as_path());
|
||||
StoredThread {
|
||||
thread_id: metadata.id,
|
||||
rollout_path: Some(metadata.rollout_path),
|
||||
@@ -315,10 +323,7 @@ async fn stored_thread_from_sqlite_metadata(
|
||||
metadata.git_origin_url,
|
||||
),
|
||||
approval_mode: parse_or_default(&metadata.approval_mode, AskForApproval::OnRequest),
|
||||
sandbox_policy: parse_or_default(
|
||||
&metadata.sandbox_policy,
|
||||
SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
permission_profile,
|
||||
token_usage: None,
|
||||
first_user_message: metadata.first_user_message,
|
||||
history: None,
|
||||
@@ -377,7 +382,7 @@ fn stored_thread_from_meta_line(
|
||||
agent_path: meta_line.meta.agent_path,
|
||||
git_info: meta_line.git,
|
||||
approval_mode: AskForApproval::OnRequest,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
token_usage: None,
|
||||
first_user_message: None,
|
||||
history: None,
|
||||
@@ -412,6 +417,7 @@ mod tests {
|
||||
|
||||
use chrono::Utc;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_state::ThreadMetadataBuilder;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -671,6 +677,84 @@ mod tests {
|
||||
assert_eq!(thread.name, Some("Saved title".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_thread_returns_permission_profile_from_sqlite_metadata() {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
let config = test_config(home.path());
|
||||
let uuid = Uuid::from_u128(225);
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
||||
let rollout_path =
|
||||
write_session_file(home.path(), "2025-01-03T12-00-00", uuid).expect("session file");
|
||||
let runtime = codex_state::StateRuntime::init(
|
||||
config.sqlite_home.clone(),
|
||||
config.default_model_provider_id.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize");
|
||||
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
|
||||
let mut builder =
|
||||
ThreadMetadataBuilder::new(thread_id, rollout_path, Utc::now(), SessionSource::Cli);
|
||||
builder.model_provider = Some(config.default_model_provider_id.clone());
|
||||
builder.cwd = home.path().to_path_buf();
|
||||
let mut metadata = builder.build(config.default_model_provider_id.as_str());
|
||||
metadata.sandbox_policy =
|
||||
serde_json::to_string(&PermissionProfile::Disabled).expect("serialize profile");
|
||||
runtime
|
||||
.upsert_thread(&metadata)
|
||||
.await
|
||||
.expect("state db upsert should succeed");
|
||||
|
||||
let thread = store
|
||||
.read_thread(ReadThreadParams {
|
||||
thread_id,
|
||||
include_archived: false,
|
||||
include_history: false,
|
||||
})
|
||||
.await
|
||||
.expect("read thread");
|
||||
|
||||
assert_eq!(thread.preview, "Hello from user");
|
||||
assert_eq!(thread.permission_profile, PermissionProfile::Disabled);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_thread_accepts_legacy_sandbox_policy_metadata() {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
let config = test_config(home.path());
|
||||
let uuid = Uuid::from_u128(226);
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
||||
let rollout_path =
|
||||
write_session_file(home.path(), "2025-01-03T12-00-00", uuid).expect("session file");
|
||||
let runtime = codex_state::StateRuntime::init(
|
||||
config.sqlite_home.clone(),
|
||||
config.default_model_provider_id.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize");
|
||||
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
|
||||
let mut builder =
|
||||
ThreadMetadataBuilder::new(thread_id, rollout_path, Utc::now(), SessionSource::Cli);
|
||||
builder.model_provider = Some(config.default_model_provider_id.clone());
|
||||
builder.cwd = home.path().to_path_buf();
|
||||
let mut metadata = builder.build(config.default_model_provider_id.as_str());
|
||||
metadata.sandbox_policy = "danger-full-access".to_string();
|
||||
runtime
|
||||
.upsert_thread(&metadata)
|
||||
.await
|
||||
.expect("state db upsert should succeed");
|
||||
|
||||
let thread = store
|
||||
.read_thread(ReadThreadParams {
|
||||
thread_id,
|
||||
include_archived: false,
|
||||
include_history: true,
|
||||
})
|
||||
.await
|
||||
.expect("read thread");
|
||||
|
||||
assert_eq!(thread.permission_profile, PermissionProfile::Disabled);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_thread_preserves_rollout_cwd_when_sqlite_metadata_exists() {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
@@ -725,6 +809,7 @@ mod tests {
|
||||
let mut metadata = builder.build(config.default_model_provider_id.as_str());
|
||||
metadata.title = "Saved title".to_string();
|
||||
metadata.first_user_message = Some("Hello from sqlite".to_string());
|
||||
metadata.sandbox_policy = "workspace-write".to_string();
|
||||
runtime
|
||||
.upsert_thread(&metadata)
|
||||
.await
|
||||
@@ -745,6 +830,19 @@ mod tests {
|
||||
assert_eq!(thread.name, Some("Saved title".to_string()));
|
||||
assert_eq!(thread.model_provider, "rollout-provider");
|
||||
assert_eq!(thread.cwd, rollout_cwd);
|
||||
let legacy_policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: Vec::new(),
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
};
|
||||
assert_eq!(
|
||||
thread.permission_profile,
|
||||
PermissionProfile::from_legacy_sandbox_policy_for_cwd(
|
||||
&legacy_policy,
|
||||
rollout_cwd.as_path()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -18,6 +18,7 @@ use tracing::warn;
|
||||
|
||||
use super::LocalThreadStore;
|
||||
use super::helpers::git_info_from_parts;
|
||||
use super::helpers::permission_profile_to_metadata_value;
|
||||
use super::live_writer;
|
||||
use crate::GitInfoPatch;
|
||||
use crate::ReadThreadParams;
|
||||
@@ -275,8 +276,8 @@ async fn apply_metadata_update(
|
||||
if let Some(approval_mode) = patch.approval_mode {
|
||||
metadata.approval_mode = enum_to_string(&approval_mode);
|
||||
}
|
||||
if let Some(sandbox_policy) = patch.sandbox_policy {
|
||||
metadata.sandbox_policy = enum_to_string(&sandbox_policy);
|
||||
if let Some(permission_profile) = patch.permission_profile {
|
||||
metadata.sandbox_policy = permission_profile_to_metadata_value(&permission_profile);
|
||||
}
|
||||
if let Some(token_usage) = patch.token_usage {
|
||||
metadata.tokens_used = token_usage.total_tokens.max(0);
|
||||
@@ -382,7 +383,7 @@ fn has_observed_metadata_facts(patch: &ThreadMetadataPatch) -> bool {
|
||||
|| patch.cwd.is_some()
|
||||
|| patch.cli_version.is_some()
|
||||
|| patch.approval_mode.is_some()
|
||||
|| patch.sandbox_policy.is_some()
|
||||
|| patch.permission_profile.is_some()
|
||||
|| patch.token_usage.is_some()
|
||||
|| patch.first_user_message.is_some()
|
||||
}
|
||||
@@ -604,6 +605,7 @@ fn rollout_path_is_archived(store: &LocalThreadStore, path: &Path) -> bool {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
@@ -845,6 +847,45 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_thread_metadata_sets_permission_profile() {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
let config = test_config(home.path());
|
||||
let runtime = codex_state::StateRuntime::init(
|
||||
config.sqlite_home.clone(),
|
||||
config.default_model_provider_id.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize");
|
||||
let store = LocalThreadStore::new(config, Some(runtime.clone()));
|
||||
let uuid = Uuid::from_u128(317);
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
||||
write_session_file(home.path(), "2025-01-03T20-30-00", uuid).expect("session file");
|
||||
|
||||
let thread = store
|
||||
.update_thread_metadata(UpdateThreadMetadataParams {
|
||||
thread_id,
|
||||
patch: ThreadMetadataPatch {
|
||||
permission_profile: Some(PermissionProfile::Disabled),
|
||||
..Default::default()
|
||||
},
|
||||
include_archived: false,
|
||||
})
|
||||
.await
|
||||
.expect("set permission profile");
|
||||
|
||||
assert_eq!(thread.permission_profile, PermissionProfile::Disabled);
|
||||
let metadata = runtime
|
||||
.get_thread(thread_id)
|
||||
.await
|
||||
.expect("sqlite metadata read")
|
||||
.expect("sqlite metadata");
|
||||
assert_eq!(
|
||||
metadata.sandbox_policy,
|
||||
serde_json::to_string(&PermissionProfile::Disabled).expect("serialize profile")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_thread_metadata_partially_updates_git_info() {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
|
||||
@@ -234,7 +234,7 @@ impl ThreadMetadataSync {
|
||||
update.model = Some(turn_ctx.model.clone());
|
||||
update.reasoning_effort = turn_ctx.effort;
|
||||
update.approval_mode = Some(turn_ctx.approval_policy);
|
||||
update.sandbox_policy = Some(turn_ctx.sandbox_policy.clone());
|
||||
update.permission_profile = Some(turn_ctx.permission_profile());
|
||||
}
|
||||
RolloutItem::EventMsg(EventMsg::UserMessage(user)) => {
|
||||
if let Some(preview) = user_message_preview(user) {
|
||||
@@ -354,7 +354,7 @@ fn update_has_metadata_facts(update: &ThreadMetadataPatch) -> bool {
|
||||
|| update.cwd.is_some()
|
||||
|| update.cli_version.is_some()
|
||||
|| update.approval_mode.is_some()
|
||||
|| update.sandbox_policy.is_some()
|
||||
|| update.permission_profile.is_some()
|
||||
|| update.token_usage.is_some()
|
||||
|| update.first_user_message.is_some()
|
||||
|| update.git_info.is_some()
|
||||
|
||||
@@ -5,11 +5,11 @@ use chrono::Utc;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::GitInfo;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::ThreadMemoryMode as MemoryMode;
|
||||
use codex_protocol::protocol::ThreadSource;
|
||||
@@ -396,8 +396,8 @@ pub struct StoredThread {
|
||||
pub git_info: Option<GitInfo>,
|
||||
/// Approval mode captured for the thread.
|
||||
pub approval_mode: AskForApproval,
|
||||
/// Sandbox policy captured for the thread.
|
||||
pub sandbox_policy: SandboxPolicy,
|
||||
/// Canonical runtime permissions captured for the thread.
|
||||
pub permission_profile: PermissionProfile,
|
||||
/// Last observed token usage.
|
||||
pub token_usage: Option<TokenUsage>,
|
||||
/// First user message observed for this thread, if any.
|
||||
@@ -519,8 +519,8 @@ pub struct ThreadMetadataPatch {
|
||||
pub cli_version: Option<String>,
|
||||
/// Approval mode.
|
||||
pub approval_mode: Option<AskForApproval>,
|
||||
/// Sandbox policy.
|
||||
pub sandbox_policy: Option<SandboxPolicy>,
|
||||
/// Canonical runtime permissions.
|
||||
pub permission_profile: Option<PermissionProfile>,
|
||||
/// Last observed token usage.
|
||||
pub token_usage: Option<TokenUsage>,
|
||||
/// First user message observed for this thread.
|
||||
@@ -589,8 +589,8 @@ impl ThreadMetadataPatch {
|
||||
if next.approval_mode.is_some() {
|
||||
self.approval_mode = next.approval_mode;
|
||||
}
|
||||
if next.sandbox_policy.is_some() {
|
||||
self.sandbox_policy = next.sandbox_policy;
|
||||
if next.permission_profile.is_some() {
|
||||
self.permission_profile = next.permission_profile;
|
||||
}
|
||||
if next.token_usage.is_some() {
|
||||
self.token_usage = next.token_usage;
|
||||
@@ -626,7 +626,7 @@ impl ThreadMetadataPatch {
|
||||
&& self.cwd.is_none()
|
||||
&& self.cli_version.is_none()
|
||||
&& self.approval_mode.is_none()
|
||||
&& self.sandbox_policy.is_none()
|
||||
&& self.permission_profile.is_none()
|
||||
&& self.token_usage.is_none()
|
||||
&& self.first_user_message.is_none()
|
||||
&& self.git_info.is_none()
|
||||
|
||||
Reference in New Issue
Block a user