mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
4816b89204
## Why
`PermissionProfile` is becoming the canonical permissions abstraction,
but the old shape only carried optional filesystem and network fields.
It could describe allowed access, but not who is responsible for
enforcing it. That made `DangerFullAccess` and `ExternalSandbox` lossy
when profiles were exported, cached, or round-tripped through app-server
APIs.
The important model change is that active permissions are now a disjoint
union over the enforcement mode. Conceptually:
```rust
pub enum PermissionProfile {
Managed {
file_system: FileSystemSandboxPolicy,
network: NetworkSandboxPolicy,
},
Disabled,
External {
network: NetworkSandboxPolicy,
},
}
```
This distinction matters because `Disabled` means Codex should apply no
outer sandbox at all, while `External` means filesystem isolation is
owned by an outside caller. Those are not equivalent to a broad managed
sandbox. For example, macOS cannot nest Seatbelt inside Seatbelt, so an
inner sandbox may require the outer Codex layer to use no sandbox rather
than a permissive one.
## How Existing Modeling Maps
Legacy `SandboxPolicy` remains a boundary projection, but it now maps
into the higher-fidelity profile model:
- `ReadOnly` and `WorkspaceWrite` map to `PermissionProfile::Managed`
with restricted filesystem entries plus the corresponding network
policy.
- `DangerFullAccess` maps to `PermissionProfile::Disabled`, preserving
the “no outer sandbox” intent instead of treating it as a lax managed
sandbox.
- `ExternalSandbox { network_access }` maps to
`PermissionProfile::External { network }`, preserving external
filesystem enforcement while still carrying the active network policy.
- Split runtime policies that legacy `SandboxPolicy` cannot faithfully
express, such as managed unrestricted filesystem plus restricted
network, stay `Managed` instead of being collapsed into
`ExternalSandbox`.
- Per-command/session/turn grants remain partial overlays via
`AdditionalPermissionProfile`; full `PermissionProfile` is reserved for
complete active runtime permissions.
## What Changed
- Change active `PermissionProfile` into a tagged union: `managed`,
`disabled`, and `external`.
- Keep partial permission grants separate with
`AdditionalPermissionProfile` for command/session/turn overlays.
- Represent managed filesystem permissions as either `restricted`
entries or `unrestricted`; `glob_scan_max_depth` is non-zero when
present.
- Preserve old rollout compatibility by accepting the pre-tagged `{
network, file_system }` profile shape during deserialization.
- Preserve fidelity for important edge cases: `DangerFullAccess`
round-trips as `disabled`, `ExternalSandbox` round-trips as `external`,
and managed unrestricted filesystem + restricted network stays managed
instead of being mistaken for external enforcement.
- Preserve configured deny-read entries and bounded glob scan depth when
full profiles are projected back into runtime policies, including
unrestricted replacements that now become `:root = write` plus deny
entries.
- Regenerate the experimental app-server v2 JSON/TypeScript schema and
update the `command/exec` README example for the tagged
`permissionProfile` shape.
## Compatibility
Legacy `SandboxPolicy` remains available at config/API boundaries as the
compatibility projection. Existing rollout lines with the old
`PermissionProfile` shape continue to load. The app-server
`permissionProfile` field is experimental, so its v2 wire shape is
intentionally updated to match the higher-fidelity model.
## Verification
- `just write-app-server-schema`
- `cargo check --tests`
- `cargo test -p codex-protocol permission_profile`
- `cargo test -p codex-protocol
preserving_deny_entries_keeps_unrestricted_policy_enforceable`
- `cargo test -p codex-app-server-protocol
permission_profile_file_system_permissions`
- `cargo test -p codex-app-server-protocol serialize_client_response`
- `cargo test -p codex-core
session_configured_reports_permission_profile_for_external_sandbox`
- `just fix`
- `just fix -p codex-protocol`
- `just fix -p codex-app-server-protocol`
- `just fix -p codex-core`
- `just fix -p codex-app-server`
758 lines
26 KiB
Rust
758 lines
26 KiB
Rust
use std::time::Instant;
|
|
|
|
use crate::facts::AppInvocation;
|
|
use crate::facts::CodexCompactionEvent;
|
|
use crate::facts::CompactionImplementation;
|
|
use crate::facts::CompactionPhase;
|
|
use crate::facts::CompactionReason;
|
|
use crate::facts::CompactionStatus;
|
|
use crate::facts::CompactionStrategy;
|
|
use crate::facts::CompactionTrigger;
|
|
use crate::facts::HookRunFact;
|
|
use crate::facts::InvocationType;
|
|
use crate::facts::PluginState;
|
|
use crate::facts::SubAgentThreadStartedInput;
|
|
use crate::facts::ThreadInitializationMode;
|
|
use crate::facts::TrackEventsContext;
|
|
use crate::facts::TurnStatus;
|
|
use crate::facts::TurnSteerRejectionReason;
|
|
use crate::facts::TurnSteerResult;
|
|
use crate::facts::TurnSubmissionType;
|
|
use crate::now_unix_seconds;
|
|
use codex_app_server_protocol::CodexErrorInfo;
|
|
use codex_login::default_client::originator;
|
|
use codex_plugin::PluginTelemetryMetadata;
|
|
use codex_protocol::approvals::NetworkApprovalProtocol;
|
|
use codex_protocol::models::AdditionalPermissionProfile;
|
|
use codex_protocol::models::SandboxPermissions;
|
|
use codex_protocol::protocol::GuardianAssessmentOutcome;
|
|
use codex_protocol::protocol::GuardianCommandSource;
|
|
use codex_protocol::protocol::GuardianRiskLevel;
|
|
use codex_protocol::protocol::GuardianUserAuthorization;
|
|
use codex_protocol::protocol::HookEventName;
|
|
use codex_protocol::protocol::HookRunStatus;
|
|
use codex_protocol::protocol::HookSource;
|
|
use codex_protocol::protocol::SubAgentSource;
|
|
use codex_protocol::protocol::TokenUsage;
|
|
use serde::Serialize;
|
|
|
|
#[derive(Clone, Copy, Debug, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum AppServerRpcTransport {
|
|
Stdio,
|
|
Websocket,
|
|
InProcess,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct TrackEventsRequest {
|
|
pub(crate) events: Vec<TrackEventRequest>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
#[serde(untagged)]
|
|
pub(crate) enum TrackEventRequest {
|
|
SkillInvocation(SkillInvocationEventRequest),
|
|
ThreadInitialized(ThreadInitializedEvent),
|
|
GuardianReview(Box<GuardianReviewEventRequest>),
|
|
AppMentioned(CodexAppMentionedEventRequest),
|
|
AppUsed(CodexAppUsedEventRequest),
|
|
HookRun(CodexHookRunEventRequest),
|
|
Compaction(Box<CodexCompactionEventRequest>),
|
|
TurnEvent(Box<CodexTurnEventRequest>),
|
|
TurnSteer(CodexTurnSteerEventRequest),
|
|
PluginUsed(CodexPluginUsedEventRequest),
|
|
PluginInstalled(CodexPluginEventRequest),
|
|
PluginUninstalled(CodexPluginEventRequest),
|
|
PluginEnabled(CodexPluginEventRequest),
|
|
PluginDisabled(CodexPluginEventRequest),
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct SkillInvocationEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) skill_id: String,
|
|
pub(crate) skill_name: String,
|
|
pub(crate) event_params: SkillInvocationEventParams,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct SkillInvocationEventParams {
|
|
pub(crate) product_client_id: Option<String>,
|
|
pub(crate) skill_scope: Option<String>,
|
|
pub(crate) repo_url: Option<String>,
|
|
pub(crate) thread_id: Option<String>,
|
|
pub(crate) invoke_type: Option<InvocationType>,
|
|
pub(crate) model_slug: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Serialize)]
|
|
pub(crate) struct CodexAppServerClientMetadata {
|
|
pub(crate) product_client_id: String,
|
|
pub(crate) client_name: Option<String>,
|
|
pub(crate) client_version: Option<String>,
|
|
pub(crate) rpc_transport: AppServerRpcTransport,
|
|
pub(crate) experimental_api_enabled: Option<bool>,
|
|
}
|
|
|
|
#[derive(Clone, Serialize)]
|
|
pub(crate) struct CodexRuntimeMetadata {
|
|
pub(crate) codex_rs_version: String,
|
|
pub(crate) runtime_os: String,
|
|
pub(crate) runtime_os_version: String,
|
|
pub(crate) runtime_arch: String,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct ThreadInitializedEventParams {
|
|
pub(crate) thread_id: String,
|
|
pub(crate) app_server_client: CodexAppServerClientMetadata,
|
|
pub(crate) runtime: CodexRuntimeMetadata,
|
|
pub(crate) model: String,
|
|
pub(crate) ephemeral: bool,
|
|
pub(crate) thread_source: Option<&'static str>,
|
|
pub(crate) initialization_mode: ThreadInitializationMode,
|
|
pub(crate) subagent_source: Option<String>,
|
|
pub(crate) parent_thread_id: Option<String>,
|
|
pub(crate) created_at: u64,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct ThreadInitializedEvent {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: ThreadInitializedEventParams,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct GuardianReviewEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: GuardianReviewEventPayload,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum GuardianReviewDecision {
|
|
Approved,
|
|
Denied,
|
|
Aborted,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum GuardianReviewTerminalStatus {
|
|
Approved,
|
|
Denied,
|
|
Aborted,
|
|
TimedOut,
|
|
FailedClosed,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum GuardianReviewFailureReason {
|
|
Timeout,
|
|
Cancelled,
|
|
PromptBuildError,
|
|
SessionError,
|
|
ParseError,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum GuardianReviewSessionKind {
|
|
TrunkNew,
|
|
TrunkReused,
|
|
EphemeralForked,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum GuardianApprovalRequestSource {
|
|
/// Approval requested directly by the main Codex turn.
|
|
MainTurn,
|
|
/// Approval requested by a delegated subagent and routed through the parent
|
|
/// session for guardian review.
|
|
DelegatedSubagent,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize)]
|
|
#[serde(tag = "type", rename_all = "snake_case")]
|
|
pub enum GuardianReviewedAction {
|
|
Shell {
|
|
sandbox_permissions: SandboxPermissions,
|
|
additional_permissions: Option<AdditionalPermissionProfile>,
|
|
},
|
|
UnifiedExec {
|
|
sandbox_permissions: SandboxPermissions,
|
|
additional_permissions: Option<AdditionalPermissionProfile>,
|
|
tty: bool,
|
|
},
|
|
Execve {
|
|
source: GuardianCommandSource,
|
|
program: String,
|
|
additional_permissions: Option<AdditionalPermissionProfile>,
|
|
},
|
|
ApplyPatch {},
|
|
NetworkAccess {
|
|
protocol: NetworkApprovalProtocol,
|
|
port: u16,
|
|
},
|
|
McpToolCall {
|
|
server: String,
|
|
tool_name: String,
|
|
connector_id: Option<String>,
|
|
connector_name: Option<String>,
|
|
tool_title: Option<String>,
|
|
},
|
|
RequestPermissions {},
|
|
}
|
|
|
|
#[derive(Clone, Serialize)]
|
|
pub struct GuardianReviewEventParams {
|
|
pub thread_id: String,
|
|
pub turn_id: String,
|
|
pub review_id: String,
|
|
pub target_item_id: Option<String>,
|
|
pub approval_request_source: GuardianApprovalRequestSource,
|
|
pub reviewed_action: GuardianReviewedAction,
|
|
pub reviewed_action_truncated: bool,
|
|
pub decision: GuardianReviewDecision,
|
|
pub terminal_status: GuardianReviewTerminalStatus,
|
|
pub failure_reason: Option<GuardianReviewFailureReason>,
|
|
pub risk_level: Option<GuardianRiskLevel>,
|
|
pub user_authorization: Option<GuardianUserAuthorization>,
|
|
pub outcome: Option<GuardianAssessmentOutcome>,
|
|
pub guardian_thread_id: Option<String>,
|
|
pub guardian_session_kind: Option<GuardianReviewSessionKind>,
|
|
pub guardian_model: Option<String>,
|
|
pub guardian_reasoning_effort: Option<String>,
|
|
pub had_prior_review_context: Option<bool>,
|
|
pub review_timeout_ms: u64,
|
|
pub tool_call_count: Option<u64>,
|
|
pub time_to_first_token_ms: Option<u64>,
|
|
pub completion_latency_ms: Option<u64>,
|
|
pub started_at: u64,
|
|
pub completed_at: Option<u64>,
|
|
pub input_tokens: Option<i64>,
|
|
pub cached_input_tokens: Option<i64>,
|
|
pub output_tokens: Option<i64>,
|
|
pub reasoning_output_tokens: Option<i64>,
|
|
pub total_tokens: Option<i64>,
|
|
}
|
|
|
|
pub struct GuardianReviewTrackContext {
|
|
thread_id: String,
|
|
turn_id: String,
|
|
review_id: String,
|
|
target_item_id: Option<String>,
|
|
approval_request_source: GuardianApprovalRequestSource,
|
|
reviewed_action: GuardianReviewedAction,
|
|
review_timeout_ms: u64,
|
|
started_at: u64,
|
|
started_instant: Instant,
|
|
}
|
|
|
|
impl GuardianReviewTrackContext {
|
|
pub fn new(
|
|
thread_id: String,
|
|
turn_id: String,
|
|
review_id: String,
|
|
target_item_id: Option<String>,
|
|
approval_request_source: GuardianApprovalRequestSource,
|
|
reviewed_action: GuardianReviewedAction,
|
|
review_timeout_ms: u64,
|
|
) -> Self {
|
|
Self {
|
|
thread_id,
|
|
turn_id,
|
|
review_id,
|
|
target_item_id,
|
|
approval_request_source,
|
|
reviewed_action,
|
|
review_timeout_ms,
|
|
started_at: now_unix_seconds(),
|
|
started_instant: Instant::now(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn event_params(
|
|
&self,
|
|
result: GuardianReviewAnalyticsResult,
|
|
) -> GuardianReviewEventParams {
|
|
GuardianReviewEventParams {
|
|
thread_id: self.thread_id.clone(),
|
|
turn_id: self.turn_id.clone(),
|
|
review_id: self.review_id.clone(),
|
|
target_item_id: self.target_item_id.clone(),
|
|
approval_request_source: self.approval_request_source,
|
|
reviewed_action: self.reviewed_action.clone(),
|
|
reviewed_action_truncated: result.reviewed_action_truncated,
|
|
decision: result.decision,
|
|
terminal_status: result.terminal_status,
|
|
failure_reason: result.failure_reason,
|
|
risk_level: result.risk_level,
|
|
user_authorization: result.user_authorization,
|
|
outcome: result.outcome,
|
|
guardian_thread_id: result.guardian_thread_id,
|
|
guardian_session_kind: result.guardian_session_kind,
|
|
guardian_model: result.guardian_model,
|
|
guardian_reasoning_effort: result.guardian_reasoning_effort,
|
|
had_prior_review_context: result.had_prior_review_context,
|
|
review_timeout_ms: self.review_timeout_ms,
|
|
// TODO(rhan-oai): plumb nested Guardian review session tool-call counts.
|
|
tool_call_count: None,
|
|
time_to_first_token_ms: result.time_to_first_token_ms,
|
|
completion_latency_ms: Some(self.started_instant.elapsed().as_millis() as u64),
|
|
started_at: self.started_at,
|
|
completed_at: Some(now_unix_seconds()),
|
|
input_tokens: result.token_usage.as_ref().map(|usage| usage.input_tokens),
|
|
cached_input_tokens: result
|
|
.token_usage
|
|
.as_ref()
|
|
.map(|usage| usage.cached_input_tokens),
|
|
output_tokens: result.token_usage.as_ref().map(|usage| usage.output_tokens),
|
|
reasoning_output_tokens: result
|
|
.token_usage
|
|
.as_ref()
|
|
.map(|usage| usage.reasoning_output_tokens),
|
|
total_tokens: result.token_usage.as_ref().map(|usage| usage.total_tokens),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct GuardianReviewAnalyticsResult {
|
|
pub decision: GuardianReviewDecision,
|
|
pub terminal_status: GuardianReviewTerminalStatus,
|
|
pub failure_reason: Option<GuardianReviewFailureReason>,
|
|
pub risk_level: Option<GuardianRiskLevel>,
|
|
pub user_authorization: Option<GuardianUserAuthorization>,
|
|
pub outcome: Option<GuardianAssessmentOutcome>,
|
|
pub guardian_thread_id: Option<String>,
|
|
pub guardian_session_kind: Option<GuardianReviewSessionKind>,
|
|
pub guardian_model: Option<String>,
|
|
pub guardian_reasoning_effort: Option<String>,
|
|
pub had_prior_review_context: Option<bool>,
|
|
pub reviewed_action_truncated: bool,
|
|
pub token_usage: Option<TokenUsage>,
|
|
pub time_to_first_token_ms: Option<u64>,
|
|
}
|
|
|
|
impl GuardianReviewAnalyticsResult {
|
|
pub fn without_session() -> Self {
|
|
Self {
|
|
decision: GuardianReviewDecision::Denied,
|
|
terminal_status: GuardianReviewTerminalStatus::FailedClosed,
|
|
failure_reason: None,
|
|
risk_level: None,
|
|
user_authorization: None,
|
|
outcome: None,
|
|
guardian_thread_id: None,
|
|
guardian_session_kind: None,
|
|
guardian_model: None,
|
|
guardian_reasoning_effort: None,
|
|
had_prior_review_context: None,
|
|
reviewed_action_truncated: false,
|
|
token_usage: None,
|
|
time_to_first_token_ms: None,
|
|
}
|
|
}
|
|
|
|
pub fn from_session(
|
|
guardian_thread_id: String,
|
|
guardian_session_kind: GuardianReviewSessionKind,
|
|
guardian_model: String,
|
|
guardian_reasoning_effort: Option<String>,
|
|
had_prior_review_context: bool,
|
|
) -> Self {
|
|
Self {
|
|
guardian_thread_id: Some(guardian_thread_id),
|
|
guardian_session_kind: Some(guardian_session_kind),
|
|
guardian_model: Some(guardian_model),
|
|
guardian_reasoning_effort,
|
|
had_prior_review_context: Some(had_prior_review_context),
|
|
..Self::without_session()
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct GuardianReviewEventPayload {
|
|
pub(crate) app_server_client: CodexAppServerClientMetadata,
|
|
pub(crate) runtime: CodexRuntimeMetadata,
|
|
#[serde(flatten)]
|
|
pub(crate) guardian_review: GuardianReviewEventParams,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexAppMetadata {
|
|
pub(crate) connector_id: Option<String>,
|
|
pub(crate) thread_id: Option<String>,
|
|
pub(crate) turn_id: Option<String>,
|
|
pub(crate) app_name: Option<String>,
|
|
pub(crate) product_client_id: Option<String>,
|
|
pub(crate) invoke_type: Option<InvocationType>,
|
|
pub(crate) model_slug: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexAppMentionedEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: CodexAppMetadata,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexAppUsedEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: CodexAppMetadata,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexHookRunMetadata {
|
|
pub(crate) thread_id: Option<String>,
|
|
pub(crate) turn_id: Option<String>,
|
|
pub(crate) model_slug: Option<String>,
|
|
pub(crate) hook_name: Option<String>,
|
|
pub(crate) hook_source: Option<&'static str>,
|
|
pub(crate) status: Option<HookRunStatus>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexHookRunEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: CodexHookRunMetadata,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexCompactionEventParams {
|
|
pub(crate) thread_id: String,
|
|
pub(crate) turn_id: String,
|
|
pub(crate) app_server_client: CodexAppServerClientMetadata,
|
|
pub(crate) runtime: CodexRuntimeMetadata,
|
|
pub(crate) thread_source: Option<&'static str>,
|
|
pub(crate) subagent_source: Option<String>,
|
|
pub(crate) parent_thread_id: Option<String>,
|
|
pub(crate) trigger: CompactionTrigger,
|
|
pub(crate) reason: CompactionReason,
|
|
pub(crate) implementation: CompactionImplementation,
|
|
pub(crate) phase: CompactionPhase,
|
|
pub(crate) strategy: CompactionStrategy,
|
|
pub(crate) status: CompactionStatus,
|
|
pub(crate) error: Option<String>,
|
|
pub(crate) active_context_tokens_before: i64,
|
|
pub(crate) active_context_tokens_after: i64,
|
|
pub(crate) started_at: u64,
|
|
pub(crate) completed_at: u64,
|
|
pub(crate) duration_ms: Option<u64>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexCompactionEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: CodexCompactionEventParams,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexTurnEventParams {
|
|
pub(crate) thread_id: String,
|
|
pub(crate) turn_id: String,
|
|
// TODO(rhan-oai): Populate once queued/default submission type is plumbed from
|
|
// the turn/start callsites instead of always being reported as None.
|
|
pub(crate) submission_type: Option<TurnSubmissionType>,
|
|
pub(crate) app_server_client: CodexAppServerClientMetadata,
|
|
pub(crate) runtime: CodexRuntimeMetadata,
|
|
pub(crate) ephemeral: bool,
|
|
pub(crate) thread_source: Option<String>,
|
|
pub(crate) initialization_mode: ThreadInitializationMode,
|
|
pub(crate) subagent_source: Option<String>,
|
|
pub(crate) parent_thread_id: Option<String>,
|
|
pub(crate) model: Option<String>,
|
|
pub(crate) model_provider: String,
|
|
pub(crate) sandbox_policy: Option<&'static str>,
|
|
pub(crate) reasoning_effort: Option<String>,
|
|
pub(crate) reasoning_summary: Option<String>,
|
|
pub(crate) service_tier: String,
|
|
pub(crate) approval_policy: String,
|
|
pub(crate) approvals_reviewer: String,
|
|
pub(crate) sandbox_network_access: bool,
|
|
pub(crate) collaboration_mode: Option<&'static str>,
|
|
pub(crate) personality: Option<String>,
|
|
pub(crate) num_input_images: usize,
|
|
pub(crate) is_first_turn: bool,
|
|
pub(crate) status: Option<TurnStatus>,
|
|
pub(crate) turn_error: Option<CodexErrorInfo>,
|
|
pub(crate) steer_count: Option<usize>,
|
|
// TODO(rhan-oai): Populate these once tool-call accounting is emitted from
|
|
// core; the schema is reserved but these fields are currently always None.
|
|
pub(crate) total_tool_call_count: Option<usize>,
|
|
pub(crate) shell_command_count: Option<usize>,
|
|
pub(crate) file_change_count: Option<usize>,
|
|
pub(crate) mcp_tool_call_count: Option<usize>,
|
|
pub(crate) dynamic_tool_call_count: Option<usize>,
|
|
pub(crate) subagent_tool_call_count: Option<usize>,
|
|
pub(crate) web_search_count: Option<usize>,
|
|
pub(crate) image_generation_count: Option<usize>,
|
|
pub(crate) input_tokens: Option<i64>,
|
|
pub(crate) cached_input_tokens: Option<i64>,
|
|
pub(crate) output_tokens: Option<i64>,
|
|
pub(crate) reasoning_output_tokens: Option<i64>,
|
|
pub(crate) total_tokens: Option<i64>,
|
|
pub(crate) duration_ms: Option<u64>,
|
|
pub(crate) started_at: Option<u64>,
|
|
pub(crate) completed_at: Option<u64>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexTurnEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: CodexTurnEventParams,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexTurnSteerEventParams {
|
|
pub(crate) thread_id: String,
|
|
pub(crate) expected_turn_id: Option<String>,
|
|
pub(crate) accepted_turn_id: Option<String>,
|
|
pub(crate) app_server_client: CodexAppServerClientMetadata,
|
|
pub(crate) runtime: CodexRuntimeMetadata,
|
|
pub(crate) thread_source: Option<String>,
|
|
pub(crate) subagent_source: Option<String>,
|
|
pub(crate) parent_thread_id: Option<String>,
|
|
pub(crate) num_input_images: usize,
|
|
pub(crate) result: TurnSteerResult,
|
|
pub(crate) rejection_reason: Option<TurnSteerRejectionReason>,
|
|
pub(crate) created_at: u64,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexTurnSteerEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: CodexTurnSteerEventParams,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexPluginMetadata {
|
|
pub(crate) plugin_id: Option<String>,
|
|
pub(crate) plugin_name: Option<String>,
|
|
pub(crate) marketplace_name: Option<String>,
|
|
pub(crate) has_skills: Option<bool>,
|
|
pub(crate) mcp_server_count: Option<usize>,
|
|
pub(crate) connector_ids: Option<Vec<String>>,
|
|
pub(crate) product_client_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexPluginUsedMetadata {
|
|
#[serde(flatten)]
|
|
pub(crate) plugin: CodexPluginMetadata,
|
|
pub(crate) thread_id: Option<String>,
|
|
pub(crate) turn_id: Option<String>,
|
|
pub(crate) model_slug: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexPluginEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: CodexPluginMetadata,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexPluginUsedEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: CodexPluginUsedMetadata,
|
|
}
|
|
|
|
pub(crate) fn plugin_state_event_type(state: PluginState) -> &'static str {
|
|
match state {
|
|
PluginState::Installed => "codex_plugin_installed",
|
|
PluginState::Uninstalled => "codex_plugin_uninstalled",
|
|
PluginState::Enabled => "codex_plugin_enabled",
|
|
PluginState::Disabled => "codex_plugin_disabled",
|
|
}
|
|
}
|
|
|
|
pub(crate) fn codex_app_metadata(
|
|
tracking: &TrackEventsContext,
|
|
app: AppInvocation,
|
|
) -> CodexAppMetadata {
|
|
CodexAppMetadata {
|
|
connector_id: app.connector_id,
|
|
thread_id: Some(tracking.thread_id.clone()),
|
|
turn_id: Some(tracking.turn_id.clone()),
|
|
app_name: app.app_name,
|
|
product_client_id: Some(originator().value),
|
|
invoke_type: app.invocation_type,
|
|
model_slug: Some(tracking.model_slug.clone()),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn codex_plugin_metadata(plugin: PluginTelemetryMetadata) -> CodexPluginMetadata {
|
|
let capability_summary = plugin.capability_summary;
|
|
CodexPluginMetadata {
|
|
plugin_id: Some(plugin.plugin_id.as_key()),
|
|
plugin_name: Some(plugin.plugin_id.plugin_name),
|
|
marketplace_name: Some(plugin.plugin_id.marketplace_name),
|
|
has_skills: capability_summary
|
|
.as_ref()
|
|
.map(|summary| summary.has_skills),
|
|
mcp_server_count: capability_summary
|
|
.as_ref()
|
|
.map(|summary| summary.mcp_server_names.len()),
|
|
connector_ids: capability_summary.map(|summary| {
|
|
summary
|
|
.app_connector_ids
|
|
.into_iter()
|
|
.map(|connector_id| connector_id.0)
|
|
.collect()
|
|
}),
|
|
product_client_id: Some(originator().value),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn codex_compaction_event_params(
|
|
input: CodexCompactionEvent,
|
|
app_server_client: CodexAppServerClientMetadata,
|
|
runtime: CodexRuntimeMetadata,
|
|
thread_source: Option<&'static str>,
|
|
subagent_source: Option<String>,
|
|
parent_thread_id: Option<String>,
|
|
) -> CodexCompactionEventParams {
|
|
CodexCompactionEventParams {
|
|
thread_id: input.thread_id,
|
|
turn_id: input.turn_id,
|
|
app_server_client,
|
|
runtime,
|
|
thread_source,
|
|
subagent_source,
|
|
parent_thread_id,
|
|
trigger: input.trigger,
|
|
reason: input.reason,
|
|
implementation: input.implementation,
|
|
phase: input.phase,
|
|
strategy: input.strategy,
|
|
status: input.status,
|
|
error: input.error,
|
|
active_context_tokens_before: input.active_context_tokens_before,
|
|
active_context_tokens_after: input.active_context_tokens_after,
|
|
started_at: input.started_at,
|
|
completed_at: input.completed_at,
|
|
duration_ms: input.duration_ms,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn codex_plugin_used_metadata(
|
|
tracking: &TrackEventsContext,
|
|
plugin: PluginTelemetryMetadata,
|
|
) -> CodexPluginUsedMetadata {
|
|
CodexPluginUsedMetadata {
|
|
plugin: codex_plugin_metadata(plugin),
|
|
thread_id: Some(tracking.thread_id.clone()),
|
|
turn_id: Some(tracking.turn_id.clone()),
|
|
model_slug: Some(tracking.model_slug.clone()),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn codex_hook_run_metadata(
|
|
tracking: &TrackEventsContext,
|
|
hook: HookRunFact,
|
|
) -> CodexHookRunMetadata {
|
|
CodexHookRunMetadata {
|
|
thread_id: Some(tracking.thread_id.clone()),
|
|
turn_id: Some(tracking.turn_id.clone()),
|
|
model_slug: Some(tracking.model_slug.clone()),
|
|
hook_name: Some(analytics_hook_event_name(hook.event_name).to_owned()),
|
|
hook_source: Some(analytics_hook_source(hook.hook_source)),
|
|
status: Some(analytics_hook_status(hook.status)),
|
|
}
|
|
}
|
|
|
|
fn analytics_hook_event_name(event_name: HookEventName) -> &'static str {
|
|
match event_name {
|
|
HookEventName::PreToolUse => "PreToolUse",
|
|
HookEventName::PermissionRequest => "PermissionRequest",
|
|
HookEventName::PostToolUse => "PostToolUse",
|
|
HookEventName::SessionStart => "SessionStart",
|
|
HookEventName::UserPromptSubmit => "UserPromptSubmit",
|
|
HookEventName::Stop => "Stop",
|
|
}
|
|
}
|
|
|
|
fn analytics_hook_source(source: HookSource) -> &'static str {
|
|
match source {
|
|
HookSource::System => "system",
|
|
HookSource::User => "user",
|
|
HookSource::Project => "project",
|
|
HookSource::Mdm => "mdm",
|
|
HookSource::SessionFlags => "session_flags",
|
|
HookSource::LegacyManagedConfigFile => "legacy_managed_config_file",
|
|
HookSource::LegacyManagedConfigMdm => "legacy_managed_config_mdm",
|
|
HookSource::Unknown => "unknown",
|
|
}
|
|
}
|
|
|
|
pub(crate) fn current_runtime_metadata() -> CodexRuntimeMetadata {
|
|
let os_info = os_info::get();
|
|
CodexRuntimeMetadata {
|
|
codex_rs_version: env!("CARGO_PKG_VERSION").to_string(),
|
|
runtime_os: std::env::consts::OS.to_string(),
|
|
runtime_os_version: os_info.version().to_string(),
|
|
runtime_arch: std::env::consts::ARCH.to_string(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn subagent_thread_started_event_request(
|
|
input: SubAgentThreadStartedInput,
|
|
) -> ThreadInitializedEvent {
|
|
let event_params = ThreadInitializedEventParams {
|
|
thread_id: input.thread_id,
|
|
app_server_client: CodexAppServerClientMetadata {
|
|
product_client_id: input.product_client_id,
|
|
client_name: Some(input.client_name),
|
|
client_version: Some(input.client_version),
|
|
rpc_transport: AppServerRpcTransport::InProcess,
|
|
experimental_api_enabled: None,
|
|
},
|
|
runtime: current_runtime_metadata(),
|
|
model: input.model,
|
|
ephemeral: input.ephemeral,
|
|
thread_source: Some("subagent"),
|
|
initialization_mode: ThreadInitializationMode::New,
|
|
subagent_source: Some(subagent_source_name(&input.subagent_source)),
|
|
parent_thread_id: input
|
|
.parent_thread_id
|
|
.or_else(|| subagent_parent_thread_id(&input.subagent_source)),
|
|
created_at: input.created_at,
|
|
};
|
|
ThreadInitializedEvent {
|
|
event_type: "codex_thread_initialized",
|
|
event_params,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn subagent_source_name(subagent_source: &SubAgentSource) -> String {
|
|
match subagent_source {
|
|
SubAgentSource::Review => "review".to_string(),
|
|
SubAgentSource::Compact => "compact".to_string(),
|
|
SubAgentSource::ThreadSpawn { .. } => "thread_spawn".to_string(),
|
|
SubAgentSource::MemoryConsolidation => "memory_consolidation".to_string(),
|
|
SubAgentSource::Other(other) => other.clone(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn subagent_parent_thread_id(subagent_source: &SubAgentSource) -> Option<String> {
|
|
match subagent_source {
|
|
SubAgentSource::ThreadSpawn {
|
|
parent_thread_id, ..
|
|
} => Some(parent_thread_id.to_string()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn analytics_hook_status(status: HookRunStatus) -> HookRunStatus {
|
|
match status {
|
|
// Running is unexpected here and normalized defensively.
|
|
HookRunStatus::Running => HookRunStatus::Failed,
|
|
other => other,
|
|
}
|
|
}
|