1use std::{collections::BTreeSet, future::Future, sync::Arc, time::Duration};
19
20use termivar_core::{
21 EntityId, EvidenceValue, HttpEvidencePredicate, OutcomeStatus, ReasoningModelError,
22 VerificationStage,
23};
24use thiserror::Error;
25use tokio_util::sync::CancellationToken;
26use url::Url;
27
28use crate::decision_runner::ContinuationAuthority;
29use crate::http_evidence::CompleteHttpResponseObserver;
30use crate::planner::ActionSuppressionContext;
31#[cfg(feature = "authorization-review")]
32use crate::rules::{Expression, KnowledgeLayer};
33use crate::{
34 AdaptationLimits, AdaptationRule, AdaptivePipelineError, BenefitScore, DecisionActionOrigin,
35 DecisionEvidenceReceipt, DecisionExecutionClass, DecisionExecutionFailureReceipt,
36 DecisionExecutionLimits, DecisionExecutionStage, DecisionExecutorRegistry, DecisionLoop,
37 DecisionLoopCommand, DecisionLoopConfig, DecisionLoopError, DecisionOutcomeReport,
38 DecisionPlanningReport, DecisionReasoningCommitReceipt, DecisionRunnerAdapter,
39 DecisionRunnerError, DecisionRunnerTurn, DecisionSession, DecisionStopReason, ExperiencePolicy,
40 ExperienceStore, ExperienceStoreError, HttpEvidenceError, HttpEvidenceExecutor,
41 HttpEvidencePolicy, HttpHeaderPayloadBinding, HttpProbe, HttpProbeMethod, KnowledgeBase,
42 KnowledgeWrite, OutcomeSelector, PipelineDirective, PlannerError, PlanningContext, RiskScore,
43 RuntimeBudget, RuntimeBudgetDimension, RuntimeLimitExceeded, RuntimeUsage,
44 StandardApiInstallReport, StandardApiReasoning, StandardApiReasoningError,
45 StandardWebActionKind, StandardWebDecisionError, StandardWebDecisionInstallReport,
46 StandardWebDecisionProfile, SubjectHttpProbeProvider, TransportDispatchAudit, VerificationCase,
47 VerificationError, HTTP_EVIDENCE_EXECUTOR_ID,
48};
49
50mod api_visibility;
51mod assessment_api_visibility;
52mod assessment_defense;
53mod assessment_item;
54mod assessment_passive;
55#[cfg(feature = "reporting")]
56mod assessment_report;
57mod assessment_review;
58mod assessment_review_projection;
59mod authority;
60#[cfg(feature = "graphql-review")]
61mod graphql_runtime;
62#[cfg(feature = "jwt-target-acceptance-review")]
63mod jwt_target_acceptance_runtime;
64#[cfg(feature = "openapi-review")]
65mod openapi_runtime;
66#[cfg(feature = "recon-ct-provider")]
67mod recon_ct_runtime;
68#[cfg(feature = "authorization-review")]
69mod resource_authorization_runtime;
70#[cfg(feature = "rest-review")]
71mod rest_runtime;
72mod scan_profile;
73#[cfg(feature = "secret-exposure-review")]
74mod secret_exposure;
75#[cfg(feature = "ssrf-oast-review")]
76mod ssrf_oast_runtime;
77#[cfg(feature = "supplied-session-review")]
78mod supplied_session_runtime;
79#[cfg(feature = "tls-negotiation-review")]
80mod tls_negotiation_review;
81#[cfg(feature = "tls-observation")]
82mod tls_observation;
83mod web_assessment;
84mod web_review_decision;
85mod web_review_execution;
86#[cfg(feature = "websocket-review")]
87mod websocket_runtime;
88#[cfg(feature = "wordpress-review")]
89mod wordpress_discovery;
90#[cfg(feature = "wordpress-review")]
91mod wordpress_fingerprint_runtime;
92#[cfg(feature = "wordpress-review")]
93mod wordpress_runtime;
94#[cfg(feature = "xml-external-entity-review")]
95mod xml_external_entity_runtime;
96
97pub use api_visibility::{
98 ApiVisibilityContextProbe, ApiVisibilityDifferentialAudit,
99 ApiVisibilityDifferentialDisposition, ApiVisibilityDifferentialRequest,
100 ApiVisibilityDifferentialRequestError, ApiVisibilityInconclusiveReason, ApiVisibilityLeg,
101 ApiVisibilityLegReceipt, RuntimeApiVisibilityError, RuntimeApiVisibilityExecutionError,
102 RuntimeApiVisibilityRunReport,
103};
104pub use assessment_api_visibility::{
105 WebAssessmentAuthorizationContextError, WebAssessmentRootAuthorizationContext,
106};
107use assessment_defense::AssessmentDefenseController;
108pub(crate) use assessment_defense::{
109 project_assessment_defense_signal, AssessmentDefenseBodyCoverage,
110 AssessmentDefenseProjectionContext, AssessmentDefenseSignal,
111};
112pub use assessment_item::{
113 AssessmentBasis, AssessmentCaseReference, AssessmentConfirmationDenial,
114 AssessmentDifferentialBasis, AssessmentDisposition, AssessmentEvidenceReference,
115 AssessmentItem, AssessmentItemProjectionError, AssessmentObservationBasis,
116 AssessmentOutcomeReference, AssessmentRemediation, AssessmentSubjectReference,
117 AssessmentVerifierBasis, ASSESSMENT_ITEM_SCHEMA, MAX_ASSESSMENT_CAPABILITY_ID_BYTES,
118 MAX_ASSESSMENT_DISPLAY_BYTES, MAX_ASSESSMENT_ITEM_EVIDENCE_REFERENCES,
119 MAX_ASSESSMENT_ITEM_SET_ITEMS,
120};
121pub(crate) use assessment_review::AssessmentReviewObserverSet;
122#[cfg(any(
123 feature = "authorization-review",
124 feature = "jwt-target-acceptance-review",
125 feature = "supplied-session-review"
126))]
127pub(crate) use authority::authenticated_transport_is_allowed;
128#[cfg(feature = "oast-native-provider")]
129pub(crate) use authority::NativeOastProviderMintToken;
130pub(crate) use authority::SharedWebRuntimeAuthority;
131#[cfg(feature = "supplied-session-review")]
132pub(crate) use authority::{OAuthCodeMintToken, OAuthRefreshMintToken};
133#[cfg(feature = "supplied-session-review")]
134pub(crate) use supplied_session_runtime::OAuthAuthorizationDispatchPermit;
135#[cfg(feature = "supplied-session-review")]
136pub(crate) use supplied_session_runtime::OAuthRefreshDispatchPermit;
137#[cfg(feature = "tls-negotiation-review")]
138pub use tls_negotiation_review::{
139 tls_negotiation_review_target_is_supported, WebAssessmentTlsNegotiationReviewAudit,
140 WebAssessmentTlsNegotiationReviewCell, TLS_NEGOTIATION_REVIEW_ACTION_TLS12,
141 TLS_NEGOTIATION_REVIEW_ACTION_TLS13, TLS_NEGOTIATION_REVIEW_AUDIT_SCHEMA,
142 TLS_NEGOTIATION_REVIEW_CAPABILITY_ID, TLS_NEGOTIATION_REVIEW_POLICY_ID,
143};
144#[cfg(feature = "tls-observation")]
145pub(crate) use tls_observation::TlsObservationCollector;
146pub(crate) use web_assessment::AssessmentDiscoveryObserver;
147use web_review_decision::NativeWebReviewDecisionProfile;
148use web_review_execution::{
149 NativeWebReviewExecutorProfile, NativeWebReviewQueryParameters, NativeWebReviewSeeds,
150};
151
152#[cfg(all(feature = "websocket-review", feature = "supplied-session-review"))]
153pub use websocket_runtime::WEBSOCKET_SUPPLIED_SESSION_AUDIT_SCHEMA;
154#[cfg(feature = "websocket-review")]
155pub use websocket_runtime::{
156 WebAssessmentWebSocketReviewAudit, WebSocketReviewClaimLimit, WebSocketReviewCompleteness,
157 WebSocketReviewExecutionLimits, WebSocketReviewFailureKind, WebSocketReviewMessageAudit,
158 WebSocketReviewMessageStatus, WebSocketReviewTerminal,
159 MAX_WEBSOCKET_REVIEW_APPLICATION_BYTES_PER_DIRECTION, MAX_WEBSOCKET_REVIEW_CONNECTIONS,
160 WEBSOCKET_REVIEW_ACTION_ID, WEBSOCKET_REVIEW_AUDIT_SCHEMA, WEBSOCKET_REVIEW_AUTHENTICATION,
161 WEBSOCKET_REVIEW_CAPABILITY_ID, WEBSOCKET_REVIEW_HTTP2_EXTENDED_CONNECT,
162 WEBSOCKET_REVIEW_TRANSPORT, WEBSOCKET_REVIEW_TRANSPORT_BYTE_SCOPE,
163};
164
165#[cfg(feature = "reporting")]
166pub use assessment_report::{
167 AssessmentRunReport, AssessmentRunReportError, ASSESSMENT_RUN_REPORT_SCHEMA,
168 MAX_ASSESSMENT_RUN_ITEMS,
169};
170
171#[cfg(feature = "supplied-session-review")]
172pub use crate::supplied_session_review::{
173 MAX_SUPPLIED_SESSION_CHECKPOINTS, MAX_SUPPLIED_SESSION_REQUESTS, MAX_SUPPLIED_SESSION_RESOURCES,
174};
175#[cfg(feature = "xml-external-entity-review")]
176pub use crate::xml_external_entity_review::{
177 XmlExternalEntityExecutionMode, XmlExternalEntityReviewAudit, XmlExternalEntityReviewOutcome,
178 XML_EXTERNAL_ENTITY_ACTIVE_VERIFICATIONS, XML_EXTERNAL_ENTITY_REVIEW_ACTION_ID,
179 XML_EXTERNAL_ENTITY_REVIEW_AUDIT_SCHEMA, XML_EXTERNAL_ENTITY_REVIEW_CAPABILITY_ID,
180 XML_EXTERNAL_ENTITY_TARGET_REQUESTS,
181};
182#[cfg(feature = "graphql-review")]
183pub(crate) use graphql_runtime::GraphqlResponsePrivacy;
184#[cfg(feature = "openapi-review")]
185pub use openapi_runtime::{
186 OpenApiCandidateSource, OpenApiRuntimeOutcome, WebAssessmentOpenApiAudit,
187 MAX_OPENAPI_REVIEW_ACTIVE_VERIFICATIONS, MAX_OPENAPI_REVIEW_DOCUMENTS,
188 MAX_OPENAPI_REVIEW_REQUESTS, OPENAPI_REVIEW_ACTION_ID, OPENAPI_REVIEW_CAPABILITY_ID,
189};
190#[cfg(feature = "recon-ct-provider")]
191pub use recon_ct_runtime::{
192 recon_ct_provider_scope_is_valid, ReconCtProviderFailureKind, WebAssessmentReconCtProviderAudit,
193};
194#[cfg(feature = "authorization-review")]
195pub use resource_authorization_runtime::{
196 WebAssessmentAuthorizationAudit, MAX_AUTHORIZATION_REVIEW_ACTIVE_VERIFICATIONS,
197 MAX_AUTHORIZATION_REVIEW_REQUESTS, MAX_AUTHORIZATION_REVIEW_RESOURCES,
198 RESOURCE_AUTHORIZATION_REVIEW_ACTION_ID, RESOURCE_AUTHORIZATION_REVIEW_CAPABILITY_ID,
199};
200#[cfg(feature = "rest-review")]
201pub use rest_runtime::{
202 RestObservedMediaClass, RestRuntimeOutcome, WebAssessmentRestAudit,
203 MAX_REST_REVIEW_ACTIVE_VERIFICATIONS, MAX_REST_REVIEW_REQUESTS, MAX_REST_REVIEW_RESOURCES,
204 REST_REVIEW_ACTION_ID, REST_REVIEW_CAPABILITY_ID,
205};
206#[cfg(feature = "scanning")]
207pub use scan_profile::{
208 BuiltInScanProfile, BuiltInScanProfileParseError, ScanProfileCapabilitiesV1,
209 ScanProfileLimitsV1, ScanProfileScope, ScanProfileSelectionError, ScanProfileV1,
210 ScanProfileV1Error, BASELINE_SCAN_PROFILE_ID, BASELINE_SCAN_PROFILE_MAX_TOTAL_REQUESTS,
211 BASELINE_SCAN_PROFILE_MAX_TOTAL_RESPONSE_BYTES, BASELINE_SCAN_PROFILE_MAX_WALL_TIME_MS,
212 SCAN_PROFILE_V1_SCHEMA, WEB_REVIEW_SCAN_PROFILE_ID,
213};
214#[cfg(all(feature = "graphql-review", feature = "secret-exposure-review"))]
215pub(crate) use secret_exposure::project_graphql_body_privacy_lineage;
216#[cfg(feature = "secret-exposure-review")]
217pub use secret_exposure::{
218 SecretExposureDetectorClass, SecretExposureResponseOutcome, WebAssessmentSecretExposureAudit,
219 MAX_SECRET_EXPOSURE_BODY_BYTES, MAX_SECRET_EXPOSURE_OCCURRENCES, MAX_SECRET_EXPOSURE_RESPONSES,
220 MAX_SECRET_EXPOSURE_RETAINED_OBSERVATIONS, MAX_SECRET_EXPOSURE_TOTAL_BODY_BYTES,
221 SECRET_EXPOSURE_AUDIT_SCHEMA, SECRET_EXPOSURE_CATALOGUE_ID, SECRET_EXPOSURE_CATALOGUE_REVISION,
222 SECRET_EXPOSURE_POLICY_ID, SECRET_EXPOSURE_REPRESENTATION,
223};
224#[cfg(feature = "ssrf-oast-review")]
225pub use ssrf_oast_runtime::{
226 SsrfOastProviderFailure, SsrfOastRuntimeOutcome, WebAssessmentSsrfOastAudit,
227 MAX_SSRF_OAST_REVIEW_ACTIVE_VERIFICATIONS, MAX_SSRF_OAST_REVIEW_PARAMETERS,
228 MAX_SSRF_OAST_REVIEW_PROVIDER_REQUESTS, MAX_SSRF_OAST_REVIEW_REQUESTS,
229 MAX_SSRF_OAST_REVIEW_RESOURCES, SSRF_OAST_REVIEW_ACTION_ID, SSRF_OAST_REVIEW_CAPABILITY_ID,
230};
231#[cfg(feature = "supplied-session-review")]
232pub use supplied_session_runtime::{
233 SuppliedSessionAuditOutcome, SuppliedSessionBodyState, SuppliedSessionCookieLifecycleAudit,
234 SuppliedSessionCookiePolicyAudit, SuppliedSessionCoverage,
235 SuppliedSessionHealthCheckpointPhase, SuppliedSessionHealthOracleAudit,
236 SuppliedSessionHealthOracleKind, SuppliedSessionHealthOutcome, SuppliedSessionLoginFormOutcome,
237 SuppliedSessionLoginSubmitOutcome, SuppliedSessionOAuthAuthorizationAudit,
238 SuppliedSessionOAuthAuthorizationOutcome, SuppliedSessionOAuthCodeAudit,
239 SuppliedSessionOAuthCodeOutcome, SuppliedSessionOAuthInitiatedTokenAudit,
240 SuppliedSessionOAuthRefreshAudit, SuppliedSessionOAuthRefreshOutcome,
241 SuppliedSessionPredicateOutcome, SuppliedSessionPrincipalAssurance,
242 SuppliedSessionResourceOutcome, WebAssessmentSuppliedSessionAudit,
243 WebAssessmentSuppliedSessionCheckpointAudit, WebAssessmentSuppliedSessionResourceAudit,
244 SUPPLIED_SESSION_AUDIT_SCHEMA, SUPPLIED_SESSION_CAPABILITY_ID,
245 SUPPLIED_SESSION_COOKIE_AUDIT_SCHEMA, SUPPLIED_SESSION_FORM_LOGIN_AUDIT_SCHEMA,
246 SUPPLIED_SESSION_OAUTH_AUTHORIZATION_AUDIT_SCHEMA, SUPPLIED_SESSION_OAUTH_CODE_AUDIT_SCHEMA,
247 SUPPLIED_SESSION_OAUTH_REFRESH_AUDIT_SCHEMA, SUPPLIED_SESSION_RENEWAL_AUDIT_SCHEMA,
248};
249#[cfg(feature = "tls-observation")]
250pub use tls_observation::{
251 TlsCertificateTimeStatus, TlsLeafObservation, TlsObservationTargetScheme,
252 WebAssessmentTlsObservationAudit, MAX_TLS_CERTIFICATE_EXTENSIONS,
253 MAX_TLS_LEAF_CERTIFICATE_BYTES, MAX_TLS_RETAINED_LEAF_OBSERVATIONS,
254 MAX_TLS_SAN_ENTRIES_PER_CERTIFICATE, MAX_TLS_SUBJECT_ALTERNATIVE_NAME_BYTES,
255 TLS_OBSERVATION_AUDIT_SCHEMA, TLS_OBSERVATION_BACKEND_LIMIT, TLS_OBSERVATION_CLOCK_ASSURANCE,
256 TLS_OBSERVATION_POLICY_ID, TLS_OBSERVATION_REVOCATION_STATUS, TLS_OBSERVATION_SOURCE_SCOPE,
257 TLS_OBSERVATION_VALIDATION_SCOPE,
258};
259pub use web_assessment::{
260 WebAssessmentCompletion, WebAssessmentDefenseAudit, WebAssessmentDefenseBodyCoverage,
261 WebAssessmentDefenseMode, WebAssessmentDefenseObservation, WebAssessmentDefenseShadowPlan,
262 WebAssessmentDefenseTransition, WebAssessmentFailureReceipt, WebAssessmentForm,
263 WebAssessmentFormMethod, WebAssessmentIncompleteReason, WebAssessmentLimits,
264 WebAssessmentLimitsError, WebAssessmentMethod, WebAssessmentProgressCheckpoint,
265 WebAssessmentProgressObserver, WebAssessmentProgressSnapshot, WebAssessmentRunReport,
266 WebAssessmentRuntime, WebAssessmentRuntimeBuilder, WebAssessmentRuntimeError,
267 WebAssessmentSubject, WebAssessmentSubjectOrigin, WebAssessmentSubjectReport,
268 WebAssessmentUsage, DEFAULT_WEB_ASSESSMENT_MAX_ACTIVE_VERIFICATIONS,
269 DEFAULT_WEB_ASSESSMENT_MAX_CANONICAL_URL_BYTES, DEFAULT_WEB_ASSESSMENT_MAX_CONTROLS_PER_FORM,
270 DEFAULT_WEB_ASSESSMENT_MAX_DEPTH, DEFAULT_WEB_ASSESSMENT_MAX_FORMS,
271 DEFAULT_WEB_ASSESSMENT_MAX_QUERY_NAMES, DEFAULT_WEB_ASSESSMENT_MAX_REFERENCES_PER_DOCUMENT,
272 DEFAULT_WEB_ASSESSMENT_MAX_RESPONSE_BODY_BYTES, DEFAULT_WEB_ASSESSMENT_MAX_RETAINED_URL_BYTES,
273 DEFAULT_WEB_ASSESSMENT_MAX_SUBJECTS, DEFAULT_WEB_ASSESSMENT_MAX_TOTAL_REQUESTS,
274 DEFAULT_WEB_ASSESSMENT_MAX_TOTAL_RESPONSE_BYTES, DEFAULT_WEB_ASSESSMENT_MAX_WALL_TIME,
275 HARD_MAX_WEB_ASSESSMENT_ACTIVE_VERIFICATIONS, HARD_MAX_WEB_ASSESSMENT_CANONICAL_URL_BYTES,
276 HARD_MAX_WEB_ASSESSMENT_CONTROLS_PER_FORM, HARD_MAX_WEB_ASSESSMENT_DEPTH,
277 HARD_MAX_WEB_ASSESSMENT_FORMS, HARD_MAX_WEB_ASSESSMENT_QUERY_NAMES,
278 HARD_MAX_WEB_ASSESSMENT_REFERENCES_PER_DOCUMENT, HARD_MAX_WEB_ASSESSMENT_RESPONSE_BODY_BYTES,
279 HARD_MAX_WEB_ASSESSMENT_RETAINED_URL_BYTES, HARD_MAX_WEB_ASSESSMENT_SUBJECTS,
280 HARD_MAX_WEB_ASSESSMENT_TOTAL_REQUESTS, HARD_MAX_WEB_ASSESSMENT_TOTAL_RESPONSE_BYTES,
281 HARD_MAX_WEB_ASSESSMENT_WALL_TIME, WEB_ASSESSMENT_CONCURRENCY,
282};
283#[cfg(feature = "wordpress-review")]
284pub use wordpress_runtime::{
285 WebAssessmentWordPressAudit, WebAssessmentWordPressDiscoveryAudit,
286 WordPressAssetFingerprintAcquisition, WordPressAssetFingerprintExecution,
287 WordPressAssetFingerprintResourceOutcome, WordPressAssetFingerprintResourceReceipt,
288 WordPressDiscoverySourceAudit, WordPressDiscoverySourceKind, WordPressDiscoverySourceOutcome,
289 WordPressPageAcquisition, WordPressPageAssociation, WordPressPageAudit, WordPressPageOutcome,
290 WordPressPageScope, WordPressPageScopeAudit, WordPressPluginDiscoveryMetadata,
291 WordPressThemeDiscoveryMetadata, WORDPRESS_DISCOVERY_OBSERVATION_CAPABILITY_ID,
292 WORDPRESS_REVIEW_CAPABILITY_ID,
293};
294
295#[cfg(all(fuzzing, feature = "wordpress-review"))]
298#[doc(hidden)]
299pub fn check_wordpress_discovery_fuzz_input(data: &[u8]) {
300 const MAX_FUZZ_INPUT_BYTES: usize = 512 * 1024;
301 if data.len() > MAX_FUZZ_INPUT_BYTES {
302 return;
303 }
304 let (scenario, payload) = data
305 .split_first()
306 .map_or((0, data), |(tag, rest)| (*tag, rest));
307 wordpress_discovery::fuzz_check_wordpress_discovery_parser(scenario, payload);
308 let origin =
309 Url::parse("https://wordpress-fuzz.invalid/").expect("fixed fuzz origin must remain valid");
310 let header_payload = if scenario % 8 == 1 {
311 payload
312 .strip_suffix(b"\n")
313 .map(|value| value.strip_suffix(b"\r").unwrap_or(value))
314 .unwrap_or(payload)
315 } else {
316 payload
317 };
318 let rest =
319 crate::http_evidence::fuzz_wordpress_rest_index_advertisement(&origin, header_payload);
320 wordpress_runtime::fuzz_check_discovery_candidate_admission(payload, &rest);
321}
322
323const DEFAULT_BUSINESS_VALUE_PERCENT: u8 = 80;
324const DEFAULT_PLANNING_BUDGET: u64 = 100;
325const DEFAULT_RISK_LIMIT_PERCENT: u8 = 40;
326const DEFAULT_MAX_ACTION_CYCLES: u32 = 8;
327const DEFAULT_FAILURE_LIMIT: u16 = 10;
328pub(crate) const BOOTSTRAP_ACTION_ID: &str = "web.action.bootstrap.http-evidence";
329pub(crate) const BOOTSTRAP_CASE_ID: &str = "case:web-runtime:bootstrap:http";
330pub(crate) const BOOTSTRAP_HYPOTHESIS_ID: &str = "hypothesis:web-runtime:bootstrap";
331#[derive(Debug, Error)]
333#[non_exhaustive]
334pub enum StandardWebDecisionRuntimeError {
335 #[error("standard web decision runtime has already started")]
337 AlreadyStarted,
338
339 #[error(transparent)]
341 Planner(#[from] PlannerError),
342
343 #[error(transparent)]
345 Decision(#[from] DecisionLoopError),
346
347 #[error(transparent)]
349 Experience(#[from] ExperienceStoreError),
350
351 #[error(transparent)]
353 Reasoning(#[from] ReasoningModelError),
354
355 #[error(transparent)]
357 Verification(#[from] VerificationError),
358
359 #[error(transparent)]
361 Http(#[from] HttpEvidenceError),
362
363 #[error(transparent)]
365 Profile(#[from] StandardWebDecisionError),
366
367 #[error(transparent)]
369 ApiReasoning(#[from] StandardApiReasoningError),
370
371 #[error("native web-review decision profile could not be composed")]
373 NativeWebReviewDecisionProfile,
374
375 #[error("native web-review execution profile could not be composed")]
377 NativeWebReviewExecutionProfile,
378
379 #[cfg(feature = "authorization-review")]
381 #[error("resource authorization review could not be composed")]
382 ResourceAuthorizationReviewComposition,
383
384 #[error(transparent)]
386 Runner(#[from] DecisionRunnerError),
387
388 #[error(
390 "execution case {case_id} emitted {observations} unsigned {predicate} observations; expected exactly one"
391 )]
392 ResponseUsageEvidence {
393 case_id: String,
395 predicate: &'static str,
397 observations: usize,
399 receipt: Box<DecisionEvidenceReceipt>,
401 },
402
403 #[error("committed assessment defense projection violated its closed schema")]
405 AssessmentDefenseProjectionInvariant {
406 receipt: Box<DecisionEvidenceReceipt>,
408 },
409
410 #[error("assessment defense planning authority invariant failed")]
412 AssessmentDefensePlanningInvariant,
413
414 #[error("runtime resource accounting requires an execution command")]
416 ExecutionMetadataUnavailable,
417
418 #[error("standard web decision runtime failed after it started: {source}")]
424 RunFailed {
425 receipt: Box<StandardWebDecisionFailureReceipt>,
427 #[source]
429 source: Box<StandardWebDecisionRuntimeError>,
430 },
431}
432
433impl StandardWebDecisionRuntimeError {
434 pub fn failure_receipt(&self) -> Option<&StandardWebDecisionFailureReceipt> {
436 match self {
437 Self::RunFailed { receipt, .. } => Some(receipt),
438 _ => None,
439 }
440 }
441
442 pub(crate) fn into_assessment_failure(
445 self,
446 ) -> (
447 StandardWebDecisionAssessmentFailureParts,
448 StandardWebDecisionRuntimeError,
449 ) {
450 match self {
451 Self::RunFailed { receipt, source } => (receipt.into_assessment_parts(), *source),
452 source => (StandardWebDecisionAssessmentFailureParts::default(), source),
453 }
454 }
455
456 pub fn into_failure_receipt(self) -> Option<StandardWebDecisionFailureReceipt> {
458 match self {
459 Self::RunFailed { receipt, .. } => Some(*receipt),
460 _ => None,
461 }
462 }
463
464 pub fn execution_failure(&self) -> Option<&DecisionExecutionFailureReceipt> {
466 match self {
467 Self::Runner(source) => source.execution_failure(),
468 Self::RunFailed { source, .. } => source.execution_failure(),
469 _ => None,
470 }
471 }
472
473 pub fn into_execution_failure(self) -> Option<DecisionExecutionFailureReceipt> {
475 match self {
476 Self::Runner(source) => source.into_execution_failure(),
477 Self::RunFailed { source, .. } => source.into_execution_failure(),
478 _ => None,
479 }
480 }
481
482 pub fn committed_evidence(&self) -> Option<&DecisionEvidenceReceipt> {
484 match self {
485 Self::Runner(source) => source.committed_evidence(),
486 Self::ResponseUsageEvidence { receipt, .. } => Some(receipt),
487 Self::AssessmentDefenseProjectionInvariant { receipt } => Some(receipt),
488 Self::RunFailed { source, .. } => source.committed_evidence(),
489 _ => None,
490 }
491 }
492
493 pub fn into_committed_evidence(self) -> Option<DecisionEvidenceReceipt> {
495 match self {
496 Self::Runner(source) => source.into_committed_evidence(),
497 Self::ResponseUsageEvidence { receipt, .. } => Some(*receipt),
498 Self::AssessmentDefenseProjectionInvariant { receipt } => Some(*receipt),
499 Self::RunFailed { source, .. } => source.into_committed_evidence(),
500 _ => None,
501 }
502 }
503
504 pub fn committed_reasoning(&self) -> Option<&DecisionReasoningCommitReceipt> {
506 match self {
507 Self::Decision(source) => source.committed_reasoning(),
508 Self::Runner(source) => source.committed_reasoning(),
509 Self::RunFailed { source, .. } => source.committed_reasoning(),
510 _ => None,
511 }
512 }
513
514 pub fn into_committed_reasoning(self) -> Option<DecisionReasoningCommitReceipt> {
516 match self {
517 Self::Decision(source) => source.into_committed_reasoning(),
518 Self::Runner(source) => source.into_committed_reasoning(),
519 Self::RunFailed { source, .. } => source.into_committed_reasoning(),
520 _ => None,
521 }
522 }
523}
524
525#[derive(Debug)]
527#[non_exhaustive]
528pub enum StandardWebDecisionRuntimeTurn {
529 Planning(Box<DecisionPlanningReport>),
531 Outcome {
533 evidence: Box<DecisionEvidenceReceipt>,
535 decision: Box<DecisionOutcomeReport>,
537 },
538}
539
540#[derive(Debug)]
546pub struct StandardWebDecisionFailureReceipt {
547 bootstrap: Option<DecisionEvidenceReceipt>,
548 completed_turns: Vec<StandardWebDecisionRuntimeTurn>,
549 usage: RuntimeUsage,
550 transport: TransportDispatchAudit,
551}
552
553impl StandardWebDecisionFailureReceipt {
554 pub fn bootstrap(&self) -> Option<&DecisionEvidenceReceipt> {
556 self.bootstrap.as_ref()
557 }
558
559 pub fn completed_turns(&self) -> &[StandardWebDecisionRuntimeTurn] {
561 &self.completed_turns
562 }
563
564 pub fn usage(&self) -> &RuntimeUsage {
566 &self.usage
567 }
568
569 pub fn transport(&self) -> &TransportDispatchAudit {
571 &self.transport
572 }
573
574 fn into_assessment_parts(self: Box<Self>) -> StandardWebDecisionAssessmentFailureParts {
575 let Self {
576 bootstrap,
577 completed_turns,
578 usage: _,
579 transport: _,
580 } = *self;
581 StandardWebDecisionAssessmentFailureParts {
582 bootstrap,
583 turns: completed_turns,
584 }
585 }
586}
587
588#[derive(Debug)]
590pub struct StandardWebDecisionRunReport {
591 bootstrap: Option<DecisionEvidenceReceipt>,
592 turns: Vec<StandardWebDecisionRuntimeTurn>,
593 unverified_evidence: Option<DecisionEvidenceReceipt>,
594 terminal: DecisionLoopCommand,
595 usage: RuntimeUsage,
596 transport: TransportDispatchAudit,
597 limit_exceeded: Option<RuntimeLimitExceeded>,
598 execution_failure: Option<DecisionExecutionFailureReceipt>,
599}
600
601pub(crate) struct StandardWebDecisionAssessmentParts {
607 pub(crate) bootstrap: Option<DecisionEvidenceReceipt>,
608 pub(crate) turns: Vec<StandardWebDecisionRuntimeTurn>,
609 pub(crate) unverified_evidence: Option<DecisionEvidenceReceipt>,
610 pub(crate) terminal: DecisionLoopCommand,
611 pub(crate) limit_exceeded: Option<RuntimeLimitExceeded>,
612 pub(crate) execution_failure: Option<DecisionExecutionFailureReceipt>,
613}
614
615#[derive(Default)]
620pub(crate) struct StandardWebDecisionAssessmentFailureParts {
621 pub(crate) bootstrap: Option<DecisionEvidenceReceipt>,
622 pub(crate) turns: Vec<StandardWebDecisionRuntimeTurn>,
623}
624
625impl StandardWebDecisionRunReport {
626 pub fn bootstrap(&self) -> Option<&DecisionEvidenceReceipt> {
628 self.bootstrap.as_ref()
629 }
630
631 pub fn turns(&self) -> &[StandardWebDecisionRuntimeTurn] {
633 &self.turns
634 }
635
636 pub fn unverified_evidence(&self) -> Option<&DecisionEvidenceReceipt> {
643 self.unverified_evidence.as_ref()
644 }
645
646 pub fn terminal(&self) -> &DecisionLoopCommand {
648 &self.terminal
649 }
650
651 pub fn usage(&self) -> &RuntimeUsage {
653 &self.usage
654 }
655
656 pub fn transport(&self) -> &TransportDispatchAudit {
658 &self.transport
659 }
660
661 pub fn limit_exceeded(&self) -> Option<&RuntimeLimitExceeded> {
663 self.limit_exceeded.as_ref()
664 }
665
666 pub fn execution_failure(&self) -> Option<&DecisionExecutionFailureReceipt> {
669 self.execution_failure.as_ref()
670 }
671
672 pub fn planning_reports(&self) -> impl Iterator<Item = &DecisionPlanningReport> {
674 self.turns.iter().filter_map(|turn| match turn {
675 StandardWebDecisionRuntimeTurn::Planning(report) => Some(report.as_ref()),
676 StandardWebDecisionRuntimeTurn::Outcome { .. } => None,
677 })
678 }
679
680 pub fn outcome_reports(&self) -> impl Iterator<Item = &DecisionOutcomeReport> {
682 self.turns.iter().filter_map(|turn| match turn {
683 StandardWebDecisionRuntimeTurn::Outcome { decision, .. } => Some(decision.as_ref()),
684 StandardWebDecisionRuntimeTurn::Planning(_) => None,
685 })
686 }
687
688 pub(crate) fn into_assessment_parts(self) -> StandardWebDecisionAssessmentParts {
689 StandardWebDecisionAssessmentParts {
690 bootstrap: self.bootstrap,
691 turns: self.turns,
692 unverified_evidence: self.unverified_evidence,
693 terminal: self.terminal,
694 limit_exceeded: self.limit_exceeded,
695 execution_failure: self.execution_failure,
696 }
697 }
698}
699
700pub struct StandardWebDecisionRuntimeBuilder {
702 target: Url,
703 http_policy: Option<HttpEvidencePolicy>,
704 business_value_percent: u8,
705 planning_budget: u64,
706 risk_limit_percent: u8,
707 adaptation_limits: AdaptationLimits,
708 experience_failure_limit: u16,
709 max_action_cycles: u32,
710 experience: ExperienceStore,
711 runtime_budget: RuntimeBudget,
712 api_reasoning_enabled: bool,
713 payload_binding: Option<HttpHeaderPayloadBinding>,
714 cancellation: CancellationToken,
715 bootstrap_probe_method: HttpProbeMethod,
716 complete_response_observer: Option<Arc<dyn CompleteHttpResponseObserver>>,
717 additional_suppressed_actions: BTreeSet<String>,
718 assessment_defense_projection: bool,
719 assessment_defense_enforcement: bool,
720 native_web_review: Option<NativeWebReviewRuntimeConfig>,
721 #[cfg(feature = "authorization-review")]
722 resource_authorization_review:
723 Option<resource_authorization_runtime::ResourceAuthorizationReviewConfig>,
724 #[cfg(feature = "openapi-review")]
725 openapi_review: Option<openapi_runtime::OpenApiReviewConfig>,
726 #[cfg(feature = "rest-review")]
727 rest_review: bool,
728 #[cfg(feature = "ssrf-oast-review")]
729 ssrf_oast_review: Option<ssrf_oast_runtime::SsrfOastReviewConfig>,
730}
731
732struct NativeWebReviewRuntimeConfig {
733 seeds: NativeWebReviewSeeds,
734 observer: Arc<dyn CompleteHttpResponseObserver>,
735 redirect_query_parameter: Option<String>,
736 reflection_query_parameter: Option<String>,
737 sql_query_parameter: Option<String>,
738 ssti_query_parameter: Option<String>,
739 #[cfg(feature = "template-evaluation-review")]
740 template_evaluation_query_parameter: Option<String>,
741 xss_query_parameter: Option<String>,
742 xss_selection: Option<web_assessment::XssProbeSelection>,
743 #[cfg(feature = "normalization-resilience")]
744 normalization_query_parameter: Option<String>,
745 #[cfg(feature = "normalization-resilience")]
746 normalization_selection: Option<web_assessment::NormalizationTransformSelection>,
747 structural_only: bool,
748}
749
750struct StandardWebDecisionRuntimePreflight {
751 config: DecisionLoopConfig,
752 subject: EntityId,
753}
754
755impl StandardWebDecisionRuntimeBuilder {
756 pub fn new(target: Url) -> Self {
758 Self {
759 target,
760 http_policy: None,
761 business_value_percent: DEFAULT_BUSINESS_VALUE_PERCENT,
762 planning_budget: DEFAULT_PLANNING_BUDGET,
763 risk_limit_percent: DEFAULT_RISK_LIMIT_PERCENT,
764 adaptation_limits: AdaptationLimits::default(),
765 experience_failure_limit: DEFAULT_FAILURE_LIMIT,
766 max_action_cycles: DEFAULT_MAX_ACTION_CYCLES,
767 experience: ExperienceStore::new(),
768 runtime_budget: RuntimeBudget::default(),
769 api_reasoning_enabled: false,
770 payload_binding: None,
771 cancellation: CancellationToken::new(),
772 bootstrap_probe_method: HttpProbeMethod::Get,
773 complete_response_observer: None,
774 additional_suppressed_actions: BTreeSet::new(),
775 assessment_defense_projection: false,
776 assessment_defense_enforcement: false,
777 native_web_review: None,
778 #[cfg(feature = "authorization-review")]
779 resource_authorization_review: None,
780 #[cfg(feature = "openapi-review")]
781 openapi_review: None,
782 #[cfg(feature = "rest-review")]
783 rest_review: false,
784 #[cfg(feature = "ssrf-oast-review")]
785 ssrf_oast_review: None,
786 }
787 }
788
789 pub fn enable_api_reasoning(mut self) -> Self {
794 self.api_reasoning_enabled = true;
795 self
796 }
797
798 pub fn http_policy(mut self, policy: HttpEvidencePolicy) -> Self {
800 self.http_policy = Some(policy);
801 self
802 }
803
804 pub fn with_payload_binding(mut self, binding: HttpHeaderPayloadBinding) -> Self {
812 self.payload_binding = Some(binding);
813 self
814 }
815
816 pub fn business_value(mut self, percent: u8) -> Self {
818 self.business_value_percent = percent;
819 self
820 }
821
822 pub fn planning_budget(mut self, budget: u64) -> Self {
824 self.planning_budget = budget;
825 self
826 }
827
828 pub fn risk_limit(mut self, percent: u8) -> Self {
830 self.risk_limit_percent = percent;
831 self
832 }
833
834 pub fn adaptation_limits(mut self, limits: AdaptationLimits) -> Self {
836 self.adaptation_limits = limits;
837 self
838 }
839
840 pub fn experience_failure_limit(mut self, limit: u16) -> Self {
842 self.experience_failure_limit = limit;
843 self
844 }
845
846 pub fn max_action_cycles(mut self, cycles: u32) -> Self {
848 self.max_action_cycles = cycles;
849 self
850 }
851
852 pub fn experience_store(mut self, experience: ExperienceStore) -> Self {
854 self.experience = experience;
855 self
856 }
857
858 pub fn runtime_budget(mut self, budget: RuntimeBudget) -> Self {
860 self.runtime_budget = budget;
861 self
862 }
863
864 pub fn cancellation_token(mut self, cancellation: CancellationToken) -> Self {
870 self.cancellation = cancellation;
871 self
872 }
873
874 pub(crate) fn with_assessment_response_observer(
880 mut self,
881 method: HttpProbeMethod,
882 observer: Arc<dyn CompleteHttpResponseObserver>,
883 ) -> Self {
884 self.bootstrap_probe_method = method;
885 self.complete_response_observer = Some(observer);
886 self.assessment_defense_projection = true;
887 if method == HttpProbeMethod::Head {
888 self.additional_suppressed_actions.extend(
889 StandardWebActionKind::all()
890 .into_iter()
891 .map(|kind| kind.action_id().to_owned()),
892 );
893 }
894 self
895 }
896
897 pub(crate) fn with_assessment_defense_enforcement(mut self, enabled: bool) -> Self {
898 self.assessment_defense_projection = true;
899 self.assessment_defense_enforcement = enabled;
900 self
901 }
902
903 #[cfg(feature = "authorization-review")]
906 pub(in crate::web_runtime) fn with_resource_authorization_review(
907 mut self,
908 config: resource_authorization_runtime::ResourceAuthorizationReviewConfig,
909 ) -> Self {
910 self.assessment_defense_projection = true;
911 self.resource_authorization_review = Some(config);
912 self
913 }
914
915 #[cfg(feature = "openapi-review")]
916 pub(in crate::web_runtime) fn with_openapi_review(
917 mut self,
918 config: openapi_runtime::OpenApiReviewConfig,
919 ) -> Self {
920 self.assessment_defense_projection = true;
921 self.openapi_review = Some(config);
922 self
923 }
924
925 #[cfg(feature = "rest-review")]
926 pub(in crate::web_runtime) fn with_rest_review(mut self) -> Self {
927 self.assessment_defense_projection = true;
928 self.rest_review = true;
929 self
930 }
931
932 #[cfg(feature = "ssrf-oast-review")]
933 pub(in crate::web_runtime) fn with_ssrf_oast_review(
934 mut self,
935 config: ssrf_oast_runtime::SsrfOastReviewConfig,
936 ) -> Self {
937 self.assessment_defense_projection = true;
938 self.ssrf_oast_review = Some(config);
939 self
940 }
941
942 pub(crate) fn with_native_web_review(
950 mut self,
951 seeds: NativeWebReviewSeeds,
952 observer: Arc<dyn CompleteHttpResponseObserver>,
953 redirect_query_parameter: Option<String>,
954 reflection_query_parameter: Option<String>,
955 sql_query_parameter: Option<String>,
956 ssti_query_parameter: Option<String>,
957 ) -> Self {
958 self.assessment_defense_projection = true;
959 self.native_web_review = Some(NativeWebReviewRuntimeConfig {
960 seeds,
961 observer,
962 redirect_query_parameter,
963 reflection_query_parameter,
964 sql_query_parameter,
965 ssti_query_parameter,
966 #[cfg(feature = "template-evaluation-review")]
967 template_evaluation_query_parameter: None,
968 xss_query_parameter: None,
969 xss_selection: None,
970 #[cfg(feature = "normalization-resilience")]
971 normalization_query_parameter: None,
972 #[cfg(feature = "normalization-resilience")]
973 normalization_selection: None,
974 structural_only: false,
975 });
976 self
977 }
978
979 #[cfg(feature = "template-evaluation-review")]
983 pub(in crate::web_runtime) fn with_native_template_evaluation_parameter(
984 mut self,
985 query_parameter: String,
986 ) -> Self {
987 let config = self
988 .native_web_review
989 .as_mut()
990 .filter(|config| !config.structural_only)
991 .expect("template evaluation augments the configured root native-review pass");
992 config.template_evaluation_query_parameter = Some(query_parameter);
993 self
994 }
995
996 pub(crate) fn with_native_structural_review(
997 mut self,
998 seeds: NativeWebReviewSeeds,
999 observer: Arc<dyn CompleteHttpResponseObserver>,
1000 sql_query_parameter: Option<String>,
1001 ssti_query_parameter: Option<String>,
1002 reflection_query_parameter: Option<String>,
1003 ) -> Self {
1004 self.assessment_defense_projection = true;
1005 self.native_web_review = Some(NativeWebReviewRuntimeConfig {
1006 seeds,
1007 observer,
1008 redirect_query_parameter: None,
1009 reflection_query_parameter,
1010 sql_query_parameter,
1011 ssti_query_parameter,
1012 #[cfg(feature = "template-evaluation-review")]
1013 template_evaluation_query_parameter: None,
1014 xss_query_parameter: None,
1015 xss_selection: None,
1016 #[cfg(feature = "normalization-resilience")]
1017 normalization_query_parameter: None,
1018 #[cfg(feature = "normalization-resilience")]
1019 normalization_selection: None,
1020 structural_only: true,
1021 });
1022 self
1023 }
1024
1025 pub(crate) fn with_native_xss_structural_review(
1029 mut self,
1030 seeds: NativeWebReviewSeeds,
1031 observer: Arc<dyn CompleteHttpResponseObserver>,
1032 query_parameter: String,
1033 selection: web_assessment::XssProbeSelection,
1034 ) -> Self {
1035 self.assessment_defense_projection = true;
1036 self.additional_suppressed_actions.extend(
1037 StandardWebActionKind::all()
1038 .into_iter()
1039 .map(|kind| kind.action_id().to_owned()),
1040 );
1041 self.native_web_review = Some(NativeWebReviewRuntimeConfig {
1042 seeds,
1043 observer,
1044 redirect_query_parameter: None,
1045 reflection_query_parameter: None,
1046 sql_query_parameter: None,
1047 ssti_query_parameter: None,
1048 #[cfg(feature = "template-evaluation-review")]
1049 template_evaluation_query_parameter: None,
1050 xss_query_parameter: Some(query_parameter),
1051 xss_selection: Some(selection),
1052 #[cfg(feature = "normalization-resilience")]
1053 normalization_query_parameter: None,
1054 #[cfg(feature = "normalization-resilience")]
1055 normalization_selection: None,
1056 structural_only: true,
1057 });
1058 self
1059 }
1060
1061 #[cfg(feature = "normalization-resilience")]
1068 pub(in crate::web_runtime) fn with_native_normalization_resilience_review(
1069 mut self,
1070 seeds: NativeWebReviewSeeds,
1071 observer: Arc<dyn CompleteHttpResponseObserver>,
1072 query_parameter: String,
1073 selection: web_assessment::NormalizationTransformSelection,
1074 ) -> Self {
1075 self.assessment_defense_projection = true;
1076 self.additional_suppressed_actions.extend(
1077 StandardWebActionKind::all()
1078 .into_iter()
1079 .map(|kind| kind.action_id().to_owned()),
1080 );
1081 self.native_web_review = Some(NativeWebReviewRuntimeConfig {
1082 seeds,
1083 observer,
1084 redirect_query_parameter: None,
1085 reflection_query_parameter: None,
1086 sql_query_parameter: None,
1087 ssti_query_parameter: None,
1088 #[cfg(feature = "template-evaluation-review")]
1089 template_evaluation_query_parameter: None,
1090 xss_query_parameter: None,
1091 xss_selection: None,
1092 normalization_query_parameter: Some(query_parameter),
1093 normalization_selection: Some(selection),
1094 structural_only: true,
1095 });
1096 self
1097 }
1098
1099 pub fn max_total_requests(mut self, limit: u32) -> Self {
1101 self.runtime_budget = self.runtime_budget.with_max_total_requests(limit);
1102 self
1103 }
1104
1105 pub fn max_wall_time(mut self, limit: Duration) -> Self {
1107 self.runtime_budget = self.runtime_budget.with_max_wall_time(limit);
1108 self
1109 }
1110
1111 pub fn max_response_bytes(mut self, limit: u64) -> Self {
1113 self.runtime_budget = self.runtime_budget.with_max_response_bytes(limit);
1114 self
1115 }
1116
1117 pub fn max_active_verifications(mut self, limit: u16) -> Self {
1119 self.runtime_budget = self.runtime_budget.with_max_active_verifications(limit);
1120 self
1121 }
1122
1123 pub fn max_same_action_attempts(mut self, limit: u16) -> Self {
1125 self.runtime_budget = self.runtime_budget.with_max_same_action_attempts(limit);
1126 self
1127 }
1128
1129 pub fn max_consecutive_no_progress_turns(mut self, limit: u16) -> Self {
1131 self.runtime_budget = self
1132 .runtime_budget
1133 .with_max_consecutive_no_progress_turns(limit);
1134 self
1135 }
1136
1137 pub fn build(self) -> Result<StandardWebDecisionRuntime, StandardWebDecisionRuntimeError> {
1139 let policy = match self.http_policy.clone() {
1140 Some(policy) => policy,
1141 None => HttpEvidencePolicy::for_origin(self.target.clone())?,
1142 };
1143 self.preflight(|target| policy.require_permitted_target(target))?;
1147 let authority = SharedWebRuntimeAuthority::new_exact_origin(
1148 &self.target,
1149 policy,
1150 self.runtime_budget,
1151 self.cancellation.clone(),
1152 )?;
1153 self.build_with_shared_authority(authority)
1157 }
1158
1159 pub(crate) fn build_with_shared_authority(
1166 self,
1167 authority: SharedWebRuntimeAuthority,
1168 ) -> Result<StandardWebDecisionRuntime, StandardWebDecisionRuntimeError> {
1169 let preflight = self.preflight(|target| authority.authorize_target(target))?;
1170 self.compose_with_shared_authority(authority, preflight)
1171 }
1172
1173 fn preflight(
1174 &self,
1175 authorize: impl FnOnce(&Url) -> Result<(), HttpEvidenceError>,
1176 ) -> Result<StandardWebDecisionRuntimePreflight, StandardWebDecisionRuntimeError> {
1177 #[cfg(feature = "rest-review")]
1178 if self.rest_review && self.openapi_review.is_none() {
1179 return Err(StandardWebDecisionRuntimeError::NativeWebReviewDecisionProfile);
1180 }
1181 let probe = HttpProbe::new(self.target.clone(), HttpProbeMethod::Get)?;
1182 authorize(probe.url())?;
1183
1184 let planning = PlanningContext::new(
1185 BenefitScore::from_percent(self.business_value_percent)?,
1186 self.planning_budget,
1187 RiskScore::from_percent(self.risk_limit_percent)?,
1188 );
1189 #[cfg(feature = "authorization-review")]
1190 let max_action_cycles = self.max_action_cycles.saturating_add(
1191 u32::from(self.resource_authorization_review.is_some())
1192 * resource_authorization_runtime::AUTHORIZATION_REVIEW_ACTION_CYCLE_ALLOWANCE,
1193 );
1194 #[cfg(not(feature = "authorization-review"))]
1195 let max_action_cycles = self.max_action_cycles;
1196 #[cfg(feature = "openapi-review")]
1197 let max_action_cycles = max_action_cycles.saturating_add(
1198 u32::from(self.openapi_review.is_some())
1199 * openapi_runtime::OPENAPI_REVIEW_ACTION_CYCLE_ALLOWANCE,
1200 );
1201 #[cfg(feature = "rest-review")]
1202 let max_action_cycles = max_action_cycles.saturating_add(
1203 u32::from(self.rest_review) * rest_runtime::REST_REVIEW_ACTION_CYCLE_ALLOWANCE,
1204 );
1205 #[cfg(feature = "template-evaluation-review")]
1206 let max_action_cycles = max_action_cycles.saturating_add(
1207 u32::from(
1208 self.native_web_review
1209 .as_ref()
1210 .and_then(|review| review.template_evaluation_query_parameter.as_ref())
1211 .is_some(),
1212 ) * u32::from(crate::template_evaluation_review::TEMPLATE_EVALUATION_SELECTED_CASES),
1213 );
1214 #[cfg(feature = "ssrf-oast-review")]
1215 let max_action_cycles = max_action_cycles.saturating_add(
1216 u32::from(self.ssrf_oast_review.is_some())
1217 * ssrf_oast_runtime::SSRF_OAST_REVIEW_ACTION_CYCLE_ALLOWANCE,
1218 );
1219 let config = DecisionLoopConfig::new(
1220 planning,
1221 self.adaptation_limits,
1222 ExperiencePolicy::new(self.experience_failure_limit)?,
1223 max_action_cycles,
1224 )?;
1225 let subject = EntityId::new(format!("endpoint:{}", self.target))?;
1226 Ok(StandardWebDecisionRuntimePreflight { config, subject })
1227 }
1228
1229 fn compose_with_shared_authority(
1230 self,
1231 authority: SharedWebRuntimeAuthority,
1232 preflight: StandardWebDecisionRuntimePreflight,
1233 ) -> Result<StandardWebDecisionRuntime, StandardWebDecisionRuntimeError> {
1234 let StandardWebDecisionRuntimePreflight { config, subject } = preflight;
1235 let mut decision_loop = DecisionLoop::new(config);
1236 let mut executors = DecisionExecutorRegistry::new();
1237
1238 let knowledge = authority.knowledge();
1239 let requests = authority.requests().clone();
1240 let profile = if self.assessment_defense_projection {
1241 StandardWebDecisionProfile::new_with_request_broker_and_assessment_projection(
1242 requests.clone(),
1243 )?
1244 } else {
1245 StandardWebDecisionProfile::new_with_request_broker(requests.clone())?
1246 };
1247 let installation = profile.install(knowledge, &mut decision_loop, &mut executors)?;
1248
1249 #[cfg(feature = "authorization-review")]
1250 let resource_authorization_review = self
1251 .resource_authorization_review
1252 .map(|config| {
1253 authority
1254 .authorize_target(config.execution_resource())
1255 .map_err(|_| {
1256 StandardWebDecisionRuntimeError::ResourceAuthorizationReviewComposition
1257 })?;
1258 resource_authorization_runtime::ResourceAuthorizationRuntimeBinding::new(
1259 config,
1260 requests.clone(),
1261 subject.clone(),
1262 )
1263 .map_err(|_| {
1264 StandardWebDecisionRuntimeError::ResourceAuthorizationReviewComposition
1265 })
1266 })
1267 .transpose()?;
1268 #[cfg(feature = "ssrf-oast-review")]
1269 let ssrf_oast_review = self.ssrf_oast_review.map(|config| {
1270 ssrf_oast_runtime::SsrfOastRuntimeBinding::new(
1271 config,
1272 authority.clone(),
1273 subject.clone(),
1274 )
1275 });
1276 #[cfg(feature = "openapi-review")]
1277 let openapi_review = self.openapi_review.map(|config| {
1278 #[cfg(all(feature = "ssrf-oast-review", feature = "openapi-review"))]
1279 if let Some(ssrf) = ssrf_oast_review.as_ref() {
1280 return openapi_runtime::OpenApiRuntimeBinding::new_with_ssrf_selection(
1281 config,
1282 requests.clone(),
1283 subject.clone(),
1284 knowledge.clone(),
1285 ssrf.selection_slot(),
1286 );
1287 }
1288 openapi_runtime::OpenApiRuntimeBinding::new(
1289 config,
1290 requests.clone(),
1291 subject.clone(),
1292 knowledge.clone(),
1293 )
1294 });
1295 #[cfg(feature = "rest-review")]
1296 let rest_review = self.rest_review.then(|| {
1297 let openapi = openapi_review
1298 .as_ref()
1299 .expect("REST preflight requires the same-run OpenAPI binding");
1300 rest_runtime::RestReviewBinding::new(
1301 openapi.rest_selection_slot(),
1302 requests.clone(),
1303 subject.clone(),
1304 openapi.response_privacy_is_value_free(),
1305 )
1306 });
1307 let native_executor_profile = match self.native_web_review {
1308 Some(config) => {
1309 let profile = {
1310 #[cfg(feature = "normalization-resilience")]
1311 {
1312 if let (Some(parameter), Some(selection)) = (
1313 config.normalization_query_parameter,
1314 config.normalization_selection,
1315 ) {
1316 NativeWebReviewExecutorProfile::new_structural_only(
1317 requests.clone(),
1318 self.target.clone(),
1319 config.seeds,
1320 config.observer,
1321 NativeWebReviewQueryParameters::normalization_only(
1322 parameter, selection,
1323 ),
1324 )
1325 } else if let (Some(parameter), Some(selection)) =
1326 (config.xss_query_parameter, config.xss_selection)
1327 {
1328 NativeWebReviewExecutorProfile::new_structural_only(
1329 requests.clone(),
1330 self.target.clone(),
1331 config.seeds,
1332 config.observer,
1333 NativeWebReviewQueryParameters::xss_only(parameter, selection),
1334 )
1335 } else if config.structural_only {
1336 NativeWebReviewExecutorProfile::new_structural_only(
1337 requests.clone(),
1338 self.target.clone(),
1339 config.seeds,
1340 config.observer,
1341 NativeWebReviewQueryParameters::structural(
1342 config.reflection_query_parameter,
1343 config.sql_query_parameter,
1344 config.ssti_query_parameter,
1345 ),
1346 )
1347 } else {
1348 NativeWebReviewExecutorProfile::new(
1349 requests.clone(),
1350 self.target.clone(),
1351 config.seeds,
1352 config.observer,
1353 NativeWebReviewQueryParameters::full(
1354 config.redirect_query_parameter,
1355 config.reflection_query_parameter,
1356 config.sql_query_parameter,
1357 config.ssti_query_parameter,
1358 #[cfg(feature = "template-evaluation-review")]
1359 config.template_evaluation_query_parameter,
1360 ),
1361 )
1362 }
1363 }
1364 #[cfg(not(feature = "normalization-resilience"))]
1365 {
1366 if let (Some(parameter), Some(selection)) =
1367 (config.xss_query_parameter, config.xss_selection)
1368 {
1369 NativeWebReviewExecutorProfile::new_structural_only(
1370 requests.clone(),
1371 self.target.clone(),
1372 config.seeds,
1373 config.observer,
1374 NativeWebReviewQueryParameters::xss_only(parameter, selection),
1375 )
1376 } else if config.structural_only {
1377 NativeWebReviewExecutorProfile::new_structural_only(
1378 requests.clone(),
1379 self.target.clone(),
1380 config.seeds,
1381 config.observer,
1382 NativeWebReviewQueryParameters::structural(
1383 config.reflection_query_parameter,
1384 config.sql_query_parameter,
1385 config.ssti_query_parameter,
1386 ),
1387 )
1388 } else {
1389 NativeWebReviewExecutorProfile::new(
1390 requests.clone(),
1391 self.target.clone(),
1392 config.seeds,
1393 config.observer,
1394 NativeWebReviewQueryParameters::full(
1395 config.redirect_query_parameter,
1396 config.reflection_query_parameter,
1397 config.sql_query_parameter,
1398 config.ssti_query_parameter,
1399 #[cfg(feature = "template-evaluation-review")]
1400 config.template_evaluation_query_parameter,
1401 ),
1402 )
1403 }
1404 }
1405 };
1406 Some(profile.map_err(|_| {
1407 StandardWebDecisionRuntimeError::NativeWebReviewExecutionProfile
1408 })?)
1409 },
1410 None => None,
1411 };
1412 let native_executor_actions = native_executor_profile
1413 .as_ref()
1414 .map(|profile| profile.actions().collect::<Vec<_>>())
1415 .unwrap_or_default();
1416 #[cfg(any(
1417 feature = "authorization-review",
1418 feature = "openapi-review",
1419 feature = "rest-review",
1420 feature = "ssrf-oast-review"
1421 ))]
1422 let mut native_review_actions = native_executor_actions.clone();
1423 #[cfg(not(any(
1424 feature = "authorization-review",
1425 feature = "openapi-review",
1426 feature = "rest-review",
1427 feature = "ssrf-oast-review"
1428 )))]
1429 let native_review_actions = native_executor_actions.clone();
1430 #[cfg(feature = "authorization-review")]
1431 if resource_authorization_review.is_some() {
1432 native_review_actions.push(
1433 crate::web_actions::NativeWebReviewActionKind::ResourceAuthorizationDifferential,
1434 );
1435 }
1436 #[cfg(feature = "openapi-review")]
1437 if openapi_review.is_some() {
1438 native_review_actions
1439 .push(crate::web_actions::NativeWebReviewActionKind::OpenApiDocumentReplay);
1440 }
1441 #[cfg(feature = "rest-review")]
1442 if rest_review.is_some() {
1443 native_review_actions
1444 .push(crate::web_actions::NativeWebReviewActionKind::RestReadOnlyReplay);
1445 }
1446 #[cfg(feature = "ssrf-oast-review")]
1447 if ssrf_oast_review.is_some() {
1448 native_review_actions
1449 .push(crate::web_actions::NativeWebReviewActionKind::SsrfOastQueryReview);
1450 }
1451 if !native_review_actions.is_empty() {
1452 let profile =
1453 NativeWebReviewDecisionProfile::for_actions(native_review_actions.iter().copied())
1454 .map_err(|_| StandardWebDecisionRuntimeError::NativeWebReviewDecisionProfile)?;
1455 debug_assert_eq!(profile.actions().collect::<Vec<_>>(), native_review_actions);
1456 let report = profile
1457 .install(&mut decision_loop)
1458 .map_err(|_| StandardWebDecisionRuntimeError::NativeWebReviewDecisionProfile)?;
1459 #[cfg(feature = "rest-review")]
1460 let rest_count = usize::from(rest_review.is_some());
1461 #[cfg(not(feature = "rest-review"))]
1462 let rest_count = 0;
1463 #[cfg(feature = "ssrf-oast-review")]
1464 let ssrf_oast_count = usize::from(ssrf_oast_review.is_some());
1465 #[cfg(not(feature = "ssrf-oast-review"))]
1466 let ssrf_oast_count = 0;
1467 let non_rest_count = native_review_actions
1468 .len()
1469 .saturating_sub(rest_count)
1470 .saturating_sub(ssrf_oast_count);
1471 debug_assert_eq!(
1472 report.reasoning_rules_inserted,
1473 usize::from(non_rest_count > 0) + rest_count + ssrf_oast_count
1474 );
1475 debug_assert_eq!(report.actions_inserted, native_review_actions.len());
1476 #[cfg(feature = "authorization-review")]
1477 let authorization_count = usize::from(resource_authorization_review.is_some());
1478 #[cfg(not(feature = "authorization-review"))]
1479 let authorization_count = 0;
1480 #[cfg(feature = "openapi-review")]
1481 let openapi_count = usize::from(openapi_review.is_some());
1482 #[cfg(not(feature = "openapi-review"))]
1483 let openapi_count = 0;
1484 debug_assert_eq!(
1485 report.passive_rules_inserted,
1486 authorization_count
1487 + (2 * openapi_count)
1488 + (2 * rest_count)
1489 + (2 * ssrf_oast_count)
1490 );
1491 debug_assert_eq!(
1492 report.active_rules_inserted,
1493 native_review_actions.len()
1494 + authorization_count
1495 + openapi_count
1496 + rest_count
1497 + ssrf_oast_count
1498 );
1499 }
1500
1501 for rule in standard_web_continuation_rules().map_err(DecisionLoopError::Adaptive)? {
1505 decision_loop
1506 .adaptive_mut()
1507 .register(rule)
1508 .map_err(DecisionLoopError::Adaptive)?;
1509 }
1510 #[cfg(feature = "authorization-review")]
1511 if resource_authorization_review.is_some() {
1512 decision_loop
1513 .adaptive_mut()
1514 .register(
1515 resource_authorization_terminal_adaptation_rule()
1516 .map_err(DecisionLoopError::Adaptive)?,
1517 )
1518 .map_err(DecisionLoopError::Adaptive)?;
1519 }
1520 let api_reasoning_installation = if self.api_reasoning_enabled {
1521 let profile = StandardApiReasoning::new()?;
1522 Some(profile.install(knowledge, decision_loop.rules_mut())?)
1523 } else {
1524 None
1525 };
1526 let http_evidence = HttpEvidenceExecutor::new_with_request_broker(
1527 requests.clone(),
1528 Arc::new(SubjectHttpProbeProvider::new(self.bootstrap_probe_method)),
1529 )?;
1530 let http_evidence = match self.payload_binding {
1531 Some(binding) => http_evidence.with_payload_binding(binding),
1532 None => http_evidence,
1533 };
1534 let http_evidence = match self.complete_response_observer {
1535 Some(observer) => http_evidence.with_complete_response_observer(observer),
1536 None => http_evidence,
1537 };
1538 let http_evidence = if self.assessment_defense_projection {
1539 http_evidence.with_assessment_defense_projection()
1540 } else {
1541 http_evidence
1542 };
1543 executors.register(Arc::new(http_evidence))?;
1544
1545 if let Some(profile) = native_executor_profile {
1546 debug_assert_eq!(
1547 profile.actions().collect::<Vec<_>>(),
1548 native_executor_actions
1549 );
1550 let report = profile
1551 .install(&mut executors)
1552 .map_err(|_| StandardWebDecisionRuntimeError::NativeWebReviewExecutionProfile)?;
1553 debug_assert_eq!(report.executors_inserted(), native_executor_actions.len());
1554 }
1555
1556 #[cfg(feature = "authorization-review")]
1557 if let Some(binding) = resource_authorization_review.as_ref() {
1558 binding
1559 .install_into_parent_registry(&mut executors)
1560 .map_err(|_| {
1561 StandardWebDecisionRuntimeError::ResourceAuthorizationReviewComposition
1562 })?;
1563 }
1564 #[cfg(feature = "openapi-review")]
1565 if let Some(binding) = openapi_review.as_ref() {
1566 binding
1567 .install_into_parent_registry(&mut executors)
1568 .map_err(|_| StandardWebDecisionRuntimeError::NativeWebReviewExecutionProfile)?;
1569 }
1570 #[cfg(feature = "rest-review")]
1571 if let Some(binding) = rest_review.as_ref() {
1572 binding
1573 .install_into_parent_registry(&mut executors)
1574 .map_err(|_| StandardWebDecisionRuntimeError::NativeWebReviewExecutionProfile)?;
1575 }
1576 #[cfg(feature = "ssrf-oast-review")]
1577 if let Some(binding) = ssrf_oast_review.as_ref() {
1578 binding
1579 .install_into_parent_registry(&mut executors)
1580 .map_err(|_| StandardWebDecisionRuntimeError::NativeWebReviewExecutionProfile)?;
1581 }
1582
1583 let mut unsupported_actions: BTreeSet<_> = StandardWebActionKind::all()
1584 .into_iter()
1585 .filter(|kind| !executors.contains(kind.executor_id()))
1586 .map(|kind| kind.action_id().to_owned())
1587 .collect();
1588 unsupported_actions.extend(self.additional_suppressed_actions);
1589
1590 Ok(StandardWebDecisionRuntime {
1591 target: self.target,
1592 subject: subject.clone(),
1593 installation,
1594 api_reasoning_installation,
1595 unsupported_actions,
1596 decision_loop,
1597 runner: DecisionRunnerAdapter::new(executors),
1598 experience: self.experience,
1599 session: DecisionSession::new(subject),
1600 authority,
1601 usage: RuntimeUsage::default(),
1602 started: false,
1603 assessment_defense: self
1604 .assessment_defense_projection
1605 .then(|| AssessmentDefenseController::new(self.assessment_defense_enforcement)),
1606 #[cfg(feature = "authorization-review")]
1607 resource_authorization_review,
1608 #[cfg(feature = "openapi-review")]
1609 openapi_review,
1610 #[cfg(feature = "rest-review")]
1611 rest_review,
1612 #[cfg(feature = "ssrf-oast-review")]
1613 ssrf_oast_review,
1614 })
1615 }
1616}
1617
1618pub struct StandardWebDecisionRuntime {
1641 target: Url,
1642 subject: EntityId,
1643 installation: StandardWebDecisionInstallReport,
1644 api_reasoning_installation: Option<StandardApiInstallReport>,
1645 unsupported_actions: BTreeSet<String>,
1646 decision_loop: DecisionLoop,
1647 runner: DecisionRunnerAdapter,
1648 experience: ExperienceStore,
1649 session: DecisionSession,
1650 authority: SharedWebRuntimeAuthority,
1651 usage: RuntimeUsage,
1652 started: bool,
1653 assessment_defense: Option<AssessmentDefenseController>,
1654 #[cfg(feature = "authorization-review")]
1655 resource_authorization_review:
1656 Option<resource_authorization_runtime::ResourceAuthorizationRuntimeBinding>,
1657 #[cfg(feature = "openapi-review")]
1658 openapi_review: Option<openapi_runtime::OpenApiRuntimeBinding>,
1659 #[cfg(feature = "rest-review")]
1660 rest_review: Option<rest_runtime::RestReviewBinding>,
1661 #[cfg(feature = "ssrf-oast-review")]
1662 ssrf_oast_review: Option<ssrf_oast_runtime::SsrfOastRuntimeBinding>,
1663}
1664
1665impl StandardWebDecisionRuntime {
1666 pub fn builder(target: Url) -> StandardWebDecisionRuntimeBuilder {
1668 StandardWebDecisionRuntimeBuilder::new(target)
1669 }
1670
1671 pub fn target(&self) -> &Url {
1673 &self.target
1674 }
1675
1676 pub fn subject(&self) -> &EntityId {
1678 &self.subject
1679 }
1680
1681 pub fn installation(&self) -> StandardWebDecisionInstallReport {
1683 self.installation
1684 }
1685
1686 pub fn api_reasoning_installation(&self) -> Option<StandardApiInstallReport> {
1688 self.api_reasoning_installation
1689 }
1690
1691 pub fn unsupported_actions(&self) -> &BTreeSet<String> {
1693 &self.unsupported_actions
1694 }
1695
1696 pub fn knowledge(&self) -> &KnowledgeBase {
1698 self.authority.knowledge()
1699 }
1700
1701 pub fn experience(&self) -> &ExperienceStore {
1703 &self.experience
1704 }
1705
1706 pub fn session(&self) -> &DecisionSession {
1708 &self.session
1709 }
1710
1711 pub const fn budget(&self) -> RuntimeBudget {
1713 self.authority.budget()
1714 }
1715
1716 pub fn usage(&self) -> &RuntimeUsage {
1718 &self.usage
1719 }
1720
1721 pub fn cancellation_token(&self) -> CancellationToken {
1726 self.authority.cancellation_token()
1727 }
1728
1729 pub fn has_started(&self) -> bool {
1731 self.started
1732 }
1733
1734 pub fn into_experience(self) -> ExperienceStore {
1736 self.experience
1737 }
1738
1739 fn action_suppression_context(&self) -> Result<ActionSuppressionContext, ()> {
1740 let defense = match &self.assessment_defense {
1741 Some(controller) => controller
1742 .defense_suppressed_actions(&self.subject, self.decision_loop.planner())?,
1743 None => BTreeSet::new(),
1744 };
1745 Ok(ActionSuppressionContext::new(
1746 self.unsupported_actions.clone(),
1747 defense,
1748 ))
1749 }
1750
1751 fn ingest_assessment_defense(
1752 &mut self,
1753 receipt: &DecisionEvidenceReceipt,
1754 require_projection: bool,
1755 ) -> Result<(), ()> {
1756 let Some(controller) = self.assessment_defense.as_mut() else {
1757 return Ok(());
1758 };
1759 controller.ingest_receipt(receipt, self.authority.knowledge(), require_projection)
1760 }
1761
1762 fn record_assessment_defense_shadow(
1763 &mut self,
1764 report: &DecisionPlanningReport,
1765 ) -> Result<(), ()> {
1766 let Some(controller) = self.assessment_defense.as_mut() else {
1767 return Ok(());
1768 };
1769 let planner = self.decision_loop.planner().clone();
1770 controller.record_shadow(report, &planner)
1771 }
1772
1773 pub(crate) fn assessment_defense_controller(&self) -> Option<&AssessmentDefenseController> {
1774 self.assessment_defense.as_ref()
1775 }
1776
1777 pub(crate) fn assessment_planner(&self) -> &crate::AttackPlanner {
1778 self.decision_loop.planner()
1779 }
1780
1781 #[cfg(feature = "authorization-review")]
1782 pub(in crate::web_runtime) fn take_resource_authorization_review(
1783 &mut self,
1784 ) -> Option<resource_authorization_runtime::ResourceAuthorizationRuntimeBinding> {
1785 self.resource_authorization_review.take()
1786 }
1787
1788 #[cfg(feature = "openapi-review")]
1789 pub(in crate::web_runtime) fn take_openapi_review(
1790 &mut self,
1791 ) -> Option<openapi_runtime::OpenApiRuntimeBinding> {
1792 self.openapi_review.take()
1793 }
1794
1795 #[cfg(feature = "rest-review")]
1796 pub(in crate::web_runtime) fn take_rest_review(
1797 &mut self,
1798 ) -> Option<rest_runtime::RestReviewBinding> {
1799 self.rest_review.take()
1800 }
1801
1802 #[cfg(feature = "ssrf-oast-review")]
1803 pub(in crate::web_runtime) fn take_ssrf_oast_review(
1804 &mut self,
1805 ) -> Option<ssrf_oast_runtime::SsrfOastRuntimeBinding> {
1806 self.ssrf_oast_review.take()
1807 }
1808
1809 pub async fn analyze(
1815 &mut self,
1816 ) -> Result<StandardWebDecisionRunReport, StandardWebDecisionRuntimeError> {
1817 if self.started {
1818 return Err(StandardWebDecisionRuntimeError::AlreadyStarted);
1819 }
1820 self.started = true;
1821 let timing = self.authority.start();
1822 let started_at = timing.started_at();
1823 let deadline = timing.deadline();
1824 let mut turns = Vec::new();
1825
1826 if self.authority.cancellation().is_cancelled() {
1827 return Ok(self.cancellation_report(None, turns, None, started_at));
1828 }
1829
1830 let bootstrap_case = match VerificationCase::new(
1831 BOOTSTRAP_CASE_ID,
1832 self.subject.clone(),
1833 BOOTSTRAP_ACTION_ID,
1834 BOOTSTRAP_HYPOTHESIS_ID,
1835 ) {
1836 Ok(case) => case,
1837 Err(source) => {
1838 return Err(self.run_failed(None, turns, source.into(), started_at));
1839 },
1840 };
1841 let bootstrap_command = DecisionLoopCommand::ExecuteAction {
1842 case: bootstrap_case,
1843 executor: Some(HTTP_EVIDENCE_EXECUTOR_ID.to_owned()),
1844 origin: DecisionActionOrigin::Bootstrap,
1845 delay_ms: None,
1846 };
1847 let (bootstrap_action_id, bootstrap_stage) = match execution_metadata(&bootstrap_command) {
1848 Ok(metadata) => metadata,
1849 Err(source) => {
1850 return Err(self.run_failed(None, turns, source, started_at));
1851 },
1852 };
1853 let bootstrap_limits = match self.reserve_execution(
1855 bootstrap_action_id,
1856 bootstrap_stage,
1857 DecisionExecutionClass::TransportBound,
1858 started_at,
1859 ) {
1860 Ok(limits) => limits,
1861 Err(limit) => {
1862 if self.authority.cancellation().is_cancelled() {
1863 return Ok(self.cancellation_report(None, turns, None, started_at));
1864 }
1865 return Ok(self.limit_report(None, turns, limit, started_at));
1866 },
1867 };
1868 if self.authority.cancellation().is_cancelled() {
1869 return Ok(self.cancellation_report(None, turns, None, started_at));
1870 }
1871 let bootstrap_result = await_execution(
1872 self.authority.cancellation(),
1873 deadline,
1874 self.runner.execute_command_with_limits(
1875 &bootstrap_command,
1876 self.authority.knowledge(),
1877 bootstrap_limits,
1878 ),
1879 )
1880 .await;
1881 let bootstrap = match bootstrap_result {
1882 RuntimeExecution::Completed(Ok(receipt)) => {
1883 self.refresh_elapsed(started_at);
1884 receipt
1885 },
1886 RuntimeExecution::Completed(Err(error)) => {
1887 self.refresh_elapsed(started_at);
1888 if let Some(limit) = error.runtime_limit().cloned() {
1889 let failure = error.into_execution_failure();
1890 return Ok(
1891 self.limit_report_with_failure(None, turns, limit, failure, started_at)
1892 );
1893 }
1894 return Err(self.run_failed(None, turns, error.into(), started_at));
1895 },
1896 RuntimeExecution::Cancelled => {
1897 return Ok(self.cancellation_report(None, turns, None, started_at));
1898 },
1899 RuntimeExecution::WallTimeExceeded => {
1900 let limit = self.wall_limit(started_at);
1901 return Ok(self.limit_report(None, turns, limit, started_at));
1902 },
1903 };
1904 let bootstrap = match self
1905 .validate_response_usage_evidence(bootstrap, DecisionExecutionClass::TransportBound)
1906 {
1907 Ok(receipt) => receipt,
1908 Err(source) => {
1909 let committed_bootstrap = source.committed_evidence().cloned();
1910 return Err(self.run_failed(committed_bootstrap, turns, source, started_at));
1911 },
1912 };
1913 if self.ingest_assessment_defense(&bootstrap, true).is_err() {
1914 let source = StandardWebDecisionRuntimeError::AssessmentDefenseProjectionInvariant {
1915 receipt: Box::new(bootstrap),
1916 };
1917 let committed_bootstrap = source.committed_evidence().cloned();
1918 return Err(self.run_failed(committed_bootstrap, turns, source, started_at));
1919 }
1920 if let Some(limit) = self.response_limit_if_exceeded(BOOTSTRAP_ACTION_ID) {
1921 return Ok(self.limit_report(Some(bootstrap), turns, limit, started_at));
1922 }
1923 let bootstrap = Some(bootstrap);
1924
1925 if self.authority.cancellation().is_cancelled() {
1926 return Ok(self.cancellation_report(bootstrap, turns, None, started_at));
1927 }
1928
1929 let mut command = DecisionLoopCommand::Replan;
1930 let mut representative_success: Option<VerificationCase> = None;
1934 let mut representative_unresolved: Option<VerificationCase> = None;
1935 let terminal = loop {
1936 match &command {
1937 DecisionLoopCommand::Replan => {
1938 if self.authority.cancellation().is_cancelled() {
1939 return Ok(self.cancellation_report(bootstrap, turns, None, started_at));
1940 }
1941 if let Some(limit) = self.wall_limit_if_reached(started_at) {
1942 return Ok(self.limit_report(bootstrap, turns, limit, started_at));
1943 }
1944 let suppressions = match self.action_suppression_context() {
1945 Ok(value) => value,
1946 Err(()) => {
1947 return Err(self.run_failed(
1948 bootstrap,
1949 turns,
1950 StandardWebDecisionRuntimeError::AssessmentDefensePlanningInvariant,
1951 started_at,
1952 ));
1953 },
1954 };
1955 let planning = match self.decision_loop.plan_next_with_action_suppressions(
1956 self.authority.knowledge(),
1957 &self.experience,
1958 &mut self.session,
1959 &suppressions,
1960 ) {
1961 Ok(planning) => planning,
1962 Err(source) => {
1963 return Err(self.run_failed(
1964 bootstrap,
1965 turns,
1966 source.into(),
1967 started_at,
1968 ));
1969 },
1970 };
1971 if self.record_assessment_defense_shadow(&planning).is_err() {
1972 return Err(self.run_failed(
1973 bootstrap,
1974 turns,
1975 StandardWebDecisionRuntimeError::AssessmentDefensePlanningInvariant,
1976 started_at,
1977 ));
1978 }
1979 command = planning.command().clone();
1980 turns.push(StandardWebDecisionRuntimeTurn::Planning(Box::new(planning)));
1981 if self.authority.cancellation().is_cancelled() {
1982 return Ok(self.cancellation_report(bootstrap, turns, None, started_at));
1983 }
1984 if is_terminal(&command) {
1985 break command.clone();
1986 }
1987 if let Some(limit) = self.wall_limit_if_reached(started_at) {
1988 return Ok(self.limit_report(bootstrap, turns, limit, started_at));
1989 }
1990 },
1991 DecisionLoopCommand::ExecuteAction { .. }
1992 | DecisionLoopCommand::CollectActiveEvidence { .. } => {
1993 if self.authority.cancellation().is_cancelled() {
1994 return Ok(self.cancellation_report(bootstrap, turns, None, started_at));
1995 }
1996 let predispatch_suppressions = match self.action_suppression_context() {
1997 Ok(value) => value,
1998 Err(()) => {
1999 return Err(self.run_failed(
2000 bootstrap,
2001 turns,
2002 StandardWebDecisionRuntimeError::AssessmentDefensePlanningInvariant,
2003 started_at,
2004 ));
2005 },
2006 };
2007 match self
2008 .runner
2009 .validate_command_suppression(&command, &predispatch_suppressions)
2010 {
2011 Err(source @ DecisionRunnerError::ActionSuppressedByDefense { .. }) => {
2012 if let Err(source) =
2013 self.decision_loop.validate_execution_command_authority(
2014 self.authority.knowledge(),
2015 &command,
2016 )
2017 {
2018 return Err(self.run_failed(
2019 bootstrap,
2020 turns,
2021 source.into(),
2022 started_at,
2023 ));
2024 }
2025 match self.runner.replan_defense_suppressed_command(
2026 &command,
2027 &mut self.session,
2028 &predispatch_suppressions,
2029 ) {
2030 Ok(true) => {
2031 command = DecisionLoopCommand::Replan;
2032 continue;
2033 },
2034 Ok(false) => {
2035 return Err(self.run_failed(
2036 bootstrap,
2037 turns,
2038 source.into(),
2039 started_at,
2040 ));
2041 },
2042 Err(source) => {
2043 return Err(self.run_failed(
2044 bootstrap,
2045 turns,
2046 source.into(),
2047 started_at,
2048 ));
2049 },
2050 }
2051 },
2052 Err(source) => {
2053 return Err(self.run_failed(
2054 bootstrap,
2055 turns,
2056 source.into(),
2057 started_at,
2058 ));
2059 },
2060 Ok(()) => {},
2061 }
2062 let (action_id, previous_stage) = match execution_metadata(&command) {
2063 Ok(metadata) => metadata,
2064 Err(source) => {
2065 return Err(self.run_failed(bootstrap, turns, source, started_at));
2066 },
2067 };
2068 let execution_class = match self.runner.execution_class_for_command(&command) {
2069 Ok(class) => class,
2070 Err(source) => {
2071 return Err(self.run_failed(
2072 bootstrap,
2073 turns,
2074 source.into(),
2075 started_at,
2076 ));
2077 },
2078 };
2079 let completed_action_id = action_id.to_owned();
2080 let limits = match self.reserve_execution(
2081 action_id,
2082 previous_stage,
2083 execution_class,
2084 started_at,
2085 ) {
2086 Ok(limits) => limits,
2087 Err(limit) => {
2088 if self.authority.cancellation().is_cancelled() {
2089 return Ok(
2090 self.cancellation_report(bootstrap, turns, None, started_at)
2091 );
2092 }
2093 return Ok(self.limit_report(bootstrap, turns, limit, started_at));
2094 },
2095 };
2096 if self.authority.cancellation().is_cancelled() {
2097 return Ok(self.cancellation_report(bootstrap, turns, None, started_at));
2098 }
2099 let evidence_result = await_execution(
2100 self.authority.cancellation(),
2101 deadline,
2102 self.runner.execute_session_command_with_limits(
2103 &command,
2104 self.authority.knowledge(),
2105 &self.session,
2106 limits,
2107 ),
2108 )
2109 .await;
2110 let evidence = match evidence_result {
2111 RuntimeExecution::Completed(Ok(receipt)) => {
2112 self.refresh_elapsed(started_at);
2113 receipt
2114 },
2115 RuntimeExecution::Completed(Err(error)) => {
2116 self.refresh_elapsed(started_at);
2117 if let Some(limit) = error.runtime_limit().cloned() {
2118 let failure = error.into_execution_failure();
2119 return Ok(self.limit_report_with_failure(
2120 bootstrap, turns, limit, failure, started_at,
2121 ));
2122 }
2123 return Err(self.run_failed(
2124 bootstrap,
2125 turns,
2126 error.into(),
2127 started_at,
2128 ));
2129 },
2130 RuntimeExecution::Cancelled => {
2131 return Ok(self.cancellation_report(bootstrap, turns, None, started_at));
2132 },
2133 RuntimeExecution::WallTimeExceeded => {
2134 let limit = self.wall_limit(started_at);
2135 return Ok(self.limit_report(bootstrap, turns, limit, started_at));
2136 },
2137 };
2138 let evidence =
2139 match self.validate_response_usage_evidence(evidence, execution_class) {
2140 Ok(receipt) => receipt,
2141 Err(source) => {
2142 return Err(self.run_failed(bootstrap, turns, source, started_at));
2143 },
2144 };
2145 if self
2146 .ingest_assessment_defense(
2147 &evidence,
2148 execution_class == DecisionExecutionClass::TransportBound,
2149 )
2150 .is_err()
2151 {
2152 let source =
2153 StandardWebDecisionRuntimeError::AssessmentDefenseProjectionInvariant {
2154 receipt: Box::new(evidence),
2155 };
2156 return Err(self.run_failed(bootstrap, turns, source, started_at));
2157 }
2158 if execution_class == DecisionExecutionClass::TransportBound {
2161 if let Some(limit) = self.response_limit_if_exceeded(&completed_action_id) {
2162 return Ok(self.limit_report_with_unverified_evidence(
2163 bootstrap, turns, evidence, limit, started_at,
2164 ));
2165 }
2166 }
2167 if self.authority.cancellation().is_cancelled() {
2168 return Ok(self.cancellation_report(
2169 bootstrap,
2170 turns,
2171 Some(evidence),
2172 started_at,
2173 ));
2174 }
2175 let resume_suppressions = match self.action_suppression_context() {
2176 Ok(value) => value,
2177 Err(()) => {
2178 return Err(self.run_failed(
2179 bootstrap,
2180 turns,
2181 StandardWebDecisionRuntimeError::AssessmentDefensePlanningInvariant,
2182 started_at,
2183 ));
2184 },
2185 };
2186 let runner_turn = self.runner.resume_session_command_with_action_suppressions(
2187 &self.decision_loop,
2188 &command,
2189 self.authority.knowledge(),
2190 &mut self.experience,
2191 &mut self.session,
2192 ContinuationAuthority::new(evidence, &resume_suppressions),
2193 );
2194 self.refresh_elapsed(started_at);
2195 let runner_turn = match runner_turn {
2196 Ok(turn) => turn,
2197 Err(source) => {
2198 return Err(self.run_failed(
2199 bootstrap,
2200 turns,
2201 source.into(),
2202 started_at,
2203 ));
2204 },
2205 };
2206 match runner_turn {
2207 DecisionRunnerTurn::Planning(planning) => {
2208 if self.record_assessment_defense_shadow(&planning).is_err() {
2209 return Err(self.run_failed(
2210 bootstrap,
2211 turns,
2212 StandardWebDecisionRuntimeError::
2213 AssessmentDefensePlanningInvariant,
2214 started_at,
2215 ));
2216 }
2217 command = planning.command().clone();
2218 turns.push(StandardWebDecisionRuntimeTurn::Planning(planning));
2219 if is_terminal(&command) {
2220 break command.clone();
2221 }
2222 if self.authority.cancellation().is_cancelled() {
2223 return Ok(
2224 self.cancellation_report(bootstrap, turns, None, started_at)
2225 );
2226 }
2227 },
2228 DecisionRunnerTurn::Outcome { evidence, decision } => {
2229 command = decision.command().clone();
2230 let progressed =
2231 outcome_made_progress(previous_stage, &command, decision.as_ref());
2232 self.usage.record_execution_progress(progressed);
2233 classify_continuation_case(
2234 decision.as_ref(),
2235 &mut representative_success,
2236 &mut representative_unresolved,
2237 );
2238 turns.push(StandardWebDecisionRuntimeTurn::Outcome {
2239 evidence,
2240 decision,
2241 });
2242 if is_terminal(&command) {
2243 break command.clone();
2244 }
2245 if self.authority.cancellation().is_cancelled() {
2246 return Ok(
2247 self.cancellation_report(bootstrap, turns, None, started_at)
2248 );
2249 }
2250 if self.usage.consecutive_no_progress_turns()
2251 >= self.authority.budget().max_consecutive_no_progress_turns()
2252 && !progressed
2253 {
2254 let limit = RuntimeLimitExceeded::new(
2255 RuntimeBudgetDimension::ConsecutiveNoProgressTurns,
2256 u64::from(
2257 self.authority.budget().max_consecutive_no_progress_turns(),
2258 ),
2259 u64::from(self.usage.consecutive_no_progress_turns()),
2260 Some(completed_action_id),
2261 );
2262 return Ok(self.limit_report(bootstrap, turns, limit, started_at));
2263 }
2264 if let Some(limit) = self.wall_limit_if_reached(started_at) {
2265 return Ok(self.limit_report(bootstrap, turns, limit, started_at));
2266 }
2267 },
2268 DecisionRunnerTurn::Terminal(terminal) => break terminal,
2269 }
2270 },
2271 DecisionLoopCommand::Complete { .. }
2272 | DecisionLoopCommand::AwaitHumanReview { .. }
2273 | DecisionLoopCommand::Halt { .. } => break command.clone(),
2274 }
2275 };
2276
2277 let terminal = self.finalize_multi_objective_terminal(
2280 terminal,
2281 representative_success,
2282 representative_unresolved,
2283 );
2284
2285 self.refresh_elapsed(started_at);
2286
2287 Ok(StandardWebDecisionRunReport {
2288 bootstrap,
2289 turns,
2290 unverified_evidence: None,
2291 terminal,
2292 usage: self.usage.clone(),
2293 transport: self.authority.request_accounting().dispatch_audit(),
2294 limit_exceeded: None,
2295 execution_failure: None,
2296 })
2297 }
2298
2299 fn finalize_multi_objective_terminal(
2309 &mut self,
2310 terminal: DecisionLoopCommand,
2311 representative_success: Option<VerificationCase>,
2312 representative_unresolved: Option<VerificationCase>,
2313 ) -> DecisionLoopCommand {
2314 if !matches!(
2315 terminal,
2316 DecisionLoopCommand::Halt {
2317 reason: DecisionStopReason::NoEligibleAction
2318 }
2319 ) {
2320 return terminal;
2321 }
2322 if let Some(case) = representative_unresolved {
2323 self.session.finalize_human_review();
2324 DecisionLoopCommand::AwaitHumanReview { case }
2325 } else if let Some(case) = representative_success {
2326 self.session.finalize_objective_complete();
2327 DecisionLoopCommand::Complete { case }
2328 } else {
2329 terminal
2330 }
2331 }
2332
2333 fn run_failed(
2334 &mut self,
2335 bootstrap: Option<DecisionEvidenceReceipt>,
2336 completed_turns: Vec<StandardWebDecisionRuntimeTurn>,
2337 source: StandardWebDecisionRuntimeError,
2338 started_at: tokio::time::Instant,
2339 ) -> StandardWebDecisionRuntimeError {
2340 self.refresh_elapsed(started_at);
2341 StandardWebDecisionRuntimeError::RunFailed {
2342 receipt: Box::new(StandardWebDecisionFailureReceipt {
2343 bootstrap,
2344 completed_turns,
2345 usage: self.usage.clone(),
2346 transport: self.authority.request_accounting().dispatch_audit(),
2347 }),
2348 source: Box::new(source),
2349 }
2350 }
2351
2352 fn reserve_execution(
2353 &mut self,
2354 action_id: &str,
2355 stage: DecisionExecutionStage,
2356 execution_class: DecisionExecutionClass,
2357 started_at: tokio::time::Instant,
2358 ) -> Result<DecisionExecutionLimits, RuntimeLimitExceeded> {
2359 if let Some(limit) = self.wall_limit_if_reached(started_at) {
2360 return Err(limit);
2361 }
2362 match execution_class {
2367 DecisionExecutionClass::TransportBound => {
2368 self.sync_request_accounting();
2369 let preflight = self
2370 .authority
2371 .request_accounting()
2372 .preflight(action_id, stage)?;
2373 self.reserve_action_attempt(action_id)?;
2374 Ok(DecisionExecutionLimits::new()
2375 .with_max_response_body_bytes(preflight.remaining_response_bytes()))
2376 },
2377 DecisionExecutionClass::LocalKnowledge => {
2378 self.reserve_action_attempt(action_id)?;
2379 Ok(DecisionExecutionLimits::new())
2380 },
2381 }
2382 }
2383
2384 fn reserve_action_attempt(&mut self, action_id: &str) -> Result<(), RuntimeLimitExceeded> {
2387 let attempts = self.usage.same_action_attempts(action_id);
2388 if attempts >= self.authority.budget().max_same_action_attempts() {
2389 return Err(RuntimeLimitExceeded::new(
2390 RuntimeBudgetDimension::SameActionAttempts,
2391 u64::from(self.authority.budget().max_same_action_attempts()),
2392 u64::from(attempts).saturating_add(1),
2393 Some(action_id.to_owned()),
2394 ));
2395 }
2396 self.usage.reserve_action_attempt(action_id);
2397 Ok(())
2398 }
2399
2400 fn validate_response_usage_evidence(
2401 &mut self,
2402 receipt: DecisionEvidenceReceipt,
2403 execution_class: DecisionExecutionClass,
2404 ) -> Result<DecisionEvidenceReceipt, StandardWebDecisionRuntimeError> {
2405 if execution_class == DecisionExecutionClass::LocalKnowledge {
2409 return Ok(receipt);
2410 }
2411 let response_body_bytes =
2412 HttpEvidencePredicate::RESPONSE_BODY_BYTES_OBSERVED.into_knowledge();
2413 let correlated: Vec<_> = receipt
2414 .evidence()
2415 .iter()
2416 .filter(|evidence| {
2417 evidence.source().correlation_id() == Some(receipt.case().id())
2418 && evidence.predicate() == &response_body_bytes
2419 })
2420 .filter_map(|evidence| match evidence.value() {
2421 EvidenceValue::Unsigned(bytes) => Some(*bytes),
2422 _ => None,
2423 })
2424 .collect();
2425 if correlated.len() != 1 {
2426 return Err(StandardWebDecisionRuntimeError::ResponseUsageEvidence {
2427 case_id: receipt.case().id().to_owned(),
2428 predicate: HttpEvidencePredicate::RESPONSE_BODY_BYTES_OBSERVED.dotted(),
2429 observations: correlated.len(),
2430 receipt: Box::new(receipt),
2431 });
2432 }
2433 Ok(receipt)
2434 }
2435
2436 fn response_limit_if_exceeded(&mut self, action_id: &str) -> Option<RuntimeLimitExceeded> {
2437 self.sync_request_accounting();
2438 let observed = self.usage.response_bytes();
2439 (observed > self.authority.budget().max_response_bytes()).then(|| {
2440 RuntimeLimitExceeded::new(
2441 RuntimeBudgetDimension::ResponseBytes,
2442 self.authority.budget().max_response_bytes(),
2443 observed,
2444 Some(action_id.to_owned()),
2445 )
2446 })
2447 }
2448
2449 fn sync_request_accounting(&mut self) {
2450 self.usage
2451 .sync_request_accounting(self.authority.request_accounting().snapshot());
2452 }
2453
2454 fn refresh_elapsed(&mut self, started_at: tokio::time::Instant) {
2455 self.sync_request_accounting();
2456 self.usage.set_elapsed(started_at.elapsed());
2457 }
2458
2459 fn wall_limit_if_reached(
2460 &mut self,
2461 started_at: tokio::time::Instant,
2462 ) -> Option<RuntimeLimitExceeded> {
2463 self.refresh_elapsed(started_at);
2464 (started_at.elapsed() >= self.authority.budget().max_wall_time())
2465 .then(|| self.wall_limit(started_at))
2466 }
2467
2468 fn wall_limit(&mut self, started_at: tokio::time::Instant) -> RuntimeLimitExceeded {
2469 self.refresh_elapsed(started_at);
2470 RuntimeLimitExceeded::new(
2471 RuntimeBudgetDimension::WallTime,
2472 self.authority.budget().max_wall_time_ms(),
2473 self.usage
2474 .elapsed_ms()
2475 .max(self.authority.budget().max_wall_time_ms()),
2476 None,
2477 )
2478 }
2479
2480 fn limit_report(
2481 &mut self,
2482 bootstrap: Option<DecisionEvidenceReceipt>,
2483 turns: Vec<StandardWebDecisionRuntimeTurn>,
2484 limit: RuntimeLimitExceeded,
2485 started_at: tokio::time::Instant,
2486 ) -> StandardWebDecisionRunReport {
2487 self.limit_report_with_failure(bootstrap, turns, limit, None, started_at)
2488 }
2489
2490 fn limit_report_with_failure(
2491 &mut self,
2492 bootstrap: Option<DecisionEvidenceReceipt>,
2493 turns: Vec<StandardWebDecisionRuntimeTurn>,
2494 limit: RuntimeLimitExceeded,
2495 execution_failure: Option<DecisionExecutionFailureReceipt>,
2496 started_at: tokio::time::Instant,
2497 ) -> StandardWebDecisionRunReport {
2498 self.refresh_elapsed(started_at);
2499 self.session.halt_for_runtime_budget();
2500 StandardWebDecisionRunReport {
2501 bootstrap,
2502 turns,
2503 unverified_evidence: None,
2504 terminal: DecisionLoopCommand::Halt {
2505 reason: crate::DecisionStopReason::RuntimeBudgetLimit,
2506 },
2507 usage: self.usage.clone(),
2508 transport: self.authority.request_accounting().dispatch_audit(),
2509 limit_exceeded: Some(limit),
2510 execution_failure,
2511 }
2512 }
2513
2514 fn limit_report_with_unverified_evidence(
2515 &mut self,
2516 bootstrap: Option<DecisionEvidenceReceipt>,
2517 turns: Vec<StandardWebDecisionRuntimeTurn>,
2518 evidence: DecisionEvidenceReceipt,
2519 limit: RuntimeLimitExceeded,
2520 started_at: tokio::time::Instant,
2521 ) -> StandardWebDecisionRunReport {
2522 self.refresh_elapsed(started_at);
2523 self.session.halt_for_runtime_budget();
2524 StandardWebDecisionRunReport {
2525 bootstrap,
2526 turns,
2527 unverified_evidence: Some(evidence),
2528 terminal: DecisionLoopCommand::Halt {
2529 reason: crate::DecisionStopReason::RuntimeBudgetLimit,
2530 },
2531 usage: self.usage.clone(),
2532 transport: self.authority.request_accounting().dispatch_audit(),
2533 limit_exceeded: Some(limit),
2534 execution_failure: None,
2535 }
2536 }
2537
2538 fn cancellation_report(
2539 &mut self,
2540 bootstrap: Option<DecisionEvidenceReceipt>,
2541 turns: Vec<StandardWebDecisionRuntimeTurn>,
2542 unverified_evidence: Option<DecisionEvidenceReceipt>,
2543 started_at: tokio::time::Instant,
2544 ) -> StandardWebDecisionRunReport {
2545 self.refresh_elapsed(started_at);
2546 self.session.halt_for_host_cancellation();
2547 StandardWebDecisionRunReport {
2548 bootstrap,
2549 turns,
2550 unverified_evidence,
2551 terminal: DecisionLoopCommand::Halt {
2552 reason: crate::DecisionStopReason::CancelledByHost,
2553 },
2554 usage: self.usage.clone(),
2555 transport: self.authority.request_accounting().dispatch_audit(),
2556 limit_exceeded: None,
2557 execution_failure: None,
2558 }
2559 }
2560}
2561
2562enum RuntimeExecution<T> {
2563 Completed(T),
2564 Cancelled,
2565 WallTimeExceeded,
2566}
2567
2568async fn await_execution<F, T>(
2569 cancellation: &CancellationToken,
2570 deadline: Option<tokio::time::Instant>,
2571 execution: F,
2572) -> RuntimeExecution<T>
2573where
2574 F: Future<Output = T>,
2575{
2576 tokio::pin!(execution);
2577 match deadline {
2578 Some(deadline) => {
2579 tokio::select! {
2580 biased;
2585 result = &mut execution => RuntimeExecution::Completed(result),
2586 () = cancellation.cancelled() => RuntimeExecution::Cancelled,
2587 () = tokio::time::sleep_until(deadline) => RuntimeExecution::WallTimeExceeded,
2588 }
2589 },
2590 None => {
2591 tokio::select! {
2592 biased;
2593 result = &mut execution => RuntimeExecution::Completed(result),
2594 () = cancellation.cancelled() => RuntimeExecution::Cancelled,
2595 }
2596 },
2597 }
2598}
2599
2600fn execution_metadata(
2601 command: &DecisionLoopCommand,
2602) -> Result<(&str, DecisionExecutionStage), StandardWebDecisionRuntimeError> {
2603 match command {
2604 DecisionLoopCommand::ExecuteAction { case, .. } => {
2605 Ok((case.action_id(), DecisionExecutionStage::Passive))
2606 },
2607 DecisionLoopCommand::CollectActiveEvidence { case } => {
2608 Ok((case.action_id(), DecisionExecutionStage::Active))
2609 },
2610 DecisionLoopCommand::Replan
2611 | DecisionLoopCommand::Complete { .. }
2612 | DecisionLoopCommand::AwaitHumanReview { .. }
2613 | DecisionLoopCommand::Halt { .. } => {
2614 Err(StandardWebDecisionRuntimeError::ExecutionMetadataUnavailable)
2615 },
2616 }
2617}
2618
2619fn is_terminal(command: &DecisionLoopCommand) -> bool {
2620 matches!(
2621 command,
2622 DecisionLoopCommand::Complete { .. }
2623 | DecisionLoopCommand::AwaitHumanReview { .. }
2624 | DecisionLoopCommand::Halt { .. }
2625 )
2626}
2627
2628fn classify_continuation_case(
2635 decision: &DecisionOutcomeReport,
2636 representative_success: &mut Option<VerificationCase>,
2637 representative_unresolved: &mut Option<VerificationCase>,
2638) {
2639 if !matches!(
2640 decision.adaptive().directive(),
2641 PipelineDirective::Replan {
2642 suppress_current_action: true
2643 }
2644 ) {
2645 return;
2646 }
2647 let report = decision.verification();
2648 match report.outcome().status() {
2649 OutcomeStatus::Success => {
2650 representative_success.get_or_insert_with(|| report.case().clone());
2651 },
2652 OutcomeStatus::Blocked => {
2653 representative_unresolved.get_or_insert_with(|| report.case().clone());
2654 },
2655 OutcomeStatus::Unknown | OutcomeStatus::NeedsReview
2656 if report.outcome().stage() == VerificationStage::Active =>
2657 {
2658 representative_unresolved.get_or_insert_with(|| report.case().clone());
2659 },
2660 _ => {},
2661 }
2662}
2663
2664fn standard_web_continuation_rules() -> Result<Vec<AdaptationRule>, AdaptivePipelineError> {
2675 let suppress = PipelineDirective::Replan {
2676 suppress_current_action: true,
2677 };
2678 Ok(vec![
2679 AdaptationRule::new(
2680 "web.continue.success",
2681 OutcomeSelector::any_stage(BTreeSet::from([OutcomeStatus::Success]))?,
2682 700,
2683 None,
2684 suppress.clone(),
2685 "record the success and continue to any other eligible objective",
2686 u16::MAX,
2687 )?,
2688 AdaptationRule::new(
2689 "web.continue.blocked",
2690 OutcomeSelector::any_stage(BTreeSet::from([OutcomeStatus::Blocked]))?,
2691 700,
2692 None,
2693 suppress.clone(),
2694 "record the blocked outcome and continue to any other eligible objective",
2695 u16::MAX,
2696 )?,
2697 AdaptationRule::new(
2698 "web.continue.active-inconclusive",
2699 OutcomeSelector::new(
2700 BTreeSet::from([OutcomeStatus::Unknown, OutcomeStatus::NeedsReview]),
2701 BTreeSet::from([VerificationStage::Active]),
2702 )?,
2703 700,
2704 None,
2705 suppress,
2706 "record the inconclusive outcome after active verification and continue",
2707 u16::MAX,
2708 )?,
2709 ])
2710}
2711
2712#[cfg(feature = "authorization-review")]
2713fn resource_authorization_terminal_adaptation_rule() -> Result<AdaptationRule, AdaptivePipelineError>
2714{
2715 AdaptationRule::new(
2716 "web.authorization-review.stop-after-terminal-phase@1",
2717 OutcomeSelector::any_stage(BTreeSet::from([OutcomeStatus::Blocked]))?,
2718 1_000,
2719 Some(Expression::equals(
2720 KnowledgeLayer::Evidence,
2721 crate::web_actions::authorization_review_phase_terminal_predicate(),
2722 EvidenceValue::Boolean(true),
2723 )),
2724 PipelineDirective::Halt,
2725 "stop optional network work after authorization review defense, rate-limit, or incomplete transport evidence",
2726 1,
2727 )
2728}
2729
2730fn outcome_made_progress(
2731 previous_stage: DecisionExecutionStage,
2732 next_command: &DecisionLoopCommand,
2733 outcome: &DecisionOutcomeReport,
2734) -> bool {
2735 let hypothesis_changed = matches!(
2736 outcome.hypothesis_write(),
2737 Some(KnowledgeWrite::Inserted | KnowledgeWrite::Updated)
2738 );
2739 let escalated_to_active = previous_stage == DecisionExecutionStage::Passive
2740 && matches!(
2741 next_command,
2742 DecisionLoopCommand::CollectActiveEvidence { .. }
2743 );
2744 let conclusive = matches!(
2745 outcome.verification().outcome().status(),
2746 OutcomeStatus::Success | OutcomeStatus::FalsePositive | OutcomeStatus::ConfirmedNegative
2747 );
2748 let suppressed_source = matches!(
2753 outcome.adaptive().directive(),
2754 PipelineDirective::Replan {
2755 suppress_current_action: true
2756 }
2757 );
2758 hypothesis_changed || escalated_to_active || conclusive || suppressed_source
2759}
2760
2761#[cfg(test)]
2762#[path = "../tests/support/http_desynchronization_lab.rs"]
2763mod http_desynchronization_lab;
2764
2765#[cfg(test)]
2766#[path = "web_runtime_tests.rs"]
2767mod tests;