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