Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/en/user-guide/chat/codex-permissions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
sidebar_position: 8
---

# Codex permission modes

Wework provides a Codex permission selector below the composer. New chats inherit the default from **Settings > General**, while existing chats keep their own selection.

## Permission modes

- **Full access**: Runs without sandboxing or approval prompts. Use only in trusted workspaces.
- **Ask for approval**: Codex works automatically inside the workspace and asks you before accessing files outside it, using blocked network access, or invoking side-effecting tools.
- **Approve for me**: Keeps the same sandbox as Ask for approval, but routes boundary-crossing requests to an independent AI reviewer. Reviewer failures and timeouts deny the action instead of widening access.

When you change the mode during execution, the current turn keeps its original mode and the new mode applies to the next turn.

## Approval scope

Approval cards show only decisions supported by the current Codex request, such as allow once, allow for the session, or decline. A persistent option appears only when Codex provides a command or network rule amendment; Wework does not broaden the proposed rule.
19 changes: 19 additions & 0 deletions docs/zh/user-guide/chat/codex-permissions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
sidebar_position: 8
---

# Codex 权限模式

Wework 在输入框下方提供 Codex 权限选择器。新会话继承“设置 > 通用”中的默认值,已有会话保留自己的选择。

## 权限模式

- **完全访问**:不使用沙箱,也不会请求批准。仅适用于可信工作区。
- **请求批准**:Codex 可在工作区内自动操作;访问工作区外文件、网络或有副作用的工具时由你批准。
- **代我审批**:保持与“请求批准”相同的沙箱,由独立 AI reviewer 批准或拒绝越界操作。AI 审批失败或超时会拒绝操作,不会自动放宽权限。

权限模式在执行期间切换时,当前轮继续使用原模式,下一轮开始使用新模式。

## 审批范围

审批卡只显示 Codex 当前请求支持的决定,例如允许本次、本会话允许或拒绝。只有 Codex 提供命令或网络规则提案时,才会显示长期允许选项;Wework 不会自行扩大规则范围。
238 changes: 207 additions & 31 deletions executor/src/agents/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -968,7 +968,7 @@ async fn run_codex_app_server_turn_on_shared_client(
thread_fields.push(("operation", thread_operation.to_owned()));
log_executor_event("codex shared thread request started", &thread_fields);
let thread = client.request(thread_operation, thread_params).await?;
validate_codex_permission_profile(thread_operation, &thread)?;
validate_codex_permission_profile(thread_operation, &thread, request)?;
let thread_id = thread
.get("thread")
.and_then(|thread| thread.get("id"))
Expand Down Expand Up @@ -1228,7 +1228,7 @@ pub async fn run_codex_app_server_turn_with_cancel(
rpc.request(thread_operation, thread_params, &mut state),
)
.await?;
validate_codex_permission_profile(thread_operation, &thread)?;
validate_codex_permission_profile(thread_operation, &thread, request)?;
let thread_id = thread
.get("thread")
.and_then(|thread| thread.get("id"))
Expand Down Expand Up @@ -1488,6 +1488,16 @@ async fn read_shared_turn_notifications(
continue;
}

if is_codex_approval_request(&message) {
spawn_shared_approval_response(
client,
&message,
request_user_input_answers.clone(),
response_error_tx.clone(),
)?;
continue;
}

if message
.get("method")
.and_then(Value::as_str)
Expand Down Expand Up @@ -1638,6 +1648,42 @@ fn spawn_shared_request_user_input_response(
Ok(())
}

fn is_codex_approval_request(message: &Value) -> bool {
matches!(
message.get("method").and_then(Value::as_str),
Some("item/commandExecution/requestApproval")
| Some("item/fileChange/requestApproval")
| Some("item/permissions/requestApproval")
)
}

fn spawn_shared_approval_response(
client: &CodexAppServerClient,
message: &Value,
responses: Option<Arc<InteractionAnswerRouter>>,
response_error_tx: mpsc::UnboundedSender<String>,
) -> Result<(), String> {
let request_id = json_rpc_request_id(message)
.ok_or_else(|| "approval request is missing JSON-RPC id".to_owned())?;
let Some(receiver) = responses else {
return Err("approval request requires a runtime response channel".to_owned());
};
let correlation_key = interaction_value_key(&request_id)
.ok_or_else(|| "approval request has invalid JSON-RPC id".to_owned())?;
let client = client.clone();
tokio::spawn(async move {
let result = async {
let response = receiver.receive(correlation_key).await?;
client.send_response(request_id, response).await
}
.await;
if let Err(error) = result {
let _ = response_error_tx.send(error);
}
});
Ok(())
}

fn spawn_shared_mcp_server_elicitation_response(
client: &CodexAppServerClient,
message: &Value,
Expand Down Expand Up @@ -2137,6 +2183,12 @@ impl JsonRpcConnection {
.await?;
continue;
}

if is_codex_approval_request(&message) {
self.answer_approval_request(&message, &mut request_user_input_answers)
.await?;
continue;
}
if message
.get("method")
.and_then(Value::as_str)
Expand Down Expand Up @@ -2176,6 +2228,27 @@ impl JsonRpcConnection {
.await
}

async fn answer_approval_request(
&mut self,
message: &Value,
responses: &mut Option<CodexRequestUserInputReceiver>,
) -> Result<(), String> {
let request_id = json_rpc_request_id(message)
.ok_or_else(|| "approval request is missing JSON-RPC id".to_owned())?;
let Some(receiver) = responses else {
return Err("approval request requires a runtime response channel".to_owned());
};
let response = receiver
.recv()
.await
.ok_or_else(|| "approval response channel closed".to_owned())?;
self.write_message(json!({
"id": request_id,
"result": response,
}))
.await
}

async fn answer_mcp_server_elicitation(
&mut self,
message: &Value,
Expand Down Expand Up @@ -4074,15 +4147,88 @@ fn resolve_codex_binary(value: &str) -> String {
}

const CODEX_DANGER_FULL_ACCESS_PERMISSION_PROFILE: &str = ":danger-full-access";
const CODEX_WORKSPACE_PERMISSION_PROFILE: &str = ":workspace";

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CodexPermissionMode {
FullAccess,
RequestApproval,
ApproveForMe,
}

impl CodexPermissionMode {
pub(crate) fn from_request(request: &ExecutionRequest) -> Self {
Self::from_value(
request
.extra
.get("permission_mode")
.or_else(|| request.extra.get("permissionMode"))
.and_then(Value::as_str),
)
}

pub(crate) fn from_value(value: Option<&str>) -> Self {
match value {
Some("request_approval") => Self::RequestApproval,
Some("approve_for_me") => Self::ApproveForMe,
_ => Self::FullAccess,
}
}

pub(crate) fn as_str(self) -> &'static str {
match self {
Self::FullAccess => "full_access",
Self::RequestApproval => "request_approval",
Self::ApproveForMe => "approve_for_me",
}
}

pub(crate) fn permission_profile(self) -> &'static str {
match self {
Self::FullAccess => CODEX_DANGER_FULL_ACCESS_PERMISSION_PROFILE,
Self::RequestApproval | Self::ApproveForMe => CODEX_WORKSPACE_PERMISSION_PROFILE,
}
}

pub(crate) fn approval_policy(self) -> &'static str {
match self {
Self::FullAccess => "never",
Self::RequestApproval | Self::ApproveForMe => "on-request",
}
}

pub(crate) fn approvals_reviewer(self) -> &'static str {
match self {
Self::ApproveForMe => "auto_review",
Self::FullAccess | Self::RequestApproval => "user",
}
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

fn insert_codex_runtime_permissions(params: &mut serde_json::Map<String, Value>) {
fn insert_codex_runtime_permissions(
params: &mut serde_json::Map<String, Value>,
request: &ExecutionRequest,
) {
let mode = CodexPermissionMode::from_request(request);
params.insert(
"permissions".to_owned(),
Value::String(CODEX_DANGER_FULL_ACCESS_PERMISSION_PROFILE.to_owned()),
Value::String(mode.permission_profile().to_owned()),
);
params.insert(
"approvalPolicy".to_owned(),
Value::String(mode.approval_policy().to_owned()),
);
params.insert(
"approvalsReviewer".to_owned(),
Value::String(mode.approvals_reviewer().to_owned()),
);
}

fn validate_codex_permission_profile(operation: &str, response: &Value) -> Result<(), String> {
fn validate_codex_permission_profile(
operation: &str,
response: &Value,
request: &ExecutionRequest,
) -> Result<(), String> {
let active_profile = response
.get("activePermissionProfile")
.and_then(|profile| profile.get("id"))
Expand All @@ -4098,9 +4244,14 @@ fn validate_codex_permission_profile(operation: &str, response: &Value) -> Resul
if active_profile.is_none() && sandbox_type.is_none() {
return Ok(());
}
if active_profile == Some(CODEX_DANGER_FULL_ACCESS_PERMISSION_PROFILE)
&& sandbox_type == Some("dangerFullAccess")
{
let mode = CodexPermissionMode::from_request(request);
let expected_sandbox = match mode {
CodexPermissionMode::FullAccess => "dangerFullAccess",
CodexPermissionMode::RequestApproval | CodexPermissionMode::ApproveForMe => {
"workspaceWrite"
}
};
if active_profile == Some(mode.permission_profile()) && sandbox_type == Some(expected_sandbox) {
return Ok(());
}

Expand All @@ -4120,11 +4271,7 @@ fn thread_start_params(request: &ExecutionRequest, launch_config: &CodexLaunchCo
if let Some(cwd) = request.cwd() {
params.insert("cwd".to_owned(), Value::String(cwd.to_owned()));
}
params.insert(
"approvalPolicy".to_owned(),
Value::String("never".to_owned()),
);
insert_codex_runtime_permissions(&mut params);
insert_codex_runtime_permissions(&mut params, request);
if request.ephemeral {
params.insert("ephemeral".to_owned(), Value::Bool(true));
}
Expand All @@ -4150,11 +4297,7 @@ fn thread_fork_params(
if let Some(cwd) = request.cwd() {
params.insert("cwd".to_owned(), Value::String(cwd.to_owned()));
}
params.insert(
"approvalPolicy".to_owned(),
Value::String("never".to_owned()),
);
insert_codex_runtime_permissions(&mut params);
insert_codex_runtime_permissions(&mut params, request);
if request.ephemeral {
params.insert("ephemeral".to_owned(), Value::Bool(true));
}
Expand Down Expand Up @@ -4213,11 +4356,7 @@ fn thread_resume_params(
if let Some(cwd) = request.cwd() {
params.insert("cwd".to_owned(), Value::String(cwd.to_owned()));
}
params.insert(
"approvalPolicy".to_owned(),
Value::String("never".to_owned()),
);
insert_codex_runtime_permissions(&mut params);
insert_codex_runtime_permissions(&mut params, request);
Value::Object(params)
}

Expand Down Expand Up @@ -4264,11 +4403,7 @@ fn turn_start_params(
Value::String(client_user_message_id.to_owned()),
);
}
params.insert(
"approvalPolicy".to_owned(),
Value::String("never".to_owned()),
);
insert_codex_runtime_permissions(&mut params);
insert_codex_runtime_permissions(&mut params, request);
if let Some(cwd) = request.cwd() {
params.insert("cwd".to_owned(), Value::String(cwd.to_owned()));
}
Expand Down Expand Up @@ -5661,6 +5796,8 @@ mod tests {
params["permissions"],
CODEX_DANGER_FULL_ACCESS_PERMISSION_PROFILE
);
assert_eq!(params["approvalPolicy"], "never");
assert_eq!(params["approvalsReviewer"], "user");
assert!(params.get("sandboxPolicy").is_none());
assert!(params.get("sandbox").is_none());
}
Expand All @@ -5673,8 +5810,12 @@ mod tests {
"sandbox": {"type": "workspaceWrite", "networkAccess": false},
});

let error = validate_codex_permission_profile("thread/resume", &response)
.expect_err("workspace-write must not be accepted");
let error = validate_codex_permission_profile(
"thread/resume",
&response,
&ExecutionRequest::default(),
)
.expect_err("workspace-write must not be accepted");

assert!(error.contains("active_profile=:workspace"));
assert!(error.contains("sandbox=workspaceWrite"));
Expand All @@ -5687,7 +5828,42 @@ mod tests {
"sandbox": {"type": "dangerFullAccess"},
});

validate_codex_permission_profile("thread/resume", &response).unwrap();
validate_codex_permission_profile("thread/resume", &response, &ExecutionRequest::default())
.unwrap();
}

#[test]
fn codex_interactive_permission_modes_use_workspace_profile() {
for (permission_mode, reviewer) in [
("request_approval", "user"),
("approve_for_me", "auto_review"),
] {
let mut request = ExecutionRequest::default();
request.extra.insert(
"permission_mode".to_owned(),
Value::String(permission_mode.to_owned()),
);

let params = turn_start_params(
"thread-1",
&request,
&CodexLaunchConfig::default(),
Vec::new(),
);
assert_eq!(params["permissions"], CODEX_WORKSPACE_PERMISSION_PROFILE);
assert_eq!(params["approvalPolicy"], "on-request");
assert_eq!(params["approvalsReviewer"], reviewer);

validate_codex_permission_profile(
"turn/start",
&json!({
"activePermissionProfile": {"id": ":workspace"},
"sandbox": {"type": "workspaceWrite"},
}),
&request,
)
.unwrap();
}
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion executor/src/agents/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ use claude_code::{
pub use claude_options::{extract_claude_options, ClaudeOptions};
pub(crate) use codex::{
combined_codex_developer_instructions, mcp_server_elicitation_request_user_input_params,
strip_wework_browser_instructions,
strip_wework_browser_instructions, CodexPermissionMode,
};
pub use codex::{
run_codex_app_server_turn, run_codex_app_server_turn_with_cancel, CodexActiveTurnCallback,
Expand Down
Loading
Loading