mirror of
https://github.com/openai/codex.git
synced 2026-11-04 00:24:43 +00:00
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
This commit is contained in:
1 parent
091e4a5d7c
commit
9fc4e5a7aa
23 files changed
+363
-75
No files matched your search
@@ -179,8 +179,8 @@ pub fn build_item_from_guardian_event(
|
|||||||
}];
|
}];
|
||||||
Some(ThreadItem::CommandExecution {
|
Some(ThreadItem::CommandExecution {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
plugin_id: None,
|
plugin_id: assessment.plugin_id.clone(),
|
||||||
script_path: None,
|
script_path: assessment.script_path.clone(),
|
||||||
command,
|
command,
|
||||||
cwd: cwd.clone().into(),
|
cwd: cwd.clone().into(),
|
||||||
process_id: None,
|
process_id: None,
|
||||||
@@ -217,8 +217,8 @@ pub fn build_item_from_guardian_event(
|
|||||||
};
|
};
|
||||||
Some(ThreadItem::CommandExecution {
|
Some(ThreadItem::CommandExecution {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
plugin_id: None,
|
plugin_id: assessment.plugin_id.clone(),
|
||||||
script_path: None,
|
script_path: assessment.script_path.clone(),
|
||||||
command,
|
command,
|
||||||
cwd: cwd.clone().into(),
|
cwd: cwd.clone().into(),
|
||||||
process_id: None,
|
process_id: None,
|
||||||
|
|||||||
@@ -2088,13 +2088,13 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rebuilds_command_execution_item_from_persisted_completion() {
|
fn preserves_command_plugin_id_across_legacy_upsert() {
|
||||||
let turn_id = "turn-1";
|
let turn_id = "turn-1";
|
||||||
let thread_id = ThreadId::new();
|
let thread_id = ThreadId::new();
|
||||||
let command_item = CoreTurnItem::CommandExecution(CoreCommandExecutionItem {
|
let command_item = CoreTurnItem::CommandExecution(CoreCommandExecutionItem {
|
||||||
id: "exec-1".to_string(),
|
id: "exec-1".to_string(),
|
||||||
plugin_id: None,
|
plugin_id: Some("sample@openai-curated".to_string()),
|
||||||
script_path: None,
|
script_path: Some("scripts/run.py".to_string()),
|
||||||
process_id: Some("pid-1".to_string()),
|
process_id: Some("pid-1".to_string()),
|
||||||
command: vec!["echo".to_string(), "hello world".to_string()],
|
command: vec!["echo".to_string(), "hello world".to_string()],
|
||||||
cwd: test_path_buf("/tmp").abs().into(),
|
cwd: test_path_buf("/tmp").abs().into(),
|
||||||
@@ -2125,6 +2125,28 @@ mod tests {
|
|||||||
item: command_item,
|
item: command_item,
|
||||||
completed_at_ms: 1_000,
|
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 {
|
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||||
turn_id: turn_id.to_string(),
|
turn_id: turn_id.to_string(),
|
||||||
started_at: None,
|
started_at: None,
|
||||||
@@ -2147,8 +2169,8 @@ mod tests {
|
|||||||
turns[0].items,
|
turns[0].items,
|
||||||
vec![ThreadItem::CommandExecution {
|
vec![ThreadItem::CommandExecution {
|
||||||
id: "exec-1".to_string(),
|
id: "exec-1".to_string(),
|
||||||
plugin_id: None,
|
plugin_id: Some("sample@openai-curated".to_string()),
|
||||||
script_path: None,
|
script_path: Some("scripts/run.py".to_string()),
|
||||||
command: "echo 'hello world'".to_string(),
|
command: "echo 'hello world'".to_string(),
|
||||||
cwd: test_path_buf("/tmp").abs().into(),
|
cwd: test_path_buf("/tmp").abs().into(),
|
||||||
process_id: Some("pid-1".to_string()),
|
process_id: Some("pid-1".to_string()),
|
||||||
@@ -3083,6 +3105,8 @@ mod tests {
|
|||||||
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||||
id: "review-guardian-exec".into(),
|
id: "review-guardian-exec".into(),
|
||||||
target_item_id: Some("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(),
|
turn_id: "turn-1".into(),
|
||||||
started_at_ms: 1_000,
|
started_at_ms: 1_000,
|
||||||
completed_at_ms: None,
|
completed_at_ms: None,
|
||||||
@@ -3102,6 +3126,8 @@ mod tests {
|
|||||||
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||||
id: "review-guardian-exec".into(),
|
id: "review-guardian-exec".into(),
|
||||||
target_item_id: Some("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(),
|
turn_id: "turn-1".into(),
|
||||||
started_at_ms: 1_000,
|
started_at_ms: 1_000,
|
||||||
completed_at_ms: Some(1_042),
|
completed_at_ms: Some(1_042),
|
||||||
@@ -3133,8 +3159,8 @@ mod tests {
|
|||||||
turns[0].items[1],
|
turns[0].items[1],
|
||||||
ThreadItem::CommandExecution {
|
ThreadItem::CommandExecution {
|
||||||
id: "guardian-exec".into(),
|
id: "guardian-exec".into(),
|
||||||
plugin_id: None,
|
plugin_id: Some("sample@openai-curated".into()),
|
||||||
script_path: None,
|
script_path: Some("scripts/run.py".into()),
|
||||||
command: "rm -rf /tmp/guardian".into(),
|
command: "rm -rf /tmp/guardian".into(),
|
||||||
cwd: test_path_buf("/tmp").abs().into(),
|
cwd: test_path_buf("/tmp").abs().into(),
|
||||||
process_id: None,
|
process_id: None,
|
||||||
@@ -3171,6 +3197,8 @@ mod tests {
|
|||||||
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||||
id: "review-guardian-execve".into(),
|
id: "review-guardian-execve".into(),
|
||||||
target_item_id: Some("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(),
|
turn_id: "turn-1".into(),
|
||||||
started_at_ms: 2_000,
|
started_at_ms: 2_000,
|
||||||
completed_at_ms: None,
|
completed_at_ms: None,
|
||||||
@@ -3201,8 +3229,8 @@ mod tests {
|
|||||||
turns[0].items[1],
|
turns[0].items[1],
|
||||||
ThreadItem::CommandExecution {
|
ThreadItem::CommandExecution {
|
||||||
id: "guardian-execve".into(),
|
id: "guardian-execve".into(),
|
||||||
plugin_id: None,
|
plugin_id: Some("sample@openai-curated".into()),
|
||||||
script_path: None,
|
script_path: Some("scripts/run.py".into()),
|
||||||
command: "/bin/rm -f /tmp/file.sqlite".into(),
|
command: "/bin/rm -f /tmp/file.sqlite".into(),
|
||||||
cwd: test_path_buf("/tmp").abs().into(),
|
cwd: test_path_buf("/tmp").abs().into(),
|
||||||
process_id: None,
|
process_id: None,
|
||||||
|
|||||||
@@ -129,6 +129,8 @@ enum CommandExecutionApprovalPresentation {
|
|||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
struct CommandExecutionCompletionItem {
|
struct CommandExecutionCompletionItem {
|
||||||
|
plugin_id: Option<String>,
|
||||||
|
script_path: Option<String>,
|
||||||
command: String,
|
command: String,
|
||||||
cwd: LegacyAppPathString,
|
cwd: LegacyAppPathString,
|
||||||
command_actions: Vec<V2ParsedCommand>,
|
command_actions: Vec<V2ParsedCommand>,
|
||||||
@@ -271,6 +273,8 @@ pub(crate) async fn apply_bespoke_event_handling(
|
|||||||
) {
|
) {
|
||||||
Some(ThreadItem::CommandExecution {
|
Some(ThreadItem::CommandExecution {
|
||||||
id,
|
id,
|
||||||
|
plugin_id,
|
||||||
|
script_path,
|
||||||
command,
|
command,
|
||||||
cwd,
|
cwd,
|
||||||
command_actions,
|
command_actions,
|
||||||
@@ -278,6 +282,8 @@ pub(crate) async fn apply_bespoke_event_handling(
|
|||||||
}) => Some((
|
}) => Some((
|
||||||
id,
|
id,
|
||||||
CommandExecutionCompletionItem {
|
CommandExecutionCompletionItem {
|
||||||
|
plugin_id,
|
||||||
|
script_path,
|
||||||
command,
|
command,
|
||||||
cwd,
|
cwd,
|
||||||
command_actions,
|
command_actions,
|
||||||
@@ -297,6 +303,8 @@ pub(crate) async fn apply_bespoke_event_handling(
|
|||||||
&conversation_id,
|
&conversation_id,
|
||||||
assessment_turn_id.clone(),
|
assessment_turn_id.clone(),
|
||||||
target_item_id.clone(),
|
target_item_id.clone(),
|
||||||
|
completion_item.plugin_id.clone(),
|
||||||
|
completion_item.script_path.clone(),
|
||||||
completion_item.command.clone(),
|
completion_item.command.clone(),
|
||||||
completion_item.cwd.clone(),
|
completion_item.cwd.clone(),
|
||||||
completion_item.command_actions.clone(),
|
completion_item.command_actions.clone(),
|
||||||
@@ -330,11 +338,9 @@ pub(crate) async fn apply_bespoke_event_handling(
|
|||||||
&conversation_id,
|
&conversation_id,
|
||||||
assessment_turn_id,
|
assessment_turn_id,
|
||||||
target_item_id,
|
target_item_id,
|
||||||
completion_item.command,
|
completion_item,
|
||||||
completion_item.cwd,
|
|
||||||
/*process_id*/ None,
|
/*process_id*/ None,
|
||||||
CommandExecutionSource::Agent,
|
CommandExecutionSource::Agent,
|
||||||
completion_item.command_actions,
|
|
||||||
completion_status,
|
completion_status,
|
||||||
&outgoing,
|
&outgoing,
|
||||||
&thread_state,
|
&thread_state,
|
||||||
@@ -588,6 +594,8 @@ pub(crate) async fn apply_bespoke_event_handling(
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let ExecApprovalRequestEvent {
|
let ExecApprovalRequestEvent {
|
||||||
call_id,
|
call_id,
|
||||||
|
plugin_id,
|
||||||
|
script_path,
|
||||||
approval_id,
|
approval_id,
|
||||||
turn_id,
|
turn_id,
|
||||||
environment_id,
|
environment_id,
|
||||||
@@ -614,6 +622,8 @@ pub(crate) async fn apply_bespoke_event_handling(
|
|||||||
} else {
|
} else {
|
||||||
let command_string = shlex_join(&command);
|
let command_string = shlex_join(&command);
|
||||||
let completion_item = CommandExecutionCompletionItem {
|
let completion_item = CommandExecutionCompletionItem {
|
||||||
|
plugin_id,
|
||||||
|
script_path,
|
||||||
command: command_string,
|
command: command_string,
|
||||||
cwd: cwd.clone().into(),
|
cwd: cwd.clone().into(),
|
||||||
command_actions: command_actions.clone(),
|
command_actions: command_actions.clone(),
|
||||||
@@ -640,6 +650,8 @@ pub(crate) async fn apply_bespoke_event_handling(
|
|||||||
&conversation_id,
|
&conversation_id,
|
||||||
event_turn_id.clone(),
|
event_turn_id.clone(),
|
||||||
call_id.clone(),
|
call_id.clone(),
|
||||||
|
completion_item.plugin_id.clone(),
|
||||||
|
completion_item.script_path.clone(),
|
||||||
completion_item.command.clone(),
|
completion_item.command.clone(),
|
||||||
completion_item.cwd.clone(),
|
completion_item.cwd.clone(),
|
||||||
completion_item.command_actions.clone(),
|
completion_item.command_actions.clone(),
|
||||||
@@ -1334,6 +1346,8 @@ async fn start_command_execution_item(
|
|||||||
conversation_id: &ThreadId,
|
conversation_id: &ThreadId,
|
||||||
turn_id: String,
|
turn_id: String,
|
||||||
item_id: String,
|
item_id: String,
|
||||||
|
plugin_id: Option<String>,
|
||||||
|
script_path: Option<String>,
|
||||||
command: String,
|
command: String,
|
||||||
cwd: LegacyAppPathString,
|
cwd: LegacyAppPathString,
|
||||||
command_actions: Vec<V2ParsedCommand>,
|
command_actions: Vec<V2ParsedCommand>,
|
||||||
@@ -1355,8 +1369,8 @@ async fn start_command_execution_item(
|
|||||||
started_at_ms: now_unix_timestamp_ms(),
|
started_at_ms: now_unix_timestamp_ms(),
|
||||||
item: ThreadItem::CommandExecution {
|
item: ThreadItem::CommandExecution {
|
||||||
id: item_id,
|
id: item_id,
|
||||||
plugin_id: None,
|
plugin_id,
|
||||||
script_path: None,
|
script_path,
|
||||||
command,
|
command,
|
||||||
cwd,
|
cwd,
|
||||||
process_id: None,
|
process_id: None,
|
||||||
@@ -1380,11 +1394,9 @@ async fn complete_command_execution_item(
|
|||||||
conversation_id: &ThreadId,
|
conversation_id: &ThreadId,
|
||||||
turn_id: String,
|
turn_id: String,
|
||||||
item_id: String,
|
item_id: String,
|
||||||
command: String,
|
completion_item: CommandExecutionCompletionItem,
|
||||||
cwd: LegacyAppPathString,
|
|
||||||
process_id: Option<String>,
|
process_id: Option<String>,
|
||||||
source: CommandExecutionSource,
|
source: CommandExecutionSource,
|
||||||
command_actions: Vec<V2ParsedCommand>,
|
|
||||||
status: CommandExecutionStatus,
|
status: CommandExecutionStatus,
|
||||||
outgoing: &ThreadScopedOutgoingMessageSender,
|
outgoing: &ThreadScopedOutgoingMessageSender,
|
||||||
thread_state: &Arc<Mutex<ThreadState>>,
|
thread_state: &Arc<Mutex<ThreadState>>,
|
||||||
@@ -1401,14 +1413,14 @@ async fn complete_command_execution_item(
|
|||||||
|
|
||||||
let item = ThreadItem::CommandExecution {
|
let item = ThreadItem::CommandExecution {
|
||||||
id: item_id,
|
id: item_id,
|
||||||
plugin_id: None,
|
plugin_id: completion_item.plugin_id,
|
||||||
script_path: None,
|
script_path: completion_item.script_path,
|
||||||
command,
|
command: completion_item.command,
|
||||||
cwd,
|
cwd: completion_item.cwd,
|
||||||
process_id,
|
process_id,
|
||||||
source,
|
source,
|
||||||
status,
|
status,
|
||||||
command_actions,
|
command_actions: completion_item.command_actions,
|
||||||
aggregated_output: None,
|
aggregated_output: None,
|
||||||
exit_code: None,
|
exit_code: None,
|
||||||
duration_ms: None,
|
duration_ms: None,
|
||||||
@@ -2051,11 +2063,9 @@ async fn on_command_execution_request_approval_response(
|
|||||||
&conversation_id,
|
&conversation_id,
|
||||||
event_turn_id.clone(),
|
event_turn_id.clone(),
|
||||||
item_id.clone(),
|
item_id.clone(),
|
||||||
completion_item.command,
|
completion_item,
|
||||||
completion_item.cwd,
|
|
||||||
/*process_id*/ None,
|
/*process_id*/ None,
|
||||||
CommandExecutionSource::Agent,
|
CommandExecutionSource::Agent,
|
||||||
completion_item.command_actions,
|
|
||||||
status,
|
status,
|
||||||
&outgoing,
|
&outgoing,
|
||||||
&thread_state,
|
&thread_state,
|
||||||
@@ -2271,6 +2281,8 @@ mod tests {
|
|||||||
|
|
||||||
fn command_execution_completion_item(command: &str) -> CommandExecutionCompletionItem {
|
fn command_execution_completion_item(command: &str) -> CommandExecutionCompletionItem {
|
||||||
CommandExecutionCompletionItem {
|
CommandExecutionCompletionItem {
|
||||||
|
plugin_id: Some("sample@openai-curated".to_string()),
|
||||||
|
script_path: Some("scripts/run.py".to_string()),
|
||||||
command: command.to_string(),
|
command: command.to_string(),
|
||||||
cwd: test_path_buf("/tmp").abs().into(),
|
cwd: test_path_buf("/tmp").abs().into(),
|
||||||
command_actions: vec![V2ParsedCommand::Unknown {
|
command_actions: vec![V2ParsedCommand::Unknown {
|
||||||
@@ -2304,6 +2316,8 @@ mod tests {
|
|||||||
GuardianAssessmentEvent {
|
GuardianAssessmentEvent {
|
||||||
id: format!("review-{id}"),
|
id: format!("review-{id}"),
|
||||||
target_item_id: Some(id.to_string()),
|
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(),
|
turn_id: turn_id.to_string(),
|
||||||
started_at_ms: 1_000,
|
started_at_ms: 1_000,
|
||||||
completed_at_ms: (!matches!(status, GuardianAssessmentStatus::InProgress))
|
completed_at_ms: (!matches!(status, GuardianAssessmentStatus::InProgress))
|
||||||
@@ -2371,6 +2385,8 @@ mod tests {
|
|||||||
&GuardianAssessmentEvent {
|
&GuardianAssessmentEvent {
|
||||||
id: "review-1".to_string(),
|
id: "review-1".to_string(),
|
||||||
target_item_id: Some("item-1".to_string()),
|
target_item_id: Some("item-1".to_string()),
|
||||||
|
plugin_id: None,
|
||||||
|
script_path: None,
|
||||||
turn_id: String::new(),
|
turn_id: String::new(),
|
||||||
started_at_ms: 1_000,
|
started_at_ms: 1_000,
|
||||||
completed_at_ms: None,
|
completed_at_ms: None,
|
||||||
@@ -2417,6 +2433,8 @@ mod tests {
|
|||||||
&GuardianAssessmentEvent {
|
&GuardianAssessmentEvent {
|
||||||
id: "review-2".to_string(),
|
id: "review-2".to_string(),
|
||||||
target_item_id: Some("item-2".to_string()),
|
target_item_id: Some("item-2".to_string()),
|
||||||
|
plugin_id: None,
|
||||||
|
script_path: None,
|
||||||
turn_id: "turn-from-assessment".to_string(),
|
turn_id: "turn-from-assessment".to_string(),
|
||||||
started_at_ms: 1_000,
|
started_at_ms: 1_000,
|
||||||
completed_at_ms: Some(1_042),
|
completed_at_ms: Some(1_042),
|
||||||
@@ -2471,6 +2489,8 @@ mod tests {
|
|||||||
&GuardianAssessmentEvent {
|
&GuardianAssessmentEvent {
|
||||||
id: "review-3".to_string(),
|
id: "review-3".to_string(),
|
||||||
target_item_id: None,
|
target_item_id: None,
|
||||||
|
plugin_id: None,
|
||||||
|
script_path: None,
|
||||||
turn_id: "turn-from-assessment".to_string(),
|
turn_id: "turn-from-assessment".to_string(),
|
||||||
started_at_ms: 1_000,
|
started_at_ms: 1_000,
|
||||||
completed_at_ms: Some(1_042),
|
completed_at_ms: Some(1_042),
|
||||||
@@ -2522,6 +2542,8 @@ mod tests {
|
|||||||
&conversation_id,
|
&conversation_id,
|
||||||
"turn-1".to_string(),
|
"turn-1".to_string(),
|
||||||
"cmd-1".to_string(),
|
"cmd-1".to_string(),
|
||||||
|
completion_item.plugin_id.clone(),
|
||||||
|
completion_item.script_path.clone(),
|
||||||
completion_item.command.clone(),
|
completion_item.command.clone(),
|
||||||
completion_item.cwd.clone(),
|
completion_item.cwd.clone(),
|
||||||
completion_item.command_actions.clone(),
|
completion_item.command_actions.clone(),
|
||||||
@@ -2541,8 +2563,8 @@ mod tests {
|
|||||||
payload.item,
|
payload.item,
|
||||||
ThreadItem::CommandExecution {
|
ThreadItem::CommandExecution {
|
||||||
id: "cmd-1".to_string(),
|
id: "cmd-1".to_string(),
|
||||||
plugin_id: None,
|
plugin_id: completion_item.plugin_id.clone(),
|
||||||
script_path: None,
|
script_path: completion_item.script_path.clone(),
|
||||||
command: completion_item.command.clone(),
|
command: completion_item.command.clone(),
|
||||||
cwd: completion_item.cwd.clone(),
|
cwd: completion_item.cwd.clone(),
|
||||||
process_id: None,
|
process_id: None,
|
||||||
@@ -2562,6 +2584,8 @@ mod tests {
|
|||||||
&conversation_id,
|
&conversation_id,
|
||||||
"turn-1".to_string(),
|
"turn-1".to_string(),
|
||||||
"cmd-1".to_string(),
|
"cmd-1".to_string(),
|
||||||
|
completion_item.plugin_id.clone(),
|
||||||
|
completion_item.script_path.clone(),
|
||||||
completion_item.command.clone(),
|
completion_item.command.clone(),
|
||||||
completion_item.cwd.clone(),
|
completion_item.cwd.clone(),
|
||||||
completion_item.command_actions.clone(),
|
completion_item.command_actions.clone(),
|
||||||
@@ -2596,6 +2620,8 @@ mod tests {
|
|||||||
&conversation_id,
|
&conversation_id,
|
||||||
"turn-1".to_string(),
|
"turn-1".to_string(),
|
||||||
"cmd-1".to_string(),
|
"cmd-1".to_string(),
|
||||||
|
completion_item.plugin_id.clone(),
|
||||||
|
completion_item.script_path.clone(),
|
||||||
completion_item.command.clone(),
|
completion_item.command.clone(),
|
||||||
completion_item.cwd.clone(),
|
completion_item.cwd.clone(),
|
||||||
completion_item.command_actions.clone(),
|
completion_item.command_actions.clone(),
|
||||||
@@ -2610,11 +2636,9 @@ mod tests {
|
|||||||
&conversation_id,
|
&conversation_id,
|
||||||
"turn-1".to_string(),
|
"turn-1".to_string(),
|
||||||
"cmd-1".to_string(),
|
"cmd-1".to_string(),
|
||||||
completion_item.command.clone(),
|
completion_item,
|
||||||
completion_item.cwd.clone(),
|
|
||||||
/*process_id*/ None,
|
/*process_id*/ None,
|
||||||
CommandExecutionSource::Agent,
|
CommandExecutionSource::Agent,
|
||||||
completion_item.command_actions.clone(),
|
|
||||||
CommandExecutionStatus::Declined,
|
CommandExecutionStatus::Declined,
|
||||||
&outgoing,
|
&outgoing,
|
||||||
&thread_state,
|
&thread_state,
|
||||||
@@ -2624,10 +2648,19 @@ mod tests {
|
|||||||
let completed = recv_broadcast_notification(&mut rx).await?;
|
let completed = recv_broadcast_notification(&mut rx).await?;
|
||||||
match completed {
|
match completed {
|
||||||
ServerNotification::ItemCompleted(payload) => {
|
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");
|
bail!("expected command execution completion");
|
||||||
};
|
};
|
||||||
assert_eq!(id, "cmd-1");
|
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);
|
assert_eq!(status, CommandExecutionStatus::Declined);
|
||||||
}
|
}
|
||||||
other => bail!("unexpected message: {other:?}"),
|
other => bail!("unexpected message: {other:?}"),
|
||||||
@@ -2637,11 +2670,9 @@ mod tests {
|
|||||||
&conversation_id,
|
&conversation_id,
|
||||||
"turn-1".to_string(),
|
"turn-1".to_string(),
|
||||||
"cmd-1".to_string(),
|
"cmd-1".to_string(),
|
||||||
completion_item.command,
|
command_execution_completion_item("printf hi"),
|
||||||
completion_item.cwd,
|
|
||||||
/*process_id*/ None,
|
/*process_id*/ None,
|
||||||
CommandExecutionSource::Agent,
|
CommandExecutionSource::Agent,
|
||||||
completion_item.command_actions,
|
|
||||||
CommandExecutionStatus::Declined,
|
CommandExecutionStatus::Declined,
|
||||||
&outgoing,
|
&outgoing,
|
||||||
&thread_state,
|
&thread_state,
|
||||||
@@ -2705,10 +2736,19 @@ mod tests {
|
|||||||
match first {
|
match first {
|
||||||
ServerNotification::ItemStarted(payload) => {
|
ServerNotification::ItemStarted(payload) => {
|
||||||
assert_eq!(payload.turn_id, "turn-guardian-approved");
|
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");
|
bail!("expected command execution item");
|
||||||
};
|
};
|
||||||
assert_eq!(id, "cmd-guardian-approved");
|
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);
|
assert_eq!(status, CommandExecutionStatus::InProgress);
|
||||||
}
|
}
|
||||||
other => bail!("unexpected message: {other:?}"),
|
other => bail!("unexpected message: {other:?}"),
|
||||||
@@ -2768,10 +2808,19 @@ mod tests {
|
|||||||
match fourth {
|
match fourth {
|
||||||
ServerNotification::ItemStarted(payload) => {
|
ServerNotification::ItemStarted(payload) => {
|
||||||
assert_eq!(payload.turn_id, "turn-guardian-denied");
|
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");
|
bail!("expected command execution item");
|
||||||
};
|
};
|
||||||
assert_eq!(id, "cmd-guardian-denied");
|
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);
|
assert_eq!(status, CommandExecutionStatus::InProgress);
|
||||||
}
|
}
|
||||||
other => bail!("unexpected message: {other:?}"),
|
other => bail!("unexpected message: {other:?}"),
|
||||||
@@ -2815,10 +2864,19 @@ mod tests {
|
|||||||
let seventh = recv_broadcast_notification(&mut rx).await?;
|
let seventh = recv_broadcast_notification(&mut rx).await?;
|
||||||
match seventh {
|
match seventh {
|
||||||
ServerNotification::ItemCompleted(payload) => {
|
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");
|
bail!("expected command execution completion");
|
||||||
};
|
};
|
||||||
assert_eq!(id, "cmd-guardian-denied");
|
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);
|
assert_eq!(status, CommandExecutionStatus::Declined);
|
||||||
}
|
}
|
||||||
other => bail!("unexpected message: {other:?}"),
|
other => bail!("unexpected message: {other:?}"),
|
||||||
|
|||||||
@@ -4137,6 +4137,7 @@ async fn command_execution_notifications_include_trusted_plugin_id() -> Result<(
|
|||||||
];
|
];
|
||||||
let server = create_mock_responses_server_sequence(responses).await;
|
let server = create_mock_responses_server_sequence(responses).await;
|
||||||
MockResponsesConfig::new(&server.uri())
|
MockResponsesConfig::new(&server.uri())
|
||||||
|
.with_approval_policy("untrusted")
|
||||||
.with_sandbox_mode("danger-full-access")
|
.with_sandbox_mode("danger-full-access")
|
||||||
.enable_feature(Feature::Plugins)
|
.enable_feature(Feature::Plugins)
|
||||||
.disable_feature(Feature::RemotePlugin)
|
.disable_feature(Feature::RemotePlugin)
|
||||||
@@ -4169,13 +4170,13 @@ async fn command_execution_notifications_include_trusted_plugin_id() -> Result<(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
for method in ["item/started", "item/completed"] {
|
for method in ["item/started", "item/completed"] {
|
||||||
timeout(DEFAULT_READ_TIMEOUT, async {
|
let status = timeout(DEFAULT_READ_TIMEOUT, async {
|
||||||
loop {
|
loop {
|
||||||
let notification = mcp.read_stream_until_notification_message(method).await?;
|
let notification = mcp.read_stream_until_notification_message(method).await?;
|
||||||
let params = notification.params.expect("item notification params");
|
let params = notification.params.expect("item notification params");
|
||||||
let item_json = params.get("item").expect("item notification item").clone();
|
let item_json = params.get("item").expect("item notification item").clone();
|
||||||
let item = serde_json::from_value::<ThreadItem>(item_json.clone())?;
|
let item = serde_json::from_value::<ThreadItem>(item_json.clone())?;
|
||||||
if matches!(item, ThreadItem::CommandExecution { .. }) {
|
if let ThreadItem::CommandExecution { status, .. } = item {
|
||||||
let emitted_script_path = item_json
|
let emitted_script_path = item_json
|
||||||
.get("scriptPath")
|
.get("scriptPath")
|
||||||
.and_then(serde_json::Value::as_str)
|
.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"),
|
!emitted_script_path.contains("plugins/cache"),
|
||||||
"scriptPath must not serialize a plugin cache path"
|
"scriptPath must not serialize a plugin cache path"
|
||||||
);
|
);
|
||||||
return Ok::<(), anyhow::Error>(());
|
return Ok::<CommandExecutionStatus, anyhow::Error>(status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await??;
|
.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(
|
timeout(
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ use async_channel::Receiver;
|
|||||||
use async_channel::Sender;
|
use async_channel::Sender;
|
||||||
use codex_analytics::GuardianApprovalRequestSource;
|
use codex_analytics::GuardianApprovalRequestSource;
|
||||||
use codex_async_utils::OrCancelExt;
|
use codex_async_utils::OrCancelExt;
|
||||||
|
use codex_core_plugins::PluginCommandAttribution;
|
||||||
use codex_extension_api::LoadedUserInstructions;
|
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::ApplyPatchApprovalRequestEvent;
|
||||||
use codex_protocol::protocol::Event;
|
use codex_protocol::protocol::Event;
|
||||||
use codex_protocol::protocol::EventMsg;
|
use codex_protocol::protocol::EventMsg;
|
||||||
@@ -34,6 +37,7 @@ use tokio_util::sync::CancellationToken;
|
|||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::guardian::GuardianApprovalRequest;
|
use crate::guardian::GuardianApprovalRequest;
|
||||||
|
use crate::guardian::GuardianReviewOptions;
|
||||||
use crate::guardian::new_guardian_review_id;
|
use crate::guardian::new_guardian_review_id;
|
||||||
use crate::guardian::routes_approval_to_guardian;
|
use crate::guardian::routes_approval_to_guardian;
|
||||||
use crate::guardian::routes_approval_to_guardian_with_reviewer;
|
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 approval_id_for_op = event.effective_approval_id();
|
||||||
let ExecApprovalRequestEvent {
|
let ExecApprovalRequestEvent {
|
||||||
call_id,
|
call_id,
|
||||||
|
plugin_id,
|
||||||
|
script_path,
|
||||||
approval_id,
|
approval_id,
|
||||||
environment_id,
|
environment_id,
|
||||||
command,
|
command,
|
||||||
@@ -503,6 +509,15 @@ async fn handle_exec_approval(
|
|||||||
available_decisions,
|
available_decisions,
|
||||||
..
|
..
|
||||||
} = event;
|
} = 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 decision = if routes_approval_to_guardian(parent_ctx) {
|
||||||
let review_cancel = cancel_token.child_token();
|
let review_cancel = cancel_token.child_token();
|
||||||
let review_rx = spawn_approval_request_review(
|
let review_rx = spawn_approval_request_review(
|
||||||
@@ -522,8 +537,11 @@ async fn handle_exec_approval(
|
|||||||
justification: None,
|
justification: None,
|
||||||
},
|
},
|
||||||
reason,
|
reason,
|
||||||
GuardianApprovalRequestSource::DelegatedSubagent,
|
GuardianReviewOptions {
|
||||||
review_cancel.clone(),
|
plugin_attribution_override: plugin_attribution.clone(),
|
||||||
|
approval_request_source: GuardianApprovalRequestSource::DelegatedSubagent,
|
||||||
|
external_cancel: Some(review_cancel.clone()),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
await_approval_with_cancel(
|
await_approval_with_cancel(
|
||||||
receive_approval_review(review_rx),
|
receive_approval_review(review_rx),
|
||||||
@@ -547,6 +565,7 @@ async fn handle_exec_approval(
|
|||||||
proposed_execpolicy_amendment,
|
proposed_execpolicy_amendment,
|
||||||
additional_permissions,
|
additional_permissions,
|
||||||
available_decisions,
|
available_decisions,
|
||||||
|
plugin_attribution,
|
||||||
),
|
),
|
||||||
parent_session,
|
parent_session,
|
||||||
&approval_id_for_op,
|
&approval_id_for_op,
|
||||||
@@ -630,8 +649,11 @@ async fn handle_patch_approval(
|
|||||||
patch,
|
patch,
|
||||||
},
|
},
|
||||||
reason.clone(),
|
reason.clone(),
|
||||||
GuardianApprovalRequestSource::DelegatedSubagent,
|
GuardianReviewOptions {
|
||||||
review_cancel.clone(),
|
plugin_attribution_override: None,
|
||||||
|
approval_request_source: GuardianApprovalRequestSource::DelegatedSubagent,
|
||||||
|
external_cancel: Some(review_cancel.clone()),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
Some(
|
Some(
|
||||||
await_approval_with_cancel(
|
await_approval_with_cancel(
|
||||||
@@ -746,8 +768,11 @@ async fn maybe_auto_review_mcp_request_user_input(
|
|||||||
new_guardian_review_id(),
|
new_guardian_review_id(),
|
||||||
build_guardian_mcp_tool_review_request(&event.call_id, &invocation, metadata.as_ref()),
|
build_guardian_mcp_tool_review_request(&event.call_id, &invocation, metadata.as_ref()),
|
||||||
/*retry_reason*/ None,
|
/*retry_reason*/ None,
|
||||||
GuardianApprovalRequestSource::DelegatedSubagent,
|
GuardianReviewOptions {
|
||||||
review_cancel.clone(),
|
plugin_attribution_override: None,
|
||||||
|
approval_request_source: GuardianApprovalRequestSource::DelegatedSubagent,
|
||||||
|
external_cancel: Some(review_cancel.clone()),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
let decision = await_approval_with_cancel(
|
let decision = await_approval_with_cancel(
|
||||||
receive_approval_review(review_rx),
|
receive_approval_review(review_rx),
|
||||||
|
|||||||
@@ -363,6 +363,8 @@ async fn handle_exec_approval_uses_call_id_for_guardian_review_and_approval_id_f
|
|||||||
&parent_ctx,
|
&parent_ctx,
|
||||||
ExecApprovalRequestEvent {
|
ExecApprovalRequestEvent {
|
||||||
call_id: "command-item-1".to_string(),
|
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()),
|
approval_id: Some("callback-approval-1".to_string()),
|
||||||
turn_id: "child-turn-1".to_string(),
|
turn_id: "child-turn-1".to_string(),
|
||||||
environment_id: Some("remote".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(),
|
assessment_event.target_item_id.as_deref(),
|
||||||
Some("command-item-1")
|
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.turn_id, parent_ctx.sub_id);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
assessment_event.status,
|
assessment_event.status,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ pub(crate) use approval_request::GuardianMcpAnnotations;
|
|||||||
pub(crate) use approval_request::GuardianNetworkAccessTrigger;
|
pub(crate) use approval_request::GuardianNetworkAccessTrigger;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) use approval_request::guardian_approval_request_to_json;
|
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::guardian_timeout_message;
|
||||||
pub(crate) use review::is_guardian_reviewer_source;
|
pub(crate) use review::is_guardian_reviewer_source;
|
||||||
pub(crate) use review::new_guardian_review_id;
|
pub(crate) use review::new_guardian_review_id;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use codex_analytics::GuardianReviewFailureReason;
|
|||||||
use codex_analytics::GuardianReviewTerminalStatus;
|
use codex_analytics::GuardianReviewTerminalStatus;
|
||||||
use codex_analytics::GuardianReviewTrackContext;
|
use codex_analytics::GuardianReviewTrackContext;
|
||||||
use codex_analytics::GuardianReviewedAction;
|
use codex_analytics::GuardianReviewedAction;
|
||||||
|
use codex_core_plugins::PluginCommandAttribution;
|
||||||
use codex_protocol::config_types::ApprovalsReviewer;
|
use codex_protocol::config_types::ApprovalsReviewer;
|
||||||
use codex_protocol::protocol::AskForApproval;
|
use codex_protocol::protocol::AskForApproval;
|
||||||
use codex_protocol::protocol::CodexErrorInfo;
|
use codex_protocol::protocol::CodexErrorInfo;
|
||||||
@@ -63,6 +64,32 @@ const GUARDIAN_TIMEOUT_INSTRUCTIONS: &str = concat!(
|
|||||||
|
|
||||||
const GUARDIAN_REVIEW_MAX_ATTEMPTS: i64 = 3;
|
const GUARDIAN_REVIEW_MAX_ATTEMPTS: i64 = 3;
|
||||||
|
|
||||||
|
fn plugin_attribution_for_guardian_request(
|
||||||
|
turn: &TurnContext,
|
||||||
|
request: &GuardianApprovalRequest,
|
||||||
|
) -> Option<PluginCommandAttribution> {
|
||||||
|
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 {
|
pub(crate) fn new_guardian_review_id() -> String {
|
||||||
uuid::Uuid::new_v4().to_string()
|
uuid::Uuid::new_v4().to_string()
|
||||||
}
|
}
|
||||||
@@ -261,11 +288,21 @@ async fn run_guardian_review(
|
|||||||
review_id: String,
|
review_id: String,
|
||||||
request: GuardianApprovalRequest,
|
request: GuardianApprovalRequest,
|
||||||
retry_reason: Option<String>,
|
retry_reason: Option<String>,
|
||||||
approval_request_source: GuardianApprovalRequestSource,
|
options: GuardianReviewOptions,
|
||||||
external_cancel: Option<CancellationToken>,
|
|
||||||
) -> ReviewDecision {
|
) -> 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 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 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 action_summary = guardian_assessment_action(&request);
|
||||||
let reviewed_action = guardian_reviewed_action(&request);
|
let reviewed_action = guardian_reviewed_action(&request);
|
||||||
let review_tracking = GuardianReviewTrackContext::new(
|
let review_tracking = GuardianReviewTrackContext::new(
|
||||||
@@ -284,6 +321,8 @@ async fn run_guardian_review(
|
|||||||
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||||
id: review_id.clone(),
|
id: review_id.clone(),
|
||||||
target_item_id: target_item_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(),
|
turn_id: assessment_turn_id.clone(),
|
||||||
started_at_ms,
|
started_at_ms,
|
||||||
completed_at_ms: None,
|
completed_at_ms: None,
|
||||||
@@ -321,6 +360,8 @@ async fn run_guardian_review(
|
|||||||
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||||
id: review_id,
|
id: review_id,
|
||||||
target_item_id,
|
target_item_id,
|
||||||
|
plugin_id: plugin_id.clone(),
|
||||||
|
script_path: script_path.clone(),
|
||||||
turn_id: assessment_turn_id.clone(),
|
turn_id: assessment_turn_id.clone(),
|
||||||
started_at_ms,
|
started_at_ms,
|
||||||
completed_at_ms: Some(completed_at_ms),
|
completed_at_ms: Some(completed_at_ms),
|
||||||
@@ -414,6 +455,8 @@ async fn run_guardian_review(
|
|||||||
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||||
id: review_id,
|
id: review_id,
|
||||||
target_item_id,
|
target_item_id,
|
||||||
|
plugin_id: plugin_id.clone(),
|
||||||
|
script_path: script_path.clone(),
|
||||||
turn_id: assessment_turn_id.clone(),
|
turn_id: assessment_turn_id.clone(),
|
||||||
started_at_ms,
|
started_at_ms,
|
||||||
completed_at_ms: Some(completed_at_ms),
|
completed_at_ms: Some(completed_at_ms),
|
||||||
@@ -449,6 +492,8 @@ async fn run_guardian_review(
|
|||||||
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||||
id: review_id,
|
id: review_id,
|
||||||
target_item_id,
|
target_item_id,
|
||||||
|
plugin_id: plugin_id.clone(),
|
||||||
|
script_path: script_path.clone(),
|
||||||
turn_id: assessment_turn_id.clone(),
|
turn_id: assessment_turn_id.clone(),
|
||||||
started_at_ms,
|
started_at_ms,
|
||||||
completed_at_ms: Some(completed_at_ms),
|
completed_at_ms: Some(completed_at_ms),
|
||||||
@@ -535,6 +580,8 @@ async fn run_guardian_review(
|
|||||||
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||||
id: review_id,
|
id: review_id,
|
||||||
target_item_id,
|
target_item_id,
|
||||||
|
plugin_id: plugin_id.clone(),
|
||||||
|
script_path: script_path.clone(),
|
||||||
turn_id: assessment_turn_id.clone(),
|
turn_id: assessment_turn_id.clone(),
|
||||||
started_at_ms,
|
started_at_ms,
|
||||||
completed_at_ms: Some(completed_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<PluginCommandAttribution>,
|
||||||
|
pub(crate) approval_request_source: GuardianApprovalRequestSource,
|
||||||
|
pub(crate) external_cancel: Option<CancellationToken>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Public entrypoint for approval requests that should be reviewed by guardian.
|
/// Public entrypoint for approval requests that should be reviewed by guardian.
|
||||||
pub(crate) async fn review_approval_request(
|
pub(crate) async fn review_approval_request(
|
||||||
session: &Arc<Session>,
|
session: &Arc<Session>,
|
||||||
@@ -584,8 +637,11 @@ pub(crate) async fn review_approval_request(
|
|||||||
review_id,
|
review_id,
|
||||||
request,
|
request,
|
||||||
retry_reason,
|
retry_reason,
|
||||||
GuardianApprovalRequestSource::MainTurn,
|
GuardianReviewOptions {
|
||||||
/*external_cancel*/ None,
|
plugin_attribution_override: None,
|
||||||
|
approval_request_source: GuardianApprovalRequestSource::MainTurn,
|
||||||
|
external_cancel: None,
|
||||||
|
},
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -596,8 +652,7 @@ pub(crate) async fn review_approval_request_with_cancel(
|
|||||||
review_id: String,
|
review_id: String,
|
||||||
request: GuardianApprovalRequest,
|
request: GuardianApprovalRequest,
|
||||||
retry_reason: Option<String>,
|
retry_reason: Option<String>,
|
||||||
approval_request_source: GuardianApprovalRequestSource,
|
options: GuardianReviewOptions,
|
||||||
cancel_token: CancellationToken,
|
|
||||||
) -> ReviewDecision {
|
) -> ReviewDecision {
|
||||||
run_guardian_review(
|
run_guardian_review(
|
||||||
Arc::clone(session),
|
Arc::clone(session),
|
||||||
@@ -605,8 +660,7 @@ pub(crate) async fn review_approval_request_with_cancel(
|
|||||||
review_id,
|
review_id,
|
||||||
request,
|
request,
|
||||||
retry_reason,
|
retry_reason,
|
||||||
approval_request_source,
|
options,
|
||||||
Some(cancel_token),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -617,8 +671,7 @@ pub(crate) fn spawn_approval_request_review(
|
|||||||
review_id: String,
|
review_id: String,
|
||||||
request: GuardianApprovalRequest,
|
request: GuardianApprovalRequest,
|
||||||
retry_reason: Option<String>,
|
retry_reason: Option<String>,
|
||||||
approval_request_source: GuardianApprovalRequestSource,
|
options: GuardianReviewOptions,
|
||||||
cancel_token: CancellationToken,
|
|
||||||
) -> oneshot::Receiver<ReviewDecision> {
|
) -> oneshot::Receiver<ReviewDecision> {
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
let spawn_result = std::thread::Builder::new()
|
let spawn_result = std::thread::Builder::new()
|
||||||
@@ -639,8 +692,7 @@ pub(crate) fn spawn_approval_request_review(
|
|||||||
review_id,
|
review_id,
|
||||||
request,
|
request,
|
||||||
retry_reason,
|
retry_reason,
|
||||||
approval_request_source,
|
options,
|
||||||
cancel_token,
|
|
||||||
));
|
));
|
||||||
let _ = tx.send(decision);
|
let _ = tx.send(decision);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1167,8 +1167,11 @@ async fn cancelled_guardian_review_emits_terminal_abort_without_warning() {
|
|||||||
.to_string(),
|
.to_string(),
|
||||||
},
|
},
|
||||||
/*retry_reason*/ None,
|
/*retry_reason*/ None,
|
||||||
GuardianApprovalRequestSource::MainTurn,
|
GuardianReviewOptions {
|
||||||
cancel_token,
|
plugin_attribution_override: None,
|
||||||
|
approval_request_source: GuardianApprovalRequestSource::MainTurn,
|
||||||
|
external_cancel: Some(cancel_token),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use codex_core_plugins::PluginCommandAttribution;
|
||||||
|
use codex_plugin::PluginId;
|
||||||
use codex_protocol::protocol::ReviewDecision;
|
use codex_protocol::protocol::ReviewDecision;
|
||||||
use codex_protocol::request_permissions::PermissionGrantScope;
|
use codex_protocol::request_permissions::PermissionGrantScope;
|
||||||
use codex_protocol::request_permissions::RequestPermissionProfile;
|
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();
|
let mut pause_state = session.subscribe_elicitation_pause_state();
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
let cwd = turn_context.cwd.clone();
|
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 request = tokio::spawn({
|
||||||
let session = session.clone();
|
let session = session.clone();
|
||||||
@@ -52,12 +58,18 @@ async fn command_approval_holds_an_elicitation_until_response() {
|
|||||||
/*proposed_execpolicy_amendment*/ None,
|
/*proposed_execpolicy_amendment*/ None,
|
||||||
/*additional_permissions*/ None,
|
/*additional_permissions*/ None,
|
||||||
/*available_decisions*/ None,
|
/*available_decisions*/ None,
|
||||||
|
/*plugin_attribution_override*/ Some(plugin_attribution),
|
||||||
)
|
)
|
||||||
.await
|
.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;
|
wait_until_held(&mut pause_state).await;
|
||||||
session
|
session
|
||||||
.notify_approval("call-1", ReviewDecision::Approved)
|
.notify_approval("call-1", ReviewDecision::Approved)
|
||||||
|
|||||||
@@ -306,6 +306,7 @@ pub(crate) struct PreviousTurnSettings {
|
|||||||
use crate::SkillMetadata;
|
use crate::SkillMetadata;
|
||||||
use crate::SkillsService;
|
use crate::SkillsService;
|
||||||
use crate::exec_policy::ExecPolicyUpdateError;
|
use crate::exec_policy::ExecPolicyUpdateError;
|
||||||
|
use crate::guardian::GuardianReviewOptions;
|
||||||
use crate::guardian::GuardianReviewSessionManager;
|
use crate::guardian::GuardianReviewSessionManager;
|
||||||
use crate::mcp::McpManager;
|
use crate::mcp::McpManager;
|
||||||
use crate::network_policy_decision::execpolicy_network_rule_amendment;
|
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::turn_timing::record_turn_ttfm_metric;
|
||||||
use crate::unified_exec::UnifiedExecProcessManager;
|
use crate::unified_exec::UnifiedExecProcessManager;
|
||||||
use crate::windows_sandbox::WindowsSandboxLevelExt;
|
use crate::windows_sandbox::WindowsSandboxLevelExt;
|
||||||
|
use codex_core_plugins::PluginCommandAttribution;
|
||||||
use codex_core_plugins::PluginsManager;
|
use codex_core_plugins::PluginsManager;
|
||||||
use codex_core_plugins::RecommendedPluginCandidatesInput;
|
use codex_core_plugins::RecommendedPluginCandidatesInput;
|
||||||
use codex_git_utils::get_git_repo_root;
|
use codex_git_utils::get_git_repo_root;
|
||||||
@@ -2276,6 +2278,7 @@ impl Session {
|
|||||||
proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
|
proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
|
||||||
additional_permissions: Option<AdditionalPermissionProfile>,
|
additional_permissions: Option<AdditionalPermissionProfile>,
|
||||||
available_decisions: Option<Vec<ReviewDecision>>,
|
available_decisions: Option<Vec<ReviewDecision>>,
|
||||||
|
plugin_attribution_override: Option<PluginCommandAttribution>,
|
||||||
) -> ReviewDecision {
|
) -> ReviewDecision {
|
||||||
let _elicitation = self.services.elicitations.register();
|
let _elicitation = self.services.elicitations.register();
|
||||||
// command-level approvals use `call_id`.
|
// command-level approvals use `call_id`.
|
||||||
@@ -2318,8 +2321,16 @@ impl Session {
|
|||||||
additional_permissions.as_ref(),
|
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 {
|
let event = EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
|
||||||
call_id,
|
call_id,
|
||||||
|
plugin_id,
|
||||||
|
script_path,
|
||||||
approval_id,
|
approval_id,
|
||||||
turn_id: turn_context.sub_id.clone(),
|
turn_id: turn_context.sub_id.clone(),
|
||||||
environment_id,
|
environment_id,
|
||||||
@@ -2448,8 +2459,12 @@ impl Session {
|
|||||||
review_id,
|
review_id,
|
||||||
request,
|
request,
|
||||||
/*retry_reason*/ None,
|
/*retry_reason*/ None,
|
||||||
codex_analytics::GuardianApprovalRequestSource::MainTurn,
|
GuardianReviewOptions {
|
||||||
cancellation_token.clone(),
|
plugin_attribution_override: None,
|
||||||
|
approval_request_source:
|
||||||
|
codex_analytics::GuardianApprovalRequestSource::MainTurn,
|
||||||
|
external_cancel: Some(cancellation_token.clone()),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
let decision = tokio::select! {
|
let decision = tokio::select! {
|
||||||
biased;
|
biased;
|
||||||
|
|||||||
@@ -687,6 +687,7 @@ impl NetworkApprovalService {
|
|||||||
/*proposed_execpolicy_amendment*/ None,
|
/*proposed_execpolicy_amendment*/ None,
|
||||||
/*additional_permissions*/ None,
|
/*additional_permissions*/ None,
|
||||||
available_decisions,
|
available_decisions,
|
||||||
|
/*plugin_attribution_override*/ None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ impl Approvable<ShellRequest> for ShellRuntime {
|
|||||||
.cloned(),
|
.cloned(),
|
||||||
req.additional_permissions.clone(),
|
req.additional_permissions.clone(),
|
||||||
available_decisions,
|
available_decisions,
|
||||||
|
/*plugin_attribution_override*/ None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -507,6 +507,7 @@ impl CoreShellActionProvider {
|
|||||||
/*proposed_execpolicy_amendment*/ None,
|
/*proposed_execpolicy_amendment*/ None,
|
||||||
additional_permissions,
|
additional_permissions,
|
||||||
Some(vec![ReviewDecision::Approved, ReviewDecision::Abort]),
|
Some(vec![ReviewDecision::Approved, ReviewDecision::Abort]),
|
||||||
|
/*plugin_attribution_override*/ None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
|
|||||||
.cloned(),
|
.cloned(),
|
||||||
req.additional_permissions.clone(),
|
req.additional_permissions.clone(),
|
||||||
available_decisions,
|
available_decisions,
|
||||||
|
/*plugin_attribution_override*/ None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -229,6 +229,8 @@ async fn run_codex_tool_session_inner(
|
|||||||
command,
|
command,
|
||||||
cwd,
|
cwd,
|
||||||
call_id,
|
call_id,
|
||||||
|
plugin_id: _,
|
||||||
|
script_path: _,
|
||||||
approval_id: _,
|
approval_id: _,
|
||||||
reason: _,
|
reason: _,
|
||||||
proposed_execpolicy_amendment: _,
|
proposed_execpolicy_amendment: _,
|
||||||
|
|||||||
@@ -183,6 +183,14 @@ pub struct GuardianAssessmentEvent {
|
|||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
#[ts(optional)]
|
#[ts(optional)]
|
||||||
pub target_item_id: Option<String>,
|
pub target_item_id: Option<String>,
|
||||||
|
/// Trusted plugin attribution for command items synthesized from this review.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
#[ts(optional)]
|
||||||
|
pub plugin_id: Option<String>,
|
||||||
|
/// 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<String>,
|
||||||
/// Turn ID that this assessment belongs to.
|
/// Turn ID that this assessment belongs to.
|
||||||
/// Uses `#[serde(default)]` for backwards compatibility.
|
/// Uses `#[serde(default)]` for backwards compatibility.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -218,6 +226,14 @@ pub struct GuardianAssessmentEvent {
|
|||||||
pub struct ExecApprovalRequestEvent {
|
pub struct ExecApprovalRequestEvent {
|
||||||
/// Identifier for the associated command execution item.
|
/// Identifier for the associated command execution item.
|
||||||
pub call_id: String,
|
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<String>,
|
||||||
|
/// Safe plugin-relative path for the command item, when available.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
#[ts(optional)]
|
||||||
|
pub script_path: Option<String>,
|
||||||
/// Identifier for this specific approval callback.
|
/// Identifier for this specific approval callback.
|
||||||
///
|
///
|
||||||
/// When absent, the approval is for the command item itself (`call_id`).
|
/// When absent, the approval is for the command item itself (`call_id`).
|
||||||
|
|||||||
@@ -152,6 +152,10 @@ impl Serialize for ToolRuntimePayload<'_> {
|
|||||||
struct ExecCommandBeginTracePayload<'a> {
|
struct ExecCommandBeginTracePayload<'a> {
|
||||||
call_id: &'a str,
|
call_id: &'a str,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[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>,
|
process_id: Option<&'a str>,
|
||||||
turn_id: &'a str,
|
turn_id: &'a str,
|
||||||
started_at_ms: i64,
|
started_at_ms: i64,
|
||||||
@@ -167,8 +171,8 @@ impl<'a> From<&'a ExecCommandBeginEvent> for ExecCommandBeginTracePayload<'a> {
|
|||||||
fn from(event: &'a ExecCommandBeginEvent) -> Self {
|
fn from(event: &'a ExecCommandBeginEvent) -> Self {
|
||||||
let ExecCommandBeginEvent {
|
let ExecCommandBeginEvent {
|
||||||
call_id,
|
call_id,
|
||||||
plugin_id: _,
|
plugin_id,
|
||||||
script_path: _,
|
script_path,
|
||||||
process_id,
|
process_id,
|
||||||
turn_id,
|
turn_id,
|
||||||
started_at_ms,
|
started_at_ms,
|
||||||
@@ -180,6 +184,8 @@ impl<'a> From<&'a ExecCommandBeginEvent> for ExecCommandBeginTracePayload<'a> {
|
|||||||
} = event;
|
} = event;
|
||||||
Self {
|
Self {
|
||||||
call_id,
|
call_id,
|
||||||
|
plugin_id: plugin_id.as_deref(),
|
||||||
|
script_path: script_path.as_deref(),
|
||||||
process_id: process_id.as_deref(),
|
process_id: process_id.as_deref(),
|
||||||
turn_id,
|
turn_id,
|
||||||
started_at_ms: *started_at_ms,
|
started_at_ms: *started_at_ms,
|
||||||
@@ -200,6 +206,10 @@ impl<'a> From<&'a ExecCommandBeginEvent> for ExecCommandBeginTracePayload<'a> {
|
|||||||
struct ExecCommandEndTracePayload<'a> {
|
struct ExecCommandEndTracePayload<'a> {
|
||||||
call_id: &'a str,
|
call_id: &'a str,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[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>,
|
process_id: Option<&'a str>,
|
||||||
turn_id: &'a str,
|
turn_id: &'a str,
|
||||||
completed_at_ms: i64,
|
completed_at_ms: i64,
|
||||||
@@ -222,8 +232,8 @@ impl<'a> From<&'a ExecCommandEndEvent> for ExecCommandEndTracePayload<'a> {
|
|||||||
fn from(event: &'a ExecCommandEndEvent) -> Self {
|
fn from(event: &'a ExecCommandEndEvent) -> Self {
|
||||||
let ExecCommandEndEvent {
|
let ExecCommandEndEvent {
|
||||||
call_id,
|
call_id,
|
||||||
plugin_id: _,
|
plugin_id,
|
||||||
script_path: _,
|
script_path,
|
||||||
process_id,
|
process_id,
|
||||||
turn_id,
|
turn_id,
|
||||||
completed_at_ms,
|
completed_at_ms,
|
||||||
@@ -242,6 +252,8 @@ impl<'a> From<&'a ExecCommandEndEvent> for ExecCommandEndTracePayload<'a> {
|
|||||||
} = event;
|
} = event;
|
||||||
Self {
|
Self {
|
||||||
call_id,
|
call_id,
|
||||||
|
plugin_id: plugin_id.as_deref(),
|
||||||
|
script_path: script_path.as_deref(),
|
||||||
process_id: process_id.as_deref(),
|
process_id: process_id.as_deref(),
|
||||||
turn_id,
|
turn_id,
|
||||||
completed_at_ms: *completed_at_ms,
|
completed_at_ms: *completed_at_ms,
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ fn exec_command_trace_payloads_use_inferred_native_cwd() -> anyhow::Result<()> {
|
|||||||
// Windows and POSIX paths on every platform.
|
// Windows and POSIX paths on every platform.
|
||||||
let begin = EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
|
let begin = EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
|
||||||
call_id: "call-begin".to_string(),
|
call_id: "call-begin".to_string(),
|
||||||
plugin_id: None,
|
plugin_id: Some("sample@openai-curated".to_string()),
|
||||||
script_path: None,
|
script_path: Some("scripts/run.py".to_string()),
|
||||||
process_id: Some("process-1".to_string()),
|
process_id: Some("process-1".to_string()),
|
||||||
turn_id: "turn-1".to_string(),
|
turn_id: "turn-1".to_string(),
|
||||||
started_at_ms: 1234,
|
started_at_ms: 1234,
|
||||||
@@ -69,8 +69,8 @@ fn exec_command_trace_payloads_use_inferred_native_cwd() -> anyhow::Result<()> {
|
|||||||
});
|
});
|
||||||
let end = EventMsg::ExecCommandEnd(ExecCommandEndEvent {
|
let end = EventMsg::ExecCommandEnd(ExecCommandEndEvent {
|
||||||
call_id: "call-end".to_string(),
|
call_id: "call-end".to_string(),
|
||||||
plugin_id: None,
|
plugin_id: Some("sample@openai-curated".to_string()),
|
||||||
script_path: None,
|
script_path: Some("scripts/run.py".to_string()),
|
||||||
process_id: None,
|
process_id: None,
|
||||||
turn_id: "turn-1".to_string(),
|
turn_id: "turn-1".to_string(),
|
||||||
completed_at_ms: 2345,
|
completed_at_ms: 2345,
|
||||||
@@ -96,6 +96,8 @@ fn exec_command_trace_payloads_use_inferred_native_cwd() -> anyhow::Result<()> {
|
|||||||
serde_json::to_value(payload)?,
|
serde_json::to_value(payload)?,
|
||||||
json!({
|
json!({
|
||||||
"call_id": "call-begin",
|
"call_id": "call-begin",
|
||||||
|
"plugin_id": "sample@openai-curated",
|
||||||
|
"script_path": "scripts/run.py",
|
||||||
"process_id": "process-1",
|
"process_id": "process-1",
|
||||||
"turn_id": "turn-1",
|
"turn_id": "turn-1",
|
||||||
"started_at_ms": 1234,
|
"started_at_ms": 1234,
|
||||||
@@ -113,6 +115,8 @@ fn exec_command_trace_payloads_use_inferred_native_cwd() -> anyhow::Result<()> {
|
|||||||
serde_json::to_value(payload)?,
|
serde_json::to_value(payload)?,
|
||||||
json!({
|
json!({
|
||||||
"call_id": "call-end",
|
"call_id": "call-end",
|
||||||
|
"plugin_id": "sample@openai-curated",
|
||||||
|
"script_path": "scripts/run.py",
|
||||||
"turn_id": "turn-1",
|
"turn_id": "turn-1",
|
||||||
"completed_at_ms": 2345,
|
"completed_at_ms": 2345,
|
||||||
"command": ["pwd"],
|
"command": ["pwd"],
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ mod tests {
|
|||||||
GuardianAssessmentEvent {
|
GuardianAssessmentEvent {
|
||||||
id: format!("review-{id}"),
|
id: format!("review-{id}"),
|
||||||
target_item_id: None,
|
target_item_id: None,
|
||||||
|
plugin_id: None,
|
||||||
|
script_path: None,
|
||||||
turn_id: "turn-1".to_string(),
|
turn_id: "turn-1".to_string(),
|
||||||
started_at_ms: 0,
|
started_at_ms: 0,
|
||||||
completed_at_ms: Some(1),
|
completed_at_ms: Some(1),
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ impl ChatWidget {
|
|||||||
self.on_guardian_assessment(GuardianAssessmentEvent {
|
self.on_guardian_assessment(GuardianAssessmentEvent {
|
||||||
id,
|
id,
|
||||||
target_item_id: None,
|
target_item_id: None,
|
||||||
|
plugin_id: None,
|
||||||
|
script_path: None,
|
||||||
turn_id,
|
turn_id,
|
||||||
started_at_ms,
|
started_at_ms,
|
||||||
completed_at_ms,
|
completed_at_ms,
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ fn auto_review_denial_event() -> GuardianAssessmentEvent {
|
|||||||
GuardianAssessmentEvent {
|
GuardianAssessmentEvent {
|
||||||
id: "auto-review-recent-1".into(),
|
id: "auto-review-recent-1".into(),
|
||||||
target_item_id: Some("target-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(),
|
turn_id: "turn-recent-1".into(),
|
||||||
started_at_ms: 0,
|
started_at_ms: 0,
|
||||||
completed_at_ms: Some(1),
|
completed_at_ms: Some(1),
|
||||||
@@ -31,6 +33,8 @@ fn guardian_command_event(
|
|||||||
GuardianAssessmentEvent {
|
GuardianAssessmentEvent {
|
||||||
id: id.to_string(),
|
id: id.to_string(),
|
||||||
target_item_id: Some(format!("{id}-target")),
|
target_item_id: Some(format!("{id}-target")),
|
||||||
|
plugin_id: None,
|
||||||
|
script_path: None,
|
||||||
turn_id: turn_id.to_string(),
|
turn_id: turn_id.to_string(),
|
||||||
started_at_ms: 0,
|
started_at_ms: 0,
|
||||||
completed_at_ms: terminal.then_some(1),
|
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 {
|
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||||
id: "guardian-1".into(),
|
id: "guardian-1".into(),
|
||||||
target_item_id: Some("guardian-target-1".into()),
|
target_item_id: Some("guardian-target-1".into()),
|
||||||
|
plugin_id: None,
|
||||||
|
script_path: None,
|
||||||
turn_id: "turn-1".into(),
|
turn_id: "turn-1".into(),
|
||||||
started_at_ms: 0,
|
started_at_ms: 0,
|
||||||
completed_at_ms: None,
|
completed_at_ms: None,
|
||||||
@@ -114,6 +120,8 @@ async fn guardian_denied_exec_renders_warning_and_denied_request() {
|
|||||||
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||||
id: "guardian-1".into(),
|
id: "guardian-1".into(),
|
||||||
target_item_id: Some("guardian-target-1".into()),
|
target_item_id: Some("guardian-target-1".into()),
|
||||||
|
plugin_id: None,
|
||||||
|
script_path: None,
|
||||||
turn_id: "turn-1".into(),
|
turn_id: "turn-1".into(),
|
||||||
started_at_ms: 0,
|
started_at_ms: 0,
|
||||||
completed_at_ms: Some(1),
|
completed_at_ms: Some(1),
|
||||||
@@ -158,6 +166,8 @@ async fn guardian_approved_exec_renders_approved_request() {
|
|||||||
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||||
id: "thread:child-thread:guardian-1".into(),
|
id: "thread:child-thread:guardian-1".into(),
|
||||||
target_item_id: Some("guardian-approved-target".into()),
|
target_item_id: Some("guardian-approved-target".into()),
|
||||||
|
plugin_id: None,
|
||||||
|
script_path: None,
|
||||||
turn_id: "turn-1".into(),
|
turn_id: "turn-1".into(),
|
||||||
started_at_ms: 0,
|
started_at_ms: 0,
|
||||||
completed_at_ms: Some(1),
|
completed_at_ms: Some(1),
|
||||||
@@ -216,6 +226,8 @@ async fn guardian_approved_request_permissions_renders_request_summary() {
|
|||||||
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||||
id: "guardian-request-permissions".into(),
|
id: "guardian-request-permissions".into(),
|
||||||
target_item_id: None,
|
target_item_id: None,
|
||||||
|
plugin_id: None,
|
||||||
|
script_path: None,
|
||||||
turn_id: "turn-1".into(),
|
turn_id: "turn-1".into(),
|
||||||
started_at_ms: 0,
|
started_at_ms: 0,
|
||||||
completed_at_ms: None,
|
completed_at_ms: None,
|
||||||
@@ -240,6 +252,8 @@ async fn guardian_approved_request_permissions_renders_request_summary() {
|
|||||||
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||||
id: "guardian-request-permissions".into(),
|
id: "guardian-request-permissions".into(),
|
||||||
target_item_id: None,
|
target_item_id: None,
|
||||||
|
plugin_id: None,
|
||||||
|
script_path: None,
|
||||||
turn_id: "turn-1".into(),
|
turn_id: "turn-1".into(),
|
||||||
started_at_ms: 0,
|
started_at_ms: 0,
|
||||||
completed_at_ms: Some(1),
|
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 {
|
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||||
id: "guardian-1".into(),
|
id: "guardian-1".into(),
|
||||||
target_item_id: Some("guardian-target-1".into()),
|
target_item_id: Some("guardian-target-1".into()),
|
||||||
|
plugin_id: None,
|
||||||
|
script_path: None,
|
||||||
turn_id: "turn-1".into(),
|
turn_id: "turn-1".into(),
|
||||||
started_at_ms: 0,
|
started_at_ms: 0,
|
||||||
completed_at_ms: None,
|
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 {
|
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||||
id: "guardian-1".into(),
|
id: "guardian-1".into(),
|
||||||
target_item_id: Some("guardian-target-1".into()),
|
target_item_id: Some("guardian-target-1".into()),
|
||||||
|
plugin_id: None,
|
||||||
|
script_path: None,
|
||||||
turn_id: "turn-1".into(),
|
turn_id: "turn-1".into(),
|
||||||
started_at_ms: 0,
|
started_at_ms: 0,
|
||||||
completed_at_ms: Some(1),
|
completed_at_ms: Some(1),
|
||||||
|
|||||||
@@ -299,6 +299,8 @@ async fn mcp_startup_complete_preserves_review_status() {
|
|||||||
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||||
id: "guardian-1".to_string(),
|
id: "guardian-1".to_string(),
|
||||||
target_item_id: Some("guardian-target-1".to_string()),
|
target_item_id: Some("guardian-target-1".to_string()),
|
||||||
|
plugin_id: None,
|
||||||
|
script_path: None,
|
||||||
turn_id: "turn-1".to_string(),
|
turn_id: "turn-1".to_string(),
|
||||||
started_at_ms: 0,
|
started_at_ms: 0,
|
||||||
completed_at_ms: None,
|
completed_at_ms: None,
|
||||||
|
|||||||
Reference in new issue
Block a user