Attribute command executions to trusted plugin scripts (#35020)

## What changed

- Resolve shell and unified-exec commands against the trusted plugin roots loaded for each turn.
- Add optional `pluginId` and safe plugin-relative `scriptPath` fields to command execution items and legacy execution events, and propagate them through app-server notifications.
- Include the attribution in command execution analytics while rejecting absolute, unsafe, and unattributed script paths.

## Testing

- Cover attribution for cached curated and remote plugin scripts from command execution through core and app-server events.
- Verify analytics serialization and unsafe-path filtering.

GitOrigin-RevId: 02fac3a233284ccfc6642fa502a95f1881dba83d
This commit is contained in:
Kyle Brown
2026-07-23 22:31:53 +00:00
committed by copyberry
parent 5bdbd3ee90
commit 84fa68b429
52 changed files with 858 additions and 32 deletions
@@ -18,6 +18,7 @@ use crate::tools::events::ToolEventCtx;
use crate::tools::events::ToolEventFailure;
use crate::tools::events::ToolEventStage;
use crate::unified_exec::head_tail_buffer::HeadTailBuffer;
use codex_core_plugins::PluginCommandAttribution;
use codex_protocol::exec_output::ExecToolCallOutput;
use codex_protocol::exec_output::StreamOutput;
use codex_protocol::protocol::EventMsg;
@@ -162,6 +163,7 @@ pub(crate) fn spawn_exit_watcher(
command: Vec<String>,
cwd: PathUri,
process_id: i32,
plugin_attribution: Option<PluginCommandAttribution>,
transcript: Arc<Mutex<HeadTailBuffer>>,
started_at: Instant,
network_denial_monitor: Option<tokio::task::JoinHandle<()>>,
@@ -190,6 +192,7 @@ pub(crate) fn spawn_exit_watcher(
command,
cwd,
Some(process_id.to_string()),
plugin_attribution,
transcript,
String::new(),
message,
@@ -205,6 +208,7 @@ pub(crate) fn spawn_exit_watcher(
command,
cwd,
Some(process_id.to_string()),
plugin_attribution,
transcript,
String::new(),
exit_code,
@@ -258,6 +262,7 @@ pub(crate) async fn emit_exec_end_for_unified_exec(
command: Vec<String>,
cwd: PathUri,
process_id: Option<String>,
plugin_attribution: Option<PluginCommandAttribution>,
transcript: Arc<Mutex<HeadTailBuffer>>,
fallback_output: String,
exit_code: i32,
@@ -283,6 +288,7 @@ pub(crate) async fn emit_exec_end_for_unified_exec(
cwd,
ExecCommandSource::UnifiedExecStartup,
process_id,
plugin_attribution,
);
emitter
.emit(
@@ -303,6 +309,7 @@ pub(crate) async fn emit_failed_exec_end_for_unified_exec(
command: Vec<String>,
cwd: PathUri,
process_id: Option<String>,
plugin_attribution: Option<PluginCommandAttribution>,
transcript: Arc<Mutex<HeadTailBuffer>>,
fallback_output: String,
message: String,
@@ -337,6 +344,7 @@ pub(crate) async fn emit_failed_exec_end_for_unified_exec(
cwd,
ExecCommandSource::UnifiedExecStartup,
process_id,
plugin_attribution,
);
emitter
.emit(
@@ -160,6 +160,7 @@ async fn exit_watcher_waits_for_late_network_denial_before_classifying_end() ->
vec!["proof".to_string()],
cwd,
/*process_id*/ 123,
/*plugin_attribution*/ None,
transcript,
Instant::now(),
Some(network_denial_monitor),
@@ -58,6 +58,7 @@ use crate::unified_exec::process::OutputBuffer;
use crate::unified_exec::process::OutputHandles;
use crate::unified_exec::process::SpawnLifecycleHandle;
use crate::unified_exec::process::UnifiedExecProcess;
use codex_core_plugins::PluginCommandAttribution;
use codex_network_proxy::NetworkProxy;
use codex_protocol::config_types::ShellEnvironmentPolicy;
use codex_protocol::error::CodexErr;
@@ -326,6 +327,7 @@ async fn emit_failed_initial_exec_end_if_unstored(
context: &UnifiedExecContext,
request: &ExecCommandRequest,
cwd: PathUri,
plugin_attribution: Option<PluginCommandAttribution>,
transcript: Arc<tokio::sync::Mutex<HeadTailBuffer>>,
fallback_output: String,
message: String,
@@ -342,6 +344,7 @@ async fn emit_failed_initial_exec_end_if_unstored(
request.command.clone(),
cwd,
Some(request.process_id.to_string()),
plugin_attribution,
transcript,
fallback_output,
message,
@@ -445,11 +448,17 @@ impl UnifiedExecProcessManager {
&context.call_id,
/*turn_diff_tracker*/ None,
);
let plugin_attribution = cwd.to_abs_path().ok().and_then(|cwd| {
context
.turn
.plugin_attribution_for_command(&request.command, &cwd)
});
let emitter = ToolEmitter::unified_exec(
&request.command,
cwd.clone(),
ExecCommandSource::UnifiedExecStartup,
Some(request.process_id.to_string()),
plugin_attribution.clone(),
);
emitter.emit(event_ctx, ToolEventStage::Begin).await;
@@ -466,6 +475,7 @@ impl UnifiedExecProcessManager {
&request.command,
request.hook_command.clone(),
cwd.clone(),
plugin_attribution.clone(),
start,
request.process_id,
request.tty,
@@ -528,6 +538,7 @@ impl UnifiedExecProcessManager {
context,
&request,
cwd.clone(),
plugin_attribution.clone(),
Arc::clone(&transcript),
text.clone(),
message.clone(),
@@ -548,6 +559,7 @@ impl UnifiedExecProcessManager {
context,
&request,
cwd.clone(),
plugin_attribution.clone(),
Arc::clone(&transcript),
text.clone(),
message.clone(),
@@ -607,6 +619,7 @@ impl UnifiedExecProcessManager {
context,
&request,
cwd.clone(),
plugin_attribution.clone(),
Arc::clone(&transcript),
text.clone(),
message.clone(),
@@ -625,6 +638,7 @@ impl UnifiedExecProcessManager {
request.command.clone(),
cwd.clone(),
Some(process_id.to_string()),
plugin_attribution.clone(),
Arc::clone(&transcript),
text.clone(),
exit,
@@ -929,6 +943,7 @@ impl UnifiedExecProcessManager {
command: &[String],
hook_command: String,
cwd: PathUri,
plugin_attribution: Option<PluginCommandAttribution>,
started_at: Instant,
process_id: i32,
tty: bool,
@@ -970,6 +985,7 @@ impl UnifiedExecProcessManager {
command.to_vec(),
cwd,
process_id,
plugin_attribution,
transcript,
started_at,
network_denial_monitor,
@@ -394,6 +394,7 @@ async fn failed_initial_end_for_unstored_process_uses_fallback_output() {
&request,
#[allow(deprecated)]
turn.cwd.clone().into(),
/*plugin_attribution*/ None,
transcript,
"PRE_DENIAL_MARKER".to_string(),
"Network access denied".to_string(),