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, Copy, Serialize, Deserialize, PartialEq, Eq)]
234#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
235#[serde(rename_all = "camelCase")]
236pub enum SubmitSettlementOutcome {
237 Settled,
238 ValidationRejected,
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
242#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
243#[serde(
244 tag = "kind",
245 rename_all = "camelCase",
246 rename_all_fields = "camelCase"
247)]
248pub enum Evidence {
249 ExecutionPath {
250 path: ExecutionPath,
251 reason: ExecutionReason,
252 state_version: u64,
253 elapsed_ms: u64,
254 bytes: Option<u64>,
255 sha256: Option<String>,
256 #[serde(default, skip_serializing_if = "Option::is_none")]
257 final_url: Option<String>,
258 #[serde(default, skip_serializing_if = "Option::is_none")]
259 content_type: Option<String>,
260 #[serde(default, skip_serializing_if = "Option::is_none")]
261 status: Option<u16>,
262 #[serde(default, skip_serializing_if = "Vec::is_empty")]
263 redirect_chain: Vec<String>,
264 },
265 Navigation {
266 url: String,
267 title: String,
268 },
269 Inspection {
270 selector: Option<String>,
271 url: String,
272 title: String,
273 text: String,
274 html: Option<String>,
275 },
276 SubmitSettlement {
280 outcome: SubmitSettlementOutcome,
281 },
282 Element {
283 selector: String,
284 text: Option<String>,
285 },
286 Upload {
287 selector: String,
288 paths: Vec<String>,
289 },
290 Page {
291 page_id: PageId,
292 url: String,
293 title: String,
294 },
295 Pages {
296 pages: Vec<PageEvidence>,
297 },
298 Popup {
299 opener_page_id: PageId,
300 page_id: PageId,
301 url: String,
302 title: String,
303 },
304 PopupClosed {
311 popup_page_id: PageId,
312 opener_page_id: PageId,
313 },
314 Download {
315 filename: String,
316 path: String,
317 bytes: u64,
318 sha256: String,
319 #[serde(default, skip_serializing_if = "Option::is_none")]
324 saved_to: Option<String>,
325 },
326 Configuration {
327 name: String,
328 value: String,
329 },
330 Resolution {
331 target: Box<crate::TargetSpec>,
332 fingerprint: Box<TargetFingerprint>,
333 candidates: Vec<CandidateEvidence>,
334 best_match_authorized: bool,
335 },
336 Wait {
337 condition: crate::WaitCondition,
338 elapsed_ms: u64,
339 observations: u64,
340 #[serde(default, skip_serializing_if = "Vec::is_empty")]
341 excluded_classes: Vec<String>,
342 #[serde(default, skip_serializing_if = "Option::is_none")]
351 observed: Option<String>,
352 },
353 Screenshot {
354 artifact_id: String,
355 media_type: String,
356 width: u32,
357 height: u32,
358 bytes: u64,
359 sha256: String,
360 },
361 BrowserExecution {
362 engine: String,
363 browser_version: String,
364 profile_id: String,
365 interaction_path: String,
366 },
367 JavaScriptResult {
368 value: serde_json::Value,
369 truncated: bool,
370 },
371 AccessibilitySnapshot {
372 page_id: PageId,
373 nodes: Vec<AccessibilityNode>,
374 truncated: bool,
375 },
376 FormSnapshot {
377 snapshot: crate::FormSnapshot,
378 },
379 FormValidation {
380 issues: Vec<crate::FormValidationIssue>,
381 },
382 ControlAction {
383 action: ControlActionEvidence,
384 },
385 StructuredExtraction {
386 page_id: PageId,
387 value: serde_json::Value,
388 truncated: bool,
389 },
390 ChallengeDetection {
397 confidence: f32,
398 detection: Option<crate::challenges::ChallengeDetection>,
399 #[serde(default, skip_serializing_if = "Option::is_none")]
400 prior_kind: Option<String>,
401 },
402 CookieState {
403 page_id: Option<PageId>,
404 cookies: Vec<crate::CookieRecord>,
405 },
406 PdfArtifact {
407 artifact_id: String,
408 media_type: String,
409 bytes: u64,
410 sha256: String,
411 },
412 Dialog {
413 dialog_type: String,
414 message: String,
415 action: String,
416 },
417 Emulation {
418 viewport: Option<crate::ViewportSize>,
419 geolocation: Option<crate::GeolocationCoordinates>,
420 },
421 HarArtifact {
422 artifact_id: String,
423 media_type: String,
424 bytes: u64,
425 sha256: String,
426 entries: u32,
427 },
428 IntentExecution {
429 record: ExecutionRecord,
430 },
431 Humanization {
436 engine: String,
437 actions: u32,
438 synthesized_ms: u64,
439 },
440 Extraction {
444 field: String,
445 #[serde(default, skip_serializing_if = "Option::is_none")]
446 value: Option<String>,
447 resolution_path: IntentResolutionPath,
448 #[serde(default, skip_serializing_if = "Option::is_none")]
449 error_code: Option<ErrorCode>,
450 },
451}
452
453#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
454#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
455#[serde(rename_all = "camelCase")]
456pub enum IntentResolutionPath {
457 Deterministic,
458 VisionFallback,
459 VisionPrefill,
462}
463
464#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
465#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
466#[serde(rename_all = "camelCase")]
467pub struct ExecutionRecord {
468 pub intent_kind: String,
469 pub purpose: Option<String>,
470 pub resolution_path: IntentResolutionPath,
471 pub plan_summary: String,
472 pub candidates: Vec<CandidateEvidence>,
473 pub wait_elapsed_ms: Option<u64>,
474 pub verification: String,
475 pub artifact_ids: Vec<String>,
476 pub vision_proposal_sha256: Option<String>,
477}
478
479impl Evidence {
480 pub fn journal_safe(&self) -> Self {
481 fn safe_url(value: &str) -> String {
482 let Ok(mut url) = url::Url::parse(value) else {
483 return "[redacted-invalid-url]".into();
484 };
485 let _ = url.set_username("");
486 let _ = url.set_password(None);
487 url.set_query(None);
488 url.set_fragment(None);
489 url.to_string()
490 }
491 let mut safe = self.clone();
492 match &mut safe {
493 Self::ExecutionPath {
494 final_url,
495 redirect_chain,
496 ..
497 } => {
498 if let Some(url) = final_url {
499 *url = safe_url(url);
500 }
501 for url in redirect_chain {
502 *url = safe_url(url);
503 }
504 }
505 Self::Navigation { url, .. }
506 | Self::Inspection { url, .. }
507 | Self::Page { url, .. }
508 | Self::Popup { url, .. } => *url = safe_url(url),
509 Self::Pages { pages } => {
510 for page in pages {
511 page.url = safe_url(&page.url);
512 }
513 }
514 Self::Upload { paths, .. } => {
515 for (index, path) in paths.iter_mut().enumerate() {
516 *path = format!("upload://evidence/{index}");
517 }
518 }
519 Self::Download {
520 path,
521 sha256,
522 saved_to,
523 ..
524 } => {
525 *path = format!("artifact://sha256/{sha256}");
526 *saved_to = None;
527 }
528 Self::PopupClosed { .. } => {}
529 Self::BrowserExecution { .. } => {}
530 Self::IntentExecution { .. } => {}
531 Self::Extraction { .. } => {}
532 _ => {}
533 }
534 safe
535 }
536}
537
538impl CommandOutcome {
539 pub fn journal_safe(&self) -> Self {
540 let mut safe = self.clone();
541 match &mut safe {
542 Self::Completed { evidence, .. }
543 | Self::NeedsReconciliation { evidence, .. }
544 | Self::Restarted { evidence, .. }
545 | Self::Failed { evidence, .. } => {
546 *evidence = evidence.iter().map(Evidence::journal_safe).collect();
547 }
548 _ => {}
549 }
550 match &mut safe {
551 Self::RetryableFailure { error, .. }
552 | Self::NeedsReconciliation { error, .. }
553 | Self::PolicyDenied { error, .. }
554 | Self::ResourceExhausted { error, .. }
555 | Self::Failed { error, .. } => error.message = "redacted durable diagnostic".into(),
556 _ => {}
557 }
558 safe
559 }
560}
561
562#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
563#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
564#[serde(rename_all = "camelCase")]
565pub enum ExecutionPath {
571 DirectHttp,
572 #[serde(alias = "chromium")]
573 Browser,
574 #[serde(alias = "chromiumFallback")]
575 BrowserFallback,
576}
577
578#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
579#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
580#[serde(rename_all = "camelCase")]
581pub enum ExecutionReason {
582 EligibleStaticDocument,
583 EligibleExplicitDownload,
584 IneligibleCommand,
585 SemanticTargetRequired,
586 JavascriptRequired,
587 UnsupportedContentType,
588 StateConflict,
589 PolicyRequired,
590 PageMutated,
593}
594
595#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
596#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
597#[serde(rename_all = "camelCase")]
598pub struct CandidateEvidence {
599 pub role: Option<String>,
600 pub name: Option<String>,
601 pub score: i32,
602 pub reasons: Vec<String>,
603}
604
605#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
606#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
607#[serde(rename_all = "camelCase")]
608pub struct TargetFingerprint {
609 pub page_id: PageId,
610 pub frame: Option<String>,
611 pub role: Option<String>,
612 pub name: Option<String>,
613 pub stable_attributes: std::collections::BTreeMap<String, String>,
614}
615
616#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
617#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
618#[serde(rename_all = "camelCase")]
619pub struct PageEvidence {
620 pub page_id: PageId,
621 pub url: String,
622 pub title: String,
623}
624
625#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
626#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
627#[serde(rename_all = "camelCase")]
628pub struct CommandError {
629 pub code: ErrorCode,
630 pub message: String,
631 pub layer: ErrorLayer,
632 pub retryable: bool,
633}
634
635#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
636#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
637#[serde(rename_all = "camelCase")]
638pub enum ErrorCode {
639 InvalidRequest,
640 NotFound,
641 DeadlineExceeded,
642 BrowserLaunchFailed,
643 BrowserCommandFailed,
644 VerificationFailed,
645 JournalFailed,
646 ResourceExhausted,
647 PolicyDenied,
648 Internal,
649 TargetNotFound,
650 TargetAmbiguous,
651 FrameNotFound,
652 ShadowRootUnavailable,
653 TargetDetached,
654 TargetObscured,
655 TargetOutOfBounds,
656 WaitConditionTimedOut,
657 ScreenshotCaptureFailed,
658 NetworkPolicyDenied,
659 HttpResponseTooLarge,
660 HttpTransferFailed,
661 HttpStateConflict,
662 HttpEquivalenceUnproven,
663 IntentCompileFailed,
664 IntentActionMismatch,
665 ObstructionSuspected,
666 VisionAssistDenied,
667 VisionAssistFailed,
668 ExpectedStatePreSatisfied,
671 BoundaryAlreadyExecuted,
675}
676
677#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
678#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
679#[serde(rename_all = "camelCase")]
680pub enum ErrorLayer {
681 Interface,
682 Broker,
683 Workflow,
684 Page,
685 Driver,
686 Browser,
687 Network,
688 Site,
689 Journal,
690}
691
692#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
693#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
694#[serde(rename_all = "camelCase")]
695pub enum CommandPhase {
696 Accepted,
697 Prepared,
698 Executing,
699 ResultPrepared,
700 Verifying,
701 Recovering,
702 Completed,
703 Failed,
704}
705
706#[derive(Debug, Error, Serialize, Deserialize)]
707#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
708pub enum RuntimeError {
709 #[error("not found: {0}")]
710 NotFound(String),
711 #[error("invalid request: {0}")]
712 InvalidRequest(String),
713 #[error("{0}")]
717 EngineUnreachable(String),
718 #[error("internal error: {0}")]
719 Internal(String),
720}