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}
41
42#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
44#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
45#[serde(rename_all = "camelCase", deny_unknown_fields)]
46pub struct AccessibilityNode {
47 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub role: Option<String>,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub name: Option<String>,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub target: Option<AccessibilityTarget>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub value: Option<String>,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub description: Option<String>,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub required: Option<bool>,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub disabled: Option<bool>,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub read_only: Option<bool>,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub invalid: Option<bool>,
65 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub checked: Option<bool>,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub autocomplete: Option<String>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub value_min: Option<String>,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub value_max: Option<String>,
73 #[serde(default, skip_serializing_if = "Vec::is_empty")]
74 pub children: Vec<AccessibilityNode>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
78#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
79#[serde(
80 tag = "status",
81 rename_all = "camelCase",
82 rename_all_fields = "camelCase"
83)]
84pub enum CommandOutcome {
85 Completed {
86 command_id: CommandId,
87 evidence: Vec<Evidence>,
88 },
89 RetryableFailure {
90 command_id: CommandId,
91 error: CommandError,
92 },
93 NeedsReconciliation {
94 command_id: CommandId,
95 error: CommandError,
96 evidence: Vec<Evidence>,
97 },
98 PolicyDenied {
99 command_id: CommandId,
100 error: CommandError,
101 },
102 ResourceExhausted {
103 command_id: CommandId,
104 error: CommandError,
105 retry_after_ms: u64,
106 },
107 Restarted {
108 command_id: CommandId,
109 prior_attempt_id: AttemptId,
110 attempt_id: AttemptId,
111 reason: String,
112 #[serde(default)]
113 evidence: Vec<Evidence>,
114 },
115 Failed {
116 command_id: CommandId,
117 error: CommandError,
118 #[serde(default)]
119 evidence: Vec<Evidence>,
120 },
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
124#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
125#[serde(
126 tag = "kind",
127 rename_all = "camelCase",
128 rename_all_fields = "camelCase"
129)]
130pub enum Evidence {
131 ExecutionPath {
132 path: ExecutionPath,
133 reason: ExecutionReason,
134 state_version: u64,
135 elapsed_ms: u64,
136 bytes: Option<u64>,
137 sha256: Option<String>,
138 #[serde(default, skip_serializing_if = "Option::is_none")]
139 final_url: Option<String>,
140 #[serde(default, skip_serializing_if = "Option::is_none")]
141 content_type: Option<String>,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
143 status: Option<u16>,
144 #[serde(default, skip_serializing_if = "Vec::is_empty")]
145 redirect_chain: Vec<String>,
146 },
147 Navigation {
148 url: String,
149 title: String,
150 },
151 Inspection {
152 selector: Option<String>,
153 url: String,
154 title: String,
155 text: String,
156 html: Option<String>,
157 },
158 Element {
159 selector: String,
160 text: Option<String>,
161 },
162 Upload {
163 selector: String,
164 paths: Vec<String>,
165 },
166 Page {
167 page_id: PageId,
168 url: String,
169 title: String,
170 },
171 Pages {
172 pages: Vec<PageEvidence>,
173 },
174 Popup {
175 opener_page_id: PageId,
176 page_id: PageId,
177 url: String,
178 title: String,
179 },
180 Download {
181 filename: String,
182 path: String,
183 bytes: u64,
184 sha256: String,
185 },
186 Configuration {
187 name: String,
188 value: String,
189 },
190 Resolution {
191 target: Box<crate::TargetSpec>,
192 fingerprint: Box<TargetFingerprint>,
193 candidates: Vec<CandidateEvidence>,
194 best_match_authorized: bool,
195 },
196 Wait {
197 condition: crate::WaitCondition,
198 elapsed_ms: u64,
199 observations: u64,
200 #[serde(default, skip_serializing_if = "Vec::is_empty")]
201 excluded_classes: Vec<String>,
202 },
203 Screenshot {
204 artifact_id: String,
205 media_type: String,
206 width: u32,
207 height: u32,
208 bytes: u64,
209 sha256: String,
210 },
211 BrowserExecution {
212 engine: String,
213 browser_version: String,
214 profile_id: String,
215 interaction_path: String,
216 },
217 JavaScriptResult {
218 value: serde_json::Value,
219 truncated: bool,
220 },
221 AccessibilitySnapshot {
222 page_id: PageId,
223 nodes: Vec<AccessibilityNode>,
224 truncated: bool,
225 },
226 FormSnapshot {
227 snapshot: crate::FormSnapshot,
228 },
229 ControlAction {
230 action: ControlActionEvidence,
231 },
232 StructuredExtraction {
233 page_id: PageId,
234 value: serde_json::Value,
235 truncated: bool,
236 },
237 CookieState {
238 page_id: Option<PageId>,
239 cookies: Vec<crate::CookieRecord>,
240 },
241 PdfArtifact {
242 artifact_id: String,
243 media_type: String,
244 bytes: u64,
245 sha256: String,
246 },
247 Dialog {
248 dialog_type: String,
249 message: String,
250 action: String,
251 },
252 Emulation {
253 viewport: Option<crate::ViewportSize>,
254 geolocation: Option<crate::GeolocationCoordinates>,
255 },
256 HarArtifact {
257 artifact_id: String,
258 media_type: String,
259 bytes: u64,
260 sha256: String,
261 entries: u32,
262 },
263 IntentExecution {
264 record: ExecutionRecord,
265 },
266 Humanization {
271 engine: String,
272 actions: u32,
273 synthesized_ms: u64,
274 },
275 Extraction {
279 field: String,
280 #[serde(default, skip_serializing_if = "Option::is_none")]
281 value: Option<String>,
282 resolution_path: IntentResolutionPath,
283 #[serde(default, skip_serializing_if = "Option::is_none")]
284 error_code: Option<ErrorCode>,
285 },
286}
287
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
289#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
290#[serde(rename_all = "camelCase")]
291pub enum IntentResolutionPath {
292 Deterministic,
293 VisionFallback,
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
297#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
298#[serde(rename_all = "camelCase")]
299pub struct ExecutionRecord {
300 pub intent_kind: String,
301 pub purpose: Option<String>,
302 pub resolution_path: IntentResolutionPath,
303 pub plan_summary: String,
304 pub candidates: Vec<CandidateEvidence>,
305 pub wait_elapsed_ms: Option<u64>,
306 pub verification: String,
307 pub artifact_ids: Vec<String>,
308 pub vision_proposal_sha256: Option<String>,
309}
310
311impl Evidence {
312 pub fn journal_safe(&self) -> Self {
313 fn safe_url(value: &str) -> String {
314 let Ok(mut url) = url::Url::parse(value) else {
315 return "[redacted-invalid-url]".into();
316 };
317 let _ = url.set_username("");
318 let _ = url.set_password(None);
319 url.set_query(None);
320 url.set_fragment(None);
321 url.to_string()
322 }
323 let mut safe = self.clone();
324 match &mut safe {
325 Self::ExecutionPath {
326 final_url,
327 redirect_chain,
328 ..
329 } => {
330 if let Some(url) = final_url {
331 *url = safe_url(url);
332 }
333 for url in redirect_chain {
334 *url = safe_url(url);
335 }
336 }
337 Self::Navigation { url, .. }
338 | Self::Inspection { url, .. }
339 | Self::Page { url, .. }
340 | Self::Popup { url, .. } => *url = safe_url(url),
341 Self::Pages { pages } => {
342 for page in pages {
343 page.url = safe_url(&page.url);
344 }
345 }
346 Self::Upload { paths, .. } => {
347 for (index, path) in paths.iter_mut().enumerate() {
348 *path = format!("upload://evidence/{index}");
349 }
350 }
351 Self::Download { path, sha256, .. } => {
352 *path = format!("artifact://sha256/{sha256}");
353 }
354 Self::BrowserExecution { .. } => {}
355 Self::IntentExecution { .. } => {}
356 Self::Extraction { .. } => {}
357 _ => {}
358 }
359 safe
360 }
361}
362
363impl CommandOutcome {
364 pub fn journal_safe(&self) -> Self {
365 let mut safe = self.clone();
366 match &mut safe {
367 Self::Completed { evidence, .. }
368 | Self::NeedsReconciliation { evidence, .. }
369 | Self::Restarted { evidence, .. }
370 | Self::Failed { evidence, .. } => {
371 *evidence = evidence.iter().map(Evidence::journal_safe).collect();
372 }
373 _ => {}
374 }
375 match &mut safe {
376 Self::RetryableFailure { error, .. }
377 | Self::NeedsReconciliation { error, .. }
378 | Self::PolicyDenied { error, .. }
379 | Self::ResourceExhausted { error, .. }
380 | Self::Failed { error, .. } => error.message = "redacted durable diagnostic".into(),
381 _ => {}
382 }
383 safe
384 }
385}
386
387#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
388#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
389#[serde(rename_all = "camelCase")]
390pub enum ExecutionPath {
391 DirectHttp,
392 Chromium,
393 ChromiumFallback,
394}
395
396#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
397#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
398#[serde(rename_all = "camelCase")]
399pub enum ExecutionReason {
400 EligibleStaticDocument,
401 EligibleExplicitDownload,
402 IneligibleCommand,
403 SemanticTargetRequired,
404 JavascriptRequired,
405 UnsupportedContentType,
406 StateConflict,
407 PolicyRequired,
408}
409
410#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
411#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
412#[serde(rename_all = "camelCase")]
413pub struct CandidateEvidence {
414 pub role: Option<String>,
415 pub name: Option<String>,
416 pub score: i32,
417 pub reasons: Vec<String>,
418}
419
420#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
421#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
422#[serde(rename_all = "camelCase")]
423pub struct TargetFingerprint {
424 pub page_id: PageId,
425 pub frame: Option<String>,
426 pub role: Option<String>,
427 pub name: Option<String>,
428 pub stable_attributes: std::collections::BTreeMap<String, String>,
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
432#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
433#[serde(rename_all = "camelCase")]
434pub struct PageEvidence {
435 pub page_id: PageId,
436 pub url: String,
437 pub title: String,
438}
439
440#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
441#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
442#[serde(rename_all = "camelCase")]
443pub struct CommandError {
444 pub code: ErrorCode,
445 pub message: String,
446 pub layer: ErrorLayer,
447 pub retryable: bool,
448}
449
450#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
451#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
452#[serde(rename_all = "camelCase")]
453pub enum ErrorCode {
454 InvalidRequest,
455 NotFound,
456 DeadlineExceeded,
457 BrowserLaunchFailed,
458 BrowserCommandFailed,
459 VerificationFailed,
460 JournalFailed,
461 ResourceExhausted,
462 PolicyDenied,
463 Internal,
464 TargetNotFound,
465 TargetAmbiguous,
466 FrameNotFound,
467 ShadowRootUnavailable,
468 TargetDetached,
469 TargetObscured,
470 TargetOutOfBounds,
471 WaitConditionTimedOut,
472 ScreenshotCaptureFailed,
473 NetworkPolicyDenied,
474 HttpResponseTooLarge,
475 HttpTransferFailed,
476 HttpStateConflict,
477 HttpEquivalenceUnproven,
478 IntentCompileFailed,
479 IntentActionMismatch,
480 ObstructionSuspected,
481 VisionAssistDenied,
482 VisionAssistFailed,
483}
484
485#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
486#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
487#[serde(rename_all = "camelCase")]
488pub enum ErrorLayer {
489 Interface,
490 Broker,
491 Workflow,
492 Page,
493 Driver,
494 Browser,
495 Network,
496 Site,
497 Journal,
498}
499
500#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
501#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
502#[serde(rename_all = "camelCase")]
503pub enum CommandPhase {
504 Accepted,
505 Prepared,
506 Executing,
507 ResultPrepared,
508 Verifying,
509 Recovering,
510 Completed,
511 Failed,
512}
513
514#[derive(Debug, Error, Serialize, Deserialize)]
515#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
516pub enum RuntimeError {
517 #[error("not found: {0}")]
518 NotFound(String),
519 #[error("invalid request: {0}")]
520 InvalidRequest(String),
521 #[error("internal error: {0}")]
522 Internal(String),
523}