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 #[serde(default, skip_serializing_if = "Vec::is_empty")]
22 pub revealed_controls: Vec<crate::forms::RevealedControl>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
27#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
28#[serde(rename_all = "camelCase", deny_unknown_fields)]
29pub struct AccessibilityTarget {
30 pub role: String,
31 pub accessible_name: String,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub ordinal: Option<usize>,
34 #[serde(default, skip_serializing_if = "Vec::is_empty")]
39 pub frame_path: Vec<crate::forms::SemanticTargetSegment>,
40}
41
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
47#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
48#[serde(rename_all = "camelCase")]
49pub struct ContextAnswer {
50 pub target: AccessibilityTarget,
51 pub confidence: f32,
52 #[serde(default)]
56 pub observed_at: ContextObservedAt,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub source: Option<ContextAnswerSource>,
61}
62
63impl Default for ContextObservedAt {
64 fn default() -> Self {
65 Self::Generation { generation: 0 }
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
72#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
73#[serde(tag = "kind", rename_all = "camelCase")]
74pub enum ContextObservedAt {
75 Generation { generation: u64 },
76 Persisted,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
81#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
82#[serde(rename_all = "kebab-case")]
83pub enum ContextAnswerSource {
84 Observed,
85 VisionPromoted,
86}
87
88#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
93#[serde(rename_all = "camelCase")]
94pub struct ContextNeighbors {
95 pub answer: ContextAnswer,
96 pub form: String,
98 pub page_pattern: String,
100 pub controls: Vec<ContextNeighborControl>,
101}
102
103#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
104#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
105#[serde(rename_all = "camelCase")]
106pub struct ContextNeighborControl {
107 pub role: String,
108 pub accessible_name: String,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub ordinal: Option<usize>,
111 pub intents: std::collections::BTreeMap<String, ContextNeighborStats>,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
117#[serde(rename_all = "camelCase")]
118pub struct ContextNeighborStats {
119 pub success_count: u64,
120 pub failure_count: u64,
121 #[serde(default, skip_serializing_if = "Option::is_none")]
123 pub last_verified_day: Option<u32>,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
125 pub source: Option<ContextAnswerSource>,
126}
127
128#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
130#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
131#[serde(rename_all = "camelCase")]
132pub struct ContextSiteView {
133 pub site_key: String,
134 pub pages: std::collections::BTreeMap<
135 String,
136 std::collections::BTreeMap<String, Vec<ContextNeighborControl>>,
137 >,
138}
139
140#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
142#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
143#[serde(rename_all = "camelCase", deny_unknown_fields)]
144pub struct AccessibilityNode {
145 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub role: Option<String>,
147 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub name: Option<String>,
149 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub target: Option<AccessibilityTarget>,
151 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub value: Option<String>,
153 #[serde(default, skip_serializing_if = "Option::is_none")]
154 pub description: Option<String>,
155 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub required: Option<bool>,
157 #[serde(default, skip_serializing_if = "Option::is_none")]
158 pub disabled: Option<bool>,
159 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub read_only: Option<bool>,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub invalid: Option<bool>,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub checked: Option<bool>,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub autocomplete: Option<String>,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub url: Option<String>,
171 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub value_min: Option<String>,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub value_max: Option<String>,
175 #[serde(default, skip_serializing_if = "Vec::is_empty")]
176 pub children: Vec<AccessibilityNode>,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
180#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
181#[serde(
182 tag = "status",
183 rename_all = "camelCase",
184 rename_all_fields = "camelCase"
185)]
186pub enum CommandOutcome {
187 Completed {
188 command_id: CommandId,
189 evidence: Vec<Evidence>,
190 },
191 RetryableFailure {
192 command_id: CommandId,
193 error: CommandError,
194 },
195 NeedsReconciliation {
196 command_id: CommandId,
197 error: CommandError,
198 evidence: Vec<Evidence>,
199 },
200 PolicyDenied {
201 command_id: CommandId,
202 error: CommandError,
203 },
204 ResourceExhausted {
205 command_id: CommandId,
206 error: CommandError,
207 retry_after_ms: u64,
208 },
209 Restarted {
210 command_id: CommandId,
211 prior_attempt_id: AttemptId,
212 attempt_id: AttemptId,
213 reason: String,
214 #[serde(default)]
215 evidence: Vec<Evidence>,
216 },
217 Failed {
218 command_id: CommandId,
219 error: CommandError,
220 #[serde(default)]
221 evidence: Vec<Evidence>,
222 },
223}
224
225pub const MAX_WAIT_OBSERVED_CHARS: usize = 512;
232
233#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
234#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
235#[serde(
236 tag = "kind",
237 rename_all = "camelCase",
238 rename_all_fields = "camelCase"
239)]
240pub enum Evidence {
241 ExecutionPath {
242 path: ExecutionPath,
243 reason: ExecutionReason,
244 state_version: u64,
245 elapsed_ms: u64,
246 bytes: Option<u64>,
247 sha256: Option<String>,
248 #[serde(default, skip_serializing_if = "Option::is_none")]
249 final_url: Option<String>,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
251 content_type: Option<String>,
252 #[serde(default, skip_serializing_if = "Option::is_none")]
253 status: Option<u16>,
254 #[serde(default, skip_serializing_if = "Vec::is_empty")]
255 redirect_chain: Vec<String>,
256 },
257 Navigation {
258 url: String,
259 title: String,
260 },
261 Inspection {
262 selector: Option<String>,
263 url: String,
264 title: String,
265 text: String,
266 html: Option<String>,
267 },
268 Element {
269 selector: String,
270 text: Option<String>,
271 },
272 Upload {
273 selector: String,
274 paths: Vec<String>,
275 },
276 Page {
277 page_id: PageId,
278 url: String,
279 title: String,
280 },
281 Pages {
282 pages: Vec<PageEvidence>,
283 },
284 Popup {
285 opener_page_id: PageId,
286 page_id: PageId,
287 url: String,
288 title: String,
289 },
290 Download {
291 filename: String,
292 path: String,
293 bytes: u64,
294 sha256: String,
295 #[serde(default, skip_serializing_if = "Option::is_none")]
299 saved_to: Option<String>,
300 },
301 Configuration {
302 name: String,
303 value: String,
304 },
305 Resolution {
306 target: Box<crate::TargetSpec>,
307 fingerprint: Box<TargetFingerprint>,
308 candidates: Vec<CandidateEvidence>,
309 best_match_authorized: bool,
310 },
311 Wait {
312 condition: crate::WaitCondition,
313 elapsed_ms: u64,
314 observations: u64,
315 #[serde(default, skip_serializing_if = "Vec::is_empty")]
316 excluded_classes: Vec<String>,
317 #[serde(default, skip_serializing_if = "Option::is_none")]
326 observed: Option<String>,
327 },
328 Screenshot {
329 artifact_id: String,
330 media_type: String,
331 width: u32,
332 height: u32,
333 bytes: u64,
334 sha256: String,
335 },
336 BrowserExecution {
337 engine: String,
338 browser_version: String,
339 profile_id: String,
340 interaction_path: String,
341 },
342 JavaScriptResult {
343 value: serde_json::Value,
344 truncated: bool,
345 },
346 AccessibilitySnapshot {
347 page_id: PageId,
348 nodes: Vec<AccessibilityNode>,
349 truncated: bool,
350 },
351 FormSnapshot {
352 snapshot: crate::FormSnapshot,
353 },
354 ControlAction {
355 action: ControlActionEvidence,
356 },
357 StructuredExtraction {
358 page_id: PageId,
359 value: serde_json::Value,
360 truncated: bool,
361 },
362 CookieState {
363 page_id: Option<PageId>,
364 cookies: Vec<crate::CookieRecord>,
365 },
366 PdfArtifact {
367 artifact_id: String,
368 media_type: String,
369 bytes: u64,
370 sha256: String,
371 },
372 Dialog {
373 dialog_type: String,
374 message: String,
375 action: String,
376 },
377 Emulation {
378 viewport: Option<crate::ViewportSize>,
379 geolocation: Option<crate::GeolocationCoordinates>,
380 },
381 HarArtifact {
382 artifact_id: String,
383 media_type: String,
384 bytes: u64,
385 sha256: String,
386 entries: u32,
387 },
388 IntentExecution {
389 record: ExecutionRecord,
390 },
391 Humanization {
396 engine: String,
397 actions: u32,
398 synthesized_ms: u64,
399 },
400 Extraction {
404 field: String,
405 #[serde(default, skip_serializing_if = "Option::is_none")]
406 value: Option<String>,
407 resolution_path: IntentResolutionPath,
408 #[serde(default, skip_serializing_if = "Option::is_none")]
409 error_code: Option<ErrorCode>,
410 },
411}
412
413#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
414#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
415#[serde(rename_all = "camelCase")]
416pub enum IntentResolutionPath {
417 Deterministic,
418 VisionFallback,
419 VisionPrefill,
422}
423
424#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
425#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
426#[serde(rename_all = "camelCase")]
427pub struct ExecutionRecord {
428 pub intent_kind: String,
429 pub purpose: Option<String>,
430 pub resolution_path: IntentResolutionPath,
431 pub plan_summary: String,
432 pub candidates: Vec<CandidateEvidence>,
433 pub wait_elapsed_ms: Option<u64>,
434 pub verification: String,
435 pub artifact_ids: Vec<String>,
436 pub vision_proposal_sha256: Option<String>,
437}
438
439impl Evidence {
440 pub fn journal_safe(&self) -> Self {
441 fn safe_url(value: &str) -> String {
442 let Ok(mut url) = url::Url::parse(value) else {
443 return "[redacted-invalid-url]".into();
444 };
445 let _ = url.set_username("");
446 let _ = url.set_password(None);
447 url.set_query(None);
448 url.set_fragment(None);
449 url.to_string()
450 }
451 let mut safe = self.clone();
452 match &mut safe {
453 Self::ExecutionPath {
454 final_url,
455 redirect_chain,
456 ..
457 } => {
458 if let Some(url) = final_url {
459 *url = safe_url(url);
460 }
461 for url in redirect_chain {
462 *url = safe_url(url);
463 }
464 }
465 Self::Navigation { url, .. }
466 | Self::Inspection { url, .. }
467 | Self::Page { url, .. }
468 | Self::Popup { url, .. } => *url = safe_url(url),
469 Self::Pages { pages } => {
470 for page in pages {
471 page.url = safe_url(&page.url);
472 }
473 }
474 Self::Upload { paths, .. } => {
475 for (index, path) in paths.iter_mut().enumerate() {
476 *path = format!("upload://evidence/{index}");
477 }
478 }
479 Self::Download {
480 path,
481 sha256,
482 saved_to,
483 ..
484 } => {
485 *path = format!("artifact://sha256/{sha256}");
486 *saved_to = None;
487 }
488 Self::BrowserExecution { .. } => {}
489 Self::IntentExecution { .. } => {}
490 Self::Extraction { .. } => {}
491 _ => {}
492 }
493 safe
494 }
495}
496
497impl CommandOutcome {
498 pub fn journal_safe(&self) -> Self {
499 let mut safe = self.clone();
500 match &mut safe {
501 Self::Completed { evidence, .. }
502 | Self::NeedsReconciliation { evidence, .. }
503 | Self::Restarted { evidence, .. }
504 | Self::Failed { evidence, .. } => {
505 *evidence = evidence.iter().map(Evidence::journal_safe).collect();
506 }
507 _ => {}
508 }
509 match &mut safe {
510 Self::RetryableFailure { error, .. }
511 | Self::NeedsReconciliation { error, .. }
512 | Self::PolicyDenied { error, .. }
513 | Self::ResourceExhausted { error, .. }
514 | Self::Failed { error, .. } => error.message = "redacted durable diagnostic".into(),
515 _ => {}
516 }
517 safe
518 }
519}
520
521#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
522#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
523#[serde(rename_all = "camelCase")]
524pub enum ExecutionPath {
525 DirectHttp,
526 Chromium,
527 ChromiumFallback,
528}
529
530#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
531#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
532#[serde(rename_all = "camelCase")]
533pub enum ExecutionReason {
534 EligibleStaticDocument,
535 EligibleExplicitDownload,
536 IneligibleCommand,
537 SemanticTargetRequired,
538 JavascriptRequired,
539 UnsupportedContentType,
540 StateConflict,
541 PolicyRequired,
542 PageMutated,
545}
546
547#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
548#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
549#[serde(rename_all = "camelCase")]
550pub struct CandidateEvidence {
551 pub role: Option<String>,
552 pub name: Option<String>,
553 pub score: i32,
554 pub reasons: Vec<String>,
555}
556
557#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
558#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
559#[serde(rename_all = "camelCase")]
560pub struct TargetFingerprint {
561 pub page_id: PageId,
562 pub frame: Option<String>,
563 pub role: Option<String>,
564 pub name: Option<String>,
565 pub stable_attributes: std::collections::BTreeMap<String, String>,
566}
567
568#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
569#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
570#[serde(rename_all = "camelCase")]
571pub struct PageEvidence {
572 pub page_id: PageId,
573 pub url: String,
574 pub title: String,
575}
576
577#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
578#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
579#[serde(rename_all = "camelCase")]
580pub struct CommandError {
581 pub code: ErrorCode,
582 pub message: String,
583 pub layer: ErrorLayer,
584 pub retryable: bool,
585}
586
587#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
588#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
589#[serde(rename_all = "camelCase")]
590pub enum ErrorCode {
591 InvalidRequest,
592 NotFound,
593 DeadlineExceeded,
594 BrowserLaunchFailed,
595 BrowserCommandFailed,
596 VerificationFailed,
597 JournalFailed,
598 ResourceExhausted,
599 PolicyDenied,
600 Internal,
601 TargetNotFound,
602 TargetAmbiguous,
603 FrameNotFound,
604 ShadowRootUnavailable,
605 TargetDetached,
606 TargetObscured,
607 TargetOutOfBounds,
608 WaitConditionTimedOut,
609 ScreenshotCaptureFailed,
610 NetworkPolicyDenied,
611 HttpResponseTooLarge,
612 HttpTransferFailed,
613 HttpStateConflict,
614 HttpEquivalenceUnproven,
615 IntentCompileFailed,
616 IntentActionMismatch,
617 ObstructionSuspected,
618 VisionAssistDenied,
619 VisionAssistFailed,
620 ExpectedStatePreSatisfied,
623}
624
625#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
626#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
627#[serde(rename_all = "camelCase")]
628pub enum ErrorLayer {
629 Interface,
630 Broker,
631 Workflow,
632 Page,
633 Driver,
634 Browser,
635 Network,
636 Site,
637 Journal,
638}
639
640#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
641#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
642#[serde(rename_all = "camelCase")]
643pub enum CommandPhase {
644 Accepted,
645 Prepared,
646 Executing,
647 ResultPrepared,
648 Verifying,
649 Recovering,
650 Completed,
651 Failed,
652}
653
654#[derive(Debug, Error, Serialize, Deserialize)]
655#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
656pub enum RuntimeError {
657 #[error("not found: {0}")]
658 NotFound(String),
659 #[error("invalid request: {0}")]
660 InvalidRequest(String),
661 #[error("internal error: {0}")]
662 Internal(String),
663}