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 Download {
305 filename: String,
306 path: String,
307 bytes: u64,
308 sha256: String,
309 #[serde(default, skip_serializing_if = "Option::is_none")]
314 saved_to: Option<String>,
315 },
316 Configuration {
317 name: String,
318 value: String,
319 },
320 Resolution {
321 target: Box<crate::TargetSpec>,
322 fingerprint: Box<TargetFingerprint>,
323 candidates: Vec<CandidateEvidence>,
324 best_match_authorized: bool,
325 },
326 Wait {
327 condition: crate::WaitCondition,
328 elapsed_ms: u64,
329 observations: u64,
330 #[serde(default, skip_serializing_if = "Vec::is_empty")]
331 excluded_classes: Vec<String>,
332 #[serde(default, skip_serializing_if = "Option::is_none")]
341 observed: Option<String>,
342 },
343 Screenshot {
344 artifact_id: String,
345 media_type: String,
346 width: u32,
347 height: u32,
348 bytes: u64,
349 sha256: String,
350 },
351 BrowserExecution {
352 engine: String,
353 browser_version: String,
354 profile_id: String,
355 interaction_path: String,
356 },
357 JavaScriptResult {
358 value: serde_json::Value,
359 truncated: bool,
360 },
361 AccessibilitySnapshot {
362 page_id: PageId,
363 nodes: Vec<AccessibilityNode>,
364 truncated: bool,
365 },
366 FormSnapshot {
367 snapshot: crate::FormSnapshot,
368 },
369 FormValidation {
370 issues: Vec<crate::FormValidationIssue>,
371 },
372 ControlAction {
373 action: ControlActionEvidence,
374 },
375 StructuredExtraction {
376 page_id: PageId,
377 value: serde_json::Value,
378 truncated: bool,
379 },
380 ChallengeDetection {
387 confidence: f32,
388 detection: Option<crate::challenges::ChallengeDetection>,
389 #[serde(default, skip_serializing_if = "Option::is_none")]
390 prior_kind: Option<String>,
391 },
392 CookieState {
393 page_id: Option<PageId>,
394 cookies: Vec<crate::CookieRecord>,
395 },
396 PdfArtifact {
397 artifact_id: String,
398 media_type: String,
399 bytes: u64,
400 sha256: String,
401 },
402 Dialog {
403 dialog_type: String,
404 message: String,
405 action: String,
406 },
407 Emulation {
408 viewport: Option<crate::ViewportSize>,
409 geolocation: Option<crate::GeolocationCoordinates>,
410 },
411 HarArtifact {
412 artifact_id: String,
413 media_type: String,
414 bytes: u64,
415 sha256: String,
416 entries: u32,
417 },
418 IntentExecution {
419 record: ExecutionRecord,
420 },
421 Humanization {
426 engine: String,
427 actions: u32,
428 synthesized_ms: u64,
429 },
430 Extraction {
434 field: String,
435 #[serde(default, skip_serializing_if = "Option::is_none")]
436 value: Option<String>,
437 resolution_path: IntentResolutionPath,
438 #[serde(default, skip_serializing_if = "Option::is_none")]
439 error_code: Option<ErrorCode>,
440 },
441}
442
443#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
444#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
445#[serde(rename_all = "camelCase")]
446pub enum IntentResolutionPath {
447 Deterministic,
448 VisionFallback,
449 VisionPrefill,
452}
453
454#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
455#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
456#[serde(rename_all = "camelCase")]
457pub struct ExecutionRecord {
458 pub intent_kind: String,
459 pub purpose: Option<String>,
460 pub resolution_path: IntentResolutionPath,
461 pub plan_summary: String,
462 pub candidates: Vec<CandidateEvidence>,
463 pub wait_elapsed_ms: Option<u64>,
464 pub verification: String,
465 pub artifact_ids: Vec<String>,
466 pub vision_proposal_sha256: Option<String>,
467}
468
469impl Evidence {
470 pub fn journal_safe(&self) -> Self {
471 fn safe_url(value: &str) -> String {
472 let Ok(mut url) = url::Url::parse(value) else {
473 return "[redacted-invalid-url]".into();
474 };
475 let _ = url.set_username("");
476 let _ = url.set_password(None);
477 url.set_query(None);
478 url.set_fragment(None);
479 url.to_string()
480 }
481 let mut safe = self.clone();
482 match &mut safe {
483 Self::ExecutionPath {
484 final_url,
485 redirect_chain,
486 ..
487 } => {
488 if let Some(url) = final_url {
489 *url = safe_url(url);
490 }
491 for url in redirect_chain {
492 *url = safe_url(url);
493 }
494 }
495 Self::Navigation { url, .. }
496 | Self::Inspection { url, .. }
497 | Self::Page { url, .. }
498 | Self::Popup { url, .. } => *url = safe_url(url),
499 Self::Pages { pages } => {
500 for page in pages {
501 page.url = safe_url(&page.url);
502 }
503 }
504 Self::Upload { paths, .. } => {
505 for (index, path) in paths.iter_mut().enumerate() {
506 *path = format!("upload://evidence/{index}");
507 }
508 }
509 Self::Download {
510 path,
511 sha256,
512 saved_to,
513 ..
514 } => {
515 *path = format!("artifact://sha256/{sha256}");
516 *saved_to = None;
517 }
518 Self::BrowserExecution { .. } => {}
519 Self::IntentExecution { .. } => {}
520 Self::Extraction { .. } => {}
521 _ => {}
522 }
523 safe
524 }
525}
526
527impl CommandOutcome {
528 pub fn journal_safe(&self) -> Self {
529 let mut safe = self.clone();
530 match &mut safe {
531 Self::Completed { evidence, .. }
532 | Self::NeedsReconciliation { evidence, .. }
533 | Self::Restarted { evidence, .. }
534 | Self::Failed { evidence, .. } => {
535 *evidence = evidence.iter().map(Evidence::journal_safe).collect();
536 }
537 _ => {}
538 }
539 match &mut safe {
540 Self::RetryableFailure { error, .. }
541 | Self::NeedsReconciliation { error, .. }
542 | Self::PolicyDenied { error, .. }
543 | Self::ResourceExhausted { error, .. }
544 | Self::Failed { error, .. } => error.message = "redacted durable diagnostic".into(),
545 _ => {}
546 }
547 safe
548 }
549}
550
551#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
552#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
553#[serde(rename_all = "camelCase")]
554pub enum ExecutionPath {
560 DirectHttp,
561 #[serde(alias = "chromium")]
562 Browser,
563 #[serde(alias = "chromiumFallback")]
564 BrowserFallback,
565}
566
567#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
568#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
569#[serde(rename_all = "camelCase")]
570pub enum ExecutionReason {
571 EligibleStaticDocument,
572 EligibleExplicitDownload,
573 IneligibleCommand,
574 SemanticTargetRequired,
575 JavascriptRequired,
576 UnsupportedContentType,
577 StateConflict,
578 PolicyRequired,
579 PageMutated,
582}
583
584#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
585#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
586#[serde(rename_all = "camelCase")]
587pub struct CandidateEvidence {
588 pub role: Option<String>,
589 pub name: Option<String>,
590 pub score: i32,
591 pub reasons: Vec<String>,
592}
593
594#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
595#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
596#[serde(rename_all = "camelCase")]
597pub struct TargetFingerprint {
598 pub page_id: PageId,
599 pub frame: Option<String>,
600 pub role: Option<String>,
601 pub name: Option<String>,
602 pub stable_attributes: std::collections::BTreeMap<String, 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 PageEvidence {
609 pub page_id: PageId,
610 pub url: String,
611 pub title: String,
612}
613
614#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
615#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
616#[serde(rename_all = "camelCase")]
617pub struct CommandError {
618 pub code: ErrorCode,
619 pub message: String,
620 pub layer: ErrorLayer,
621 pub retryable: bool,
622}
623
624#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
625#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
626#[serde(rename_all = "camelCase")]
627pub enum ErrorCode {
628 InvalidRequest,
629 NotFound,
630 DeadlineExceeded,
631 BrowserLaunchFailed,
632 BrowserCommandFailed,
633 VerificationFailed,
634 JournalFailed,
635 ResourceExhausted,
636 PolicyDenied,
637 Internal,
638 TargetNotFound,
639 TargetAmbiguous,
640 FrameNotFound,
641 ShadowRootUnavailable,
642 TargetDetached,
643 TargetObscured,
644 TargetOutOfBounds,
645 WaitConditionTimedOut,
646 ScreenshotCaptureFailed,
647 NetworkPolicyDenied,
648 HttpResponseTooLarge,
649 HttpTransferFailed,
650 HttpStateConflict,
651 HttpEquivalenceUnproven,
652 IntentCompileFailed,
653 IntentActionMismatch,
654 ObstructionSuspected,
655 VisionAssistDenied,
656 VisionAssistFailed,
657 ExpectedStatePreSatisfied,
660 BoundaryAlreadyExecuted,
664}
665
666#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
667#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
668#[serde(rename_all = "camelCase")]
669pub enum ErrorLayer {
670 Interface,
671 Broker,
672 Workflow,
673 Page,
674 Driver,
675 Browser,
676 Network,
677 Site,
678 Journal,
679}
680
681#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
682#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
683#[serde(rename_all = "camelCase")]
684pub enum CommandPhase {
685 Accepted,
686 Prepared,
687 Executing,
688 ResultPrepared,
689 Verifying,
690 Recovering,
691 Completed,
692 Failed,
693}
694
695#[derive(Debug, Error, Serialize, Deserialize)]
696#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
697pub enum RuntimeError {
698 #[error("not found: {0}")]
699 NotFound(String),
700 #[error("invalid request: {0}")]
701 InvalidRequest(String),
702 #[error("{0}")]
706 EngineUnreachable(String),
707 #[error("internal error: {0}")]
708 Internal(String),
709}