clients: send permission profiles to app-server

This commit is contained in:
Michael Bolin
2026-04-16 18:48:33 -07:00
parent 0091f74cf4
commit ad29119e25
4 changed files with 141 additions and 9 deletions

View File

@@ -75,6 +75,7 @@ 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::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::ReviewRequest;
use codex_protocol::protocol::ReviewTarget;
@@ -722,6 +723,18 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
items,
output_schema,
} => {
let (sandbox_policy, permission_profile) = if matches!(
default_sandbox_policy,
SandboxPolicy::ExternalSandbox { .. }
) {
(Some(default_sandbox_policy.clone().into()), None)
} else {
let permission_profile = PermissionProfile::from_legacy_sandbox_policy(
default_sandbox_policy,
&default_cwd,
);
(None, Some(permission_profile.into()))
};
let response: TurnStartResponse = send_request_with_response(
&client,
ClientRequest::TurnStart {
@@ -733,8 +746,8 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
cwd: Some(default_cwd),
approval_policy: Some(default_approval_policy.into()),
approvals_reviewer: None,
sandbox_policy: Some(default_sandbox_policy.clone().into()),
permission_profile: None,
sandbox_policy,
permission_profile,
model: None,
service_tier: None,
effort: default_effort,
@@ -908,13 +921,19 @@ fn sandbox_mode_from_policy(
}
fn thread_start_params_from_config(config: &Config) -> ThreadStartParams {
let permission_profile = permission_profile_override_from_config(config);
let sandbox = permission_profile
.is_none()
.then(|| sandbox_mode_from_policy(config.permissions.sandbox_policy.get()))
.flatten();
ThreadStartParams {
model: config.model.clone(),
model_provider: Some(config.model_provider_id.clone()),
cwd: Some(config.cwd.to_string_lossy().to_string()),
approval_policy: Some(config.permissions.approval_policy.value().into()),
approvals_reviewer: approvals_reviewer_override_from_config(config),
sandbox: sandbox_mode_from_policy(config.permissions.sandbox_policy.get()),
sandbox,
permission_profile,
config: config_request_overrides_from_config(config),
ephemeral: Some(config.ephemeral),
..ThreadStartParams::default()
@@ -922,6 +941,11 @@ fn thread_start_params_from_config(config: &Config) -> ThreadStartParams {
}
fn thread_resume_params_from_config(config: &Config, thread_id: String) -> ThreadResumeParams {
let permission_profile = permission_profile_override_from_config(config);
let sandbox = permission_profile
.is_none()
.then(|| sandbox_mode_from_policy(config.permissions.sandbox_policy.get()))
.flatten();
ThreadResumeParams {
thread_id,
model: config.model.clone(),
@@ -929,12 +953,26 @@ fn thread_resume_params_from_config(config: &Config, thread_id: String) -> Threa
cwd: Some(config.cwd.to_string_lossy().to_string()),
approval_policy: Some(config.permissions.approval_policy.value().into()),
approvals_reviewer: approvals_reviewer_override_from_config(config),
sandbox: sandbox_mode_from_policy(config.permissions.sandbox_policy.get()),
sandbox,
permission_profile,
config: config_request_overrides_from_config(config),
..ThreadResumeParams::default()
}
}
fn permission_profile_override_from_config(
config: &Config,
) -> Option<codex_app_server_protocol::PermissionProfile> {
if matches!(
config.permissions.sandbox_policy.get(),
SandboxPolicy::ExternalSandbox { .. }
) {
None
} else {
Some(config.permissions.permission_profile().into())
}
}
fn config_request_overrides_from_config(config: &Config) -> Option<HashMap<String, Value>> {
config
.active_profile

View File

@@ -361,6 +361,11 @@ async fn thread_start_params_include_review_policy_when_review_policy_is_manual_
params.approvals_reviewer,
Some(codex_app_server_protocol::ApprovalsReviewer::User)
);
assert_eq!(params.sandbox, None);
assert_eq!(
params.permission_profile,
Some(config.permissions.permission_profile().into())
);
}
#[tokio::test]

View File

@@ -3232,6 +3232,7 @@ impl App {
approval_policy: self.config.permissions.approval_policy.value(),
approvals_reviewer: self.config.approvals_reviewer,
sandbox_policy: self.config.permissions.sandbox_policy.get().clone(),
permission_profile: self.config.permissions.permission_profile(),
cwd: thread.cwd.clone(),
instruction_source_paths: Vec::new(),
reasoning_effort: self.chat_widget.current_reasoning_effort(),
@@ -8954,6 +8955,11 @@ guardian_approval = true
ThreadSessionState {
approval_policy: AskForApproval::OnRequest,
sandbox_policy: SandboxPolicy::new_workspace_write_policy(),
permission_profile:
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::new_workspace_write_policy(),
std::path::Path::new("/tmp/agent"),
),
rollout_path: Some(test_path_buf("/tmp/agent-rollout.jsonl")),
..test_thread_session(agent_thread_id, test_path_buf("/tmp/agent"))
},
@@ -9167,6 +9173,11 @@ guardian_approval = true
ThreadSessionState {
approval_policy: AskForApproval::OnRequest,
sandbox_policy: SandboxPolicy::new_workspace_write_policy(),
permission_profile:
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::new_workspace_write_policy(),
std::path::Path::new("/tmp/agent"),
),
rollout_path: Some(test_path_buf("/tmp/agent-rollout.jsonl")),
..test_thread_session(agent_thread_id, test_path_buf("/tmp/agent"))
},
@@ -9220,6 +9231,11 @@ guardian_approval = true
let primary_session = ThreadSessionState {
approval_policy: AskForApproval::OnRequest,
sandbox_policy: SandboxPolicy::new_workspace_write_policy(),
permission_profile:
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::new_workspace_write_policy(),
std::path::Path::new("/tmp/main"),
),
..test_thread_session(main_thread_id, test_path_buf("/tmp/main"))
};
@@ -9331,6 +9347,11 @@ guardian_approval = true
let primary_session = ThreadSessionState {
approval_policy: AskForApproval::OnRequest,
sandbox_policy: SandboxPolicy::new_workspace_write_policy(),
permission_profile:
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::new_workspace_write_policy(),
std::path::Path::new("/tmp/main"),
),
..test_thread_session(main_thread_id, test_path_buf("/tmp/main"))
};
@@ -9806,6 +9827,11 @@ guardian_approval = true
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile:
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::new_read_only_policy(),
cwd.as_path(),
),
cwd: cwd.abs(),
instruction_source_paths: Vec::new(),
reasoning_effort: None,

View File

@@ -75,6 +75,7 @@ use codex_app_server_protocol::TurnSteerParams;
use codex_app_server_protocol::TurnSteerResponse;
use codex_otel::TelemetryAuthMode;
use codex_protocol::ThreadId;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ModelAvailabilityNux;
use codex_protocol::openai_models::ModelPreset;
use codex_protocol::openai_models::ModelUpgrade;
@@ -136,6 +137,7 @@ pub(crate) struct ThreadSessionState {
pub(crate) approval_policy: AskForApproval,
pub(crate) approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer,
pub(crate) sandbox_policy: SandboxPolicy,
pub(crate) permission_profile: PermissionProfile,
pub(crate) cwd: AbsolutePathBuf,
pub(crate) instruction_source_paths: Vec<AbsolutePathBuf>,
pub(crate) reasoning_effort: Option<codex_protocol::openai_models::ReasoningEffort>,
@@ -432,6 +434,14 @@ impl AppServerSession {
output_schema: Option<serde_json::Value>,
) -> Result<TurnStartResponse> {
let request_id = self.next_request_id();
let (sandbox_policy, permission_profile) = match sandbox_policy {
policy @ SandboxPolicy::ExternalSandbox { .. } => (Some(policy.into()), None),
policy => {
let permission_profile =
PermissionProfile::from_legacy_sandbox_policy(&policy, &cwd);
(None, Some(permission_profile.into()))
}
};
self.client
.request_typed(ClientRequest::TurnStart {
request_id,
@@ -442,8 +452,8 @@ impl AppServerSession {
cwd: Some(cwd),
approval_policy: Some(approval_policy.into()),
approvals_reviewer: Some(approvals_reviewer.into()),
sandbox_policy: Some(sandbox_policy.into()),
permission_profile: None,
sandbox_policy,
permission_profile,
model: Some(model),
service_tier,
effort,
@@ -898,19 +908,38 @@ fn sandbox_mode_from_policy(
}
}
fn permission_profile_override_from_config(
config: &Config,
) -> Option<codex_app_server_protocol::PermissionProfile> {
if matches!(
config.permissions.sandbox_policy.get(),
SandboxPolicy::ExternalSandbox { .. }
) {
None
} else {
Some(config.permissions.permission_profile().into())
}
}
fn thread_start_params_from_config(
config: &Config,
thread_params_mode: ThreadParamsMode,
remote_cwd_override: Option<&std::path::Path>,
session_start_source: Option<ThreadStartSource>,
) -> ThreadStartParams {
let permission_profile = permission_profile_override_from_config(config);
let sandbox = permission_profile
.is_none()
.then(|| sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone()))
.flatten();
ThreadStartParams {
model: config.model.clone(),
model_provider: thread_params_mode.model_provider_from_config(config),
cwd: thread_cwd_from_config(config, thread_params_mode, remote_cwd_override),
approval_policy: Some(config.permissions.approval_policy.value().into()),
approvals_reviewer: approvals_reviewer_override_from_config(config),
sandbox: sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone()),
sandbox,
permission_profile,
config: config_request_overrides_from_config(config),
ephemeral: Some(config.ephemeral),
session_start_source,
@@ -925,6 +954,11 @@ fn thread_resume_params_from_config(
thread_params_mode: ThreadParamsMode,
remote_cwd_override: Option<&std::path::Path>,
) -> ThreadResumeParams {
let permission_profile = permission_profile_override_from_config(&config);
let sandbox = permission_profile
.is_none()
.then(|| sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone()))
.flatten();
ThreadResumeParams {
thread_id: thread_id.to_string(),
model: config.model.clone(),
@@ -932,7 +966,8 @@ fn thread_resume_params_from_config(
cwd: thread_cwd_from_config(&config, thread_params_mode, remote_cwd_override),
approval_policy: Some(config.permissions.approval_policy.value().into()),
approvals_reviewer: approvals_reviewer_override_from_config(&config),
sandbox: sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone()),
sandbox,
permission_profile,
config: config_request_overrides_from_config(&config),
persist_extended_history: true,
..ThreadResumeParams::default()
@@ -945,6 +980,11 @@ fn thread_fork_params_from_config(
thread_params_mode: ThreadParamsMode,
remote_cwd_override: Option<&std::path::Path>,
) -> ThreadForkParams {
let permission_profile = permission_profile_override_from_config(&config);
let sandbox = permission_profile
.is_none()
.then(|| sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone()))
.flatten();
ThreadForkParams {
thread_id: thread_id.to_string(),
model: config.model.clone(),
@@ -952,7 +992,8 @@ fn thread_fork_params_from_config(
cwd: thread_cwd_from_config(&config, thread_params_mode, remote_cwd_override),
approval_policy: Some(config.permissions.approval_policy.value().into()),
approvals_reviewer: approvals_reviewer_override_from_config(&config),
sandbox: sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone()),
sandbox,
permission_profile,
config: config_request_overrides_from_config(&config),
ephemeral: config.ephemeral,
persist_extended_history: true,
@@ -1027,6 +1068,7 @@ async fn thread_session_state_from_thread_start_response(
response.approval_policy.to_core(),
response.approvals_reviewer.to_core(),
response.sandbox.to_core(),
response.permission_profile.clone().into(),
response.cwd.clone(),
response.instruction_sources.clone(),
response.reasoning_effort,
@@ -1050,6 +1092,7 @@ async fn thread_session_state_from_thread_resume_response(
response.approval_policy.to_core(),
response.approvals_reviewer.to_core(),
response.sandbox.to_core(),
response.permission_profile.clone().into(),
response.cwd.clone(),
response.instruction_sources.clone(),
response.reasoning_effort,
@@ -1073,6 +1116,7 @@ async fn thread_session_state_from_thread_fork_response(
response.approval_policy.to_core(),
response.approvals_reviewer.to_core(),
response.sandbox.to_core(),
response.permission_profile.clone().into(),
response.cwd.clone(),
response.instruction_sources.clone(),
response.reasoning_effort,
@@ -1115,6 +1159,7 @@ async fn thread_session_state_from_thread_response(
approval_policy: AskForApproval,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer,
sandbox_policy: SandboxPolicy,
permission_profile: PermissionProfile,
cwd: AbsolutePathBuf,
instruction_source_paths: Vec<AbsolutePathBuf>,
reasoning_effort: Option<codex_protocol::openai_models::ReasoningEffort>,
@@ -1140,6 +1185,7 @@ async fn thread_session_state_from_thread_response(
approval_policy,
approvals_reviewer,
sandbox_policy,
permission_profile,
cwd,
instruction_source_paths,
reasoning_effort,
@@ -1231,6 +1277,11 @@ mod tests {
);
assert_eq!(params.cwd, Some(config.cwd.to_string_lossy().to_string()));
assert_eq!(params.sandbox, None);
assert_eq!(
params.permission_profile,
Some(config.permissions.permission_profile().into())
);
assert_eq!(params.model_provider, Some(config.model_provider_id));
}
@@ -1389,6 +1440,10 @@ mod tests {
started.session.instruction_source_paths,
response.instruction_sources
);
assert_eq!(
started.session.permission_profile,
response.permission_profile.clone().into()
);
assert_eq!(started.turns.len(), 1);
assert_eq!(started.turns[0], response.thread.turns[0]);
}
@@ -1417,6 +1472,10 @@ mod tests {
AskForApproval::Never,
codex_protocol::config_types::ApprovalsReviewer::User,
SandboxPolicy::new_read_only_policy(),
PermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::new_read_only_policy(),
std::path::Path::new("/tmp/project"),
),
test_path_buf("/tmp/project").abs(),
Vec::new(),
/*reasoning_effort*/ None,
@@ -1447,6 +1506,10 @@ mod tests {
AskForApproval::Never,
codex_protocol::config_types::ApprovalsReviewer::User,
SandboxPolicy::new_read_only_policy(),
PermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::new_read_only_policy(),
std::path::Path::new("/tmp/project"),
),
test_path_buf("/tmp/project").abs(),
Vec::new(),
/*reasoning_effort*/ None,