From 9fc4e5a7aaf0d4da64e8bef74d2aac02c2d12c79 Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Thu, 23 Jul 2026 23:08:55 +0000 Subject: [PATCH] Preserve plugin attribution across command approvals (#35029) ## What changed - Add optional `plugin_id` and `script_path` fields to execution approval and guardian assessment events. - Propagate validated plugin attribution through delegated approvals, guardian-reviewed command items, app-server notifications, thread history, and rollout traces. - Preserve attribution on both started and completed command items, including declined commands. ## Testing - Extend core, app-server, thread-history, and rollout-trace tests to cover plugin attribution propagation and serialization. GitOrigin-RevId: 723684d010cab04142918d7a95e06ed95d008da1 --- .../src/protocol/item_builders.rs | 8 +- .../src/protocol/thread_history.rs | 46 +++++-- .../app-server/src/bespoke_event_handling.rs | 114 +++++++++++++----- .../app-server/tests/suite/v2/turn_start.rs | 28 ++++- codex-rs/core/src/codex_delegate.rs | 37 +++++- codex-rs/core/src/codex_delegate_tests.rs | 10 ++ codex-rs/core/src/guardian/mod.rs | 1 + codex-rs/core/src/guardian/review.rs | 76 ++++++++++-- codex-rs/core/src/guardian/tests.rs | 7 +- .../src/session/elicitation_holders_tests.rs | 14 ++- codex-rs/core/src/session/mod.rs | 19 ++- codex-rs/core/src/tools/network_approval.rs | 1 + codex-rs/core/src/tools/runtimes/shell.rs | 1 + .../tools/runtimes/shell/unix_escalation.rs | 1 + .../core/src/tools/runtimes/unified_exec.rs | 1 + codex-rs/mcp-server/src/codex_tool_runner.rs | 2 + codex-rs/protocol/src/approvals.rs | 16 +++ codex-rs/rollout-trace/src/protocol_event.rs | 20 ++- .../rollout-trace/src/protocol_event_tests.rs | 12 +- codex-rs/tui/src/auto_review_denials.rs | 2 + .../tui/src/chatwidget/protocol_requests.rs | 2 + codex-rs/tui/src/chatwidget/tests/guardian.rs | 18 +++ .../tui/src/chatwidget/tests/mcp_startup.rs | 2 + 23 files changed, 363 insertions(+), 75 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol/item_builders.rs b/codex-rs/app-server-protocol/src/protocol/item_builders.rs index 4a9f6c90a6..5b5b9d2cfa 100644 --- a/codex-rs/app-server-protocol/src/protocol/item_builders.rs +++ b/codex-rs/app-server-protocol/src/protocol/item_builders.rs @@ -179,8 +179,8 @@ pub fn build_item_from_guardian_event( }]; Some(ThreadItem::CommandExecution { id: id.clone(), - plugin_id: None, - script_path: None, + plugin_id: assessment.plugin_id.clone(), + script_path: assessment.script_path.clone(), command, cwd: cwd.clone().into(), process_id: None, @@ -217,8 +217,8 @@ pub fn build_item_from_guardian_event( }; Some(ThreadItem::CommandExecution { id: id.clone(), - plugin_id: None, - script_path: None, + plugin_id: assessment.plugin_id.clone(), + script_path: assessment.script_path.clone(), command, cwd: cwd.clone().into(), process_id: None, diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history.rs b/codex-rs/app-server-protocol/src/protocol/thread_history.rs index 509582e3bc..a1c1cf66c9 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -2088,13 +2088,13 @@ mod tests { } #[test] - fn rebuilds_command_execution_item_from_persisted_completion() { + fn preserves_command_plugin_id_across_legacy_upsert() { let turn_id = "turn-1"; let thread_id = ThreadId::new(); let command_item = CoreTurnItem::CommandExecution(CoreCommandExecutionItem { id: "exec-1".to_string(), - plugin_id: None, - script_path: None, + plugin_id: Some("sample@openai-curated".to_string()), + script_path: Some("scripts/run.py".to_string()), process_id: Some("pid-1".to_string()), command: vec!["echo".to_string(), "hello world".to_string()], cwd: test_path_buf("/tmp").abs().into(), @@ -2125,6 +2125,28 @@ mod tests { item: command_item, completed_at_ms: 1_000, }), + EventMsg::ExecCommandEnd(ExecCommandEndEvent { + call_id: "exec-1".to_string(), + plugin_id: Some("sample@openai-curated".to_string()), + script_path: Some("scripts/run.py".to_string()), + process_id: Some("pid-1".to_string()), + turn_id: turn_id.to_string(), + completed_at_ms: 1_000, + command: vec!["echo".to_string(), "hello world".to_string()], + cwd: test_path_buf("/tmp").abs().into(), + parsed_cmd: vec![ParsedCommand::Unknown { + cmd: "echo hello world".to_string(), + }], + source: ExecCommandSource::Agent, + interaction_input: None, + stdout: "hello world\n".to_string(), + stderr: String::new(), + aggregated_output: "hello world\n".to_string(), + exit_code: 0, + duration: Duration::from_millis(12), + formatted_output: "hello world\n".to_string(), + status: CoreExecCommandStatus::Completed, + }), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: turn_id.to_string(), started_at: None, @@ -2147,8 +2169,8 @@ mod tests { turns[0].items, vec![ThreadItem::CommandExecution { id: "exec-1".to_string(), - plugin_id: None, - script_path: None, + plugin_id: Some("sample@openai-curated".to_string()), + script_path: Some("scripts/run.py".to_string()), command: "echo 'hello world'".to_string(), cwd: test_path_buf("/tmp").abs().into(), process_id: Some("pid-1".to_string()), @@ -3083,6 +3105,8 @@ mod tests { EventMsg::GuardianAssessment(GuardianAssessmentEvent { id: "review-guardian-exec".into(), target_item_id: Some("guardian-exec".into()), + plugin_id: Some("sample@openai-curated".into()), + script_path: Some("scripts/run.py".into()), turn_id: "turn-1".into(), started_at_ms: 1_000, completed_at_ms: None, @@ -3102,6 +3126,8 @@ mod tests { EventMsg::GuardianAssessment(GuardianAssessmentEvent { id: "review-guardian-exec".into(), target_item_id: Some("guardian-exec".into()), + plugin_id: Some("sample@openai-curated".into()), + script_path: Some("scripts/run.py".into()), turn_id: "turn-1".into(), started_at_ms: 1_000, completed_at_ms: Some(1_042), @@ -3133,8 +3159,8 @@ mod tests { turns[0].items[1], ThreadItem::CommandExecution { id: "guardian-exec".into(), - plugin_id: None, - script_path: None, + plugin_id: Some("sample@openai-curated".into()), + script_path: Some("scripts/run.py".into()), command: "rm -rf /tmp/guardian".into(), cwd: test_path_buf("/tmp").abs().into(), process_id: None, @@ -3171,6 +3197,8 @@ mod tests { EventMsg::GuardianAssessment(GuardianAssessmentEvent { id: "review-guardian-execve".into(), target_item_id: Some("guardian-execve".into()), + plugin_id: Some("sample@openai-curated".into()), + script_path: Some("scripts/run.py".into()), turn_id: "turn-1".into(), started_at_ms: 2_000, completed_at_ms: None, @@ -3201,8 +3229,8 @@ mod tests { turns[0].items[1], ThreadItem::CommandExecution { id: "guardian-execve".into(), - plugin_id: None, - script_path: None, + plugin_id: Some("sample@openai-curated".into()), + script_path: Some("scripts/run.py".into()), command: "/bin/rm -f /tmp/file.sqlite".into(), cwd: test_path_buf("/tmp").abs().into(), process_id: None, diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index a96b826fa9..fac6bd5fd4 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -129,6 +129,8 @@ enum CommandExecutionApprovalPresentation { #[derive(Debug, PartialEq)] struct CommandExecutionCompletionItem { + plugin_id: Option, + script_path: Option, command: String, cwd: LegacyAppPathString, command_actions: Vec, @@ -271,6 +273,8 @@ pub(crate) async fn apply_bespoke_event_handling( ) { Some(ThreadItem::CommandExecution { id, + plugin_id, + script_path, command, cwd, command_actions, @@ -278,6 +282,8 @@ pub(crate) async fn apply_bespoke_event_handling( }) => Some(( id, CommandExecutionCompletionItem { + plugin_id, + script_path, command, cwd, command_actions, @@ -297,6 +303,8 @@ pub(crate) async fn apply_bespoke_event_handling( &conversation_id, assessment_turn_id.clone(), target_item_id.clone(), + completion_item.plugin_id.clone(), + completion_item.script_path.clone(), completion_item.command.clone(), completion_item.cwd.clone(), completion_item.command_actions.clone(), @@ -330,11 +338,9 @@ pub(crate) async fn apply_bespoke_event_handling( &conversation_id, assessment_turn_id, target_item_id, - completion_item.command, - completion_item.cwd, + completion_item, /*process_id*/ None, CommandExecutionSource::Agent, - completion_item.command_actions, completion_status, &outgoing, &thread_state, @@ -588,6 +594,8 @@ pub(crate) async fn apply_bespoke_event_handling( .collect::>(); let ExecApprovalRequestEvent { call_id, + plugin_id, + script_path, approval_id, turn_id, environment_id, @@ -614,6 +622,8 @@ pub(crate) async fn apply_bespoke_event_handling( } else { let command_string = shlex_join(&command); let completion_item = CommandExecutionCompletionItem { + plugin_id, + script_path, command: command_string, cwd: cwd.clone().into(), command_actions: command_actions.clone(), @@ -640,6 +650,8 @@ pub(crate) async fn apply_bespoke_event_handling( &conversation_id, event_turn_id.clone(), call_id.clone(), + completion_item.plugin_id.clone(), + completion_item.script_path.clone(), completion_item.command.clone(), completion_item.cwd.clone(), completion_item.command_actions.clone(), @@ -1334,6 +1346,8 @@ async fn start_command_execution_item( conversation_id: &ThreadId, turn_id: String, item_id: String, + plugin_id: Option, + script_path: Option, command: String, cwd: LegacyAppPathString, command_actions: Vec, @@ -1355,8 +1369,8 @@ async fn start_command_execution_item( started_at_ms: now_unix_timestamp_ms(), item: ThreadItem::CommandExecution { id: item_id, - plugin_id: None, - script_path: None, + plugin_id, + script_path, command, cwd, process_id: None, @@ -1380,11 +1394,9 @@ async fn complete_command_execution_item( conversation_id: &ThreadId, turn_id: String, item_id: String, - command: String, - cwd: LegacyAppPathString, + completion_item: CommandExecutionCompletionItem, process_id: Option, source: CommandExecutionSource, - command_actions: Vec, status: CommandExecutionStatus, outgoing: &ThreadScopedOutgoingMessageSender, thread_state: &Arc>, @@ -1401,14 +1413,14 @@ async fn complete_command_execution_item( let item = ThreadItem::CommandExecution { id: item_id, - plugin_id: None, - script_path: None, - command, - cwd, + plugin_id: completion_item.plugin_id, + script_path: completion_item.script_path, + command: completion_item.command, + cwd: completion_item.cwd, process_id, source, status, - command_actions, + command_actions: completion_item.command_actions, aggregated_output: None, exit_code: None, duration_ms: None, @@ -2051,11 +2063,9 @@ async fn on_command_execution_request_approval_response( &conversation_id, event_turn_id.clone(), item_id.clone(), - completion_item.command, - completion_item.cwd, + completion_item, /*process_id*/ None, CommandExecutionSource::Agent, - completion_item.command_actions, status, &outgoing, &thread_state, @@ -2271,6 +2281,8 @@ mod tests { fn command_execution_completion_item(command: &str) -> CommandExecutionCompletionItem { CommandExecutionCompletionItem { + plugin_id: Some("sample@openai-curated".to_string()), + script_path: Some("scripts/run.py".to_string()), command: command.to_string(), cwd: test_path_buf("/tmp").abs().into(), command_actions: vec![V2ParsedCommand::Unknown { @@ -2304,6 +2316,8 @@ mod tests { GuardianAssessmentEvent { id: format!("review-{id}"), target_item_id: Some(id.to_string()), + plugin_id: Some("sample@openai-curated".to_string()), + script_path: Some("scripts/run.py".to_string()), turn_id: turn_id.to_string(), started_at_ms: 1_000, completed_at_ms: (!matches!(status, GuardianAssessmentStatus::InProgress)) @@ -2371,6 +2385,8 @@ mod tests { &GuardianAssessmentEvent { id: "review-1".to_string(), target_item_id: Some("item-1".to_string()), + plugin_id: None, + script_path: None, turn_id: String::new(), started_at_ms: 1_000, completed_at_ms: None, @@ -2417,6 +2433,8 @@ mod tests { &GuardianAssessmentEvent { id: "review-2".to_string(), target_item_id: Some("item-2".to_string()), + plugin_id: None, + script_path: None, turn_id: "turn-from-assessment".to_string(), started_at_ms: 1_000, completed_at_ms: Some(1_042), @@ -2471,6 +2489,8 @@ mod tests { &GuardianAssessmentEvent { id: "review-3".to_string(), target_item_id: None, + plugin_id: None, + script_path: None, turn_id: "turn-from-assessment".to_string(), started_at_ms: 1_000, completed_at_ms: Some(1_042), @@ -2522,6 +2542,8 @@ mod tests { &conversation_id, "turn-1".to_string(), "cmd-1".to_string(), + completion_item.plugin_id.clone(), + completion_item.script_path.clone(), completion_item.command.clone(), completion_item.cwd.clone(), completion_item.command_actions.clone(), @@ -2541,8 +2563,8 @@ mod tests { payload.item, ThreadItem::CommandExecution { id: "cmd-1".to_string(), - plugin_id: None, - script_path: None, + plugin_id: completion_item.plugin_id.clone(), + script_path: completion_item.script_path.clone(), command: completion_item.command.clone(), cwd: completion_item.cwd.clone(), process_id: None, @@ -2562,6 +2584,8 @@ mod tests { &conversation_id, "turn-1".to_string(), "cmd-1".to_string(), + completion_item.plugin_id.clone(), + completion_item.script_path.clone(), completion_item.command.clone(), completion_item.cwd.clone(), completion_item.command_actions.clone(), @@ -2596,6 +2620,8 @@ mod tests { &conversation_id, "turn-1".to_string(), "cmd-1".to_string(), + completion_item.plugin_id.clone(), + completion_item.script_path.clone(), completion_item.command.clone(), completion_item.cwd.clone(), completion_item.command_actions.clone(), @@ -2610,11 +2636,9 @@ mod tests { &conversation_id, "turn-1".to_string(), "cmd-1".to_string(), - completion_item.command.clone(), - completion_item.cwd.clone(), + completion_item, /*process_id*/ None, CommandExecutionSource::Agent, - completion_item.command_actions.clone(), CommandExecutionStatus::Declined, &outgoing, &thread_state, @@ -2624,10 +2648,19 @@ mod tests { let completed = recv_broadcast_notification(&mut rx).await?; match completed { ServerNotification::ItemCompleted(payload) => { - let ThreadItem::CommandExecution { id, status, .. } = payload.item else { + let ThreadItem::CommandExecution { + id, + plugin_id, + script_path, + status, + .. + } = payload.item + else { bail!("expected command execution completion"); }; assert_eq!(id, "cmd-1"); + assert_eq!(plugin_id.as_deref(), Some("sample@openai-curated")); + assert_eq!(script_path.as_deref(), Some("scripts/run.py")); assert_eq!(status, CommandExecutionStatus::Declined); } other => bail!("unexpected message: {other:?}"), @@ -2637,11 +2670,9 @@ mod tests { &conversation_id, "turn-1".to_string(), "cmd-1".to_string(), - completion_item.command, - completion_item.cwd, + command_execution_completion_item("printf hi"), /*process_id*/ None, CommandExecutionSource::Agent, - completion_item.command_actions, CommandExecutionStatus::Declined, &outgoing, &thread_state, @@ -2705,10 +2736,19 @@ mod tests { match first { ServerNotification::ItemStarted(payload) => { assert_eq!(payload.turn_id, "turn-guardian-approved"); - let ThreadItem::CommandExecution { id, status, .. } = payload.item else { + let ThreadItem::CommandExecution { + id, + plugin_id, + script_path, + status, + .. + } = payload.item + else { bail!("expected command execution item"); }; assert_eq!(id, "cmd-guardian-approved"); + assert_eq!(plugin_id.as_deref(), Some("sample@openai-curated")); + assert_eq!(script_path.as_deref(), Some("scripts/run.py")); assert_eq!(status, CommandExecutionStatus::InProgress); } other => bail!("unexpected message: {other:?}"), @@ -2768,10 +2808,19 @@ mod tests { match fourth { ServerNotification::ItemStarted(payload) => { assert_eq!(payload.turn_id, "turn-guardian-denied"); - let ThreadItem::CommandExecution { id, status, .. } = payload.item else { + let ThreadItem::CommandExecution { + id, + plugin_id, + script_path, + status, + .. + } = payload.item + else { bail!("expected command execution item"); }; assert_eq!(id, "cmd-guardian-denied"); + assert_eq!(plugin_id.as_deref(), Some("sample@openai-curated")); + assert_eq!(script_path.as_deref(), Some("scripts/run.py")); assert_eq!(status, CommandExecutionStatus::InProgress); } other => bail!("unexpected message: {other:?}"), @@ -2815,10 +2864,19 @@ mod tests { let seventh = recv_broadcast_notification(&mut rx).await?; match seventh { ServerNotification::ItemCompleted(payload) => { - let ThreadItem::CommandExecution { id, status, .. } = payload.item else { + let ThreadItem::CommandExecution { + id, + plugin_id, + script_path, + status, + .. + } = payload.item + else { bail!("expected command execution completion"); }; assert_eq!(id, "cmd-guardian-denied"); + assert_eq!(plugin_id.as_deref(), Some("sample@openai-curated")); + assert_eq!(script_path.as_deref(), Some("scripts/run.py")); assert_eq!(status, CommandExecutionStatus::Declined); } other => bail!("unexpected message: {other:?}"), diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 3b92db9213..3d0be58c54 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -4137,6 +4137,7 @@ async fn command_execution_notifications_include_trusted_plugin_id() -> Result<( ]; let server = create_mock_responses_server_sequence(responses).await; MockResponsesConfig::new(&server.uri()) + .with_approval_policy("untrusted") .with_sandbox_mode("danger-full-access") .enable_feature(Feature::Plugins) .disable_feature(Feature::RemotePlugin) @@ -4169,13 +4170,13 @@ async fn command_execution_notifications_include_trusted_plugin_id() -> Result<( .await?; for method in ["item/started", "item/completed"] { - timeout(DEFAULT_READ_TIMEOUT, async { + let status = timeout(DEFAULT_READ_TIMEOUT, async { loop { let notification = mcp.read_stream_until_notification_message(method).await?; let params = notification.params.expect("item notification params"); let item_json = params.get("item").expect("item notification item").clone(); let item = serde_json::from_value::(item_json.clone())?; - if matches!(item, ThreadItem::CommandExecution { .. }) { + if let ThreadItem::CommandExecution { status, .. } = item { let emitted_script_path = item_json .get("scriptPath") .and_then(serde_json::Value::as_str) @@ -4192,11 +4193,32 @@ async fn command_execution_notifications_include_trusted_plugin_id() -> Result<( !emitted_script_path.contains("plugins/cache"), "scriptPath must not serialize a plugin cache path" ); - return Ok::<(), anyhow::Error>(()); + return Ok::(status); } } }) .await??; + if method == "item/started" { + let server_req = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_request_message(), + ) + .await??; + let ServerRequest::CommandExecutionRequestApproval { request_id, params } = server_req + else { + panic!("expected CommandExecutionRequestApproval request"); + }; + assert_eq!(params.item_id, "plugin-command"); + mcp.send_response( + request_id, + serde_json::to_value(CommandExecutionRequestApprovalResponse { + decision: CommandExecutionApprovalDecision::Decline, + })?, + ) + .await?; + } else { + assert_eq!(status, CommandExecutionStatus::Declined); + } } timeout( diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index 5f463b7d08..2c57444dc5 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -5,7 +5,10 @@ use async_channel::Receiver; use async_channel::Sender; use codex_analytics::GuardianApprovalRequestSource; use codex_async_utils::OrCancelExt; +use codex_core_plugins::PluginCommandAttribution; use codex_extension_api::LoadedUserInstructions; +use codex_plugin::PluginId; +use codex_protocol::items::is_safe_plugin_relative_path; use codex_protocol::protocol::ApplyPatchApprovalRequestEvent; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; @@ -34,6 +37,7 @@ use tokio_util::sync::CancellationToken; use crate::config::Config; use crate::guardian::GuardianApprovalRequest; +use crate::guardian::GuardianReviewOptions; use crate::guardian::new_guardian_review_id; use crate::guardian::routes_approval_to_guardian; use crate::guardian::routes_approval_to_guardian_with_reviewer; @@ -492,6 +496,8 @@ async fn handle_exec_approval( let approval_id_for_op = event.effective_approval_id(); let ExecApprovalRequestEvent { call_id, + plugin_id, + script_path, approval_id, environment_id, command, @@ -503,6 +509,15 @@ async fn handle_exec_approval( available_decisions, .. } = event; + let plugin_attribution = plugin_id + .zip(script_path) + .and_then(|(plugin_id, script_path)| { + let plugin_id = PluginId::parse(&plugin_id).ok()?; + is_safe_plugin_relative_path(&script_path).then_some(PluginCommandAttribution { + plugin_id, + normalized_relative_path: script_path, + }) + }); let decision = if routes_approval_to_guardian(parent_ctx) { let review_cancel = cancel_token.child_token(); let review_rx = spawn_approval_request_review( @@ -522,8 +537,11 @@ async fn handle_exec_approval( justification: None, }, reason, - GuardianApprovalRequestSource::DelegatedSubagent, - review_cancel.clone(), + GuardianReviewOptions { + plugin_attribution_override: plugin_attribution.clone(), + approval_request_source: GuardianApprovalRequestSource::DelegatedSubagent, + external_cancel: Some(review_cancel.clone()), + }, ); await_approval_with_cancel( receive_approval_review(review_rx), @@ -547,6 +565,7 @@ async fn handle_exec_approval( proposed_execpolicy_amendment, additional_permissions, available_decisions, + plugin_attribution, ), parent_session, &approval_id_for_op, @@ -630,8 +649,11 @@ async fn handle_patch_approval( patch, }, reason.clone(), - GuardianApprovalRequestSource::DelegatedSubagent, - review_cancel.clone(), + GuardianReviewOptions { + plugin_attribution_override: None, + approval_request_source: GuardianApprovalRequestSource::DelegatedSubagent, + external_cancel: Some(review_cancel.clone()), + }, ); Some( await_approval_with_cancel( @@ -746,8 +768,11 @@ async fn maybe_auto_review_mcp_request_user_input( new_guardian_review_id(), build_guardian_mcp_tool_review_request(&event.call_id, &invocation, metadata.as_ref()), /*retry_reason*/ None, - GuardianApprovalRequestSource::DelegatedSubagent, - review_cancel.clone(), + GuardianReviewOptions { + plugin_attribution_override: None, + approval_request_source: GuardianApprovalRequestSource::DelegatedSubagent, + external_cancel: Some(review_cancel.clone()), + }, ); let decision = await_approval_with_cancel( receive_approval_review(review_rx), diff --git a/codex-rs/core/src/codex_delegate_tests.rs b/codex-rs/core/src/codex_delegate_tests.rs index 3a8f1e4589..4a4d061f46 100644 --- a/codex-rs/core/src/codex_delegate_tests.rs +++ b/codex-rs/core/src/codex_delegate_tests.rs @@ -363,6 +363,8 @@ async fn handle_exec_approval_uses_call_id_for_guardian_review_and_approval_id_f &parent_ctx, ExecApprovalRequestEvent { call_id: "command-item-1".to_string(), + plugin_id: Some("sample@openai-curated".to_string()), + script_path: Some("scripts/run.py".to_string()), approval_id: Some("callback-approval-1".to_string()), turn_id: "child-turn-1".to_string(), environment_id: Some("remote".to_string()), @@ -406,6 +408,14 @@ async fn handle_exec_approval_uses_call_id_for_guardian_review_and_approval_id_f assessment_event.target_item_id.as_deref(), Some("command-item-1") ); + assert_eq!( + assessment_event.plugin_id.as_deref(), + Some("sample@openai-curated") + ); + assert_eq!( + assessment_event.script_path.as_deref(), + Some("scripts/run.py") + ); assert_eq!(assessment_event.turn_id, parent_ctx.sub_id); assert_eq!( assessment_event.status, diff --git a/codex-rs/core/src/guardian/mod.rs b/codex-rs/core/src/guardian/mod.rs index 65faad5a60..35035f96ed 100644 --- a/codex-rs/core/src/guardian/mod.rs +++ b/codex-rs/core/src/guardian/mod.rs @@ -28,6 +28,7 @@ pub(crate) use approval_request::GuardianMcpAnnotations; pub(crate) use approval_request::GuardianNetworkAccessTrigger; #[cfg(test)] pub(crate) use approval_request::guardian_approval_request_to_json; +pub(crate) use review::GuardianReviewOptions; pub(crate) use review::guardian_timeout_message; pub(crate) use review::is_guardian_reviewer_source; pub(crate) use review::new_guardian_review_id; diff --git a/codex-rs/core/src/guardian/review.rs b/codex-rs/core/src/guardian/review.rs index c4912ebc91..d0a7c05541 100644 --- a/codex-rs/core/src/guardian/review.rs +++ b/codex-rs/core/src/guardian/review.rs @@ -5,6 +5,7 @@ use codex_analytics::GuardianReviewFailureReason; use codex_analytics::GuardianReviewTerminalStatus; use codex_analytics::GuardianReviewTrackContext; use codex_analytics::GuardianReviewedAction; +use codex_core_plugins::PluginCommandAttribution; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::CodexErrorInfo; @@ -63,6 +64,32 @@ const GUARDIAN_TIMEOUT_INSTRUCTIONS: &str = concat!( const GUARDIAN_REVIEW_MAX_ATTEMPTS: i64 = 3; +fn plugin_attribution_for_guardian_request( + turn: &TurnContext, + request: &GuardianApprovalRequest, +) -> Option { + match request { + GuardianApprovalRequest::Shell { command, cwd, .. } + | GuardianApprovalRequest::ExecCommand { command, cwd, .. } => { + turn.plugin_attribution_for_command(command, cwd) + } + #[cfg(unix)] + GuardianApprovalRequest::Execve { + program, argv, cwd, .. + } => { + let command = if argv.is_empty() { + vec![program.clone()] + } else { + std::iter::once(program.clone()) + .chain(argv.iter().skip(1).cloned()) + .collect() + }; + turn.plugin_attribution_for_command(&command, cwd) + } + _ => None, + } +} + pub(crate) fn new_guardian_review_id() -> String { uuid::Uuid::new_v4().to_string() } @@ -261,11 +288,21 @@ async fn run_guardian_review( review_id: String, request: GuardianApprovalRequest, retry_reason: Option, - approval_request_source: GuardianApprovalRequestSource, - external_cancel: Option, + options: GuardianReviewOptions, ) -> ReviewDecision { + let GuardianReviewOptions { + plugin_attribution_override, + approval_request_source, + external_cancel, + } = options; let target_item_id = guardian_request_target_item_id(&request).map(str::to_string); let assessment_turn_id = guardian_request_turn_id(&request, &turn.sub_id).to_string(); + let plugin_attribution = plugin_attribution_override + .or_else(|| plugin_attribution_for_guardian_request(turn.as_ref(), &request)); + let (plugin_id, script_path) = plugin_attribution + .as_ref() + .map(PluginCommandAttribution::serialized_fields) + .unzip(); let action_summary = guardian_assessment_action(&request); let reviewed_action = guardian_reviewed_action(&request); let review_tracking = GuardianReviewTrackContext::new( @@ -284,6 +321,8 @@ async fn run_guardian_review( EventMsg::GuardianAssessment(GuardianAssessmentEvent { id: review_id.clone(), target_item_id: target_item_id.clone(), + plugin_id: plugin_id.clone(), + script_path: script_path.clone(), turn_id: assessment_turn_id.clone(), started_at_ms, completed_at_ms: None, @@ -321,6 +360,8 @@ async fn run_guardian_review( EventMsg::GuardianAssessment(GuardianAssessmentEvent { id: review_id, target_item_id, + plugin_id: plugin_id.clone(), + script_path: script_path.clone(), turn_id: assessment_turn_id.clone(), started_at_ms, completed_at_ms: Some(completed_at_ms), @@ -414,6 +455,8 @@ async fn run_guardian_review( EventMsg::GuardianAssessment(GuardianAssessmentEvent { id: review_id, target_item_id, + plugin_id: plugin_id.clone(), + script_path: script_path.clone(), turn_id: assessment_turn_id.clone(), started_at_ms, completed_at_ms: Some(completed_at_ms), @@ -449,6 +492,8 @@ async fn run_guardian_review( EventMsg::GuardianAssessment(GuardianAssessmentEvent { id: review_id, target_item_id, + plugin_id: plugin_id.clone(), + script_path: script_path.clone(), turn_id: assessment_turn_id.clone(), started_at_ms, completed_at_ms: Some(completed_at_ms), @@ -535,6 +580,8 @@ async fn run_guardian_review( EventMsg::GuardianAssessment(GuardianAssessmentEvent { id: review_id, target_item_id, + plugin_id: plugin_id.clone(), + script_path: script_path.clone(), turn_id: assessment_turn_id.clone(), started_at_ms, completed_at_ms: Some(completed_at_ms), @@ -568,6 +615,12 @@ async fn run_guardian_review( } } +pub(crate) struct GuardianReviewOptions { + pub(crate) plugin_attribution_override: Option, + pub(crate) approval_request_source: GuardianApprovalRequestSource, + pub(crate) external_cancel: Option, +} + /// Public entrypoint for approval requests that should be reviewed by guardian. pub(crate) async fn review_approval_request( session: &Arc, @@ -584,8 +637,11 @@ pub(crate) async fn review_approval_request( review_id, request, retry_reason, - GuardianApprovalRequestSource::MainTurn, - /*external_cancel*/ None, + GuardianReviewOptions { + plugin_attribution_override: None, + approval_request_source: GuardianApprovalRequestSource::MainTurn, + external_cancel: None, + }, )) .await } @@ -596,8 +652,7 @@ pub(crate) async fn review_approval_request_with_cancel( review_id: String, request: GuardianApprovalRequest, retry_reason: Option, - approval_request_source: GuardianApprovalRequestSource, - cancel_token: CancellationToken, + options: GuardianReviewOptions, ) -> ReviewDecision { run_guardian_review( Arc::clone(session), @@ -605,8 +660,7 @@ pub(crate) async fn review_approval_request_with_cancel( review_id, request, retry_reason, - approval_request_source, - Some(cancel_token), + options, ) .await } @@ -617,8 +671,7 @@ pub(crate) fn spawn_approval_request_review( review_id: String, request: GuardianApprovalRequest, retry_reason: Option, - approval_request_source: GuardianApprovalRequestSource, - cancel_token: CancellationToken, + options: GuardianReviewOptions, ) -> oneshot::Receiver { let (tx, rx) = oneshot::channel(); let spawn_result = std::thread::Builder::new() @@ -639,8 +692,7 @@ pub(crate) fn spawn_approval_request_review( review_id, request, retry_reason, - approval_request_source, - cancel_token, + options, )); let _ = tx.send(decision); }); diff --git a/codex-rs/core/src/guardian/tests.rs b/codex-rs/core/src/guardian/tests.rs index 3dfa56a4e5..acd0fe9c86 100644 --- a/codex-rs/core/src/guardian/tests.rs +++ b/codex-rs/core/src/guardian/tests.rs @@ -1167,8 +1167,11 @@ async fn cancelled_guardian_review_emits_terminal_abort_without_warning() { .to_string(), }, /*retry_reason*/ None, - GuardianApprovalRequestSource::MainTurn, - cancel_token, + GuardianReviewOptions { + plugin_attribution_override: None, + approval_request_source: GuardianApprovalRequestSource::MainTurn, + external_cancel: Some(cancel_token), + }, ) .await; diff --git a/codex-rs/core/src/session/elicitation_holders_tests.rs b/codex-rs/core/src/session/elicitation_holders_tests.rs index df03aeb291..83158e96b5 100644 --- a/codex-rs/core/src/session/elicitation_holders_tests.rs +++ b/codex-rs/core/src/session/elicitation_holders_tests.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; +use codex_core_plugins::PluginCommandAttribution; +use codex_plugin::PluginId; use codex_protocol::protocol::ReviewDecision; use codex_protocol::request_permissions::PermissionGrantScope; use codex_protocol::request_permissions::RequestPermissionProfile; @@ -34,6 +36,10 @@ async fn command_approval_holds_an_elicitation_until_response() { let mut pause_state = session.subscribe_elicitation_pause_state(); #[allow(deprecated)] let cwd = turn_context.cwd.clone(); + let plugin_attribution = PluginCommandAttribution { + plugin_id: PluginId::parse("sample@openai-curated").expect("valid plugin id"), + normalized_relative_path: "scripts/run.py".to_string(), + }; let request = tokio::spawn({ let session = session.clone(); @@ -52,12 +58,18 @@ async fn command_approval_holds_an_elicitation_until_response() { /*proposed_execpolicy_amendment*/ None, /*additional_permissions*/ None, /*available_decisions*/ None, + /*plugin_attribution_override*/ Some(plugin_attribution), ) .await } }); - events.recv().await.expect("approval event"); + let event = events.recv().await.expect("approval event"); + let codex_protocol::protocol::EventMsg::ExecApprovalRequest(event) = event.msg else { + panic!("expected command approval event"); + }; + assert_eq!(event.plugin_id.as_deref(), Some("sample@openai-curated")); + assert_eq!(event.script_path.as_deref(), Some("scripts/run.py")); wait_until_held(&mut pause_state).await; session .notify_approval("call-1", ReviewDecision::Approved) diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 54ed257889..7f8a6ca546 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -306,6 +306,7 @@ pub(crate) struct PreviousTurnSettings { use crate::SkillMetadata; use crate::SkillsService; use crate::exec_policy::ExecPolicyUpdateError; +use crate::guardian::GuardianReviewOptions; use crate::guardian::GuardianReviewSessionManager; use crate::mcp::McpManager; use crate::network_policy_decision::execpolicy_network_rule_amendment; @@ -334,6 +335,7 @@ use crate::turn_timing::TurnTimingState; use crate::turn_timing::record_turn_ttfm_metric; use crate::unified_exec::UnifiedExecProcessManager; use crate::windows_sandbox::WindowsSandboxLevelExt; +use codex_core_plugins::PluginCommandAttribution; use codex_core_plugins::PluginsManager; use codex_core_plugins::RecommendedPluginCandidatesInput; use codex_git_utils::get_git_repo_root; @@ -2276,6 +2278,7 @@ impl Session { proposed_execpolicy_amendment: Option, additional_permissions: Option, available_decisions: Option>, + plugin_attribution_override: Option, ) -> ReviewDecision { let _elicitation = self.services.elicitations.register(); // command-level approvals use `call_id`. @@ -2318,8 +2321,16 @@ impl Session { additional_permissions.as_ref(), ) }); + let plugin_attribution = plugin_attribution_override + .or_else(|| turn_context.plugin_attribution_for_command(&command, &cwd)); + let (plugin_id, script_path) = plugin_attribution + .as_ref() + .map(PluginCommandAttribution::serialized_fields) + .unzip(); let event = EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent { call_id, + plugin_id, + script_path, approval_id, turn_id: turn_context.sub_id.clone(), environment_id, @@ -2448,8 +2459,12 @@ impl Session { review_id, request, /*retry_reason*/ None, - codex_analytics::GuardianApprovalRequestSource::MainTurn, - cancellation_token.clone(), + GuardianReviewOptions { + plugin_attribution_override: None, + approval_request_source: + codex_analytics::GuardianApprovalRequestSource::MainTurn, + external_cancel: Some(cancellation_token.clone()), + }, ); let decision = tokio::select! { biased; diff --git a/codex-rs/core/src/tools/network_approval.rs b/codex-rs/core/src/tools/network_approval.rs index 05ad9f9675..4e723ef2d6 100644 --- a/codex-rs/core/src/tools/network_approval.rs +++ b/codex-rs/core/src/tools/network_approval.rs @@ -687,6 +687,7 @@ impl NetworkApprovalService { /*proposed_execpolicy_amendment*/ None, /*additional_permissions*/ None, available_decisions, + /*plugin_attribution_override*/ None, ) .await }; diff --git a/codex-rs/core/src/tools/runtimes/shell.rs b/codex-rs/core/src/tools/runtimes/shell.rs index ac8e020599..2abbdea0aa 100644 --- a/codex-rs/core/src/tools/runtimes/shell.rs +++ b/codex-rs/core/src/tools/runtimes/shell.rs @@ -170,6 +170,7 @@ impl Approvable for ShellRuntime { .cloned(), req.additional_permissions.clone(), available_decisions, + /*plugin_attribution_override*/ None, ) .await }) diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index 5a8e0d3c8c..14a68c9df4 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -507,6 +507,7 @@ impl CoreShellActionProvider { /*proposed_execpolicy_amendment*/ None, additional_permissions, Some(vec![ReviewDecision::Approved, ReviewDecision::Abort]), + /*plugin_attribution_override*/ None, ) .await }) diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index 5dcb394956..9b0aeed52c 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -209,6 +209,7 @@ impl Approvable for UnifiedExecRuntime<'_> { .cloned(), req.additional_permissions.clone(), available_decisions, + /*plugin_attribution_override*/ None, ) .await }) diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index 4a166226c9..0e9131b234 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -229,6 +229,8 @@ async fn run_codex_tool_session_inner( command, cwd, call_id, + plugin_id: _, + script_path: _, approval_id: _, reason: _, proposed_execpolicy_amendment: _, diff --git a/codex-rs/protocol/src/approvals.rs b/codex-rs/protocol/src/approvals.rs index e7f1bc899a..44dc8d7d7c 100644 --- a/codex-rs/protocol/src/approvals.rs +++ b/codex-rs/protocol/src/approvals.rs @@ -183,6 +183,14 @@ pub struct GuardianAssessmentEvent { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub target_item_id: Option, + /// Trusted plugin attribution for command items synthesized from this review. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub plugin_id: Option, + /// Safe plugin-relative path for command items synthesized from this review. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub script_path: Option, /// Turn ID that this assessment belongs to. /// Uses `#[serde(default)]` for backwards compatibility. #[serde(default)] @@ -218,6 +226,14 @@ pub struct GuardianAssessmentEvent { pub struct ExecApprovalRequestEvent { /// Identifier for the associated command execution item. pub call_id: String, + /// Trusted plugin attribution for the command item, when available. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub plugin_id: Option, + /// Safe plugin-relative path for the command item, when available. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub script_path: Option, /// Identifier for this specific approval callback. /// /// When absent, the approval is for the command item itself (`call_id`). diff --git a/codex-rs/rollout-trace/src/protocol_event.rs b/codex-rs/rollout-trace/src/protocol_event.rs index 8f7f7ae73a..22d2974514 100644 --- a/codex-rs/rollout-trace/src/protocol_event.rs +++ b/codex-rs/rollout-trace/src/protocol_event.rs @@ -152,6 +152,10 @@ impl Serialize for ToolRuntimePayload<'_> { struct ExecCommandBeginTracePayload<'a> { call_id: &'a str, #[serde(skip_serializing_if = "Option::is_none")] + plugin_id: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + script_path: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] process_id: Option<&'a str>, turn_id: &'a str, started_at_ms: i64, @@ -167,8 +171,8 @@ impl<'a> From<&'a ExecCommandBeginEvent> for ExecCommandBeginTracePayload<'a> { fn from(event: &'a ExecCommandBeginEvent) -> Self { let ExecCommandBeginEvent { call_id, - plugin_id: _, - script_path: _, + plugin_id, + script_path, process_id, turn_id, started_at_ms, @@ -180,6 +184,8 @@ impl<'a> From<&'a ExecCommandBeginEvent> for ExecCommandBeginTracePayload<'a> { } = event; Self { call_id, + plugin_id: plugin_id.as_deref(), + script_path: script_path.as_deref(), process_id: process_id.as_deref(), turn_id, started_at_ms: *started_at_ms, @@ -200,6 +206,10 @@ impl<'a> From<&'a ExecCommandBeginEvent> for ExecCommandBeginTracePayload<'a> { struct ExecCommandEndTracePayload<'a> { call_id: &'a str, #[serde(skip_serializing_if = "Option::is_none")] + plugin_id: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + script_path: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] process_id: Option<&'a str>, turn_id: &'a str, completed_at_ms: i64, @@ -222,8 +232,8 @@ impl<'a> From<&'a ExecCommandEndEvent> for ExecCommandEndTracePayload<'a> { fn from(event: &'a ExecCommandEndEvent) -> Self { let ExecCommandEndEvent { call_id, - plugin_id: _, - script_path: _, + plugin_id, + script_path, process_id, turn_id, completed_at_ms, @@ -242,6 +252,8 @@ impl<'a> From<&'a ExecCommandEndEvent> for ExecCommandEndTracePayload<'a> { } = event; Self { call_id, + plugin_id: plugin_id.as_deref(), + script_path: script_path.as_deref(), process_id: process_id.as_deref(), turn_id, completed_at_ms: *completed_at_ms, diff --git a/codex-rs/rollout-trace/src/protocol_event_tests.rs b/codex-rs/rollout-trace/src/protocol_event_tests.rs index 6fb2a189ea..1f75c61748 100644 --- a/codex-rs/rollout-trace/src/protocol_event_tests.rs +++ b/codex-rs/rollout-trace/src/protocol_event_tests.rs @@ -56,8 +56,8 @@ fn exec_command_trace_payloads_use_inferred_native_cwd() -> anyhow::Result<()> { // Windows and POSIX paths on every platform. let begin = EventMsg::ExecCommandBegin(ExecCommandBeginEvent { call_id: "call-begin".to_string(), - plugin_id: None, - script_path: None, + plugin_id: Some("sample@openai-curated".to_string()), + script_path: Some("scripts/run.py".to_string()), process_id: Some("process-1".to_string()), turn_id: "turn-1".to_string(), started_at_ms: 1234, @@ -69,8 +69,8 @@ fn exec_command_trace_payloads_use_inferred_native_cwd() -> anyhow::Result<()> { }); let end = EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id: "call-end".to_string(), - plugin_id: None, - script_path: None, + plugin_id: Some("sample@openai-curated".to_string()), + script_path: Some("scripts/run.py".to_string()), process_id: None, turn_id: "turn-1".to_string(), completed_at_ms: 2345, @@ -96,6 +96,8 @@ fn exec_command_trace_payloads_use_inferred_native_cwd() -> anyhow::Result<()> { serde_json::to_value(payload)?, json!({ "call_id": "call-begin", + "plugin_id": "sample@openai-curated", + "script_path": "scripts/run.py", "process_id": "process-1", "turn_id": "turn-1", "started_at_ms": 1234, @@ -113,6 +115,8 @@ fn exec_command_trace_payloads_use_inferred_native_cwd() -> anyhow::Result<()> { serde_json::to_value(payload)?, json!({ "call_id": "call-end", + "plugin_id": "sample@openai-curated", + "script_path": "scripts/run.py", "turn_id": "turn-1", "completed_at_ms": 2345, "command": ["pwd"], diff --git a/codex-rs/tui/src/auto_review_denials.rs b/codex-rs/tui/src/auto_review_denials.rs index 149a60f049..36031c6d6b 100644 --- a/codex-rs/tui/src/auto_review_denials.rs +++ b/codex-rs/tui/src/auto_review_denials.rs @@ -87,6 +87,8 @@ mod tests { GuardianAssessmentEvent { id: format!("review-{id}"), target_item_id: None, + plugin_id: None, + script_path: None, turn_id: "turn-1".to_string(), started_at_ms: 0, completed_at_ms: Some(1), diff --git a/codex-rs/tui/src/chatwidget/protocol_requests.rs b/codex-rs/tui/src/chatwidget/protocol_requests.rs index 5729c073fd..c60c368fdb 100644 --- a/codex-rs/tui/src/chatwidget/protocol_requests.rs +++ b/codex-rs/tui/src/chatwidget/protocol_requests.rs @@ -93,6 +93,8 @@ impl ChatWidget { self.on_guardian_assessment(GuardianAssessmentEvent { id, target_item_id: None, + plugin_id: None, + script_path: None, turn_id, started_at_ms, completed_at_ms, diff --git a/codex-rs/tui/src/chatwidget/tests/guardian.rs b/codex-rs/tui/src/chatwidget/tests/guardian.rs index 7a5fdc288d..bedba04b90 100644 --- a/codex-rs/tui/src/chatwidget/tests/guardian.rs +++ b/codex-rs/tui/src/chatwidget/tests/guardian.rs @@ -5,6 +5,8 @@ fn auto_review_denial_event() -> GuardianAssessmentEvent { GuardianAssessmentEvent { id: "auto-review-recent-1".into(), target_item_id: Some("target-auto-review-recent-1".into()), + plugin_id: None, + script_path: None, turn_id: "turn-recent-1".into(), started_at_ms: 0, completed_at_ms: Some(1), @@ -31,6 +33,8 @@ fn guardian_command_event( GuardianAssessmentEvent { id: id.to_string(), target_item_id: Some(format!("{id}-target")), + plugin_id: None, + script_path: None, turn_id: turn_id.to_string(), started_at_ms: 0, completed_at_ms: terminal.then_some(1), @@ -100,6 +104,8 @@ async fn guardian_denied_exec_renders_warning_and_denied_request() { chat.on_guardian_assessment(GuardianAssessmentEvent { id: "guardian-1".into(), target_item_id: Some("guardian-target-1".into()), + plugin_id: None, + script_path: None, turn_id: "turn-1".into(), started_at_ms: 0, completed_at_ms: None, @@ -114,6 +120,8 @@ async fn guardian_denied_exec_renders_warning_and_denied_request() { chat.on_guardian_assessment(GuardianAssessmentEvent { id: "guardian-1".into(), target_item_id: Some("guardian-target-1".into()), + plugin_id: None, + script_path: None, turn_id: "turn-1".into(), started_at_ms: 0, completed_at_ms: Some(1), @@ -158,6 +166,8 @@ async fn guardian_approved_exec_renders_approved_request() { chat.on_guardian_assessment(GuardianAssessmentEvent { id: "thread:child-thread:guardian-1".into(), target_item_id: Some("guardian-approved-target".into()), + plugin_id: None, + script_path: None, turn_id: "turn-1".into(), started_at_ms: 0, completed_at_ms: Some(1), @@ -216,6 +226,8 @@ async fn guardian_approved_request_permissions_renders_request_summary() { chat.on_guardian_assessment(GuardianAssessmentEvent { id: "guardian-request-permissions".into(), target_item_id: None, + plugin_id: None, + script_path: None, turn_id: "turn-1".into(), started_at_ms: 0, completed_at_ms: None, @@ -240,6 +252,8 @@ async fn guardian_approved_request_permissions_renders_request_summary() { chat.on_guardian_assessment(GuardianAssessmentEvent { id: "guardian-request-permissions".into(), target_item_id: None, + plugin_id: None, + script_path: None, turn_id: "turn-1".into(), started_at_ms: 0, completed_at_ms: Some(1), @@ -290,6 +304,8 @@ async fn guardian_timed_out_exec_renders_warning_and_timed_out_request() { chat.on_guardian_assessment(GuardianAssessmentEvent { id: "guardian-1".into(), target_item_id: Some("guardian-target-1".into()), + plugin_id: None, + script_path: None, turn_id: "turn-1".into(), started_at_ms: 0, completed_at_ms: None, @@ -304,6 +320,8 @@ async fn guardian_timed_out_exec_renders_warning_and_timed_out_request() { chat.on_guardian_assessment(GuardianAssessmentEvent { id: "guardian-1".into(), target_item_id: Some("guardian-target-1".into()), + plugin_id: None, + script_path: None, turn_id: "turn-1".into(), started_at_ms: 0, completed_at_ms: Some(1), diff --git a/codex-rs/tui/src/chatwidget/tests/mcp_startup.rs b/codex-rs/tui/src/chatwidget/tests/mcp_startup.rs index e2992585a4..97bfaae227 100644 --- a/codex-rs/tui/src/chatwidget/tests/mcp_startup.rs +++ b/codex-rs/tui/src/chatwidget/tests/mcp_startup.rs @@ -299,6 +299,8 @@ async fn mcp_startup_complete_preserves_review_status() { chat.on_guardian_assessment(GuardianAssessmentEvent { id: "guardian-1".to_string(), target_item_id: Some("guardian-target-1".to_string()), + plugin_id: None, + script_path: None, turn_id: "turn-1".to_string(), started_at_ms: 0, completed_at_ms: None,