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, Copy, PartialEq, Eq, Serialize, Deserialize)]
142#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
143pub enum ContextMissReason {
144 #[serde(rename = "notRemembered")]
145 NotRemembered,
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
150#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
151pub enum ContextNextStep {
152 #[serde(rename = "a11y_snapshot")]
153 A11ySnapshot,
154}
155
156#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
158#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
159#[serde(rename_all = "camelCase", deny_unknown_fields)]
160pub struct ContextAskResponse {
161 pub answer: Option<ContextAnswer>,
162 pub hit: bool,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub reason: Option<ContextMissReason>,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub next_step: Option<ContextNextStep>,
167}
168
169#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
171#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
172#[serde(rename_all = "camelCase", deny_unknown_fields)]
173pub struct ContextNeighborsResponse {
174 pub neighbors: Option<ContextNeighbors>,
175 pub hit: bool,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
177 pub reason: Option<ContextMissReason>,
178 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub next_step: Option<ContextNextStep>,
180}
181
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
184#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
185#[serde(rename_all = "camelCase", deny_unknown_fields)]
186pub struct ContextSiteResponse {
187 pub site: Option<ContextSiteView>,
188}
189
190#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
192#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
193#[serde(rename_all = "camelCase", deny_unknown_fields)]
194pub struct AccessibilityNode {
195 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub role: Option<String>,
197 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub name: Option<String>,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub target: Option<AccessibilityTarget>,
201 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub value: Option<String>,
203 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub description: Option<String>,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub required: Option<bool>,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub disabled: Option<bool>,
209 #[serde(default, skip_serializing_if = "Option::is_none")]
210 pub read_only: Option<bool>,
211 #[serde(default, skip_serializing_if = "Option::is_none")]
212 pub invalid: Option<bool>,
213 #[serde(default, skip_serializing_if = "Option::is_none")]
214 pub checked: Option<bool>,
215 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub autocomplete: Option<String>,
217 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub url: Option<String>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub value_min: Option<String>,
223 #[serde(default, skip_serializing_if = "Option::is_none")]
224 pub value_max: Option<String>,
225 #[serde(default, skip_serializing_if = "Vec::is_empty")]
226 pub children: Vec<AccessibilityNode>,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
230#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
231#[serde(
232 tag = "status",
233 rename_all = "camelCase",
234 rename_all_fields = "camelCase"
235)]
236pub enum CommandOutcome {
237 Completed {
238 command_id: CommandId,
239 evidence: Vec<Evidence>,
240 },
241 RetryableFailure {
242 command_id: CommandId,
243 error: CommandError,
244 },
245 NeedsReconciliation {
246 command_id: CommandId,
247 error: CommandError,
248 evidence: Vec<Evidence>,
249 },
250 PolicyDenied {
251 command_id: CommandId,
252 error: CommandError,
253 },
254 ResourceExhausted {
255 command_id: CommandId,
256 error: CommandError,
257 retry_after_ms: u64,
258 },
259 Restarted {
260 command_id: CommandId,
261 prior_attempt_id: AttemptId,
262 attempt_id: AttemptId,
263 reason: String,
264 #[serde(default)]
265 evidence: Vec<Evidence>,
266 },
267 Failed {
268 command_id: CommandId,
269 error: CommandError,
270 #[serde(default)]
271 evidence: Vec<Evidence>,
272 },
273}
274
275pub const MAX_WAIT_OBSERVED_CHARS: usize = 512;
282
283#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
284#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
285#[serde(rename_all = "camelCase")]
286pub enum SubmitSettlementOutcome {
287 Settled,
288 ValidationRejected,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
292#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
293#[serde(
294 tag = "kind",
295 rename_all = "camelCase",
296 rename_all_fields = "camelCase"
297)]
298pub enum Evidence {
299 ExecutionPath {
300 path: ExecutionPath,
301 reason: ExecutionReason,
302 state_version: u64,
303 elapsed_ms: u64,
304 bytes: Option<u64>,
305 sha256: Option<String>,
306 #[serde(default, skip_serializing_if = "Option::is_none")]
307 final_url: Option<String>,
308 #[serde(default, skip_serializing_if = "Option::is_none")]
309 content_type: Option<String>,
310 #[serde(default, skip_serializing_if = "Option::is_none")]
311 status: Option<u16>,
312 #[serde(default, skip_serializing_if = "Vec::is_empty")]
313 redirect_chain: Vec<String>,
314 },
315 Navigation {
316 url: String,
317 title: String,
318 },
319 Inspection {
320 selector: Option<String>,
321 url: String,
322 title: String,
323 text: String,
324 html: Option<String>,
325 },
326 SubmitSettlement {
330 outcome: SubmitSettlementOutcome,
331 },
332 Element {
333 selector: String,
334 text: Option<String>,
335 },
336 Upload {
337 selector: String,
338 paths: Vec<String>,
339 },
340 Page {
341 page_id: PageId,
342 url: String,
343 title: String,
344 },
345 PageGeneration {
347 page_id: PageId,
348 generation: u64,
349 },
350 Pages {
351 pages: Vec<PageEvidence>,
352 },
353 Popup {
354 opener_page_id: PageId,
355 page_id: PageId,
356 url: String,
357 title: String,
358 },
359 PopupClosed {
366 popup_page_id: PageId,
367 opener_page_id: PageId,
368 },
369 Download {
370 filename: String,
371 path: String,
372 bytes: u64,
373 sha256: String,
374 #[serde(default, skip_serializing_if = "Option::is_none")]
379 saved_to: Option<String>,
380 },
381 Configuration {
382 name: String,
383 value: String,
384 },
385 Resolution {
386 target: Box<crate::TargetSpec>,
387 fingerprint: Box<TargetFingerprint>,
388 candidates: Vec<CandidateEvidence>,
389 best_match_authorized: bool,
390 },
391 Wait {
392 condition: crate::WaitCondition,
393 elapsed_ms: u64,
394 observations: u64,
395 #[serde(default, skip_serializing_if = "Vec::is_empty")]
396 excluded_classes: Vec<String>,
397 #[serde(default, skip_serializing_if = "Option::is_none")]
406 observed: Option<String>,
407 },
408 Screenshot {
409 artifact_id: String,
410 media_type: String,
411 width: u32,
412 height: u32,
413 bytes: u64,
414 sha256: String,
415 },
416 BrowserExecution {
417 engine: String,
418 browser_version: String,
419 profile_id: String,
420 interaction_path: String,
421 },
422 JavaScriptResult {
423 value: serde_json::Value,
424 truncated: bool,
425 },
426 AccessibilitySnapshot {
427 page_id: PageId,
428 nodes: Vec<AccessibilityNode>,
429 truncated: bool,
430 },
431 FormSnapshot {
432 snapshot: crate::FormSnapshot,
433 },
434 FormValidation {
435 issues: Vec<crate::FormValidationIssue>,
436 },
437 ControlAction {
438 action: ControlActionEvidence,
439 },
440 StructuredExtraction {
441 page_id: PageId,
442 value: serde_json::Value,
443 truncated: bool,
444 },
445 ChallengeDetection {
452 confidence: f32,
453 detection: Option<crate::challenges::ChallengeDetection>,
454 #[serde(default, skip_serializing_if = "Option::is_none")]
455 prior_kind: Option<String>,
456 },
457 CookieState {
458 page_id: Option<PageId>,
459 cookies: Vec<crate::CookieRecord>,
460 },
461 PdfArtifact {
462 artifact_id: String,
463 media_type: String,
464 bytes: u64,
465 sha256: String,
466 },
467 Dialog {
468 dialog_type: String,
469 message: String,
470 action: String,
471 },
472 Emulation {
473 viewport: Option<crate::ViewportSize>,
474 geolocation: Option<crate::GeolocationCoordinates>,
475 },
476 HarArtifact {
477 artifact_id: String,
478 media_type: String,
479 bytes: u64,
480 sha256: String,
481 entries: u32,
482 },
483 IntentExecution {
484 record: ExecutionRecord,
485 },
486 Humanization {
491 engine: String,
492 actions: u32,
493 synthesized_ms: u64,
494 },
495 Extraction {
499 field: String,
500 #[serde(default, skip_serializing_if = "Option::is_none")]
501 value: Option<String>,
502 resolution_path: IntentResolutionPath,
503 #[serde(default, skip_serializing_if = "Option::is_none")]
504 error_code: Option<ErrorCode>,
505 },
506}
507
508#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
509#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
510#[serde(rename_all = "camelCase")]
511pub enum IntentResolutionPath {
512 Deterministic,
513 VisionFallback,
514 VisionPrefill,
516}
517
518#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
519#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
520#[serde(rename_all = "camelCase")]
521pub struct ExecutionRecord {
522 pub intent_kind: String,
523 pub purpose: Option<String>,
524 pub resolution_path: IntentResolutionPath,
525 pub plan_summary: String,
526 pub candidates: Vec<CandidateEvidence>,
527 pub wait_elapsed_ms: Option<u64>,
528 pub verification: String,
529 pub artifact_ids: Vec<String>,
530 pub vision_proposal_sha256: Option<String>,
531}
532
533impl Evidence {
534 pub fn journal_safe(&self) -> Self {
535 fn safe_url(value: &str) -> String {
536 let Ok(mut url) = url::Url::parse(value) else {
537 return "[redacted-invalid-url]".into();
538 };
539 let _ = url.set_username("");
540 let _ = url.set_password(None);
541 url.set_query(None);
542 url.set_fragment(None);
543 url.to_string()
544 }
545 let mut safe = self.clone();
546 match &mut safe {
547 Self::ExecutionPath {
548 final_url,
549 redirect_chain,
550 ..
551 } => {
552 if let Some(url) = final_url {
553 *url = safe_url(url);
554 }
555 for url in redirect_chain {
556 *url = safe_url(url);
557 }
558 }
559 Self::Navigation { url, .. }
560 | Self::Inspection { url, .. }
561 | Self::Page { url, .. }
562 | Self::Popup { url, .. } => *url = safe_url(url),
563 Self::Pages { pages } => {
564 for page in pages {
565 page.url = safe_url(&page.url);
566 }
567 }
568 Self::Upload { paths, .. } => {
569 for (index, path) in paths.iter_mut().enumerate() {
570 *path = format!("upload://evidence/{index}");
571 }
572 }
573 Self::Download {
574 path,
575 sha256,
576 saved_to,
577 ..
578 } => {
579 *path = format!("artifact://sha256/{sha256}");
580 *saved_to = None;
581 }
582 Self::PopupClosed { .. } => {}
583 Self::BrowserExecution { .. } => {}
584 Self::IntentExecution { .. } => {}
585 Self::Extraction { .. } => {}
586 _ => {}
587 }
588 safe
589 }
590}
591
592impl CommandOutcome {
593 pub fn journal_safe(&self) -> Self {
594 let mut safe = self.clone();
595 match &mut safe {
596 Self::Completed { evidence, .. }
597 | Self::NeedsReconciliation { evidence, .. }
598 | Self::Restarted { evidence, .. }
599 | Self::Failed { evidence, .. } => {
600 *evidence = evidence.iter().map(Evidence::journal_safe).collect();
601 }
602 _ => {}
603 }
604 match &mut safe {
605 Self::RetryableFailure { error, .. }
606 | Self::NeedsReconciliation { error, .. }
607 | Self::PolicyDenied { error, .. }
608 | Self::ResourceExhausted { error, .. }
609 | Self::Failed { error, .. } => error.message = "redacted durable diagnostic".into(),
610 _ => {}
611 }
612 safe
613 }
614}
615
616#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
617#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
618#[serde(rename_all = "camelCase")]
619pub enum ExecutionPath {
625 DirectHttp,
626 #[serde(alias = "chromium")]
627 Browser,
628 #[serde(alias = "chromiumFallback")]
629 BrowserFallback,
630}
631
632#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
633#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
634#[serde(rename_all = "camelCase")]
635pub enum ExecutionReason {
636 EligibleStaticDocument,
637 EligibleExplicitDownload,
638 IneligibleCommand,
639 SemanticTargetRequired,
640 JavascriptRequired,
641 UnsupportedContentType,
642 StateConflict,
643 PolicyRequired,
644 PageMutated,
647}
648
649#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
650#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
651#[serde(rename_all = "camelCase")]
652pub struct CandidateEvidence {
653 pub role: Option<String>,
654 pub name: Option<String>,
655 pub score: i32,
656 pub reasons: Vec<String>,
657}
658
659#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
660#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
661#[serde(rename_all = "camelCase")]
662pub struct TargetFingerprint {
663 pub page_id: PageId,
664 pub frame: Option<String>,
665 pub role: Option<String>,
666 pub name: Option<String>,
667 pub stable_attributes: std::collections::BTreeMap<String, String>,
668}
669
670#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
671#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
672#[serde(rename_all = "camelCase")]
673pub struct PageEvidence {
674 pub page_id: PageId,
675 pub url: String,
676 pub title: String,
677}
678
679#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
680#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
681#[serde(rename_all = "camelCase")]
682pub struct CommandError {
683 pub code: ErrorCode,
684 pub message: String,
685 pub layer: ErrorLayer,
686 pub retryable: bool,
687}
688
689#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
690#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
691#[serde(rename_all = "camelCase")]
692pub enum ErrorCode {
693 InvalidRequest,
694 NotFound,
695 DeadlineExceeded,
696 BrowserLaunchFailed,
697 BrowserCommandFailed,
698 VerificationFailed,
699 JournalFailed,
700 ResourceExhausted,
701 PolicyDenied,
702 Internal,
703 TargetNotFound,
704 TargetAmbiguous,
705 FrameNotFound,
706 ShadowRootUnavailable,
707 TargetDetached,
708 TargetObscured,
709 TargetOutOfBounds,
710 WaitConditionTimedOut,
711 ScreenshotCaptureFailed,
712 NetworkPolicyDenied,
713 HttpResponseTooLarge,
714 HttpTransferFailed,
715 HttpStateConflict,
716 HttpEquivalenceUnproven,
717 IntentCompileFailed,
718 IntentActionMismatch,
719 ObstructionSuspected,
720 VisionAssistDenied,
721 VisionAssistFailed,
722 ExpectedStatePreSatisfied,
725 BoundaryAlreadyExecuted,
729}
730
731#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
732#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
733#[serde(rename_all = "camelCase")]
734pub enum ErrorLayer {
735 Interface,
736 Broker,
737 Workflow,
738 Page,
739 Driver,
740 Browser,
741 Network,
742 Site,
743 Journal,
744}
745
746#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
747#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
748#[serde(rename_all = "camelCase")]
749pub enum CommandPhase {
750 Accepted,
751 Prepared,
752 Executing,
753 ResultPrepared,
754 Verifying,
755 Recovering,
756 Completed,
757 Failed,
758}
759
760#[derive(Debug, Error, Serialize, Deserialize)]
761#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
762pub enum RuntimeError {
763 #[error("not found: {0}")]
764 NotFound(String),
765 #[error("invalid request: {0}")]
766 InvalidRequest(String),
767 #[error("{0}")]
771 EngineUnreachable(String),
772 #[error("internal error: {0}")]
773 Internal(String),
774}