Skip to content

Commit 5893323

Browse files
authored
feat(analytics): add guardian review event schema (#17055)
Just the analytics schema definition for guardian evaluations. No wiring done yet.
1 parent b114781 commit 5893323

7 files changed

Lines changed: 241 additions & 2 deletions

File tree

codex-rs/analytics/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ codex-plugin = { workspace = true }
2020
codex-protocol = { workspace = true }
2121
os_info = { workspace = true }
2222
serde = { workspace = true, features = ["derive"] }
23+
serde_json = { workspace = true }
2324
sha1 = { workspace = true }
2425
tokio = { workspace = true, features = [
2526
"macros",
@@ -29,4 +30,3 @@ tracing = { workspace = true, features = ["log"] }
2930

3031
[dev-dependencies]
3132
pretty_assertions = { workspace = true }
32-
serde_json = { workspace = true }

codex-rs/analytics/src/client.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::events::AppServerRpcTransport;
2+
use crate::events::GuardianReviewEventParams;
23
use crate::events::TrackEventRequest;
34
use crate::events::TrackEventsRequest;
45
use crate::events::current_runtime_metadata;
@@ -151,6 +152,12 @@ impl AnalyticsEventsClient {
151152
));
152153
}
153154

155+
pub fn track_guardian_review(&self, input: GuardianReviewEventParams) {
156+
self.record_fact(AnalyticsFact::Custom(CustomAnalyticsFact::GuardianReview(
157+
Box::new(input),
158+
)));
159+
}
160+
154161
pub fn track_app_mentioned(&self, tracking: TrackEventsContext, mentions: Vec<AppInvocation>) {
155162
if mentions.is_empty() {
156163
return;

codex-rs/analytics/src/events.rs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ use crate::facts::SubAgentThreadStartedInput;
66
use crate::facts::TrackEventsContext;
77
use codex_login::default_client::originator;
88
use codex_plugin::PluginTelemetryMetadata;
9+
use codex_protocol::approvals::NetworkApprovalProtocol;
10+
use codex_protocol::models::PermissionProfile;
11+
use codex_protocol::models::SandboxPermissions;
912
use codex_protocol::protocol::SessionSource;
1013
use codex_protocol::protocol::SubAgentSource;
1114
use serde::Serialize;
@@ -36,6 +39,7 @@ pub(crate) struct TrackEventsRequest {
3639
pub(crate) enum TrackEventRequest {
3740
SkillInvocation(SkillInvocationEventRequest),
3841
ThreadInitialized(ThreadInitializedEvent),
42+
GuardianReview(Box<GuardianReviewEventRequest>),
3943
AppMentioned(CodexAppMentionedEventRequest),
4044
AppUsed(CodexAppUsedEventRequest),
4145
Compaction(Box<CodexCompactionEventRequest>),
@@ -101,6 +105,179 @@ pub(crate) struct ThreadInitializedEvent {
101105
pub(crate) event_params: ThreadInitializedEventParams,
102106
}
103107

108+
#[derive(Serialize)]
109+
pub(crate) struct GuardianReviewEventRequest {
110+
pub(crate) event_type: &'static str,
111+
pub(crate) event_params: GuardianReviewEventPayload,
112+
}
113+
114+
#[derive(Clone, Copy, Debug, Serialize)]
115+
#[serde(rename_all = "snake_case")]
116+
pub enum GuardianReviewDecision {
117+
Approved,
118+
Denied,
119+
Aborted,
120+
}
121+
122+
#[derive(Clone, Copy, Debug, Serialize)]
123+
#[serde(rename_all = "snake_case")]
124+
pub enum GuardianReviewTerminalStatus {
125+
Approved,
126+
Denied,
127+
Aborted,
128+
TimedOut,
129+
FailedClosed,
130+
}
131+
132+
#[derive(Clone, Copy, Debug, Serialize)]
133+
#[serde(rename_all = "snake_case")]
134+
pub enum GuardianReviewFailureReason {
135+
Timeout,
136+
Cancelled,
137+
PromptBuildError,
138+
SessionError,
139+
ParseError,
140+
}
141+
142+
#[derive(Clone, Copy, Debug, Serialize)]
143+
#[serde(rename_all = "snake_case")]
144+
pub enum GuardianReviewSessionKind {
145+
TrunkNew,
146+
TrunkReused,
147+
EphemeralForked,
148+
}
149+
150+
#[derive(Clone, Copy, Debug, Serialize)]
151+
#[serde(rename_all = "lowercase")]
152+
pub enum GuardianReviewRiskLevel {
153+
Low,
154+
Medium,
155+
High,
156+
Critical,
157+
}
158+
159+
#[derive(Clone, Copy, Debug, Serialize)]
160+
#[serde(rename_all = "lowercase")]
161+
pub enum GuardianReviewUserAuthorization {
162+
Unknown,
163+
Low,
164+
Medium,
165+
High,
166+
}
167+
168+
#[derive(Clone, Copy, Debug, Serialize)]
169+
#[serde(rename_all = "lowercase")]
170+
pub enum GuardianReviewOutcome {
171+
Allow,
172+
Deny,
173+
}
174+
175+
#[derive(Clone, Copy, Debug, Serialize)]
176+
#[serde(rename_all = "snake_case")]
177+
pub enum GuardianApprovalRequestSource {
178+
/// Approval requested directly by the main Codex turn.
179+
MainTurn,
180+
/// Approval requested by a delegated subagent and routed through the parent
181+
/// session for guardian review.
182+
DelegatedSubagent,
183+
}
184+
185+
#[derive(Clone, Debug, Serialize)]
186+
#[serde(tag = "type", rename_all = "snake_case")]
187+
pub enum GuardianReviewedAction {
188+
Shell {
189+
command: Vec<String>,
190+
command_display: String,
191+
cwd: String,
192+
sandbox_permissions: SandboxPermissions,
193+
additional_permissions: Option<PermissionProfile>,
194+
justification: Option<String>,
195+
},
196+
UnifiedExec {
197+
command: Vec<String>,
198+
command_display: String,
199+
cwd: String,
200+
sandbox_permissions: SandboxPermissions,
201+
additional_permissions: Option<PermissionProfile>,
202+
justification: Option<String>,
203+
tty: bool,
204+
},
205+
Execve {
206+
source: GuardianCommandSource,
207+
program: String,
208+
argv: Vec<String>,
209+
cwd: String,
210+
additional_permissions: Option<PermissionProfile>,
211+
},
212+
ApplyPatch {
213+
cwd: String,
214+
files: Vec<String>,
215+
},
216+
NetworkAccess {
217+
target: String,
218+
host: String,
219+
protocol: NetworkApprovalProtocol,
220+
port: u16,
221+
},
222+
McpToolCall {
223+
server: String,
224+
tool_name: String,
225+
connector_id: Option<String>,
226+
connector_name: Option<String>,
227+
tool_title: Option<String>,
228+
},
229+
}
230+
231+
#[derive(Clone, Copy, Debug, Serialize)]
232+
#[serde(rename_all = "snake_case")]
233+
pub enum GuardianCommandSource {
234+
Shell,
235+
UnifiedExec,
236+
}
237+
238+
#[derive(Clone, Serialize)]
239+
pub struct GuardianReviewEventParams {
240+
pub thread_id: String,
241+
pub turn_id: String,
242+
pub review_id: String,
243+
pub target_item_id: String,
244+
pub retry_reason: Option<String>,
245+
pub approval_request_source: GuardianApprovalRequestSource,
246+
pub reviewed_action: GuardianReviewedAction,
247+
pub reviewed_action_truncated: bool,
248+
pub decision: GuardianReviewDecision,
249+
pub terminal_status: GuardianReviewTerminalStatus,
250+
pub failure_reason: Option<GuardianReviewFailureReason>,
251+
pub risk_level: Option<GuardianReviewRiskLevel>,
252+
pub user_authorization: Option<GuardianReviewUserAuthorization>,
253+
pub outcome: Option<GuardianReviewOutcome>,
254+
pub rationale: Option<String>,
255+
pub guardian_thread_id: Option<String>,
256+
pub guardian_session_kind: Option<GuardianReviewSessionKind>,
257+
pub guardian_model: Option<String>,
258+
pub guardian_reasoning_effort: Option<String>,
259+
pub had_prior_review_context: Option<bool>,
260+
pub review_timeout_ms: u64,
261+
pub tool_call_count: u64,
262+
pub time_to_first_token_ms: Option<u64>,
263+
pub completion_latency_ms: Option<u64>,
264+
pub started_at: u64,
265+
pub completed_at: Option<u64>,
266+
pub input_tokens: Option<i64>,
267+
pub cached_input_tokens: Option<i64>,
268+
pub output_tokens: Option<i64>,
269+
pub reasoning_output_tokens: Option<i64>,
270+
pub total_tokens: Option<i64>,
271+
}
272+
273+
#[derive(Serialize)]
274+
pub(crate) struct GuardianReviewEventPayload {
275+
pub(crate) app_server_client: CodexAppServerClientMetadata,
276+
pub(crate) runtime: CodexRuntimeMetadata,
277+
#[serde(flatten)]
278+
pub(crate) guardian_review: GuardianReviewEventParams,
279+
}
280+
104281
#[derive(Serialize)]
105282
pub(crate) struct CodexAppMetadata {
106283
pub(crate) connector_id: Option<String>,

codex-rs/analytics/src/facts.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::events::AppServerRpcTransport;
22
use crate::events::CodexRuntimeMetadata;
3+
use crate::events::GuardianReviewEventParams;
34
use codex_app_server_protocol::ClientRequest;
45
use codex_app_server_protocol::ClientResponse;
56
use codex_app_server_protocol::InitializeParams;
@@ -154,6 +155,7 @@ pub(crate) enum AnalyticsFact {
154155
pub(crate) enum CustomAnalyticsFact {
155156
SubAgentThreadStarted(SubAgentThreadStartedInput),
156157
Compaction(Box<CodexCompactionEvent>),
158+
GuardianReview(Box<GuardianReviewEventParams>),
157159
SkillInvoked(SkillInvokedInput),
158160
AppMentioned(AppMentionedInput),
159161
AppUsed(AppUsedInput),

codex-rs/analytics/src/lib.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ mod reducer;
55

66
pub use client::AnalyticsEventsClient;
77
pub use events::AppServerRpcTransport;
8+
pub use events::GuardianApprovalRequestSource;
9+
pub use events::GuardianCommandSource;
10+
pub use events::GuardianReviewDecision;
11+
pub use events::GuardianReviewEventParams;
12+
pub use events::GuardianReviewFailureReason;
13+
pub use events::GuardianReviewOutcome;
14+
pub use events::GuardianReviewRiskLevel;
15+
pub use events::GuardianReviewSessionKind;
16+
pub use events::GuardianReviewTerminalStatus;
17+
pub use events::GuardianReviewUserAuthorization;
18+
pub use events::GuardianReviewedAction;
819
pub use facts::AppInvocation;
920
pub use facts::CodexCompactionEvent;
1021
pub use facts::CompactionImplementation;

codex-rs/analytics/src/reducer.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ use crate::events::CodexCompactionEventRequest;
66
use crate::events::CodexPluginEventRequest;
77
use crate::events::CodexPluginUsedEventRequest;
88
use crate::events::CodexRuntimeMetadata;
9+
use crate::events::GuardianReviewEventParams;
10+
use crate::events::GuardianReviewEventPayload;
11+
use crate::events::GuardianReviewEventRequest;
912
use crate::events::SkillInvocationEventParams;
1013
use crate::events::SkillInvocationEventRequest;
1114
use crate::events::ThreadInitializationMode;
@@ -120,6 +123,9 @@ impl AnalyticsReducer {
120123
CustomAnalyticsFact::Compaction(input) => {
121124
self.ingest_compaction(*input, out);
122125
}
126+
CustomAnalyticsFact::GuardianReview(input) => {
127+
self.ingest_guardian_review(*input, out);
128+
}
123129
CustomAnalyticsFact::SkillInvoked(input) => {
124130
self.ingest_skill_invoked(input, out).await;
125131
}
@@ -174,6 +180,42 @@ impl AnalyticsReducer {
174180
));
175181
}
176182

183+
fn ingest_guardian_review(
184+
&mut self,
185+
input: GuardianReviewEventParams,
186+
out: &mut Vec<TrackEventRequest>,
187+
) {
188+
let Some(connection_id) = self.thread_connections.get(&input.thread_id) else {
189+
tracing::warn!(
190+
thread_id = %input.thread_id,
191+
turn_id = %input.turn_id,
192+
review_id = %input.review_id,
193+
"dropping guardian analytics event: missing thread connection metadata"
194+
);
195+
return;
196+
};
197+
let Some(connection_state) = self.connections.get(connection_id) else {
198+
tracing::warn!(
199+
thread_id = %input.thread_id,
200+
turn_id = %input.turn_id,
201+
review_id = %input.review_id,
202+
connection_id,
203+
"dropping guardian analytics event: missing connection metadata"
204+
);
205+
return;
206+
};
207+
out.push(TrackEventRequest::GuardianReview(Box::new(
208+
GuardianReviewEventRequest {
209+
event_type: "codex_guardian_review",
210+
event_params: GuardianReviewEventPayload {
211+
app_server_client: connection_state.app_server_client.clone(),
212+
runtime: connection_state.runtime.clone(),
213+
guardian_review: input,
214+
},
215+
},
216+
)));
217+
}
218+
177219
async fn ingest_skill_invoked(
178220
&mut self,
179221
input: SkillInvokedInput,

codex-rs/core/src/guardian/review_session.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ impl GuardianReviewSessionManager {
414414
let snapshot = state.last_committed_fork_snapshot.as_ref()?;
415415
match &snapshot.initial_history {
416416
InitialHistory::Forked(items) => Some(items.clone()),
417-
InitialHistory::New | InitialHistory::Resumed(_) => None,
417+
_ => None,
418418
}
419419
}
420420

0 commit comments

Comments
 (0)