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