mirror of
https://github.com/openai/codex.git
synced 2026-11-07 16:35:32 +00:00
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:
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();
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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)>,
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
Reference in new issue
Block a user