Skip to content
Open
Show file tree
Hide file tree
Changes from all 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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions codex-rs/app-server-protocol/src/protocol/v2/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ pub struct AppToolsConfig {
pub struct AppConfig {
#[serde(default = "default_enabled")]
pub enabled: bool,
pub approvals_reviewer: Option<ApprovalsReviewer>,
pub destructive_enabled: Option<bool>,
pub open_world_enabled: Option<bool>,
pub default_tools_approval_mode: Option<AppToolApproval>,
Expand Down
14 changes: 14 additions & 0 deletions codex-rs/app-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1715,6 +1715,20 @@ The server also emits `app/list/updated` notifications whenever either source (a
}
```

Connected apps may override the thread's approval reviewer in `config.toml`.
When omitted, the app inherits the top-level `approvals_reviewer` value:

```toml
approvals_reviewer = "auto_review"

[apps.demo-app]
approvals_reviewer = "user"
```

Setting the app value to `"user"` routes its approval prompts to the user
instead of Guardian; setting it to `"auto_review"` opts that app into Guardian
review when allowed by configuration requirements.

Invoke an app by inserting `$<app-slug>` in the text input. The slug is derived from the app name and lowercased with non-alphanumeric characters replaced by `-` (for example, "Demo App" becomes `$demo-app`). Add a `mention` input item (recommended) so the server uses the exact `app://<connector-id>` path rather than guessing by name. Plugins use the same `mention` item shape, but with `plugin://<plugin-name>@<marketplace-name>` paths from `plugin/installed` or `plugin/list`.

Example:
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/src/config_manager_service_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ async fn write_value_supports_nested_app_paths() -> Result<()> {
"app1".to_string(),
AppConfig {
enabled: false,
approvals_reviewer: None,
destructive_enabled: None,
open_world_enabled: None,
default_tools_approval_mode: Some(AppToolApproval::Prompt),
Expand Down
13 changes: 13 additions & 0 deletions codex-rs/app-server/tests/suite/v2/config_rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use app_test_support::test_tmp_path_buf;
use app_test_support::to_response;
use codex_app_server_protocol::AppConfig;
use codex_app_server_protocol::AppToolApproval;
use codex_app_server_protocol::ApprovalsReviewer;
use codex_app_server_protocol::AppsConfig;
use codex_app_server_protocol::AskForApproval;
use codex_app_server_protocol::ConfigBatchWriteParams;
Expand Down Expand Up @@ -333,6 +334,7 @@ async fn config_read_includes_apps() -> Result<()> {
r#"
[apps.app1]
enabled = false
approvals_reviewer = "user"
destructive_enabled = false
default_tools_approval_mode = "prompt"
"#,
Expand Down Expand Up @@ -368,6 +370,7 @@ default_tools_approval_mode = "prompt"
"app1".to_string(),
AppConfig {
enabled: false,
approvals_reviewer: Some(ApprovalsReviewer::User),
destructive_enabled: Some(false),
open_world_enabled: None,
default_tools_approval_mode: Some(AppToolApproval::Prompt),
Expand All @@ -384,6 +387,16 @@ default_tools_approval_mode = "prompt"
profile: None,
}
);
assert_eq!(
origins
.get("apps.app1.approvals_reviewer")
.expect("origin")
.name,
ConfigLayerSource::User {
file: user_file.clone(),
profile: None,
}
);
assert_eq!(
origins
.get("apps.app1.destructive_enabled")
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/config/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,10 @@ pub struct AppConfig {
#[serde(default = "default_enabled")]
pub enabled: bool,

/// Reviewer for approval prompts from this app, overriding the thread default.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub approvals_reviewer: Option<ApprovalsReviewer>,

/// Whether tools with `destructive_hint = true` are allowed for this app.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub destructive_enabled: Option<bool>,
Expand Down
8 changes: 8 additions & 0 deletions codex-rs/core/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@
"additionalProperties": false,
"description": "Config values for a single app/connector.",
"properties": {
"approvals_reviewer": {
"allOf": [
{
"$ref": "#/definitions/ApprovalsReviewer"
}
],
"description": "Reviewer for approval prompts from this app, overriding the thread default."
},
"default_tools_approval_mode": {
"allOf": [
{
Expand Down
25 changes: 16 additions & 9 deletions codex-rs/core/src/codex_delegate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,15 @@ use crate::config::Config;
use crate::guardian::GuardianApprovalRequest;
use crate::guardian::new_guardian_review_id;
use crate::guardian::routes_approval_to_guardian;
use crate::guardian::routes_approval_to_guardian_with_reviewer;
use crate::guardian::spawn_approval_request_review;
use crate::mcp_tool_call::MCP_TOOL_APPROVAL_ACCEPT;
use crate::mcp_tool_call::MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION;
use crate::mcp_tool_call::MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC;
use crate::mcp_tool_call::build_guardian_mcp_tool_review_request;
use crate::mcp_tool_call::is_mcp_tool_approval_question_id;
use crate::mcp_tool_call::lookup_mcp_tool_metadata;
use crate::mcp_tool_call::mcp_approvals_reviewer;
use crate::session::Codex;
use crate::session::CodexSpawnArgs;
use crate::session::CodexSpawnOk;
Expand Down Expand Up @@ -628,15 +630,14 @@ async fn handle_request_user_input(
event: RequestUserInputEvent,
cancel_token: &CancellationToken,
) {
if routes_approval_to_guardian(parent_ctx)
&& let Some(response) = maybe_auto_review_mcp_request_user_input(
parent_session,
parent_ctx,
pending_mcp_invocations,
&event,
cancel_token,
)
.await
if let Some(response) = maybe_auto_review_mcp_request_user_input(
parent_session,
parent_ctx,
pending_mcp_invocations,
&event,
cancel_token,
)
.await
{
let _ = codex.submit(Op::UserInputAnswer { id, response }).await;
return;
Expand Down Expand Up @@ -690,6 +691,12 @@ async fn maybe_auto_review_mcp_request_user_input(
&invocation.tool,
)
.await;
if !routes_approval_to_guardian_with_reviewer(
parent_ctx,
mcp_approvals_reviewer(parent_ctx, &invocation, metadata.as_ref()),
) {
return None;
}
let review_cancel = cancel_token.child_token();
let review_rx = spawn_approval_request_review(
Arc::clone(parent_session),
Expand Down
20 changes: 20 additions & 0 deletions codex-rs/core/src/connectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use crate::plugins::list_tool_suggest_discoverable_plugins;
use crate::session::INITIAL_SUBMIT_ID;
use codex_config::AppsRequirementsToml;
use codex_config::types::AppToolApproval;
use codex_config::types::ApprovalsReviewer;
use codex_config::types::AppsConfigToml;
use codex_config::types::ToolSuggestDiscoverableType;
use codex_core_plugins::PluginsManager;
Expand Down Expand Up @@ -566,6 +567,25 @@ pub(crate) fn codex_app_tool_is_enabled(config: &Config, tool_info: &ToolInfo) -
.enabled
}

pub(crate) fn app_approvals_reviewer(
config: &Config,
connector_id: Option<&str>,
) -> ApprovalsReviewer {
read_user_apps_config(config)
.as_ref()
.and_then(|apps_config| connector_id.and_then(|id| apps_config.apps.get(id)))
.and_then(|app| app.approvals_reviewer)
.filter(|reviewer| {
config
.config_layer_stack
.requirements()
.approvals_reviewer
.can_set(reviewer)
.is_ok()
})
.unwrap_or(config.approvals_reviewer)
}

fn read_apps_config(config: &Config) -> Option<AppsConfigToml> {
let apps_config = read_user_apps_config(config);
let had_apps_config = apps_config.is_some();
Expand Down
Loading
Loading