Track artifact operations from trusted plugin markers (#38057)

## What changed

- Recognize validated create and edit marker commands from the trusted presentations, documents, spreadsheets, and PDF plugins.
- Emit a `codex_artifact_operation` analytics event plus started-count and expected-output-count metrics when a recognized marker starts through unified exec.
- Attribute remote executor commands only when their plugin cache path and script contents match a trusted local plugin, including primary-runtime plugins.

## Testing

- Cover supported marker combinations and reject mismatched plugins, paths, arguments, counts, and output formats.
- Cover local, relocated remote, and Windows-style plugin attribution, including rejection of modified remote scripts.

GitOrigin-RevId: 475cc1689aa7f371a24dec0a8bdb6f7fd5a2e922
This commit is contained in:
edwardysun3 authored and copyberry committed 2026-08-11 19:26:41 +00:00
1 parent 285abc0368
commit 44d992c14e
18 files changed
+971 -60

No files matched your search

@@ -41,12 +41,16 @@ use crate::events::codex_app_metadata;
use crate::events::codex_hook_run_metadata; use crate::events::codex_hook_run_metadata;
use crate::events::codex_plugin_metadata; use crate::events::codex_plugin_metadata;
use crate::events::codex_plugin_used_metadata; use crate::events::codex_plugin_used_metadata;
use crate::events::current_runtime_metadata;
use crate::events::subagent_thread_started_event_request; use crate::events::subagent_thread_started_event_request;
use crate::facts::AnalyticsFact; use crate::facts::AnalyticsFact;
use crate::facts::AnalyticsJsonRpcError; use crate::facts::AnalyticsJsonRpcError;
use crate::facts::AppInvocation; use crate::facts::AppInvocation;
use crate::facts::AppMentionedInput; use crate::facts::AppMentionedInput;
use crate::facts::AppUsedInput; use crate::facts::AppUsedInput;
use crate::facts::ArtifactOperation;
use crate::facts::ArtifactOperationInput;
use crate::facts::ArtifactOperationLifecycle;
use crate::facts::CodeModeToolCallFact; use crate::facts::CodeModeToolCallFact;
use crate::facts::CodeModeToolCallStatus; use crate::facts::CodeModeToolCallStatus;
use crate::facts::CodexCompactionEvent; use crate::facts::CodexCompactionEvent;
@@ -3745,6 +3749,62 @@ fn plugin_used_dedupe_is_keyed_by_turn_and_plugin() {
assert_eq!(queue.should_enqueue_plugin_used(&turn_2, &plugin), true); assert_eq!(queue.should_enqueue_plugin_used(&turn_2, &plugin), true);
} }
#[tokio::test]
async fn reducer_ingests_artifact_operation_fact() {
let mut reducer = AnalyticsReducer::default();
let mut events = Vec::new();
reducer
.ingest(
AnalyticsFact::Custom(CustomAnalyticsFact::ArtifactOperation(
ArtifactOperationInput {
tracking: test_tracking_context("thread-1", "turn-1"),
operation: ArtifactOperation {
item_id: "call-1".to_string(),
lifecycle: ArtifactOperationLifecycle::Started,
occurred_at_ms: 1_786_000_000_000,
plugin_id: "presentations@openai-primary-runtime".to_string(),
script_path: "skills/presentations/container_tools/mark_artifact_operation_started.mjs".to_string(),
skill: "presentations".to_string(),
artifact_type: "presentation".to_string(),
operation_kind: "create".to_string(),
expected_output_count: 2,
output_format: "pptx".to_string(),
execution_backend: "unified_exec".to_string(),
},
},
)),
&mut events,
)
.await;
assert!(events[0].can_send_with_api_key_auth());
assert_eq!(
serde_json::to_value(events).expect("serialize events"),
json!([{
"event_type": "codex_artifact_operation",
"event_params": {
"thread_id": "thread-1",
"turn_id": "turn-1",
"item_id": "call-1",
"lifecycle": "started",
"occurred_at_ms": 1_786_000_000_000_u64,
"product_client_id": TEST_PRODUCT_CLIENT_ID,
"runtime": serde_json::to_value(current_runtime_metadata())
.expect("serialize runtime metadata"),
"model_slug": "gpt-5",
"plugin_id": "presentations@openai-primary-runtime",
"script_path": "skills/presentations/container_tools/mark_artifact_operation_started.mjs",
"skill": "presentations",
"artifact_type": "presentation",
"operation_kind": "create",
"expected_output_count": 2,
"output_format": "pptx",
"execution_backend": "unified_exec"
}
}])
);
}
#[tokio::test] #[tokio::test]
async fn reducer_ingests_skill_invoked_fact() { async fn reducer_ingests_skill_invoked_fact() {
let mut reducer = AnalyticsReducer::default(); let mut reducer = AnalyticsReducer::default();
+15
View File
@@ -9,6 +9,8 @@ use crate::facts::AnalyticsJsonRpcError;
use crate::facts::AppInvocation; use crate::facts::AppInvocation;
use crate::facts::AppMentionedInput; use crate::facts::AppMentionedInput;
use crate::facts::AppUsedInput; use crate::facts::AppUsedInput;
use crate::facts::ArtifactOperation;
use crate::facts::ArtifactOperationInput;
use crate::facts::CodexGoalEvent; use crate::facts::CodexGoalEvent;
use crate::facts::CustomAnalyticsFact; use crate::facts::CustomAnalyticsFact;
use crate::facts::ExternalAgentConfigImportCompletedInput; use crate::facts::ExternalAgentConfigImportCompletedInput;
@@ -269,6 +271,19 @@ impl AnalyticsEventsClient {
))); )));
} }
pub fn track_artifact_operation(
&self,
tracking: TrackEventsContext,
operation: ArtifactOperation,
) {
self.record_fact(AnalyticsFact::Custom(
CustomAnalyticsFact::ArtifactOperation(ArtifactOperationInput {
tracking,
operation,
}),
));
}
pub fn track_initialize( pub fn track_initialize(
&self, &self,
connection_id: u64, connection_id: u64,
+40
View File
@@ -42,8 +42,16 @@ use crate::events::ThreadArchiveEventParams;
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
use crate::events::ToolItemTerminalStatus; use crate::events::ToolItemTerminalStatus;
use crate::events::TrackEventRequest; use crate::events::TrackEventRequest;
#[cfg(debug_assertions)]
use crate::events::codex_artifact_operation_event_request;
use crate::facts::AnalyticsFact; use crate::facts::AnalyticsFact;
#[cfg(debug_assertions)]
use crate::facts::ArtifactOperation;
#[cfg(debug_assertions)]
use crate::facts::ArtifactOperationLifecycle;
use crate::facts::InvocationType; use crate::facts::InvocationType;
#[cfg(debug_assertions)]
use crate::facts::TrackEventsContext;
use codex_app_server_protocol::ApprovalsReviewer as AppServerApprovalsReviewer; use codex_app_server_protocol::ApprovalsReviewer as AppServerApprovalsReviewer;
use codex_app_server_protocol::AskForApproval as AppServerAskForApproval; use codex_app_server_protocol::AskForApproval as AppServerAskForApproval;
use codex_app_server_protocol::ClientRequest; use codex_app_server_protocol::ClientRequest;
@@ -138,6 +146,32 @@ fn sample_skill_track_event(thread_id: &str, plugin_id: Option<&str>) -> TrackEv
}) })
} }
#[cfg(debug_assertions)]
fn sample_artifact_operation_event(thread_id: &str) -> TrackEventRequest {
TrackEventRequest::ArtifactOperation(codex_artifact_operation_event_request(
TrackEventsContext {
model_slug: "gpt-5.1-codex".to_string(),
thread_id: thread_id.to_string(),
turn_id: "turn-1".to_string(),
product_client_id: "codex_desktop".to_string(),
},
ArtifactOperation {
item_id: format!("item-{thread_id}"),
lifecycle: ArtifactOperationLifecycle::Started,
occurred_at_ms: 1,
plugin_id: "presentations@openai-primary-runtime".to_string(),
script_path: "skills/presentations/container_tools/mark_artifact_operation_started.mjs"
.to_string(),
skill: "presentations".to_string(),
artifact_type: "presentation".to_string(),
operation_kind: "create".to_string(),
expected_output_count: 1,
output_format: "pptx".to_string(),
execution_backend: "unified_exec".to_string(),
},
))
}
fn sample_regular_track_event(thread_id: &str) -> TrackEventRequest { fn sample_regular_track_event(thread_id: &str) -> TrackEventRequest {
sample_skill_track_event(thread_id, /*plugin_id*/ None) sample_skill_track_event(thread_id, /*plugin_id*/ None)
} }
@@ -393,6 +427,7 @@ async fn api_key_auth_sends_only_plugin_events_to_codex_backend() {
sample_plugin_used_track_event("plugin-used", Some("sample@test")), sample_plugin_used_track_event("plugin-used", Some("sample@test")),
sample_skill_track_event("plugin-skill", Some("sample@test")), sample_skill_track_event("plugin-skill", Some("sample@test")),
sample_mcp_tool_call_event("plugin-mcp", Some("sample@test")), sample_mcp_tool_call_event("plugin-mcp", Some("sample@test")),
sample_artifact_operation_event("plugin-artifact"),
], ],
) )
.await; .await;
@@ -442,6 +477,11 @@ async fn api_key_auth_sends_only_plugin_events_to_codex_backend() {
"plugin_id": "sample@test", "plugin_id": "sample@test",
"thread_id": "plugin-mcp", "thread_id": "plugin-mcp",
}), }),
serde_json::json!({
"event_type": "codex_artifact_operation",
"plugin_id": "presentations@openai-primary-runtime",
"thread_id": "plugin-artifact",
}),
] ]
); );
+57
View File
@@ -2,6 +2,8 @@ use std::time::Instant;
use crate::facts::AcceptedLineFingerprint; use crate::facts::AcceptedLineFingerprint;
use crate::facts::AppInvocation; use crate::facts::AppInvocation;
use crate::facts::ArtifactOperation;
use crate::facts::ArtifactOperationLifecycle;
use crate::facts::CodexCompactionEvent; use crate::facts::CodexCompactionEvent;
use crate::facts::CodexErrKind; use crate::facts::CodexErrKind;
use crate::facts::CodexGoalEvent; use crate::facts::CodexGoalEvent;
@@ -72,6 +74,7 @@ pub(crate) enum TrackEventRequest {
Goal(Box<CodexGoalEventRequest>), Goal(Box<CodexGoalEventRequest>),
TurnEvent(Box<CodexTurnEventRequest>), TurnEvent(Box<CodexTurnEventRequest>),
TurnSteer(CodexTurnSteerEventRequest), TurnSteer(CodexTurnSteerEventRequest),
ArtifactOperation(CodexArtifactOperationEventRequest),
CommandExecution(CodexCommandExecutionEventRequest), CommandExecution(CodexCommandExecutionEventRequest),
FileChange(CodexFileChangeEventRequest), FileChange(CodexFileChangeEventRequest),
McpToolCall(CodexMcpToolCallEventRequest), McpToolCall(CodexMcpToolCallEventRequest),
@@ -93,6 +96,59 @@ pub(crate) enum TrackEventRequest {
ExternalAgentConfigImportFailure(CodexOnboardingExternalAgentImportFailureEventRequest), ExternalAgentConfigImportFailure(CodexOnboardingExternalAgentImportFailureEventRequest),
} }
#[derive(Serialize)]
pub(crate) struct CodexArtifactOperationEventParams {
pub(crate) thread_id: String,
pub(crate) turn_id: String,
pub(crate) item_id: String,
pub(crate) lifecycle: ArtifactOperationLifecycle,
pub(crate) occurred_at_ms: u64,
pub(crate) product_client_id: String,
pub(crate) runtime: CodexRuntimeMetadata,
pub(crate) model_slug: String,
pub(crate) plugin_id: String,
pub(crate) script_path: String,
pub(crate) skill: String,
pub(crate) artifact_type: String,
pub(crate) operation_kind: String,
pub(crate) expected_output_count: u32,
pub(crate) output_format: String,
pub(crate) execution_backend: String,
}
#[derive(Serialize)]
pub(crate) struct CodexArtifactOperationEventRequest {
pub(crate) event_type: &'static str,
pub(crate) event_params: CodexArtifactOperationEventParams,
}
pub(crate) fn codex_artifact_operation_event_request(
tracking: TrackEventsContext,
operation: ArtifactOperation,
) -> CodexArtifactOperationEventRequest {
CodexArtifactOperationEventRequest {
event_type: "codex_artifact_operation",
event_params: CodexArtifactOperationEventParams {
thread_id: tracking.thread_id,
turn_id: tracking.turn_id,
item_id: operation.item_id,
lifecycle: operation.lifecycle,
occurred_at_ms: operation.occurred_at_ms,
product_client_id: tracking.product_client_id,
runtime: current_runtime_metadata(),
model_slug: tracking.model_slug,
plugin_id: operation.plugin_id,
script_path: operation.script_path,
skill: operation.skill,
artifact_type: operation.artifact_type,
operation_kind: operation.operation_kind,
expected_output_count: operation.expected_output_count,
output_format: operation.output_format,
execution_backend: operation.execution_backend,
},
}
}
impl TrackEventRequest { impl TrackEventRequest {
pub(crate) fn should_send_in_isolated_request(&self) -> bool { pub(crate) fn should_send_in_isolated_request(&self) -> bool {
matches!(self, Self::AcceptedLineFingerprints(_)) matches!(self, Self::AcceptedLineFingerprints(_))
@@ -103,6 +159,7 @@ impl TrackEventRequest {
Self::PluginUsed(event) => event.event_params.plugin.plugin_id.is_some(), Self::PluginUsed(event) => event.event_params.plugin.plugin_id.is_some(),
Self::SkillInvocation(event) => event.event_params.plugin_id.is_some(), Self::SkillInvocation(event) => event.event_params.plugin_id.is_some(),
Self::McpToolCall(event) => event.event_params.plugin_id.is_some(), Self::McpToolCall(event) => event.event_params.plugin_id.is_some(),
Self::ArtifactOperation(event) => !event.event_params.plugin_id.is_empty(),
_ => false, _ => false,
} }
} }
+27
View File
@@ -45,6 +45,27 @@ pub struct TrackEventsContext {
pub product_client_id: String, pub product_client_id: String,
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ArtifactOperationLifecycle {
Started,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ArtifactOperation {
pub item_id: String,
pub lifecycle: ArtifactOperationLifecycle,
pub occurred_at_ms: u64,
pub plugin_id: String,
pub script_path: String,
pub skill: String,
pub artifact_type: String,
pub operation_kind: String,
pub expected_output_count: u32,
pub output_format: String,
pub execution_backend: String,
}
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum CodeModeToolCallFact { pub enum CodeModeToolCallFact {
CellStarted { CellStarted {
@@ -489,6 +510,7 @@ pub(crate) enum AnalyticsFact {
} }
pub(crate) enum CustomAnalyticsFact { pub(crate) enum CustomAnalyticsFact {
ArtifactOperation(ArtifactOperationInput),
CodeModeToolCall(CodeModeToolCallFact), CodeModeToolCall(CodeModeToolCallFact),
SubAgentThreadStarted(SubAgentThreadStartedInput), SubAgentThreadStarted(SubAgentThreadStartedInput),
Compaction(Box<CodexCompactionEvent>), Compaction(Box<CodexCompactionEvent>),
@@ -511,6 +533,11 @@ pub(crate) enum CustomAnalyticsFact {
ExternalAgentConfigImportFailure(ExternalAgentConfigImportFailureInput), ExternalAgentConfigImportFailure(ExternalAgentConfigImportFailureInput),
} }
pub(crate) struct ArtifactOperationInput {
pub tracking: TrackEventsContext,
pub operation: ArtifactOperation,
}
pub(crate) struct SkillInvokedInput { pub(crate) struct SkillInvokedInput {
pub tracking: TrackEventsContext, pub tracking: TrackEventsContext,
pub invocations: Vec<SkillInvocation>, pub invocations: Vec<SkillInvocation>,
+2
View File
@@ -26,6 +26,8 @@ pub use events::GuardianReviewedAction;
pub use facts::AcceptedLineFingerprint; pub use facts::AcceptedLineFingerprint;
pub use facts::AnalyticsJsonRpcError; pub use facts::AnalyticsJsonRpcError;
pub use facts::AppInvocation; pub use facts::AppInvocation;
pub use facts::ArtifactOperation;
pub use facts::ArtifactOperationLifecycle;
pub use facts::CodeModeToolCallFact; pub use facts::CodeModeToolCallFact;
pub use facts::CodeModeToolCallStatus; pub use facts::CodeModeToolCallStatus;
pub use facts::CodexCompactionEvent; pub use facts::CodexCompactionEvent;
+15
View File
@@ -61,6 +61,7 @@ use crate::events::ToolItemTerminalStatus;
use crate::events::TrackEventRequest; use crate::events::TrackEventRequest;
use crate::events::WebSearchActionKind; use crate::events::WebSearchActionKind;
use crate::events::codex_app_metadata; use crate::events::codex_app_metadata;
use crate::events::codex_artifact_operation_event_request;
use crate::events::codex_compaction_event_params; use crate::events::codex_compaction_event_params;
use crate::events::codex_goal_event_params; use crate::events::codex_goal_event_params;
use crate::events::codex_hook_run_metadata; use crate::events::codex_hook_run_metadata;
@@ -74,6 +75,7 @@ use crate::facts::AnalyticsFact;
use crate::facts::AnalyticsJsonRpcError; use crate::facts::AnalyticsJsonRpcError;
use crate::facts::AppMentionedInput; use crate::facts::AppMentionedInput;
use crate::facts::AppUsedInput; use crate::facts::AppUsedInput;
use crate::facts::ArtifactOperationInput;
use crate::facts::CodeModeToolCallFact; use crate::facts::CodeModeToolCallFact;
use crate::facts::CodeModeToolCallStatus; use crate::facts::CodeModeToolCallStatus;
use crate::facts::CodexCompactionEvent; use crate::facts::CodexCompactionEvent;
@@ -550,6 +552,9 @@ impl AnalyticsReducer {
self.ingest_server_request_aborted(completed_at_ms, request_id, out); self.ingest_server_request_aborted(completed_at_ms, request_id, out);
} }
AnalyticsFact::Custom(input) => match input { AnalyticsFact::Custom(input) => match input {
CustomAnalyticsFact::ArtifactOperation(input) => {
self.ingest_artifact_operation(input, out);
}
CustomAnalyticsFact::CodeModeToolCall(input) => { CustomAnalyticsFact::CodeModeToolCall(input) => {
self.ingest_code_mode_tool_call(input, out); self.ingest_code_mode_tool_call(input, out);
} }
@@ -614,6 +619,16 @@ impl AnalyticsReducer {
} }
} }
fn ingest_artifact_operation(
&mut self,
input: ArtifactOperationInput,
out: &mut Vec<TrackEventRequest>,
) {
out.push(TrackEventRequest::ArtifactOperation(
codex_artifact_operation_event_request(input.tracking, input.operation),
));
}
fn ingest_code_mode_tool_call( fn ingest_code_mode_tool_call(
&mut self, &mut self,
input: CodeModeToolCallFact, input: CodeModeToolCallFact,
@@ -0,0 +1,108 @@
use crate::PluginCommandAttribution;
use crate::command_script_arguments;
const PRIMARY_RUNTIME_MARKETPLACE_NAME: &str = "openai-primary-runtime";
const MAX_EXPECTED_OUTPUT_COUNT: u32 = 100;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ArtifactOperation {
pub plugin_name: &'static str,
pub script_path: &'static str,
pub artifact_type: &'static str,
pub operation_kind: &'static str,
pub expected_output_count: u32,
pub output_format: &'static str,
}
struct ArtifactSkill {
plugin_name: &'static str,
script_path: &'static str,
artifact_type: &'static str,
output_formats: &'static [&'static str],
}
const ARTIFACT_SKILLS: &[ArtifactSkill] = &[
ArtifactSkill {
plugin_name: "presentations",
script_path: "skills/presentations/container_tools/mark_artifact_operation_started.mjs",
artifact_type: "presentation",
output_formats: &["ppt", "pptx"],
},
ArtifactSkill {
plugin_name: "documents",
script_path: "skills/documents/container_tools/mark_artifact_operation_started.mjs",
artifact_type: "document",
output_formats: &["doc", "docx"],
},
ArtifactSkill {
plugin_name: "spreadsheets",
script_path: "skills/spreadsheets/container_tools/mark_artifact_operation_started.mjs",
artifact_type: "spreadsheet",
output_formats: &["csv", "tsv", "xls", "xlsm", "xlsx"],
},
ArtifactSkill {
plugin_name: "pdf",
script_path: "skills/pdf/container_tools/mark_artifact_operation_started.mjs",
artifact_type: "pdf",
output_formats: &["pdf"],
},
];
pub fn recognize_artifact_operation(
attribution: Option<&PluginCommandAttribution>,
command: &[String],
) -> Option<ArtifactOperation> {
let attribution = attribution?;
if attribution.plugin_id.marketplace_name != PRIMARY_RUNTIME_MARKETPLACE_NAME {
return None;
}
let skill = ARTIFACT_SKILLS.iter().find(|skill| {
attribution.plugin_id.plugin_name == skill.plugin_name
&& attribution.normalized_relative_path == skill.script_path
})?;
let script_arguments = command_script_arguments(command)?;
let [
operation_kind_flag,
operation_kind,
expected_output_count_flag,
expected_output_count,
output_format_flag,
output_format,
] = script_arguments.as_slice()
else {
return None;
};
if operation_kind_flag != "--operation-kind"
|| expected_output_count_flag != "--expected-output-count"
|| output_format_flag != "--output-format"
{
return None;
}
let operation_kind = match operation_kind.as_str() {
"create" => "create",
"edit" => "edit",
_ => return None,
};
let expected_output_count = expected_output_count.parse::<u32>().ok()?;
if !(1..=MAX_EXPECTED_OUTPUT_COUNT).contains(&expected_output_count) {
return None;
}
let output_format = skill
.output_formats
.iter()
.copied()
.find(|known_format| known_format.eq_ignore_ascii_case(output_format))?;
Some(ArtifactOperation {
plugin_name: skill.plugin_name,
script_path: skill.script_path,
artifact_type: skill.artifact_type,
operation_kind,
expected_output_count,
output_format,
})
}
#[cfg(test)]
#[path = "artifact_operation_tests.rs"]
mod tests;
@@ -0,0 +1,151 @@
use super::*;
use codex_plugin::PluginId;
use pretty_assertions::assert_eq;
#[test]
fn recognizes_supported_artifact_markers() {
for skill in ARTIFACT_SKILLS {
for operation_kind in ["create", "edit"] {
let attribution = attribution(skill.plugin_name, skill.script_path);
assert_eq!(
recognize_artifact_operation(
Some(&attribution),
&marker_command(
skill.script_path,
operation_kind,
"2",
skill.output_formats[0],
),
),
Some(ArtifactOperation {
plugin_name: skill.plugin_name,
script_path: skill.script_path,
artifact_type: skill.artifact_type,
operation_kind,
expected_output_count: 2,
output_format: skill.output_formats[0],
})
);
}
}
}
#[test]
fn rejects_untrusted_mismatched_or_invalid_markers() {
let presentation = &ARTIFACT_SKILLS[0];
let cases = [
(
None,
marker_command(presentation.script_path, "create", "1", "pptx"),
),
(
Some(PluginCommandAttribution {
plugin_id: PluginId::parse("documents@openai-primary-runtime").expect("plugin id"),
normalized_relative_path: presentation.script_path.to_string(),
}),
marker_command(presentation.script_path, "create", "1", "pptx"),
),
(
Some(attribution(
"presentations",
"skills/presentations/container_tools/render_slides.mjs",
)),
marker_command(presentation.script_path, "create", "1", "pptx"),
),
(
Some(attribution(
presentation.plugin_name,
presentation.script_path,
)),
command_with_arguments(presentation.script_path, Vec::new()),
),
(
Some(attribution(
presentation.plugin_name,
presentation.script_path,
)),
command_with_arguments(
presentation.script_path,
marker_arguments("read", "1", "pptx"),
),
),
(
Some(attribution(
presentation.plugin_name,
presentation.script_path,
)),
command_with_arguments(
presentation.script_path,
marker_arguments("create", "0", "pptx"),
),
),
(
Some(attribution(
presentation.plugin_name,
presentation.script_path,
)),
command_with_arguments(
presentation.script_path,
marker_arguments("create", "101", "pptx"),
),
),
(
Some(attribution(
presentation.plugin_name,
presentation.script_path,
)),
command_with_arguments(
presentation.script_path,
marker_arguments("create", "1", "html"),
),
),
];
for (attribution, command) in cases {
assert_eq!(
recognize_artifact_operation(attribution.as_ref(), &command),
None
);
}
}
fn attribution(plugin_name: &str, script_path: &str) -> PluginCommandAttribution {
PluginCommandAttribution {
plugin_id: PluginId::parse(&format!("{plugin_name}@openai-primary-runtime"))
.expect("plugin id"),
normalized_relative_path: script_path.to_string(),
}
}
fn marker_command(
script_path: &str,
operation_kind: &str,
expected_output_count: &str,
output_format: &str,
) -> Vec<String> {
command_with_arguments(
script_path,
marker_arguments(operation_kind, expected_output_count, output_format),
)
}
fn command_with_arguments(script_path: &str, arguments: Vec<String>) -> Vec<String> {
[vec!["node".to_string(), script_path.to_string()], arguments].concat()
}
fn marker_arguments(
operation_kind: &str,
expected_output_count: &str,
output_format: &str,
) -> Vec<String> {
[
"--operation-kind",
operation_kind,
"--expected-output-count",
expected_output_count,
"--output-format",
output_format,
]
.map(str::to_string)
.to_vec()
}
+4
View File
@@ -1,4 +1,5 @@
mod app_mcp_routing; mod app_mcp_routing;
mod artifact_operation;
mod command_migration; mod command_migration;
mod discoverable; mod discoverable;
mod error_subtype; mod error_subtype;
@@ -43,6 +44,8 @@ pub type LoadedPlugin = codex_plugin::LoadedPlugin<codex_config::McpServerConfig
pub type PluginLoadOutcome = codex_plugin::PluginLoadOutcome<codex_config::McpServerConfig>; pub type PluginLoadOutcome = codex_plugin::PluginLoadOutcome<codex_config::McpServerConfig>;
pub use app_mcp_routing::apps_route_available; pub use app_mcp_routing::apps_route_available;
pub use artifact_operation::ArtifactOperation;
pub use artifact_operation::recognize_artifact_operation;
pub use command_migration::CommandDescriptionMode; pub use command_migration::CommandDescriptionMode;
pub use command_migration::CommandMigrationProfile; pub use command_migration::CommandMigrationProfile;
pub use command_migration::RewriteProfile as CommandRewriteProfile; pub use command_migration::RewriteProfile as CommandRewriteProfile;
@@ -78,3 +81,4 @@ pub use remote::RecommendedPlugin;
pub use remote::RecommendedPluginsMode; pub use remote::RecommendedPluginsMode;
pub use script_attribution::PluginCommandAttribution; pub use script_attribution::PluginCommandAttribution;
pub use script_attribution::TrustedPluginRoots; pub use script_attribution::TrustedPluginRoots;
pub use script_attribution::command_script_arguments;
@@ -488,13 +488,34 @@ fn managed_local_marketplace_name(codex_home: &Path, root: &Path) -> Option<&'st
} }
} }
let runtime_root = dirs::cache_dir()? let runtime_root = primary_runtime_marketplace_root()?;
.join("codex-runtimes/codex-primary-runtime/plugins")
.join(OPENAI_PRIMARY_RUNTIME_MARKETPLACE_NAME);
paths_match_after_normalization(root, &runtime_root) paths_match_after_normalization(root, &runtime_root)
.then_some(OPENAI_PRIMARY_RUNTIME_MARKETPLACE_NAME) .then_some(OPENAI_PRIMARY_RUNTIME_MARKETPLACE_NAME)
} }
pub(crate) fn primary_runtime_marketplace_root() -> Option<PathBuf> {
Some(
primary_runtime_cache_dir()?
.join("codex-runtimes/codex-primary-runtime/plugins")
.join(OPENAI_PRIMARY_RUNTIME_MARKETPLACE_NAME),
)
}
#[cfg(target_os = "windows")]
fn primary_runtime_cache_dir() -> Option<PathBuf> {
primary_runtime_cache_dir_from_user_profile(std::env::var_os("USERPROFILE").map(PathBuf::from))
}
#[cfg(target_os = "windows")]
fn primary_runtime_cache_dir_from_user_profile(user_profile: Option<PathBuf>) -> Option<PathBuf> {
user_profile.map(|profile| profile.join(".cache"))
}
#[cfg(not(target_os = "windows"))]
fn primary_runtime_cache_dir() -> Option<PathBuf> {
dirs::cache_dir()
}
fn git_hostname(url: &str) -> Option<String> { fn git_hostname(url: &str) -> Option<String> {
if let Ok(url) = Url::parse(url) { if let Ok(url) = Url::parse(url) {
return url.host_str().map(str::to_ascii_lowercase); return url.host_str().map(str::to_ascii_lowercase);
@@ -7,12 +7,23 @@ use codex_config::RequirementsLayerEntry;
use codex_config::compose_requirements; use codex_config::compose_requirements;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use std::fs; use std::fs;
#[cfg(target_os = "windows")]
use std::path::PathBuf;
use tempfile::TempDir; use tempfile::TempDir;
fn config_layer_stack(requirements_toml: &str) -> ConfigLayerStack { fn config_layer_stack(requirements_toml: &str) -> ConfigLayerStack {
config_layer_stack_with_user_config(requirements_toml, /*user_config*/ None) config_layer_stack_with_user_config(requirements_toml, /*user_config*/ None)
} }
#[cfg(target_os = "windows")]
#[test]
fn primary_runtime_cache_uses_user_profile_on_windows() {
assert_eq!(
primary_runtime_cache_dir_from_user_profile(Some(PathBuf::from(r"C:\Users\user"))),
Some(PathBuf::from(r"C:\Users\user\.cache"))
);
}
fn config_layer_stack_with_user_config( fn config_layer_stack_with_user_config(
requirements_toml: &str, requirements_toml: &str,
user_config: Option<(&str, AbsolutePathBuf)>, user_config: Option<(&str, AbsolutePathBuf)>,
+221 -54
View File
@@ -1,20 +1,26 @@
use crate::OPENAI_API_CURATED_MARKETPLACE_NAME; use crate::OPENAI_API_CURATED_MARKETPLACE_NAME;
use crate::OPENAI_CURATED_MARKETPLACE_NAME; use crate::OPENAI_CURATED_MARKETPLACE_NAME;
use crate::OPENAI_PRIMARY_RUNTIME_MARKETPLACE_NAME;
use crate::PluginLoadOutcome; use crate::PluginLoadOutcome;
use crate::loader::curated_plugin_cache_version; use crate::loader::curated_plugin_cache_version;
use crate::marketplace::load_marketplace; use crate::marketplace::MarketplacePluginSource;
use crate::marketplace::find_marketplace_plugin;
use crate::marketplace_policy::primary_runtime_marketplace_root;
use crate::remote::REMOTE_GLOBAL_MARKETPLACE_NAME; use crate::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;
use crate::startup_sync::curated_plugins_api_marketplace_path; use crate::startup_sync::curated_plugins_api_marketplace_path;
use crate::startup_sync::curated_plugins_repo_path; use crate::startup_sync::curated_plugins_repo_path;
use crate::startup_sync::read_curated_plugins_sha; use crate::startup_sync::read_curated_plugins_sha;
use crate::store::DEFAULT_PLUGIN_VERSION; use crate::store::DEFAULT_PLUGIN_VERSION;
use crate::store::PluginStore; use crate::store::PluginStore;
use crate::store::plugin_version_for_source;
use codex_exec_server::ExecutorFileSystem;
use codex_plugin::PluginId; use codex_plugin::PluginId;
use codex_protocol::items::is_safe_plugin_relative_path; use codex_protocol::items::is_safe_plugin_relative_path;
use codex_shell_command::bash::extract_bash_command; use codex_shell_command::bash::extract_bash_command;
use codex_shell_command::bash::parse_shell_lc_plain_commands; use codex_shell_command::bash::parse_shell_lc_plain_commands;
use codex_shell_command::parse_command::is_pathish; use codex_shell_command::parse_command::is_pathish;
use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use std::collections::HashSet; use std::collections::HashSet;
use std::path::Component; use std::path::Component;
use std::path::Path; use std::path::Path;
@@ -43,8 +49,8 @@ impl PluginCommandAttribution {
} }
/// Active first-party roots eligible for command attribution. /// Active first-party roots eligible for command attribution.
/// Trusted means OpenAI-shipped synced code or a server-installed global /// Trusted means OpenAI-shipped synced or bundled runtime code, or a
/// remote plugin cache entry, not a local override. /// server-installed global remote plugin cache entry, not a local override.
#[derive(Clone, Debug, Default, PartialEq, Eq)] #[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TrustedPluginRoots { pub struct TrustedPluginRoots {
roots: Vec<TrustedPluginRoot>, roots: Vec<TrustedPluginRoot>,
@@ -52,6 +58,7 @@ pub struct TrustedPluginRoots {
impl TrustedPluginRoots { impl TrustedPluginRoots {
pub fn from_plugin_load_outcome(loaded_plugins: &PluginLoadOutcome, codex_home: &Path) -> Self { pub fn from_plugin_load_outcome(loaded_plugins: &PluginLoadOutcome, codex_home: &Path) -> Self {
let primary_runtime_marketplace_root = primary_runtime_marketplace_root();
let Ok(store) = PluginStore::try_new(codex_home.to_path_buf()) else { let Ok(store) = PluginStore::try_new(codex_home.to_path_buf()) else {
return Self::default(); return Self::default();
}; };
@@ -62,44 +69,12 @@ impl TrustedPluginRoots {
.filter(|plugin| plugin.is_active()) .filter(|plugin| plugin.is_active())
.filter_map(|plugin| { .filter_map(|plugin| {
let plugin_id = PluginId::parse(&plugin.config_name).ok()?; let plugin_id = PluginId::parse(&plugin.config_name).ok()?;
let expected_root = match plugin_id.marketplace_name.as_str() { let expected_root = Self::expected_plugin_root(
REMOTE_GLOBAL_MARKETPLACE_NAME => { &store,
let active_version = store.active_plugin_version(&plugin_id)?; codex_home,
if active_version == DEFAULT_PLUGIN_VERSION &plugin_id,
|| store.remote_plugin_id(&plugin_id).ok().flatten().is_none() primary_runtime_marketplace_root.as_deref(),
{ )?;
return None;
}
store.plugin_root(&plugin_id, &active_version)
}
OPENAI_CURATED_MARKETPLACE_NAME | OPENAI_API_CURATED_MARKETPLACE_NAME => {
let curated_sha = read_curated_plugins_sha(codex_home)?;
let expected_root = store
.plugin_root(&plugin_id, &curated_plugin_cache_version(&curated_sha));
let marketplace_path = match plugin_id.marketplace_name.as_str() {
OPENAI_CURATED_MARKETPLACE_NAME => {
curated_plugins_repo_path(codex_home)
.join(".agents/plugins/marketplace.json")
}
OPENAI_API_CURATED_MARKETPLACE_NAME => {
curated_plugins_api_marketplace_path(codex_home)
}
_ => return None,
};
let marketplace_path = AbsolutePathBuf::try_from(marketplace_path).ok()?;
let marketplace = load_marketplace(&marketplace_path).ok()?;
if marketplace.name != plugin_id.marketplace_name
|| !marketplace
.plugins
.iter()
.any(|plugin| plugin.name == plugin_id.plugin_name)
{
return None;
}
expected_root
}
_ => return None,
};
if plugin.root != expected_root || !expected_root.as_path().is_dir() { if plugin.root != expected_root || !expected_root.as_path().is_dir() {
return None; return None;
} }
@@ -113,6 +88,74 @@ impl TrustedPluginRoots {
Self { roots } Self { roots }
} }
fn expected_plugin_root(
store: &PluginStore,
codex_home: &Path,
plugin_id: &PluginId,
primary_runtime_marketplace_root: Option<&Path>,
) -> Option<AbsolutePathBuf> {
match plugin_id.marketplace_name.as_str() {
REMOTE_GLOBAL_MARKETPLACE_NAME => {
let active_version = store.active_plugin_version(plugin_id)?;
if active_version == DEFAULT_PLUGIN_VERSION
|| store.remote_plugin_id(plugin_id).ok().flatten().is_none()
{
return None;
}
Some(store.plugin_root(plugin_id, &active_version))
}
OPENAI_CURATED_MARKETPLACE_NAME | OPENAI_API_CURATED_MARKETPLACE_NAME => {
let curated_sha = read_curated_plugins_sha(codex_home)?;
let expected_root =
store.plugin_root(plugin_id, &curated_plugin_cache_version(&curated_sha));
let marketplace_path = match plugin_id.marketplace_name.as_str() {
OPENAI_CURATED_MARKETPLACE_NAME => curated_plugins_repo_path(codex_home)
.join(".agents/plugins/marketplace.json"),
OPENAI_API_CURATED_MARKETPLACE_NAME => {
curated_plugins_api_marketplace_path(codex_home)
}
_ => return None,
};
Self::marketplace_plugin(marketplace_path, plugin_id)?;
Some(expected_root)
}
OPENAI_PRIMARY_RUNTIME_MARKETPLACE_NAME => {
let marketplace_root = primary_runtime_marketplace_root?;
let expected_source_root = AbsolutePathBuf::from_absolute_path_checked(
marketplace_root
.join("plugins")
.join(&plugin_id.plugin_name),
)
.ok()?;
let marketplace_plugin = Self::marketplace_plugin(
marketplace_root.join(".agents/plugins/marketplace.json"),
plugin_id,
)?;
let MarketplacePluginSource::Local { path: source_root } =
marketplace_plugin.source
else {
return None;
};
if source_root != expected_source_root {
return None;
}
let plugin_version = plugin_version_for_source(source_root.as_path()).ok()?;
Some(store.plugin_root(plugin_id, &plugin_version))
}
_ => None,
}
}
fn marketplace_plugin(
marketplace_path: std::path::PathBuf,
plugin_id: &PluginId,
) -> Option<crate::marketplace::ResolvedMarketplacePlugin> {
let marketplace_path =
AbsolutePathBuf::from_absolute_path_checked(marketplace_path).ok()?;
let plugin = find_marketplace_plugin(&marketplace_path, &plugin_id.plugin_name).ok()?;
(plugin.plugin_id == *plugin_id).then_some(plugin)
}
/// Resolves one exact command to one trusted plugin script. /// Resolves one exact command to one trusted plugin script.
/// ///
/// Complex shell syntax, missing files, symlink escapes, and overlapping /// Complex shell syntax, missing files, symlink escapes, and overlapping
@@ -123,11 +166,11 @@ impl TrustedPluginRoots {
cwd: &AbsolutePathBuf, cwd: &AbsolutePathBuf,
) -> Option<PluginCommandAttribution> { ) -> Option<PluginCommandAttribution> {
let command = single_plain_command(command)?; let command = single_plain_command(command)?;
let script = script_argument(command.as_slice())?; let invocation = script_invocation(command.as_slice())?;
let script = if Path::new(script).is_absolute() { let script = if Path::new(invocation.script).is_absolute() {
AbsolutePathBuf::from_absolute_path_checked(script).ok()? AbsolutePathBuf::from_absolute_path_checked(invocation.script).ok()?
} else { } else {
cwd.join(script) cwd.join(invocation.script)
} }
.canonicalize() .canonicalize()
.ok()?; .ok()?;
@@ -149,6 +192,115 @@ impl TrustedPluginRoots {
let attribution = matches.next()?; let attribution = matches.next()?;
matches.next().is_none().then_some(attribution) matches.next().is_none().then_some(attribution)
} }
/// Resolves a trusted script on the selected executor filesystem.
///
/// Remote commands can use a path convention that the app-server host cannot
/// canonicalize. Match the target-native path to one trusted local plugin
/// script, then require the executor-side file to have the same contents.
pub async fn resolve_executor_attribution(
&self,
command: &[String],
cwd: &PathUri,
file_system: &dyn ExecutorFileSystem,
) -> Option<PluginCommandAttribution> {
let command = single_plain_command(command)?;
let invocation = script_invocation(command.as_slice())?;
let script = cwd.join(invocation.script).ok()?;
let candidate = self.local_candidate_for_executor_script(&script)?;
let script = file_system
.canonicalize(&script, /*sandbox*/ None)
.await
.ok()?;
if !executor_plugin_root_matches(&script, &candidate.attribution) {
return None;
}
let metadata = file_system
.get_metadata(&script, /*sandbox*/ None)
.await
.ok()?;
if !metadata.is_file || metadata.size != candidate.contents.len() as u64 {
return None;
}
let contents = file_system
.read_file(&script, /*sandbox*/ None)
.await
.ok()?;
(contents == candidate.contents).then_some(candidate.attribution)
}
fn local_candidate_for_executor_script(
&self,
script: &PathUri,
) -> Option<ExecutorAttributionCandidate> {
let suffixes = normalized_script_suffixes(script);
let mut matches = self.roots.iter().filter_map(|root| {
let (script, normalized_relative_path) = suffixes.iter().find_map(|suffix| {
let script = root.root.join(suffix).canonicalize().ok()?;
let relative_path = script.as_path().strip_prefix(root.root.as_path()).ok()?;
if !script.as_path().is_file() {
return None;
}
let normalized_relative_path = normalized_relative_script_path(relative_path)?;
Some((script, normalized_relative_path))
})?;
Some(ExecutorAttributionCandidate {
attribution: PluginCommandAttribution {
plugin_id: root.plugin_id.clone(),
normalized_relative_path,
},
contents: std::fs::read(script.as_path()).ok()?,
})
});
let candidate = matches.next()?;
matches.next().is_none().then_some(candidate)
}
}
struct ExecutorAttributionCandidate {
attribution: PluginCommandAttribution,
contents: Vec<u8>,
}
fn normalized_script_suffixes(script: &PathUri) -> Vec<String> {
let path = script.inferred_native_path_string().replace('\\', "/");
let components = path
.split('/')
.filter(|component| !component.is_empty())
.collect::<Vec<_>>();
(0..components.len())
.filter_map(|start| {
let suffix = components[start..].join("/");
is_safe_plugin_relative_path(&suffix).then_some(suffix)
})
.collect()
}
fn executor_plugin_root_matches(script: &PathUri, attribution: &PluginCommandAttribution) -> bool {
let relative_depth = attribution.normalized_relative_path.split('/').count();
let Some(root) = script.ancestors().nth(relative_depth) else {
return false;
};
let path = root.inferred_native_path_string().replace('\\', "/");
let components = path
.split('/')
.filter(|component| !component.is_empty())
.collect::<Vec<_>>();
let [.., plugins, cache, marketplace, plugin, version] = components.as_slice() else {
return false;
};
*plugins == "plugins"
&& *cache == "cache"
&& *marketplace == attribution.plugin_id.marketplace_name.as_str()
&& *plugin == attribution.plugin_id.plugin_name.as_str()
&& !version.is_empty()
}
/// Returns the structurally parsed arguments following a single script command.
/// Callers must keep the values in-process and must not log or serialize them.
pub fn command_script_arguments(command: &[String]) -> Option<Vec<String>> {
let command = single_plain_command(command)?;
Some(script_invocation(command.as_slice())?.arguments.to_vec())
} }
/// Converts a path already proven to be below a trusted plugin root into the /// Converts a path already proven to be below a trusted plugin root into the
@@ -186,14 +338,22 @@ fn single_plain_command(command: &[String]) -> Option<Vec<String>> {
Some(command.to_vec()) Some(command.to_vec())
} }
fn script_argument(command: &[String]) -> Option<&str> { struct ScriptInvocation<'a> {
script: &'a str,
arguments: &'a [String],
}
fn script_invocation(command: &[String]) -> Option<ScriptInvocation<'_>> {
let [program, args @ ..] = command else { let [program, args @ ..] = command else {
return None; return None;
}; };
if let Some(interpreter) = interpreter_name(program) { if let Some(interpreter) = interpreter_name(program) {
return interpreter_script_argument(&interpreter, args); return interpreter_script_invocation(&interpreter, args);
} }
is_pathish(program).then_some(program) is_pathish(program).then_some(ScriptInvocation {
script: program,
arguments: args,
})
} }
fn interpreter_name(program: &str) -> Option<String> { fn interpreter_name(program: &str) -> Option<String> {
@@ -218,25 +378,32 @@ fn interpreter_name(program: &str) -> Option<String> {
.then(|| basename.to_string()) .then(|| basename.to_string())
} }
fn interpreter_script_argument<'a>(interpreter: &str, args: &'a [String]) -> Option<&'a str> { fn interpreter_script_invocation<'a>(
interpreter: &str,
args: &'a [String],
) -> Option<ScriptInvocation<'a>> {
if matches!(interpreter, "powershell" | "pwsh") { if matches!(interpreter, "powershell" | "pwsh") {
let [file_flag, script, ..] = args else { let [file_flag, script, arguments @ ..] = args else {
return None; return None;
}; };
return (file_flag.eq_ignore_ascii_case("-file") && !script.starts_with('-')) return (file_flag.eq_ignore_ascii_case("-file") && !script.starts_with('-'))
.then_some(script); .then_some(ScriptInvocation { script, arguments });
} }
let mut args = args; let mut args = args;
loop { loop {
match args { match args {
[separator, script, ..] if separator == "--" && !script.starts_with('-') => { [separator, script, arguments @ ..]
return Some(script); if separator == "--" && !script.starts_with('-') =>
{
return Some(ScriptInvocation { script, arguments });
} }
[flag, remaining @ ..] if safe_interpreter_flag(interpreter, flag) => { [flag, remaining @ ..] if safe_interpreter_flag(interpreter, flag) => {
args = remaining; args = remaining;
} }
[script, ..] if !script.starts_with('-') => return Some(script), [script, arguments @ ..] if !script.starts_with('-') => {
return Some(ScriptInvocation { script, arguments });
}
_ => return None, _ => return None,
} }
} }
@@ -10,6 +10,7 @@ use crate::test_support::write_curated_plugin_sha_with;
use crate::test_support::write_openai_api_curated_marketplace; use crate::test_support::write_openai_api_curated_marketplace;
use crate::test_support::write_openai_curated_marketplace; use crate::test_support::write_openai_curated_marketplace;
use codex_plugin::PluginLoadOutcome; use codex_plugin::PluginLoadOutcome;
use codex_utils_path_uri::PathUri;
use codex_utils_plugins::SkillDiscoveryMode; use codex_utils_plugins::SkillDiscoveryMode;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use std::collections::HashMap; use std::collections::HashMap;
@@ -93,12 +94,152 @@ fn script_fixture() -> (TempDir, AbsolutePathBuf, AbsolutePathBuf) {
let script = script.canonicalize().expect("canonical script"); let script = script.canonicalize().expect("canonical script");
(temp, root, script) (temp, root, script)
} }
#[test]
fn resolves_primary_runtime_scripts_from_the_installed_plugin_cache() {
let temp = TempDir::new().expect("temp dir");
let marketplace_root = temp.path().join("openai-primary-runtime");
let source_root = marketplace_root.join("plugins/presentations");
fs::create_dir_all(source_root.join(".codex-plugin")).expect("create manifest directory");
fs::write(
source_root.join(".codex-plugin/plugin.json"),
r#"{"name":"presentations","version":"0.1.29"}"#,
)
.expect("write plugin manifest");
let relative_script_path =
"skills/presentations/container_tools/mark_artifact_operation_started.mjs";
let source_script = source_root.join(relative_script_path);
fs::create_dir_all(source_script.parent().expect("script parent")).expect("create scripts");
fs::write(&source_script, "#!/usr/bin/env node\n").expect("write script");
fs::create_dir_all(marketplace_root.join(".agents/plugins"))
.expect("create marketplace directory");
fs::write(
marketplace_root.join(".agents/plugins/marketplace.json"),
r#"{
"name": "openai-primary-runtime",
"plugins": [
{
"name": "presentations",
"source": {
"source": "local",
"path": "./plugins/presentations"
}
}
]
}"#,
)
.expect("write marketplace manifest");
let plugin_id = PluginId::parse("presentations@openai-primary-runtime").expect("plugin id");
let plugin_root = PluginStore::new(temp.path().to_path_buf())
.install(path(&source_root), plugin_id.clone())
.expect("install plugin")
.installed_path;
let script = plugin_root.join(relative_script_path);
let store = PluginStore::new(temp.path().to_path_buf());
assert_eq!(
TrustedPluginRoots::expected_plugin_root(
&store,
temp.path(),
&plugin_id,
Some(&marketplace_root),
),
Some(plugin_root.clone())
);
let roots = TrustedPluginRoots {
roots: vec![TrustedPluginRoot {
plugin_id: plugin_id.clone(),
root: plugin_root.canonicalize().expect("canonical plugin root"),
}],
};
assert_eq!(
roots.resolve_attribution(
&command(&[
"node",
script.to_string_lossy().as_ref(),
"--operation-kind",
"create",
]),
&path(temp.path()),
),
Some(PluginCommandAttribution {
plugin_id,
normalized_relative_path: relative_script_path.to_string(),
})
);
}
fn roots_for(codex_home: &Path, plugins: Vec<LoadedPlugin>) -> TrustedPluginRoots { fn roots_for(codex_home: &Path, plugins: Vec<LoadedPlugin>) -> TrustedPluginRoots {
TrustedPluginRoots::from_plugin_load_outcome( TrustedPluginRoots::from_plugin_load_outcome(
&PluginLoadOutcome::from_plugins(plugins), &PluginLoadOutcome::from_plugins(plugins),
codex_home, codex_home,
) )
} }
#[tokio::test]
async fn resolves_relocated_script_through_executor_filesystem() {
let (temp, root, script) = script_fixture();
let roots = roots_for(
temp.path(),
vec![loaded_plugin(
"sample@openai-curated",
root.as_path(),
ENABLED,
)],
);
let executor = TempDir::new().expect("executor temp dir");
let executor_root = executor
.path()
.join("plugins/cache/openai-curated/sample/remote-version");
let executor_script = executor_root.join("scripts/run.py");
fs::create_dir_all(executor_script.parent().expect("script parent"))
.expect("create executor scripts");
fs::copy(script.as_path(), &executor_script).expect("copy script to executor");
let cwd = PathUri::from_host_native_path(&executor_root).expect("executor root URI");
let environment =
codex_exec_server::Environment::create_for_tests(/*exec_server_url*/ None)
.expect("local executor environment");
assert_eq!(
roots
.resolve_executor_attribution(
&command(&["python", "scripts/run.py"]),
&cwd,
environment.get_filesystem().as_ref(),
)
.await,
Some(PluginCommandAttribution {
plugin_id: PluginId::parse("sample@openai-curated").expect("plugin id"),
normalized_relative_path: "scripts/run.py".to_string(),
})
);
fs::write(&executor_script, "print('modified')\n").expect("modify executor script");
assert_eq!(
roots
.resolve_executor_attribution(
&command(&["python", "scripts/run.py"]),
&cwd,
environment.get_filesystem().as_ref(),
)
.await,
None
);
}
#[test]
fn recognizes_windows_executor_plugin_cache_root() {
let attribution = PluginCommandAttribution {
plugin_id: PluginId::parse("presentations@openai-primary-runtime").expect("plugin id"),
normalized_relative_path:
"skills/presentations/container_tools/mark_artifact_operation_started.mjs".to_string(),
};
let script = PathUri::parse(
"file:///C:/Users/user/.codex/plugins/cache/openai-primary-runtime/presentations/0.1.29/skills/presentations/container_tools/mark_artifact_operation_started.mjs",
)
.expect("Windows script URI");
assert!(executor_plugin_root_matches(&script, &attribution));
}
fn assert_untrusted(codex_home: &Path, config_name: &str, root: &Path) { fn assert_untrusted(codex_home: &Path, config_name: &str, root: &Path) {
assert!( assert!(
roots_for(codex_home, vec![loaded_plugin(config_name, root, ENABLED)]) roots_for(codex_home, vec![loaded_plugin(config_name, root, ENABLED)])
@@ -243,6 +384,17 @@ fn resolves_local_attribution_for_safe_interpreters_and_wrappers() {
] { ] {
assert_eq!(roots.resolve_attribution(&command, &root), expected); assert_eq!(roots.resolve_attribution(&command, &root), expected);
} }
let wrapped_command = command(&[
"bash",
"-lc",
&format!("node {script} --operation-kind create"),
]);
assert_eq!(roots.resolve_attribution(&wrapped_command, &root), expected);
assert_eq!(
command_script_arguments(&wrapped_command),
Some(command(&["--operation-kind", "create"]))
);
} }
#[test] #[test]
+13
View File
@@ -5,6 +5,7 @@ use crate::shell_snapshot::ShellSnapshotFile;
use crate::tools::sandboxing::executor_windows_sandbox_level; use crate::tools::sandboxing::executor_windows_sandbox_level;
use codex_core_plugins::PluginCommandAttribution; use codex_core_plugins::PluginCommandAttribution;
use codex_core_plugins::TrustedPluginRoots; use codex_core_plugins::TrustedPluginRoots;
use codex_exec_server::ExecutorFileSystem;
use codex_file_system::FileSystemSandboxContext; use codex_file_system::FileSystemSandboxContext;
use codex_model_provider::SharedModelProvider; use codex_model_provider::SharedModelProvider;
use codex_protocol::SessionId; use codex_protocol::SessionId;
@@ -205,6 +206,18 @@ impl TurnContext {
.resolve_attribution(command, cwd) .resolve_attribution(command, cwd)
} }
pub(crate) async fn plugin_attribution_for_executor_command(
&self,
command: &[String],
cwd: &PathUri,
file_system: &dyn ExecutorFileSystem,
) -> Option<PluginCommandAttribution> {
self.extension_data
.get::<TrustedPluginRoots>()?
.resolve_executor_attribution(command, cwd, file_system)
.await
}
pub(crate) fn approval_policy(&self) -> AskForApproval { pub(crate) fn approval_policy(&self) -> AskForApproval {
self.config.permissions.approval_policy.value() self.config.permissions.approval_policy.value()
} }
+52
View File
@@ -3,8 +3,14 @@ use crate::session::session::Session;
use crate::session::turn_context::TurnContext; use crate::session::turn_context::TurnContext;
use crate::tools::context::SharedTurnDiffTracker; use crate::tools::context::SharedTurnDiffTracker;
use crate::tools::sandboxing::ToolError; use crate::tools::sandboxing::ToolError;
use codex_analytics::ArtifactOperation;
use codex_analytics::ArtifactOperationLifecycle;
use codex_analytics::build_track_events_context;
use codex_apply_patch::AppliedPatchDelta; use codex_apply_patch::AppliedPatchDelta;
use codex_core_plugins::PluginCommandAttribution; use codex_core_plugins::PluginCommandAttribution;
use codex_core_plugins::recognize_artifact_operation;
use codex_otel::ARTIFACT_OPERATION_EXPECTED_OUTPUT_COUNT_METRIC;
use codex_otel::ARTIFACT_OPERATION_STARTED_METRIC;
use codex_protocol::error::CodexErrorDetails; use codex_protocol::error::CodexErrorDetails;
use codex_protocol::error::SandboxErr; use codex_protocol::error::SandboxErr;
use codex_protocol::exec_output::ExecToolCallOutput; use codex_protocol::exec_output::ExecToolCallOutput;
@@ -101,6 +107,52 @@ fn tracker_update_for_known_delta<'a>(
} }
async fn emit_exec_command_begin(ctx: ToolEventCtx<'_>, exec_input: &ExecCommandInput<'_>) { async fn emit_exec_command_begin(ctx: ToolEventCtx<'_>, exec_input: &ExecCommandInput<'_>) {
if exec_input.source == ExecCommandSource::UnifiedExecStartup
&& let Some(attribution) = exec_input.plugin_attribution
&& let Some(operation) = recognize_artifact_operation(Some(attribution), exec_input.command)
{
let metric_tags = [
("skill", operation.plugin_name),
("artifact_type", operation.artifact_type),
("operation_kind", operation.operation_kind),
("output_format", operation.output_format),
("execution_backend", "unified_exec"),
];
ctx.turn.session_telemetry.counter(
ARTIFACT_OPERATION_STARTED_METRIC,
/*inc*/ 1,
&metric_tags,
);
ctx.turn.session_telemetry.histogram(
ARTIFACT_OPERATION_EXPECTED_OUTPUT_COUNT_METRIC,
i64::from(operation.expected_output_count),
&metric_tags,
);
ctx.session
.services
.analytics_events_client
.track_artifact_operation(
build_track_events_context(
ctx.turn.model_info.slug.clone(),
ctx.session.thread_id.to_string(),
ctx.turn.sub_id.clone(),
ctx.turn.originator.clone(),
),
ArtifactOperation {
item_id: ctx.call_id.to_string(),
lifecycle: ArtifactOperationLifecycle::Started,
occurred_at_ms: codex_analytics::now_unix_millis(),
plugin_id: attribution.plugin_id.as_key(),
script_path: operation.script_path.to_string(),
skill: operation.plugin_name.to_string(),
artifact_type: operation.artifact_type.to_string(),
operation_kind: operation.operation_kind.to_string(),
expected_output_count: operation.expected_output_count,
output_format: operation.output_format.to_string(),
execution_backend: "unified_exec".to_string(),
},
);
}
let (plugin_id, script_path) = plugin_attribution_fields(exec_input.plugin_attribution); let (plugin_id, script_path) = plugin_attribution_fields(exec_input.plugin_attribution);
ctx.session ctx.session
.emit_turn_item_started( .emit_turn_item_started(
@@ -466,12 +466,25 @@ impl UnifiedExecProcessManager {
&context.call_id, &context.call_id,
/*turn_diff_tracker*/ None, /*turn_diff_tracker*/ None,
); );
let plugin_attribution = cwd.to_abs_path().ok().and_then(|cwd| { let plugin_attribution = if request.turn_environment.environment.is_remote() {
let file_system = request.turn_environment.environment.get_filesystem();
context context
.step_context .step_context
.turn .turn
.plugin_attribution_for_command(&request.command, &cwd) .plugin_attribution_for_executor_command(
}); &request.command,
&cwd,
file_system.as_ref(),
)
.await
} else {
cwd.to_abs_path().ok().and_then(|cwd| {
context
.step_context
.turn
.plugin_attribution_for_command(&request.command, &cwd)
})
};
let emitter = ToolEmitter::unified_exec( let emitter = ToolEmitter::unified_exec(
&request.command, &request.command,
cwd.clone(), cwd.clone(),
+3
View File
@@ -1,6 +1,9 @@
pub const TOOL_CALL_COUNT_METRIC: &str = "codex.tool.call"; pub const TOOL_CALL_COUNT_METRIC: &str = "codex.tool.call";
pub const TOOL_CALL_DURATION_METRIC: &str = "codex.tool.call.duration_ms"; pub const TOOL_CALL_DURATION_METRIC: &str = "codex.tool.call.duration_ms";
pub const TOOL_CALL_UNIFIED_EXEC_METRIC: &str = "codex.tool.unified_exec"; pub const TOOL_CALL_UNIFIED_EXEC_METRIC: &str = "codex.tool.unified_exec";
pub const ARTIFACT_OPERATION_STARTED_METRIC: &str = "codex.artifact.operation.started";
pub const ARTIFACT_OPERATION_EXPECTED_OUTPUT_COUNT_METRIC: &str =
"codex.artifact.operation.expected_output_count";
pub const PROCESS_START_METRIC: &str = "codex.process.start"; pub const PROCESS_START_METRIC: &str = "codex.process.start";
pub const API_CALL_COUNT_METRIC: &str = "codex.api_request"; pub const API_CALL_COUNT_METRIC: &str = "codex.api_request";
pub const API_CALL_DURATION_METRIC: &str = "codex.api_request.duration_ms"; pub const API_CALL_DURATION_METRIC: &str = "codex.api_request.duration_ms";