1use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6use crate::{AttemptId, CommandId, PageId};
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
9#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
10#[serde(rename_all = "camelCase", deny_unknown_fields)]
11pub struct ControlActionEvidence {
12 pub operation: crate::FormControlOperation,
13 pub target: crate::FormControlTarget,
14 pub state: crate::FormControlState,
15 pub validity: crate::FormControlValidity,
16 pub node_replaced: bool,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
22#[serde(rename_all = "camelCase", deny_unknown_fields)]
23pub struct AccessibilityTarget {
24 pub role: String,
25 pub accessible_name: String,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
27 pub ordinal: Option<usize>,
28}
29
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
37#[serde(rename_all = "camelCase")]
38pub struct ContextAnswer {
39 pub target: AccessibilityTarget,
40 pub confidence: f32,
41}
42
43#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
45#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
46#[serde(rename_all = "camelCase", deny_unknown_fields)]
47pub struct AccessibilityNode {
48 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub role: Option<String>,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub name: Option<String>,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub target: Option<AccessibilityTarget>,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub value: Option<String>,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub description: Option<String>,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub required: Option<bool>,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub disabled: Option<bool>,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub read_only: Option<bool>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub invalid: Option<bool>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub checked: Option<bool>,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub autocomplete: Option<String>,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub value_min: Option<String>,
72 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub value_max: Option<String>,
74 #[serde(default, skip_serializing_if = "Vec::is_empty")]
75 pub children: Vec<AccessibilityNode>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
79#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
80#[serde(
81 tag = "status",
82 rename_all = "camelCase",
83 rename_all_fields = "camelCase"
84)]
85pub enum CommandOutcome {
86 Completed {
87 command_id: CommandId,
88 evidence: Vec<Evidence>,
89 },
90 RetryableFailure {
91 command_id: CommandId,
92 error: CommandError,
93 },
94 NeedsReconciliation {
95 command_id: CommandId,
96 error: CommandError,
97 evidence: Vec<Evidence>,
98 },
99 PolicyDenied {
100 command_id: CommandId,
101 error: CommandError,
102 },
103 ResourceExhausted {
104 command_id: CommandId,
105 error: CommandError,
106 retry_after_ms: u64,
107 },
108 Restarted {
109 command_id: CommandId,
110 prior_attempt_id: AttemptId,
111 attempt_id: AttemptId,
112 reason: String,
113 #[serde(default)]
114 evidence: Vec<Evidence>,
115 },
116 Failed {
117 command_id: CommandId,
118 error: CommandError,
119 #[serde(default)]
120 evidence: Vec<Evidence>,
121 },
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
125#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
126#[serde(
127 tag = "kind",
128 rename_all = "camelCase",
129 rename_all_fields = "camelCase"
130)]
131pub enum Evidence {
132 ExecutionPath {
133 path: ExecutionPath,
134 reason: ExecutionReason,
135 state_version: u64,
136 elapsed_ms: u64,
137 bytes: Option<u64>,
138 sha256: Option<String>,
139 #[serde(default, skip_serializing_if = "Option::is_none")]
140 final_url: Option<String>,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
142 content_type: Option<String>,
143 #[serde(default, skip_serializing_if = "Option::is_none")]
144 status: Option<u16>,
145 #[serde(default, skip_serializing_if = "Vec::is_empty")]
146 redirect_chain: Vec<String>,
147 },
148 Navigation {
149 url: String,
150 title: String,
151 },
152 Inspection {
153 selector: Option<String>,
154 url: String,
155 title: String,
156 text: String,
157 html: Option<String>,
158 },
159 Element {
160 selector: String,
161 text: Option<String>,
162 },
163 Upload {
164 selector: String,
165 paths: Vec<String>,
166 },
167 Page {
168 page_id: PageId,
169 url: String,
170 title: String,
171 },
172 Pages {
173 pages: Vec<PageEvidence>,
174 },
175 Popup {
176 opener_page_id: PageId,
177 page_id: PageId,
178 url: String,
179 title: String,
180 },
181 Download {
182 filename: String,
183 path: String,
184 bytes: u64,
185 sha256: String,
186 },
187 Configuration {
188 name: String,
189 value: String,
190 },
191 Resolution {
192 target: Box<crate::TargetSpec>,
193 fingerprint: Box<TargetFingerprint>,
194 candidates: Vec<CandidateEvidence>,
195 best_match_authorized: bool,
196 },
197 Wait {
198 condition: crate::WaitCondition,
199 elapsed_ms: u64,
200 observations: u64,
201 #[serde(default, skip_serializing_if = "Vec::is_empty")]
202 excluded_classes: Vec<String>,
203 },
204 Screenshot {
205 artifact_id: String,
206 media_type: String,
207 width: u32,
208 height: u32,
209 bytes: u64,
210 sha256: String,
211 },
212 BrowserExecution {
213 engine: String,
214 browser_version: String,
215 profile_id: String,
216 interaction_path: String,
217 },
218 JavaScriptResult {
219 value: serde_json::Value,
220 truncated: bool,
221 },
222 AccessibilitySnapshot {
223 page_id: PageId,
224 nodes: Vec<AccessibilityNode>,
225 truncated: bool,
226 },
227 FormSnapshot {
228 snapshot: crate::FormSnapshot,
229 },
230 ControlAction {
231 action: ControlActionEvidence,
232 },
233 StructuredExtraction {
234 page_id: PageId,
235 value: serde_json::Value,
236 truncated: bool,
237 },
238 CookieState {
239 page_id: Option<PageId>,
240 cookies: Vec<crate::CookieRecord>,
241 },
242 PdfArtifact {
243 artifact_id: String,
244 media_type: String,
245 bytes: u64,
246 sha256: String,
247 },
248 Dialog {
249 dialog_type: String,
250 message: String,
251 action: String,
252 },
253 Emulation {
254 viewport: Option<crate::ViewportSize>,
255 geolocation: Option<crate::GeolocationCoordinates>,
256 },
257 HarArtifact {
258 artifact_id: String,
259 media_type: String,
260 bytes: u64,
261 sha256: String,
262 entries: u32,
263 },
264 IntentExecution {
265 record: ExecutionRecord,
266 },
267 Humanization {
279 engine: String,
280 actions: u32,
281 synthesized_ms: u64,
282 },
283 Extraction {
291 field: String,
292 #[serde(default, skip_serializing_if = "Option::is_none")]
293 value: Option<String>,
294 resolution_path: IntentResolutionPath,
295 #[serde(default, skip_serializing_if = "Option::is_none")]
296 error_code: Option<ErrorCode>,
297 },
298}
299
300#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
301#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
302#[serde(rename_all = "camelCase")]
303pub enum IntentResolutionPath {
304 Deterministic,
305 VisionFallback,
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
309#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
310#[serde(rename_all = "camelCase")]
311pub struct ExecutionRecord {
312 pub intent_kind: String,
313 pub purpose: Option<String>,
314 pub resolution_path: IntentResolutionPath,
315 pub plan_summary: String,
316 pub candidates: Vec<CandidateEvidence>,
317 pub wait_elapsed_ms: Option<u64>,
318 pub verification: String,
319 pub artifact_ids: Vec<String>,
320 pub vision_proposal_sha256: Option<String>,
321}
322
323impl Evidence {
324 pub fn journal_safe(&self) -> Self {
325 fn safe_url(value: &str) -> String {
326 let Ok(mut url) = url::Url::parse(value) else {
327 return "[redacted-invalid-url]".into();
328 };
329 let _ = url.set_username("");
330 let _ = url.set_password(None);
331 url.set_query(None);
332 url.set_fragment(None);
333 url.to_string()
334 }
335 let mut safe = self.clone();
336 match &mut safe {
337 Self::ExecutionPath {
338 final_url,
339 redirect_chain,
340 ..
341 } => {
342 if let Some(url) = final_url {
343 *url = safe_url(url);
344 }
345 for url in redirect_chain {
346 *url = safe_url(url);
347 }
348 }
349 Self::Navigation { url, .. }
350 | Self::Inspection { url, .. }
351 | Self::Page { url, .. }
352 | Self::Popup { url, .. } => *url = safe_url(url),
353 Self::Pages { pages } => {
354 for page in pages {
355 page.url = safe_url(&page.url);
356 }
357 }
358 Self::Upload { paths, .. } => {
359 for (index, path) in paths.iter_mut().enumerate() {
360 *path = format!("upload://evidence/{index}");
361 }
362 }
363 Self::Download { path, sha256, .. } => {
364 *path = format!("artifact://sha256/{sha256}");
365 }
366 Self::BrowserExecution { .. } => {}
367 Self::IntentExecution { .. } => {}
368 Self::Extraction { .. } => {}
369 _ => {}
370 }
371 safe
372 }
373}
374
375impl CommandOutcome {
376 pub fn journal_safe(&self) -> Self {
377 let mut safe = self.clone();
378 match &mut safe {
379 Self::Completed { evidence, .. }
380 | Self::NeedsReconciliation { evidence, .. }
381 | Self::Restarted { evidence, .. }
382 | Self::Failed { evidence, .. } => {
383 *evidence = evidence.iter().map(Evidence::journal_safe).collect();
384 }
385 _ => {}
386 }
387 match &mut safe {
388 Self::RetryableFailure { error, .. }
389 | Self::NeedsReconciliation { error, .. }
390 | Self::PolicyDenied { error, .. }
391 | Self::ResourceExhausted { error, .. }
392 | Self::Failed { error, .. } => error.message = "redacted durable diagnostic".into(),
393 _ => {}
394 }
395 safe
396 }
397}
398
399#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
400#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
401#[serde(rename_all = "camelCase")]
402pub enum ExecutionPath {
403 DirectHttp,
404 Chromium,
405 ChromiumFallback,
406}
407
408#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
409#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
410#[serde(rename_all = "camelCase")]
411pub enum ExecutionReason {
412 EligibleStaticDocument,
413 EligibleExplicitDownload,
414 IneligibleCommand,
415 SemanticTargetRequired,
416 JavascriptRequired,
417 UnsupportedContentType,
418 StateConflict,
419 PolicyRequired,
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
423#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
424#[serde(rename_all = "camelCase")]
425pub struct CandidateEvidence {
426 pub role: Option<String>,
427 pub name: Option<String>,
428 pub score: i32,
429 pub reasons: Vec<String>,
430}
431
432#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
433#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
434#[serde(rename_all = "camelCase")]
435pub struct TargetFingerprint {
436 pub page_id: PageId,
437 pub frame: Option<String>,
438 pub role: Option<String>,
439 pub name: Option<String>,
440 pub stable_attributes: std::collections::BTreeMap<String, String>,
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
444#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
445#[serde(rename_all = "camelCase")]
446pub struct PageEvidence {
447 pub page_id: PageId,
448 pub url: String,
449 pub title: String,
450}
451
452#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
453#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
454#[serde(rename_all = "camelCase")]
455pub struct CommandError {
456 pub code: ErrorCode,
457 pub message: String,
458 pub layer: ErrorLayer,
459 pub retryable: bool,
460}
461
462#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
463#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
464#[serde(rename_all = "camelCase")]
465pub enum ErrorCode {
466 InvalidRequest,
467 NotFound,
468 DeadlineExceeded,
469 BrowserLaunchFailed,
470 BrowserCommandFailed,
471 VerificationFailed,
472 JournalFailed,
473 ResourceExhausted,
474 PolicyDenied,
475 Internal,
476 TargetNotFound,
477 TargetAmbiguous,
478 FrameNotFound,
479 ShadowRootUnavailable,
480 TargetDetached,
481 TargetObscured,
482 TargetOutOfBounds,
483 WaitConditionTimedOut,
484 ScreenshotCaptureFailed,
485 NetworkPolicyDenied,
486 HttpResponseTooLarge,
487 HttpTransferFailed,
488 HttpStateConflict,
489 HttpEquivalenceUnproven,
490 IntentCompileFailed,
491 IntentActionMismatch,
492 ObstructionSuspected,
493 VisionAssistDenied,
494 VisionAssistFailed,
495}
496
497#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
498#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
499#[serde(rename_all = "camelCase")]
500pub enum ErrorLayer {
501 Interface,
502 Broker,
503 Workflow,
504 Page,
505 Driver,
506 Browser,
507 Network,
508 Site,
509 Journal,
510}
511
512#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
513#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
514#[serde(rename_all = "camelCase")]
515pub enum CommandPhase {
516 Accepted,
517 Prepared,
518 Executing,
519 ResultPrepared,
520 Verifying,
521 Recovering,
522 Completed,
523 Failed,
524}
525
526#[derive(Debug, Error, Serialize, Deserialize)]
527#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
528pub enum RuntimeError {
529 #[error("not found: {0}")]
530 NotFound(String),
531 #[error("invalid request: {0}")]
532 InvalidRequest(String),
533 #[error("internal error: {0}")]
534 Internal(String),
535}