permissions: expose active profile metadata

This commit is contained in:
Michael Bolin
2026-04-28 23:50:17 -07:00
parent e6db1a9442
commit acdc76a0f8
68 changed files with 1983 additions and 1626 deletions
+87 -47
View File
@@ -25,6 +25,8 @@ use codex_app_server_protocol::ConfigWarningNotification;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::McpServerElicitationAction;
use codex_app_server_protocol::McpServerElicitationRequestResponse;
use codex_app_server_protocol::PermissionProfileModificationParams;
use codex_app_server_protocol::PermissionProfileSelectionParams;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ReviewStartParams;
use codex_app_server_protocol::ReviewStartResponse;
@@ -76,13 +78,14 @@ use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID;
use codex_otel::set_parent_from_context;
use codex_otel::traceparent_context_from_env;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::ActivePermissionProfileModification;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::ReviewRequest;
use codex_protocol::protocol::ReviewTarget;
use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::RolloutLine;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionConfiguredEvent;
use codex_protocol::protocol::SessionSource;
use codex_protocol::user_input::UserInput;
@@ -394,6 +397,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
approvals_reviewer: None,
sandbox_mode,
permission_profile: None,
default_permissions: None,
cwd: resolved_cwd,
model_provider: model_provider.clone(),
service_tier: None,
@@ -660,37 +664,24 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
// Handle resume subcommand through existing `thread/list` + `thread/resume`
// APIs so exec no longer reaches into rollout storage directly.
let (primary_thread_id, fallback_session_configured) =
if let Some(ExecCommand::Resume(args)) = command.as_ref() {
if let Some(thread_id) = resolve_resume_thread_id(&client, &config, args).await? {
let response: ThreadResumeResponse = send_request_with_response(
&client,
ClientRequest::ThreadResume {
request_id: request_ids.next(),
params: thread_resume_params_from_config(&config, thread_id),
},
"thread/resume",
)
.await
.map_err(anyhow::Error::msg)?;
let session_configured = session_configured_from_thread_resume_response(&response)
let (primary_thread_id, fallback_session_configured) = if let Some(ExecCommand::Resume(args)) =
command.as_ref()
{
if let Some(thread_id) = resolve_resume_thread_id(&client, &config, args).await? {
let response: ThreadResumeResponse = send_request_with_response(
&client,
ClientRequest::ThreadResume {
request_id: request_ids.next(),
params: thread_resume_params_from_config(&config, thread_id),
},
"thread/resume",
)
.await
.map_err(anyhow::Error::msg)?;
let session_configured =
session_configured_from_thread_resume_response(&response, &config)
.map_err(anyhow::Error::msg)?;
(session_configured.session_id, session_configured)
} else {
let response: ThreadStartResponse = send_request_with_response(
&client,
ClientRequest::ThreadStart {
request_id: request_ids.next(),
params: thread_start_params_from_config(&config),
},
"thread/start",
)
.await
.map_err(anyhow::Error::msg)?;
let session_configured = session_configured_from_thread_start_response(&response)
.map_err(anyhow::Error::msg)?;
(session_configured.session_id, session_configured)
}
(session_configured.session_id, session_configured)
} else {
let response: ThreadStartResponse = send_request_with_response(
&client,
@@ -702,10 +693,26 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
)
.await
.map_err(anyhow::Error::msg)?;
let session_configured = session_configured_from_thread_start_response(&response)
.map_err(anyhow::Error::msg)?;
let session_configured =
session_configured_from_thread_start_response(&response, &config)
.map_err(anyhow::Error::msg)?;
(session_configured.session_id, session_configured)
};
}
} else {
let response: ThreadStartResponse = send_request_with_response(
&client,
ClientRequest::ThreadStart {
request_id: request_ids.next(),
params: thread_start_params_from_config(&config),
},
"thread/start",
)
.await
.map_err(anyhow::Error::msg)?;
let session_configured = session_configured_from_thread_start_response(&response, &config)
.map_err(anyhow::Error::msg)?;
(session_configured.session_id, session_configured)
};
let primary_thread_id_for_span = primary_thread_id.to_string();
// Use the start/resume response as the authoritative bootstrap payload.
@@ -740,7 +747,6 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
items,
output_schema,
} => {
let permission_profile = Some(config.permissions.permission_profile().into());
let response: TurnStartResponse = send_request_with_response(
&client,
ClientRequest::TurnStart {
@@ -754,7 +760,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
approval_policy: Some(default_approval_policy.into()),
approvals_reviewer: None,
sandbox_policy: None,
permission_profile,
permissions: None,
model: None,
service_tier: None,
effort: default_effort,
@@ -918,7 +924,7 @@ fn thread_start_params_from_config(config: &Config) -> ThreadStartParams {
approval_policy: Some(config.permissions.approval_policy.value().into()),
approvals_reviewer: approvals_reviewer_override_from_config(config),
sandbox: None,
permission_profile: Some(config.permissions.permission_profile().into()),
permissions: permissions_selection_from_config(config),
config: config_request_overrides_from_config(config),
ephemeral: Some(config.ephemeral),
..ThreadStartParams::default()
@@ -934,12 +940,37 @@ fn thread_resume_params_from_config(config: &Config, thread_id: String) -> Threa
approval_policy: Some(config.permissions.approval_policy.value().into()),
approvals_reviewer: approvals_reviewer_override_from_config(config),
sandbox: None,
permission_profile: Some(config.permissions.permission_profile().into()),
permissions: permissions_selection_from_config(config),
config: config_request_overrides_from_config(config),
..ThreadResumeParams::default()
}
}
fn permissions_selection_from_config(config: &Config) -> Option<PermissionProfileSelectionParams> {
config
.permissions
.active_permission_profile()
.map(permissions_selection_from_active_profile)
}
fn permissions_selection_from_active_profile(
active: ActivePermissionProfile,
) -> PermissionProfileSelectionParams {
let modifications = active
.modifications
.into_iter()
.map(|modification| match modification {
ActivePermissionProfileModification::AdditionalWritableRoot { path } => {
PermissionProfileModificationParams::AdditionalWritableRoot { path }
}
})
.collect::<Vec<_>>();
PermissionProfileSelectionParams::Profile {
id: active.id,
modifications: (!modifications.is_empty()).then_some(modifications),
}
}
fn config_request_overrides_from_config(config: &Config) -> Option<HashMap<String, Value>> {
config
.active_profile
@@ -972,6 +1003,7 @@ where
fn session_configured_from_thread_start_response(
response: &ThreadStartResponse,
config: &Config,
) -> Result<SessionConfiguredEvent, String> {
session_configured_from_thread_response(
&response.thread.id,
@@ -982,8 +1014,12 @@ fn session_configured_from_thread_start_response(
response.service_tier,
response.approval_policy.to_core(),
response.approvals_reviewer.to_core(),
response.sandbox.to_core(),
response.permission_profile.clone().map(Into::into),
response
.permission_profile
.clone()
.map(Into::into)
.unwrap_or_else(|| config.permissions.permission_profile()),
response.active_permission_profile.clone().map(Into::into),
response.cwd.clone(),
response.reasoning_effort,
)
@@ -991,6 +1027,7 @@ fn session_configured_from_thread_start_response(
fn session_configured_from_thread_resume_response(
response: &ThreadResumeResponse,
config: &Config,
) -> Result<SessionConfiguredEvent, String> {
session_configured_from_thread_response(
&response.thread.id,
@@ -1001,8 +1038,12 @@ fn session_configured_from_thread_resume_response(
response.service_tier,
response.approval_policy.to_core(),
response.approvals_reviewer.to_core(),
response.sandbox.to_core(),
response.permission_profile.clone().map(Into::into),
response
.permission_profile
.clone()
.map(Into::into)
.unwrap_or_else(|| config.permissions.permission_profile()),
response.active_permission_profile.clone().map(Into::into),
response.cwd.clone(),
response.reasoning_effort,
)
@@ -1030,8 +1071,8 @@ fn session_configured_from_thread_response(
service_tier: Option<codex_protocol::config_types::ServiceTier>,
approval_policy: AskForApproval,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer,
sandbox_policy: SandboxPolicy,
permission_profile: Option<PermissionProfile>,
permission_profile: PermissionProfile,
active_permission_profile: Option<codex_protocol::models::ActivePermissionProfile>,
cwd: AbsolutePathBuf,
reasoning_effort: Option<codex_protocol::openai_models::ReasoningEffort>,
) -> Result<SessionConfiguredEvent, String> {
@@ -1047,9 +1088,8 @@ fn session_configured_from_thread_response(
service_tier,
approval_policy,
approvals_reviewer,
permission_profile: permission_profile.unwrap_or_else(|| {
PermissionProfile::from_legacy_sandbox_policy_for_cwd(&sandbox_policy, cwd.as_path())
}),
permission_profile,
active_permission_profile,
cwd,
reasoning_effort,
history_log_id: 0,
+44 -17
View File
@@ -363,8 +363,8 @@ async fn thread_start_params_include_review_policy_when_review_policy_is_manual_
);
assert_eq!(params.sandbox, None);
assert_eq!(
params.permission_profile,
Some(config.permissions.permission_profile().into())
params.permissions,
permissions_selection_from_config(&config)
);
}
@@ -391,9 +391,45 @@ async fn thread_start_params_include_review_policy_when_auto_review_is_enabled()
);
}
#[test]
fn session_configured_from_thread_response_uses_review_policy_from_response() {
let response = ThreadStartResponse {
#[tokio::test]
async fn session_configured_from_thread_response_uses_review_policy_from_response() {
let codex_home = tempdir().expect("create temp codex home");
let cwd = tempdir().expect("create temp cwd");
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(cwd.path().to_path_buf()))
.build()
.await
.expect("build config");
let response = sample_thread_start_response();
let event = session_configured_from_thread_start_response(&response, &config)
.expect("build bootstrap session configured event");
assert_eq!(event.approvals_reviewer, ApprovalsReviewer::AutoReview);
}
#[tokio::test]
async fn session_configured_from_thread_response_uses_permission_profile_from_response() {
let codex_home = tempdir().expect("create temp codex home");
let cwd = tempdir().expect("create temp cwd");
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(cwd.path().to_path_buf()))
.build()
.await
.expect("build config");
let mut response = sample_thread_start_response();
response.permission_profile = Some(PermissionProfile::Disabled.into());
let event = session_configured_from_thread_start_response(&response, &config)
.expect("build bootstrap session configured event");
assert_eq!(event.permission_profile, PermissionProfile::Disabled);
}
fn sample_thread_start_response() -> ThreadStartResponse {
ThreadStartResponse {
thread: codex_app_server_protocol::Thread {
id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(),
forked_from_id: None,
@@ -426,17 +462,8 @@ fn session_configured_from_thread_response_uses_review_policy_from_response() {
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
},
permission_profile: Some(
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
&codex_protocol::protocol::SandboxPolicy::new_workspace_write_policy(),
)
.into(),
),
permission_profile: None,
active_permission_profile: None,
reasoning_effort: None,
};
let event = session_configured_from_thread_start_response(&response)
.expect("build bootstrap session configured event");
assert_eq!(event.approvals_reviewer, ApprovalsReviewer::AutoReview);
}
}
@@ -115,6 +115,7 @@ fn session_configured_produces_thread_started_event() {
approval_policy: AskForApproval::Never,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/tmp/project").abs(),
reasoning_effort: None,
history_log_id: 0,