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:
Kyle Brown
2026-07-23 23:13:59 +00:00
committed by copyberry
parent 091e4a5d7c
commit 9fc4e5a7aa
23 changed files with 363 additions and 75 deletions
@@ -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,
@@ -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,