1use sha2::{Digest, Sha256};
4use termivar_core::{ApiSurfaceKind, ApiVisibilityPairKind};
5
6use super::super::super::{await_execution, RuntimeExecution, StandardWebDecisionRuntime};
7use super::{
8 encode_digest, update_framed, ApiVisibilityDifferentialAudit,
9 ApiVisibilityDifferentialDisposition, ApiVisibilityDifferentialRequest,
10 ApiVisibilityInconclusiveReason, ApiVisibilityLeg, ApiVisibilityLegReceipt,
11 RuntimeApiVisibilityExecutionError, RuntimeApiVisibilityRunReport,
12};
13use crate::{
14 api_observation::{api_visibility_review_for_commit, ingest_api_visibility_observation},
15 ApiObservationReceipt, ApiVisibilityComparator, ApiVisibilityReview,
16 ApiVisibilityReviewDisposition, DecisionExecutionClass, DecisionExecutionFailureKind,
17 DecisionExecutionStage, ProfiledApiVisibilityComparison, ProfiledApiVisibilityError,
18 RuntimeLimitExceeded,
19};
20
21const CONTROL_ACTION_ID: &str = "api.visibility.authorization-context.control";
22const CANDIDATE_ACTION_ID: &str = "api.visibility.authorization-context.candidate";
23const OBSERVATION_COMPONENT: &str = "runtime.api-visibility-differential";
24const RESPONSE_DIGEST_DOMAIN: &[u8] = b"venom.api-visibility.response-body.v1\0";
25
26impl StandardWebDecisionRuntime {
27 pub async fn run_api_visibility_pair(
78 &mut self,
79 request: ApiVisibilityDifferentialRequest,
80 ) -> Result<RuntimeApiVisibilityRunReport, RuntimeApiVisibilityExecutionError> {
81 if self.started {
82 return Err(RuntimeApiVisibilityExecutionError::AlreadyStarted);
83 }
84 if self.api_reasoning_installation.is_none() {
85 return Err(RuntimeApiVisibilityExecutionError::ApiReasoningDisabled);
86 }
87 if request.control.probe.url() != &self.target
88 || request.candidate.probe.url() != &self.target
89 {
90 return Err(RuntimeApiVisibilityExecutionError::RuntimeTargetMismatch);
91 }
92
93 let control_requests = self
97 .authority
98 .requests()
99 .isolated()
100 .map_err(|source| RuntimeApiVisibilityExecutionError::TransportSetup { source })?;
101 let candidate_requests = self
102 .authority
103 .requests()
104 .isolated()
105 .map_err(|source| RuntimeApiVisibilityExecutionError::TransportSetup { source })?;
106 let evidence_reliability = self.authority.requests().policy().reliability();
107
108 self.started = true;
109 let timing = self.authority.start();
110 let started_at = timing.started_at();
111 let deadline = timing.deadline();
112 let mut audit = ApiVisibilityDifferentialAudit::for_request(&request);
113 if self.authority.cancellation().is_cancelled() {
114 return Ok(self.visibility_cancelled_report(audit, None, started_at));
115 }
116
117 let control_limits = match self.reserve_execution(
118 CONTROL_ACTION_ID,
119 DecisionExecutionStage::Active,
120 DecisionExecutionClass::TransportBound,
121 started_at,
122 ) {
123 Ok(limits) => limits,
124 Err(limit) => {
125 return Ok(self.visibility_limit_report(
126 audit,
127 Some(ApiVisibilityLeg::Control),
128 limit,
129 started_at,
130 ));
131 },
132 };
133 let control_result = await_execution(
134 self.authority.cancellation(),
135 deadline,
136 control_requests.collect_for_runtime(
137 CONTROL_ACTION_ID,
138 DecisionExecutionStage::Active,
139 None,
140 control_limits,
141 &request.control.probe,
142 ),
143 )
144 .await;
145 let control_response = match control_result {
146 RuntimeExecution::Completed(Ok(response)) => response,
147 RuntimeExecution::Completed(Err(error)) => {
148 return Ok(self.visibility_collection_report(
149 audit,
150 ApiVisibilityLeg::Control,
151 error,
152 started_at,
153 ));
154 },
155 RuntimeExecution::Cancelled => {
156 return Ok(self.visibility_cancelled_report(
157 audit,
158 Some(ApiVisibilityLeg::Control),
159 started_at,
160 ));
161 },
162 RuntimeExecution::WallTimeExceeded => {
163 let limit = self.wall_limit(started_at);
164 return Ok(self.visibility_limit_report(
165 audit,
166 Some(ApiVisibilityLeg::Control),
167 limit,
168 started_at,
169 ));
170 },
171 };
172 audit.control = Some(leg_receipt(
173 ApiVisibilityLeg::Control,
174 &control_response,
175 &request.request_template_sha256,
176 ));
177 self.refresh_elapsed(started_at);
178 if let Some(limit) = self.response_limit_if_exceeded(CONTROL_ACTION_ID) {
179 return Ok(self.visibility_limit_report(
180 audit,
181 Some(ApiVisibilityLeg::Control),
182 limit,
183 started_at,
184 ));
185 }
186 if self.authority.cancellation().is_cancelled() {
187 return Ok(self.visibility_cancelled_report(
188 audit,
189 Some(ApiVisibilityLeg::Control),
190 started_at,
191 ));
192 }
193 if let Some(limit) = self.wall_limit_if_reached(started_at) {
194 return Ok(self.visibility_limit_report(
195 audit,
196 Some(ApiVisibilityLeg::Control),
197 limit,
198 started_at,
199 ));
200 }
201 let control_document = match response_document(&control_response) {
202 Ok(document) => document,
203 Err(reason) => {
204 return Ok(self.visibility_inconclusive_report(
205 audit,
206 ApiVisibilityLeg::Control,
207 reason,
208 started_at,
209 ));
210 },
211 };
212 let comparator = ApiVisibilityComparator::default();
213 let control_view = match comparator.capture_profiled_view(
214 &request.profile,
215 request.control.context_id.as_str(),
216 request.resource_scope.as_str(),
217 ApiSurfaceKind::JsonHttp,
218 control_response.status(),
219 &control_document,
220 ) {
221 Ok(view) => view,
222 Err(_) => {
223 return Ok(self.visibility_inconclusive_report(
224 audit,
225 ApiVisibilityLeg::Control,
226 ApiVisibilityInconclusiveReason::CanonicalizationRejected,
227 started_at,
228 ));
229 },
230 };
231 drop(control_document);
232 drop(control_response);
233
234 if self.authority.cancellation().is_cancelled() {
235 return Ok(self.visibility_cancelled_report(
236 audit,
237 Some(ApiVisibilityLeg::Candidate),
238 started_at,
239 ));
240 }
241 if let Some(limit) = self.wall_limit_if_reached(started_at) {
242 return Ok(self.visibility_limit_report(
243 audit,
244 Some(ApiVisibilityLeg::Candidate),
245 limit,
246 started_at,
247 ));
248 }
249 let candidate_limits = match self.reserve_execution(
250 CANDIDATE_ACTION_ID,
251 DecisionExecutionStage::Active,
252 DecisionExecutionClass::TransportBound,
253 started_at,
254 ) {
255 Ok(limits) => limits,
256 Err(limit) => {
257 return Ok(self.visibility_limit_report(
258 audit,
259 Some(ApiVisibilityLeg::Candidate),
260 limit,
261 started_at,
262 ));
263 },
264 };
265 let candidate_result = await_execution(
266 self.authority.cancellation(),
267 deadline,
268 candidate_requests.collect_for_runtime(
269 CANDIDATE_ACTION_ID,
270 DecisionExecutionStage::Active,
271 None,
272 candidate_limits,
273 &request.candidate.probe,
274 ),
275 )
276 .await;
277 let candidate_response = match candidate_result {
278 RuntimeExecution::Completed(Ok(response)) => response,
279 RuntimeExecution::Completed(Err(error)) => {
280 return Ok(self.visibility_collection_report(
281 audit,
282 ApiVisibilityLeg::Candidate,
283 error,
284 started_at,
285 ));
286 },
287 RuntimeExecution::Cancelled => {
288 return Ok(self.visibility_cancelled_report(
289 audit,
290 Some(ApiVisibilityLeg::Candidate),
291 started_at,
292 ));
293 },
294 RuntimeExecution::WallTimeExceeded => {
295 let limit = self.wall_limit(started_at);
296 return Ok(self.visibility_limit_report(
297 audit,
298 Some(ApiVisibilityLeg::Candidate),
299 limit,
300 started_at,
301 ));
302 },
303 };
304 audit.candidate = Some(leg_receipt(
305 ApiVisibilityLeg::Candidate,
306 &candidate_response,
307 &request.request_template_sha256,
308 ));
309 self.refresh_elapsed(started_at);
310 if let Some(limit) = self.response_limit_if_exceeded(CANDIDATE_ACTION_ID) {
311 return Ok(self.visibility_limit_report(
312 audit,
313 Some(ApiVisibilityLeg::Candidate),
314 limit,
315 started_at,
316 ));
317 }
318 if self.authority.cancellation().is_cancelled() {
319 return Ok(self.visibility_cancelled_report(
320 audit,
321 Some(ApiVisibilityLeg::Candidate),
322 started_at,
323 ));
324 }
325 if let Some(limit) = self.wall_limit_if_reached(started_at) {
326 return Ok(self.visibility_limit_report(
327 audit,
328 Some(ApiVisibilityLeg::Candidate),
329 limit,
330 started_at,
331 ));
332 }
333 let candidate_document = match response_document(&candidate_response) {
334 Ok(document) => document,
335 Err(reason) => {
336 return Ok(self.visibility_inconclusive_report(
337 audit,
338 ApiVisibilityLeg::Candidate,
339 reason,
340 started_at,
341 ));
342 },
343 };
344 let candidate_view = match comparator.capture_profiled_view(
345 &request.profile,
346 request.candidate.context_id.as_str(),
347 request.resource_scope.as_str(),
348 ApiSurfaceKind::JsonHttp,
349 candidate_response.status(),
350 &candidate_document,
351 ) {
352 Ok(view) => view,
353 Err(_) => {
354 return Ok(self.visibility_inconclusive_report(
355 audit,
356 ApiVisibilityLeg::Candidate,
357 ApiVisibilityInconclusiveReason::CanonicalizationRejected,
358 started_at,
359 ));
360 },
361 };
362 drop(candidate_document);
363 drop(candidate_response);
364
365 if self.authority.cancellation().is_cancelled() {
366 return Ok(self.visibility_cancelled_report(
367 audit,
368 Some(ApiVisibilityLeg::Candidate),
369 started_at,
370 ));
371 }
372 if let Some(limit) = self.wall_limit_if_reached(started_at) {
373 return Ok(self.visibility_limit_report(
374 audit,
375 Some(ApiVisibilityLeg::Candidate),
376 limit,
377 started_at,
378 ));
379 }
380
381 let comparison = match comparator.compare_profiled(
382 &request.profile,
383 request.comparison_id.into_string(),
384 ApiVisibilityPairKind::AuthorizationContext,
385 request.dimension,
386 &control_view,
387 &candidate_view,
388 request.observed_at_ms,
389 ) {
390 Ok(comparison) => comparison,
391 Err(source) => {
392 return Err(RuntimeApiVisibilityExecutionError::Comparison {
393 audit: Box::new(self.finish_visibility_audit(audit, started_at)),
394 source,
395 });
396 },
397 };
398 if self.authority.cancellation().is_cancelled() {
399 return Ok(self.visibility_cancelled_after_comparison_report(
400 audit, comparison, None, None, started_at,
401 ));
402 }
403 if let Some(limit) = self.wall_limit_if_reached(started_at) {
404 return Ok(self.visibility_limit_after_comparison_report(
405 audit, comparison, None, None, limit, started_at,
406 ));
407 }
408
409 let observation = match comparison
410 .to_observation(OBSERVATION_COMPONENT, evidence_reliability)
411 .map_err(ProfiledApiVisibilityError::from)
412 {
413 Ok(observation) => observation,
414 Err(source) => {
415 return Err(RuntimeApiVisibilityExecutionError::ObservationBuild {
416 audit: Box::new(self.finish_visibility_audit(audit, started_at)),
417 comparison: Box::new(comparison),
418 source,
419 });
420 },
421 };
422 let observation = match ingest_api_visibility_observation(
423 observation,
424 &request.resource_scope,
425 self.authority.knowledge(),
426 self.decision_loop.rules(),
427 ) {
428 Ok(observation) => observation,
429 Err(source) => {
430 return Err(RuntimeApiVisibilityExecutionError::Observation {
431 audit: Box::new(self.finish_visibility_audit(audit, started_at)),
432 comparison: Box::new(comparison),
433 source,
434 });
435 },
436 };
437 let Some(review) =
438 api_visibility_review_for_commit(self.authority.knowledge(), observation.commit())
439 else {
440 return Err(RuntimeApiVisibilityExecutionError::ReviewProjection {
441 audit: Box::new(self.finish_visibility_audit(audit, started_at)),
442 comparison: Box::new(comparison),
443 observation: Box::new(observation),
444 });
445 };
446 let disposition = match review.disposition() {
447 ApiVisibilityReviewDisposition::NoDifferenceObserved => {
448 ApiVisibilityDifferentialDisposition::NoDifferenceObserved
449 },
450 ApiVisibilityReviewDisposition::UnresolvedDifference => {
451 ApiVisibilityDifferentialDisposition::UnresolvedDifference
452 },
453 ApiVisibilityReviewDisposition::AwaitHumanReview => {
454 ApiVisibilityDifferentialDisposition::AwaitHumanReview
455 },
456 };
457 if self.authority.cancellation().is_cancelled() {
458 return Ok(self.visibility_cancelled_after_comparison_report(
459 audit,
460 comparison,
461 Some(observation),
462 Some(review),
463 started_at,
464 ));
465 }
466 if let Some(limit) = self.wall_limit_if_reached(started_at) {
467 return Ok(self.visibility_limit_after_comparison_report(
468 audit,
469 comparison,
470 Some(observation),
471 Some(review),
472 limit,
473 started_at,
474 ));
475 }
476 let audit = self.finish_visibility_audit(audit, started_at);
477 Ok(RuntimeApiVisibilityRunReport {
478 disposition,
479 stopped_leg: None,
480 inconclusive_reason: None,
481 audit,
482 comparison: Some(comparison),
483 observation: Some(observation),
484 review: Some(review),
485 limit_exceeded: None,
486 })
487 }
488
489 fn finish_visibility_audit(
490 &mut self,
491 mut audit: ApiVisibilityDifferentialAudit,
492 started_at: tokio::time::Instant,
493 ) -> ApiVisibilityDifferentialAudit {
494 self.refresh_elapsed(started_at);
495 audit.usage = self.usage.clone();
496 audit.transport = self.authority.request_accounting().dispatch_audit();
497 audit
498 }
499
500 fn visibility_cancelled_report(
501 &mut self,
502 audit: ApiVisibilityDifferentialAudit,
503 stopped_leg: Option<ApiVisibilityLeg>,
504 started_at: tokio::time::Instant,
505 ) -> RuntimeApiVisibilityRunReport {
506 RuntimeApiVisibilityRunReport {
507 disposition: ApiVisibilityDifferentialDisposition::CancelledByHost,
508 stopped_leg,
509 inconclusive_reason: None,
510 audit: self.finish_visibility_audit(audit, started_at),
511 comparison: None,
512 observation: None,
513 review: None,
514 limit_exceeded: None,
515 }
516 }
517
518 fn visibility_limit_report(
519 &mut self,
520 audit: ApiVisibilityDifferentialAudit,
521 stopped_leg: Option<ApiVisibilityLeg>,
522 limit: RuntimeLimitExceeded,
523 started_at: tokio::time::Instant,
524 ) -> RuntimeApiVisibilityRunReport {
525 RuntimeApiVisibilityRunReport {
526 disposition: ApiVisibilityDifferentialDisposition::RuntimeBudgetLimit,
527 stopped_leg,
528 inconclusive_reason: None,
529 audit: self.finish_visibility_audit(audit, started_at),
530 comparison: None,
531 observation: None,
532 review: None,
533 limit_exceeded: Some(limit),
534 }
535 }
536
537 fn visibility_cancelled_after_comparison_report(
538 &mut self,
539 audit: ApiVisibilityDifferentialAudit,
540 comparison: ProfiledApiVisibilityComparison,
541 observation: Option<ApiObservationReceipt>,
542 review: Option<ApiVisibilityReview>,
543 started_at: tokio::time::Instant,
544 ) -> RuntimeApiVisibilityRunReport {
545 RuntimeApiVisibilityRunReport {
546 disposition: ApiVisibilityDifferentialDisposition::CancelledByHost,
547 stopped_leg: None,
548 inconclusive_reason: None,
549 audit: self.finish_visibility_audit(audit, started_at),
550 comparison: Some(comparison),
551 observation,
552 review,
553 limit_exceeded: None,
554 }
555 }
556
557 fn visibility_limit_after_comparison_report(
558 &mut self,
559 audit: ApiVisibilityDifferentialAudit,
560 comparison: ProfiledApiVisibilityComparison,
561 observation: Option<ApiObservationReceipt>,
562 review: Option<ApiVisibilityReview>,
563 limit: RuntimeLimitExceeded,
564 started_at: tokio::time::Instant,
565 ) -> RuntimeApiVisibilityRunReport {
566 RuntimeApiVisibilityRunReport {
567 disposition: ApiVisibilityDifferentialDisposition::RuntimeBudgetLimit,
568 stopped_leg: None,
569 inconclusive_reason: None,
570 audit: self.finish_visibility_audit(audit, started_at),
571 comparison: Some(comparison),
572 observation,
573 review,
574 limit_exceeded: Some(limit),
575 }
576 }
577
578 fn visibility_inconclusive_report(
579 &mut self,
580 audit: ApiVisibilityDifferentialAudit,
581 stopped_leg: ApiVisibilityLeg,
582 reason: ApiVisibilityInconclusiveReason,
583 started_at: tokio::time::Instant,
584 ) -> RuntimeApiVisibilityRunReport {
585 RuntimeApiVisibilityRunReport {
586 disposition: ApiVisibilityDifferentialDisposition::Inconclusive,
587 stopped_leg: Some(stopped_leg),
588 inconclusive_reason: Some(reason),
589 audit: self.finish_visibility_audit(audit, started_at),
590 comparison: None,
591 observation: None,
592 review: None,
593 limit_exceeded: None,
594 }
595 }
596
597 fn visibility_collection_report(
598 &mut self,
599 audit: ApiVisibilityDifferentialAudit,
600 stopped_leg: ApiVisibilityLeg,
601 error: crate::http_evidence::HttpRequestBrokerError,
602 started_at: tokio::time::Instant,
603 ) -> RuntimeApiVisibilityRunReport {
604 let kind = error.failure_kind();
605 if let Some(limit) = error.into_runtime_limit() {
606 return self.visibility_limit_report(audit, Some(stopped_leg), limit, started_at);
607 }
608 self.visibility_inconclusive_report(
609 audit,
610 stopped_leg,
611 inconclusive_reason(kind),
612 started_at,
613 )
614 }
615}
616fn leg_receipt(
617 leg: ApiVisibilityLeg,
618 response: &crate::http_evidence::CollectedHttpResponse,
619 request_template_sha256: &str,
620) -> ApiVisibilityLegReceipt {
621 let mut hasher = Sha256::new();
622 hasher.update(RESPONSE_DIGEST_DOMAIN);
623 update_framed(&mut hasher, response.body());
624 ApiVisibilityLegReceipt {
625 leg,
626 status: response.status(),
627 retained_body_bytes: u64::try_from(response.body().len()).unwrap_or(u64::MAX),
628 body_truncated: response.body_truncated(),
629 json_compatible_media_type: response.has_json_compatible_media_type(),
630 request_template_sha256: request_template_sha256.to_owned(),
631 response_body_sha256: encode_digest(hasher.finalize().as_slice()),
632 }
633}
634
635fn response_document(
636 response: &crate::http_evidence::CollectedHttpResponse,
637) -> Result<serde_json::Value, ApiVisibilityInconclusiveReason> {
638 if response.status() == 429 {
639 return Err(ApiVisibilityInconclusiveReason::RateLimited);
640 }
641 if response.status() >= 500 {
642 return Err(ApiVisibilityInconclusiveReason::ServerError);
643 }
644 if response.body_truncated() {
645 return Err(ApiVisibilityInconclusiveReason::TruncatedResponse);
646 }
647 if !response.has_json_compatible_media_type() {
648 return Err(ApiVisibilityInconclusiveReason::NonJsonResponse);
649 }
650 serde_json::from_slice(response.body())
651 .map_err(|_| ApiVisibilityInconclusiveReason::MalformedJson)
652}
653
654fn inconclusive_reason(kind: DecisionExecutionFailureKind) -> ApiVisibilityInconclusiveReason {
655 match kind {
656 DecisionExecutionFailureKind::NotApplicable => {
657 ApiVisibilityInconclusiveReason::NotApplicable
658 },
659 DecisionExecutionFailureKind::BlockedByPolicy => {
660 ApiVisibilityInconclusiveReason::BlockedByPolicy
661 },
662 DecisionExecutionFailureKind::TransportFailure => {
663 ApiVisibilityInconclusiveReason::TransportFailure
664 },
665 DecisionExecutionFailureKind::RequestTimeout => {
666 ApiVisibilityInconclusiveReason::RequestTimeout
667 },
668 _ => ApiVisibilityInconclusiveReason::ExecutorFailure,
669 }
670}